From dda993e23cf5f1bb775a60054000f18ddbae6989 Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Thu, 4 Dec 2025 22:02:59 +0100 Subject: [PATCH 001/358] Include JaninoLoader --- dependencies.gradle | 2 ++ repositories.gradle | 5 +++++ tools/standalone-JaninoLoader-1.0.0-ALPHA.jar | Bin 0 -> 1129766 bytes 3 files changed, 7 insertions(+) create mode 100644 tools/standalone-JaninoLoader-1.0.0-ALPHA.jar diff --git a/dependencies.gradle b/dependencies.gradle index a7c959872..3748832d6 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -38,6 +38,8 @@ dependencies { runtimeOnlyNonPublishable("com.github.GTNewHorizons:NotEnoughItems:2.5.4-GTNH:dev") runtimeOnlyNonPublishable("curse.maven:Nashorn-374413:2924946") + shadowImplementation(project.files("tools/standalone-JaninoLoader-1.0.0-ALPHA.jar")) + if(embedMixin){ // Normal Jar will include built in Mixins shadowImplementation("org.spongepowered:mixin:0.7.11-SNAPSHOT"){ diff --git a/repositories.gradle b/repositories.gradle index 12ce0d893..b105750db 100644 --- a/repositories.gradle +++ b/repositories.gradle @@ -5,4 +5,9 @@ repositories { name = "sponge" url = "https://repo.spongepowered.org/maven/" } + repositories { + flatDir { + dirs 'lib' + } + } } diff --git a/tools/standalone-JaninoLoader-1.0.0-ALPHA.jar b/tools/standalone-JaninoLoader-1.0.0-ALPHA.jar new file mode 100644 index 0000000000000000000000000000000000000000..6b1b77aec1fa8f5719f128bd0b529d1683b3b9fe GIT binary patch literal 1129766 zcmbq*19YTawsx#eI%dbVZQHh0v5k%@w(X>oj?Ipfj%~B!j*UOx%-lQk&BdCzf7LpL zwf4K~yl3zIoQ<@S2860QSO?_e0rtns#{>3b$cd>6(MidRGk}9A{wLv2 zpJ1Ijf3Wi}9|YJR1MMFP<%Hy=#Klxp>E*<)<;F&3W$5Uq5oGA7C&ni0l^N$)w)S`S z!Ty1nT1ViYeii(K8NmKezxU51AJhGV!~)=A>1zDHQlkHf(%IhD%+l4_*~sZn7cKez z`M!z&r;DtN>;QK5HugrQX8$_}@_&hgkptjQ^WZfJ`qN4<{>xlU&1}ppj9ko||HRLl zDo97WV<83=6a?h-V@UoV_{o0UtcZ<~v$Kf3ohiTtU~fln^2gU=`F445CL~+}iO;-Xhh-MZ0M4Q zEfUcQdDWi~k6!W2&%mU1F@$QRlT!G$%~}0(PGK3AchAOb+;0VAX1O(^si!HPh1sSe zMhZvDo|dLqO-`Q+dl$iEE}sqEyNTTEO9n$0``&Sk?P}6-eBs&h)1zw#a?86vQovj8 zyEBtY@oyLy(y-(rFdaz|ikfKoZrYn>8ot)2u(0f}O5icIUoKqA3L-oAF;90vE>E_N zk6*$5ZXef{!8*w|hls@=3%>C&1pnoQm-@?EE1UhHW3m8e7c)CEr@t<^t$Zz9uOQm! z_m%R*%tOq>9Xfe#xIjfD7iaJJ&`zffsp{KZ+!qi5X`7S0AnXJ|$)DG*{vc_Em_!j% zWbi6Y6eKi#8$&S)3BUVQrLh!tqUgKQ)j{RyWGwEk{a2Cv1~IQRH7Og`lb25 zj|PXh9als)$R@dCO3D1@(!kcS%?8||88kjM3u5^o+9b-xh)EJcd=s)2{&cpIuj2Ui z0^#pVpzIc`<6otOCjOy!${&Ej`>!v7sHdHgEx<&?-qrzN^B=b#tt6)a#)RZ6W{Qp^ z7ToU-g6YMCzh@kn5f>LHxHF)#Y@3p>P6u#@#edboe*t}?+_e$ok77E{pH27uJ)U{= zJaqEqD@_A$mbWFNsxKacD(Q24P0Du3E9)Hj-jC0FvZg8Yjuohw&S~3P<-~SH^XZpl z1l0_k6C@&!e*2?h<4QWZ} z0`u$2WQ&Yi$ZBSIRgY*hoxYoJjaHxoC&da-vV(?mH7WTWuAY4;be@lY%vXdkyKd#1 zu;@1N@WiNhg=`b@R3 zHokz3BOindFP>=?9+3a_g$CWg;VbcesHgZm3U4{yt1^!`cAlot`~WH@z=V?wce%L! z>l4MF<@4B!2B!2OABYdg75P8QN7xl$^MQwd)zV63IhhYw@GY}$uB@!ozTN5Bz1z`; z=qb;m39S2;7lvQIS|c}1ZYBWX|Mh(+Gj9v=LL$6gMhIE7_JDC%cB3QRbNFO+_S09n ziWN=EuSU~@3DNW-G_nwr{X+Eff~4@z@{_59(WdNU;2dnVSOj$=KMnz%^YkPU=u?l=lHH5X)3QG+EEEH&#&;Q^iq$ECo=55#@*Nl zI(PKN_~)Y%0biPH)ml^D1Zo z>e=6LFv%5!g-{dvtv8d_DACN9o?1twj(nu-jj?Iq+f|~+ryf0i)f+Y2 znP+XNC9`W%ri5^tD;0NHB{s)MV^{J8t@9RK30eSgmcc;h=)@nOisKtBQ@lT2laYa+T>i8*{J{*AQ8u&m!!#W}fq;npyUe6i{=!t)-qiC? z3NWC)YBwv0#Ftqjl?c?&-}j@7Yf==Uf{8~G{X!%bAuT3M);~MA+@{0jW?CKp^pl4n zY*P>{=fj4`;}2}L{^Zj z<`d2JIN|N<_VL~#tjU|Y$~oJ!iA(ExXE)iZ83H zbaqbUnYq{LQ5wKQL7g&BJ5}jrENEV0S`(AoBw9^xGs1esD z^5Z&5g@C9%Ci<$g%cbbkR+OcOF_`m30fh>lBmgV=D?-HJ&yuddPEF01 zs3nOjAfxaiL}zB8dg&oIYE{8+%dZLIP(zk}QG@A**#aE$Zh?MGvV#UDaI>taPea23 zLX_TbHd))G@{O;vd$~O!P5$vOGA(O&)o}u$C$<7IxP8;S7+~#T*QTYGE*Q-Y^zprF zb&DuvxqmjRU$9+FR3AVH|6vWP|3^#s%f|hOIgm=(IlCCynV5~SOUkoh6B$>I5m;#jI zOUB#4C?$?4pZ}-BJBh_ijG!)E9TSy3ScK%eCW%YVVYLJ5As!Yz{S-LRp~Apy?Wyl| zRs}~X%bYGFHRuj)JKVIKRDd}qo{>$)e9B;OP=%47s;yc~@e8z;>Y9U(d`z3X(zhDcB!G=kLWRF)ZhB#6kfiScogWA`oh(kYI&x)8zt-1G;E4wR-kxD zEdnnO6${w8xA*cNFmy=Px5HU~*~7DFW9C8#xj6_eq~$1ZiQdi5shqHe3@U6;mZv?E z(qu|@`JUu<7_6SsbIGx2RD9(XTde zi-(qC8243Be|aFR8#l`%xP9_OOq&5j20<5@s)rdX%~IqAZ*L?F1IcOq7U9^F2@92~ zJeBoPaFUx*SzHS6p#C!g>>&qB2qI?451o!<8dQss*pbMkbl7O=Uq#VF$~3lMRLhze z)aPom>7T2rvncSf+!t`uX2@Me>RK8%8iwJXdWuq^^E0|vGm{0!YW{Wleu%+w*1be4FDp%=#mz5E-htKwTx`C~Wouc%45+jcq%CykcW!s* zYbsw!zbq}8JK7{t!PXx}(gGU}>V5G|h;!D6Yx~*_=l}hsdfuwz%(BisvlG2xsVm6! zZuPXe28ggWK;Bd{9*?)CgVH*Q%($Dh4~nmThkR_q@u<4n(dY$^ysC3MX#aOR-n03$ z=R*H4+3{b(%>O3V=A#MHVZy%sE&G88Adny+68|FtN9&yy2Ev9UFhIZ*ryN3{i-A!)JqZ48d>ncG^8To275YiTNJX8~SOd!GNu7 zQ?tGS@DLx%!mrlw3iQ0tz~24?7TVlKFDu&|eVOKbB#2CTMWO;3JM!o0`tvM8qqkd)U(UvOu9R@2&2U=Dl_}NY^BF(3?v*3onDU`9MoAN1*r-u2?P2Y-K$uV|NLXv7-B_P;T85W{6_u- z`rv51F?A48Nf%FYGSS#Q#LpNqMg@pZl*rqg5akrGs%@;uDG^KdNh|nvMdh5ZYS_ zoh_jGD|%0R9$0Zm6$ct!U{YiO9lxO@{GjPdjG06G>Zx5c?=$KPNgAc7T$lp?LuS?x5Jv;czCz7~|hc^JGHs#h;8 zjyZn(0l76eBTC^3Y*$<2;8h}7=6>A8Ix)cDRtyO)aAFO|zvniC27zz#J2vo+OpCo$ zryjCT@cKwpkF7;(Ayro=V}DHAE#Vx3qKHZl&Oqwa@iJkLRv*QUoPw^Cc70x3$h{&$ zvy$s`^DUd5^ei(kX0}GLJ_?Z1S%b5!?H6QP?O1|Kv6;Dc-e(3D2=j^rMKwtk+68Jz zBY_>rAll0;wY@>+=Hi(zgJ0XJ%MOkVKvY!AzBuWt(t7*av#B~JulBlNdm&P#5giYt zr2=VefXiK%VDNn7u9X_@R^GSo`fo@$-~1W`63t(k<0)!4?OV`ElhGJG2Tqpw?SW0b zZQm`Ab%s&gQ+4%yAU!;4(i->JZhs&46gvRqZ`JQpI)Kgh; z`|uDXkV@&Bp(YlJgknZ4GPd}nSQv3eD}dNzR%v~_j-+Qyf_-{xpNxe1e0ithMD+VS zbq&*=L~u|N{TYO_BcqQt!>1Ro1S3;;?B~a3&Ucx4UsKnV8!jp4R_336&Qa*7BIu*d z0(%(!1pWQjF7@ysJsYXVQ2GuPIA`Yc+OXUz@o3k>!9|8L%!90`K0VRGpO0VH)R0Hm zspJ5|f1r>Fv5?@fu=s#KtQCHz-Ud!71QRi#T-x6jOvC z-vQ0Atje!X)hrpQz(!I;|v zplj&eGJ)TQWCk8a`*-)L^^yCPIYB#^ziNaJYec?TgLzaQcF4Qv6ZC$$7$n>X<_NvO z=uo@x(gElC!4rJH)rp{Z0m^hKgiZiQ9EJFulcqoMSt8JKNJ8wF2{k{yD4KiZ1cmSe z>A8|)b%ZlDf3aXR#jVSVB+a)-yhVlM=_nKOLf{Z2@CYPqsGxFOx z%ZAO`@j1(1)7k55sL~mLAxUhvuI(gar?1z(#Uy%6vk@^F6Po6n8BK%HwX)omtJAw) zBeGgjG74C3s=17XoqT}@L?ZA5ys+8IIeT40vfRK*!$q}TvZx}aavpJRoZY|&T*gGCAgZS0}Oh+rW!5f>1q<;cgC zw_0-O9pq;*X3A;P0(bOmS}HjkQV3Kj3J5%KI6;?d$=jUXrNENSF&t+p(=9fXd9W-q z;^MO_-qRUz4*JPHqdpk(Wm-gEq+B=i3;EQ6=ME*z`4191Ss0!YH6QH(-?=GvX#kYd z7j&D9TwL_mD3evJX88lbRKjNH|=H=?dEh*nEMq@8L%GrezbuwtWu za!b6{fl_LH7%o;zA?r9z1v-7~Hj6djJZw=UW%&{=r4E@uRIt_`yz;81Y_hw&_Ms*T z!jWK>X+4k#5{|NcLTiI-nm=qU24$s3pJQAZ2gKNP(RlcSp3_gcO>-6;U~}g$xUw`Z zJrNALG1Yve`cf~yMnJRqKpMhacn z;Yc#^#eP-|>bgqm0Gma>`#4R9#NLQMpLwC!A>xEjk+s~52hAo+f!U0BAD#5`tY11p zru2EPPt}}_32|AZibZMJ4c+!~B=z2=w4@LsKq7&nOguM@Q5hEUgc@XG;~Wo5NzOsT zE6!Y_re#Jw-Vt~7>~e>eYJhTKJOR#*cH_jWL9~J+$+<&x$!PV-RNFxzvZ7zQdheV{ zES{&oZUP0PZsGg3D{$vpBMA8J$J#_ z8->ZQ@|$gr^|g~4V5l33KvymVIzqu`lV=RSwC4-cJ1xKcXT8&U7?XCPIU}d2;xwOo z4_|dFALz%`Ae~lc1|9c;2!BCRW&|=P}_S zw^ywZ6<^UWOpuQ$-MQYY`lK5iN|Gymv-HjicB#TZ>&TX<>c}3Rmd^Y9VxtYo4vtCg ziEaqREb?nxxAbGwq|$f%$+{6>lp?wp*KYCOKwITux05LGTUEcV_(aOl73bwQ(PCWT zagLv}9v5Z&x(ZCm%a~Lz$I7p9V>QMbaFfiJ(e3C^rsE9uLqXnoy$DI!Vbo#My;X&e z8#T-s#on2}=xnU4epPm#Cx177A7O!MX1KFoQpUU7jD5;4KRCvwHP#1MT8c}WHAIl(r8%0(r*`fQF#E{|~@?XXz9lC$mtuo8}K##J`7MLj`%Vu|v0 z!U|q8S5llT$j-SX$_ngyw}Z&m>|I+8`jvsBbqB}51K_?EFHt1AP4$@ZJOBa*XHwWr z-PCbC1qWw+QvV49rnUV_h9DAkznZYhwSs?8kKQ&FO2y0 z^f8n=9=h`CgvmI3RjG6c_zu*G9qnEKO~0I8jw#-tA<8LQ%r7Ct85rYvW>Ctnp>-`r z7hon)2IAJfloFev#r-sS45Nh%*HgYb&!ZxnKK;vqKvYBQ7ScqCZ7 zL19--ECVAZVjI{8&;H=xz5!LG{~S4nGm^;`49gpNttLxr?2F@{tgdol@q*`X)X=fM z>Y8D_5#YX~S9iP=Vc0E{9CA`#Aj(Z7qtu{juQ|u59Rxx5z0x;2$_EhsBoFgCfJ~yG z?Pza0pT)12gEjN_m)FNH-M(w1M3LX6EA|`^7VCd*0Z};Q)<$}|ZBfxt)lk`kn6?5g zh=ER>Ljul2`}#v=t-SCj{PvMs$RatoytnBC2} z@zHqZ^dZb{au5rZAa+5MM7930m=b@(7l;#EOi@@0KP=tY6IcFd+oSkRMxg`eE|b_} zD3f06;KVPo9RXjmcxX^{E}5vWk0kg?nTD-PAg&6AJ#+?-tPv~XFk1dqunzt=4a5dhlVo)J#&&etov2*$Uu8YM3JjI`F~H%3cX z>Ew)N)2{rN^Q*nJXCkOik{lIP_u3L1FVJj=2f`niHof?m0rcQ+>pib9Ls4o zPp5(>W>ap*O>B&BMWMiGD>Y8j0wwJBD2rdcMY|hjubkFj70lXjH1ppjG3Jy>5AzSE z7%1|eM64rD=L8chq*h6J3L7nd2QJ@zM&Xfs6FhFeOQYZlB^`!;+#hC$gq*2}N5pW9 zQ3s-oF#S&FM`k=zD_GjRW8|`>Cze}9D_~v0AemeBj4tjq0^rc{fzlvX@PfO=@2J<9 z=|&iR;dX}G#*9L|V~1>xzM&uUEzj>_P#C|c+i=|{_`5p(bD50lUsA{arbIRl8?0l; zMj!I^L$5hL^jhlQVf5ETGr-P5!Pv^o#6`-^!PP~@#mUUb_HT7k)&B_H^)yp-@*zd{ zcPcsB3q0tqgbA6b$h7-{>}St3OPbop&uK3e9t;>SF$n&sYTurs3Be_&Jxpb@{POvD zvcJCVo^XIzQ)Xg`yFoxXkzY}!QSLMaC>J#$tTEM$#MyXv5cvR?r6UO(+Q-*RI+qWG z6sLjALJP$-^H4$AXGTR90?zIE|%*(kJupkM8(<)aNLf2{fAF%XNstE z`c}QyQm0=f(-ohY4VTK1gjC93YwV!)B~C+yx_#RR^p3nbUCz2M%`}!7L|sztoSQdL zVxCxNfN3t+Grvm445`jJLdtQ~?Cm;TMKz=*5#ZOz>KVYQeFbzrlTW?*K zZ^IjE!Ez%>PL5r#N;o%5s+^SsVDw1#Z$xO>SNojuix)d?mD$vFkGVn5A5z!D&L z7P1Iglw81GW*hUqgVr)g;;NJE>=A8;vi2cu6Q3uEmmcPj+X5fV+5B^E5+#lCQ(ef@ zcV(G1yUp+{;4~bPnYBo5aZV|?1`%knHQ8&x%(pV+nWf_3d#XVg|7hTyw@E{=JgdiZo|Bjlg7@3>>wGjMInfyjAPaj=1^f$W{&*42b>rD1_oipI#40(P+NpZ(m2YGe&7<_$$9I`Yh`T&9|nl6&k_pjP1qz&<6bLC-y?FGbJ zfy7lU0hX`lzKO%K<|_t%wht!f?W3)~vYxZ|b253~x^@x4ITb2gjKR;T`@PmGxA7^Q z&U${CT|+MWDD_PfuSGuZZTT(p5@rf5!w;J;g+n`zh3{fb*XT|7NUVU)M+E^AD=bA+u|`;b*4dZP?}Krc>Wi7z7XH zJ{9?v3q6V-p}#S=jb8-o-tvJ2T($7-fQMRC{1;Jnf%%Jute13DnoDPdE{7c-UXn}$ z>5@q+5Ba`p9KZd3?U%N!nwKBAU<`nVLZATPpTd@jHx+kyMVoIy)c*i;2CVNCrEcR@caedfkoBfDX9sZ2~dEN$&X(TmV-euwm)#h;+n30zCpRn zd0df)u0zoerUAsaI5yYPrJfC829Wb)%Gn)%+aQlyM7qjEv8AzW8hrL1lPsMk0dH9Q za)ljWE8d^5+i$90FVJA655>#Ze&wA(2i~&uy}mMc{9F_ zBnB4&a*@Nn)W*psg$OzO>+ICguGmVd-*y7gi}b0iT<^q$pNln|Li4^tV#g@4eAY;b z;Pv1%21bW1W4Do7f@`BNK6$?k5^M9CKVnoOSvs2uyRD?NvX}<(G*k4`GEV9j{QjAw zM-Yw$ZcAMOTMM|6;Rd#LpOR*e8}cf}%2GMAq_aV*3_Fn;%b_a3GzCr1HBGsg_a<;r zZV@2B$576{n3=Zx;a?9{ehqm#3b!~{ zoaQb^e&VVn>!xm$x&L+sGeE-coJc)oZaVyJv>1+?cI{gscy`C(klo(6ctdQu+K}DN z@kLMdE()XWf-IvJHBr-ahdQe{l-XKW#V&eB`R;g&>X5k2Osxp}_{DJxyncy2-g}QV z&fE80f6lVPPphsC1#&|?jwF#8w4fy>pPJxk(=|a(Trlz@apS*7^2S$x3Y~IMB7{qi z6z5xzr#dU)jS~IkrN=22KVhrlqC_k0qWf6{v$#N30?`$9zeUB+Hv_DqTLg)ggvCds*I{H47wDIWOx|y2{Frq-IJq@^1S&H$cmz zqdbXhnHHRuRB>=@jQ{zh1w2%XK1OTe^JrQ!zTgolsOLSzv2VqBAr&c$N+6Rw#;duL zl62POj-Lr7TI3+Hc*{W4ZOF?sN~oq{1ka0Y zo6@+C<ndh4>V3NUC9gvPkx5fAEDOWaqrX-|R`36GP^4WlrcdAJz zZ)HB)(}Y#SBHgsOZ-x47StD89yjb9+q^@qIH-RMW$NpRhZZgAuP6;?~l)$BaQnKzr zi1Y|{(Vh&y;hoCYBYSzz1_fs`{?@$|W;+1xJ;zsRjTB>@PIsUDDgCwp8ZG-JPaV4B zej#(7BSlkX_6Xq-l7?=eG`|itvrvLRT^xisBU-~lNp0@Hk(l)r1InW~CoWc83J%3S ztDyX7TR*YNlMLAM!1BmCZ3c$K*xMAp^O(%{K|!hS(@D=~v6@WItN^)DWo7DeO-fH4 zBx?=Vd-0sv`A#Kzufe5Kb(2!A%0!4arF0f$vzX-3`PD4raLwa>uWb}din+v>gOsfA zkWE=^zZ|RWxM}g` zUtc^rcyZHqd)a&y1j6`KF8! z-er>bU^a{S!Dl!zx~dPnvIE*+I-#vCNR7IBLJu4$Z=YF_62#8|y`cD)RybQ;IC;74 zt7kz(VMuviMvzgbQa_zl6#{dJdq58C2 z)^zkwWDjSzx3G-(p*>P~hJhh4AvhvDvz{(0mTZ3of+9c@wKye2WM>ddRKhpN#$9a9 z1g}>Y1={zh*#KO7N#^pi&xvID0y|RHyyrl#z8y9M!$zQseUZ?C*;9uE6oZYY2hT}$ zBb+DI97Y7OfJX9G0ka}maSKwnad4aPV%Sk57;_h@c}L`{5)Kp-SCh~(Rs4iU&;cw7 z&AtWJT<&<$w)Jce$!*C?{IWxpaTe_yRC&6~#hJ2hio@Bw8)j4z)Kd6m!5G#uJjznS zr>hS%gw4&nP#r-uJ}4`_&gu!@_ufz@lbmU)zVx_=Pr=(G46QJ!!onMw6XV#4C`GSl zlXdNjq>aa32lJib_GV=ukDVEJ8Dm`o89#&@3<)pphtbqx9qKSah%x+oM~K!jQNEfF zSj>ebTFjGbArPn!tdVj0sW3ty-ne%A^@$EUaFvE*;afCaLOA@wh_;jiJez02GV+)T z4J>X!zcW#l zC#jMV~qK(M=X+H&}uFR#xCws|*;8f+c@$tkZUOV|K zONE)utRe!}n`DO$;6BcV!6Wst!i>P#R^+G$zYB`lPdtQK5vybLW9E%aenI=XplfQc z#H6i+1)hMiE0z>L>vTUzmO2}Z;SwiWYxVLm7jW~qUEHT%!dJS|vV(ym$;hfzNzQx1 zW0bBVD@6Gtv(zMv$gzgld|sX>FuZ!BK*y@F?VCV!63yjC8yI`6K~L)|2?sw7HNP`P z^-4$8=^loU;LBMRRx9MwA-)?Sy(*wJ;!GB5pO%^jIx>Ap!w|phz=X|yar#CN1d%@& z(b<9vU^iepXTkJL&?DhFhdzcw&^NSPRpMx;F_@e>NMY&lZ3FLkFu8M74H&kP4ots7 znj99Ua-7gVZ>S$aQYW~LF|^efXFSGNO7uj#9qf0P+d*=3qWvWci7hW@jsi{Rjmn?= z^UD?S;I}DqgxrGw{=|lBW--VlykEt@XC;>`%r9LkMM5DcyQ*+WPOB~v&DR^qKI*;# zLxNyTWBcfuvEmX$^auBHF~UkTyvEqbZ*szAn+XSyFEIkk-tj`$JMaHcq5mUi5Y_Us zWd0H6qkc5Ai2eV>K>v6gx>G~X9aRj=ujz5gv^}XoGQU8xMmWPbNRnnskzaetl2TF{ znVMkLG(&#bruSSNaqURp!efYFOIeBf*H0w|z9AX^xTguz_Q$2(BS5qM>BK2d;=N~% z`+={;)uKSx+vhD(d@HPOIdlWk_+C2nX0dikeS`9rLZJbXpuk%W(4O9&|d}=h7xrCv)XC#oL^~Y&|wwyZNyUC3ClhZ`U?{ zckyPbo`At;GS6v`M*aGAFn3)@Z;VyL$g1&5I63S*D)R5lqh@BqAzY@qYl1x+Uzt*o zRTG;oU29FRxsBj$AmVpYc5&3h$C(q=;8V*q$*g~jmJ3emVAT6!EV5Ir#cwV9BnH1c z4ro74OLBlHwPMzdZuQJdf7M;WVEj45&91!I06+l;#E7us5Hvuv4Wr;h;xw=V1zbj8?KY)j^*N|Qrv5NgN+R@-Av=J}|u zhUzBV75^Edk?{tQC3tF9NGeDzgrr33E@+w{TBWi6+#D0ibE;=Flu8r3t7*5!;6h)PcSp6=;hB-FLv!l=iF?4OOOJ= zqeuRl52973UKDPmbf1Iloo!Vn8ulOOi`CSfN=t%DF0=px+I{r3qq?{a(CqyDLy(Ag zyOcBgwe7e^9;ncVZBuGKt{oxGuDE^AaCgmv^c4o?^viDJ3ztxL$4jw9c)txbhln~# z37hw4bApdKBcisn)g0_H7oqSltnd;tM{B4itM#DKUE3moI_-zVSmxrCX=^KD5=x&# z+=+J)%8Q$9kEj*H%)2GSSV^~E;^(BE4$LrQuk6$mW5RFMeLkzwCL zA8ooL-c57tXP&C;jHEEn69`Bm78jhVus_|A+$fqqZkm+661SG+60UOAnexaR_^W)$ zcO$UAC9UdO6O{VI>T-a-vRd` zAV-QTqV&oaQH}N^X8o!{dO_=}M;bt3Ip~0jL$E_i=q-0tKp}c>82A)4$8I6_RDU;J z5&NwRn(sZ~BrHOOTK+pHc%T(^PA|t#WL*`D5bbSpF)I{db0MTyQ@I!sa#v|mj3K+r&3nMTA(bv9ekS1FC<=t~+=Ay<{GrTcawi6gR$Gc?@s&*IQF||_ur(s>o0)_gZ8ob@l zXlgxndt`3wBfO>SPq;XI$X>=bH#3E^v-e=_IyO63928`UC2h#D!=j;P%5})!oK4#! ztnrWa6iQN%s}`p)6}>X*_GB}a$2H31Gh{Er09M((Hh_dGaqiuk&ja1`F4|DMeH z=MF}c4^dH-vJlGJ}34G}joarro?HOe5pC^MlTH7-d@w=gO_Haa%C=sYn=_j62&&Phg= zewd+r?@uj||2%EPf1HG^k(-&_pPD!RnVjUmlK*MKe{Ea*Us+K9Yb^d7PBq%%nks^i zo?9QM+gd(yh)n+$CkK05dJkKhY;{}vRd%GebAxGmyQe8yV>emKOrUj$J z=D>K%bXc)+A4gpH=ihws3ADpbYwQHXc)wfQf5o^{NZ7k`AvUQ3$OoKw#S$-9Jy;{) z$OtTr!KUCkaqXk14zPZi%0LoBO0lC!C2!BKBlrc88}7$nXPYBD9UZ~)b-J}In=%jN z+E)n#7|9EhHxLo$?jK_YuVnF+oH+r7OQxD{#2A z!MIO|la&)jz@hTEDU}q_Rn1_%6hP#CF=q2341~qr);`J?#cm=G4**Am=`4k5X2sV| z=Jks%vE+_)p!2RQz_qF=rZHw`5^nibSx67Nn{jCwCdT0di%eP?Jp;>cPxc8;vHT>s zBAzQ)+RoI9tCKE1$Q29B%a|^8Ur5@uKvPNI;bjsCWnoas8j1MZuG;SRi+A@`;4JQp zqb<@;S3?{_;%i+vyxw}ltn!h)^KupeVd2FV>XH8(bzWa^cAjy+2C}FnXfUyYM zx7T*W(A7(MX3q6WJ^t6;&&uC3Z|42y{RDsfRxEz#+8k<`yg=Kx0)~6EdYM|5XhRs4 zkXv$}O#>o6oi@`J14_H|lFxf#W25U>d!rvvhl|j(F2894@s&%$Ck)PkIM>GVs5h&% zQSuEk5_aa78_fE@3jDLk?0+&3N3%}R z5|gEw9x-wohluW*)SlFB?dbS8YBpFCTJGaV+QqhSbFb&Qt`mQiJ5m0EOP!hYtF01b zKDS+%8nsIDO0uqJ-5VPomsoCutUNT?ZXMx!h0#k36jgb#8tj;OtY+%%=FN9H`$u<; z6rzPghs~R)Hhc{~a9 z`m&rMWPBL5EWO-M>*-G2bH?K>`Bp!0b8~yxXeW49_KBn@tq1cU!`e8j2cKzKBFSW5 z1|Au*3$bzabffO)am=x|w{y6;@NK1@JEv{Jk1@ea7k$8JYA^d{Jix&(2TsRqOq@RR z-bGe4+G&$49X$lqo<<$`m7c#6z+3nWT3a4c+!iCdmYlir04i7MBhD?2p`n^8kF7^; z*hTw(4Iv`TEr*BJ$f8u4+=uCur4;z%iVsNiHle+)JGl^Xoskp|;LT-(GiSXMx=ngH z#{A~+sj+cnHP>Qk6}3NL!ti5o{@ZZj;AHP$ z=Hvn}b5?PqmzJZIl2ei_S9PG3l%0@NyO5)m8CPLksGnom-VW7K^tsVcbc>HT$ znwEN4_5+MTNBV+Clp`ZpuCHyPZ{cs@qdi*xp*#Jbu@>TEz8~QFzk(^k-)R10$M|1~ z|LC3ngW-P?|2G(W|FLMNf{@^>9~fH>0|G+xKV0*dBFx9>cC){>5tDNKwb+=fuA?$5 zh{d-GgjxZCRH7xuq*ClH_7H<)&@5AL{buGdRZ13&LQX25$h_-+C1g>-{q3;czvq4D z!4?KmT3d$a)s4e@V)^w4pVam9?+yO1Cz`fG@XcZ^B)xjb=3;56+c_}zg^Cx15%j!K z#`v+Orlz!vE~ic`Wy>kMPNRCe=28}ix@!mRytYo?-l zIJ4Y0JBuGT#M(6_t?z(824v*DGIfAvrOw!iJfDelAf0BX#-pWC zGm-e3?pP&Y1JYGSC490AjJKvC@>fbklWcPCS=u`3_vWx)Ff~m!>TFWmq11%LV~A3C z+Iku9O60kRO%!J$TXcYlS>{-u_f*++%fw74YT)nr`*Kx9qGlL69%aKUBPXM$fC?YqX*de0*vnPhat&|!{ZRVN4L zLp8}NWEGq*9aNp$(-Qd5EOf{mFZWNTe6A4CoOe%bky@d?8_ww1@~A3Uae* zZ;)-`!dQbaTetmzQ9lYLA4iKjnIx}Kg$c?{e|R#FfOeYueyBW{1G{&jH?XiB-Xd^FCs74Y!W|1`H>ERep~CYuCc}M3 zrW}x_&Mo3H$gJl(9*v3qjQx52XJ%DxMQr+apvl<&&1earwcJ{h99x>tR=rVp94UnW`Oa;uG%IQ94yWbmYr94F1ycZk@XbFP9EDt{t;1@B1ff8A7 zF*;6s+~-=)!KlK<8KjdNAOF@wWSKh*i3ZGbN9qY?M4Ez!wMIKhquM%Q7pXgrw;4v+ z$}?B=vTA1D<|!vb>?=31?BkMYA4MYbDo+~2DQFrYYSa8No5871H*K4Ys*->tbVgKY z=7G)qHh4Ze9NV&xWc}rUxQ}01Q(0r46@AjU?I#(m);#OF<4z}^OU|O3Ub|WlT*YnD z7lGlL8tYxP8Kres1~LA;{g>Xv(u$7CBMk({m|AB4bz%ZIuc_}_tP_-nePTe9c9Al| zm|tkR17eUQ#x1eUXt;fjkQ9y)`2e2f)aED4{w6vdA!!%{m~188!sCjAnvU~5Tj4Ff z?_0JVeGW}KYC_j=Vz#A2KjY-w#Rn~-DgaLrf}*x+@TF?w%Wd5LUGtwR5q~q|E@{#C zd6IBctAPC8_Jt*&d_Y2lbC~9v@F2kSO+x<%dwu`p*o*P(qlP57o zC)dRr`KGO&L+yJON6QB{_F#n561Fgxg$v+psFl5}?w%cUEFI)ogq}b1))B>{CAvUiH60_5pBa8>ZGTL;$ei?#=k{KaR)6y=8Fz7dbBijb1{gcI7bh*Ny3eHtlj6VckC zC64yb;{K}=^%4*xDoIIpxCcjdyIeco;fhlOTj?#Uz8v?G3X-lit@&JN4=9b9mSgOv zo818vz4nO)(ECQgqL{*tZ_|45}a~OR#&=VuUyN z45+J7Q78gtcP z`tGMX^l6;mD`-f&g-4WC=*z~cs@|)s=z9^T(I*cBCLt9!r_=?X!6jd4F1Fm`2+kQF z>)n@#Ad+Nm+;&Qu`GO$E$02~Tkc(1*BCG~iF?k4|zP*3?ds^Y)V+#;LZG52oI10A& z;Q{`S{@36BvgB`h08-}vXlp(D<3J-{Vzuq=%KxM69iuD%wr$a>RE&yk+qP}nPQ`XA zw(ZJ_ZKGn_wylbBSG}{(yZ^h}-RHIUVSW95SZ&TRdmp3sKKe6!H5%12S536r0o(Y} zq@XZTWkSOJxrq~TGRx!hE3yT4g$DnUGrA$yM2CY)EvK*4KW2G3K3rCBZTW$?M|65< zEQh7MYL4&t2T#x_QJ6uk)Ll>aHU%T~&Hj8HK(-BnF6V;}$T*}m#89)+<=d{BQeEr3 z-sTi0J0Je_lxA37B)j&%u#4K~=2E(z%eKjAS?ig(5(`Fi~yK^F$ zu+g&(k{!D|B+@f9)w0QU27z6(oYP?h@l)cI-x`$n?`_CWv;<;u4o_4>iNqWyCo$9N zy+VSVV)oGkkXi(Pgh2aqp;_vZMM*gzOAB~MkYK(a+eCrp#_ROg_!0c+d5-AkoXs7Y z-yE`fsIe_csg4^wmFSCR^>4U?OLuU52jS$cOX#g^v4U&fd;fkpc+7Q~Cs0hIs$FaP z_K$(YU(DekFEM-z#2Q^dk2S@Az#RX=`I10`pwWMuSfk~5<@*>Ae3nvLETt*@!~MS! z&O<}NN6A12p_GRC$B(cU$5AXNWZ&*#Jbisvgd~su=@0O{=H7U1Y5ws3{9+$2?9c1( zBb4RjQ_1Qj&yh2MzIdRtX6~yG{W#L~Z2SCvF9vq{O8diu1D#k-n3=dG@?-e9?eUg)U|0uhw> zS+#X{mVhhhEk8F%L2c^zTtlO|6jXolf*ivJLkyA^diVz-h!0$#8@Hb}hT*#meB#5_ z0D}Qm5hRod3WQyNC5qoa{bgqf6fYf9F|6D__aa z86f!TsU540f-5T`pc>A(D1#%u*99|(&7{vJxWJv&7U+DNAMTt2nN%Oz7eao0lzLTvn7V2c(XgF0fEzXN=bT z0?T&oLiM(PxbS87GkC4S-`@(Ee42Oq_PhI6$(3taNB^`(r)oJ{MW@t7v}ov{ZSrzO zLJ_9dDOq4zS!#psf{BK=AM#}_@!A=;Zius3tD?UGSr%blYghgwe5PEn89Of*qA=MW zabJ0qs{#Is$JE1`NBxq+yQ^R;l$#QaR+D-&H(fE#HFTHHvHdx}oRYq?PjmAigOBJ~ zgi`V8W|#EbfK2nnTBmPl8WHAqkw{uQg$^#2@|X9%-9K7OC9Njl?{Z@+j;*7mhVJ2a zBWe517E*&wG>qf=LzKo?9+;1e z6bj=yAvc;4jl1sI&d_b9ataX}0i`0s*>`$3m7 z!W=y<9?-6DZY}Ns~WZ{&%zmIRz)Oz1{|DjI)dq5BLIR0D$59lV4$^RA0|KAU& zt09TtUz@ruFxCx7E1aAiUH&$gC2d{pj1B+yxvf+?vqb`WfWhOyA{O;Z78jeERQ>fV zDj&cT8bD>?j6e!3jg0CB;5ORE*}Md?^~*k(Uiu)e^Py6Bvy=)G- zKkRQ<^UJZ&VhJ8Kp9tTo0257MwadE1=#uK&V!V{3%!NgYk^PF2W4{fi>%wd_@1}hz zW)qi_8@w!YPy^aHe|0h;4K{0gm3`!F=0&G7p!UAbw4`H@0ZSewW+ZLaL{FNvaUU_M zT=k$op{9r7B>kexs;t0(VvrY8_tm?3+nM{Zz0bJN3bA>fBXsi;Ki%-$5m8=#5+?l& zn^l>zkoq~$Ic1ypncgUL*g|T#fGBAJD|wMEZ0AQ&gO88tbD*o~!SFPHWYFx&2m3Wq z9;hUK&@h8yU2EYY>Ma?)(ejQ?a~6YDoaj~8Zj+eh$ZBb6MkydIY@WoLvEQ|nnm-vz z5K(rK>h0{(!%NIV-MbQ=AY^8i75u_dyjMm8n>u(1cb9+xt9)k67C~4D|Rr3B85Dgz#Vk`ob3cA2>1e!te~1>rx1D2__#XD` z5W1gU1Ov{vFCc+27*>8EKnvHWaSN7!(^vRNZ-{%&dD|`P}KOAHM=YaYs*zF6p4>6W-yGs?S0im_^G^SBCM7(w52+;E$#>&dEe{&kNSP9K;A zV3V%uN51b4!H)9qspUy^$-QE(@%H(_URhw1nE?QSf(pCLjI|o5)rn-Z?}JLq$2t|~ z%ZuiE^7MSdRXOenL?_5-BZYMfHRZQ$lRKg`9ui(AArs5U)5vKE7c@1MPd}HM=`2DKq;Zyx{ zrY~l0&QatKC>>t5(D;mT=`74?{$s*mV1Zx`!gQseW7rd<1OJU8kGR&+@!8j#FE{A3 zbFl=o%fpZdVFh?~`ft|&tm7n$-zanf{N9U;% zS$Up<3Px04>HN+30TJMaN0_9L0qOKbRs_6GRwOg*J;*tCL!^WBm7B$H6P~e$umt{B z$(FIw`&ZFF-1H6ZV;0;ju6q78P#^hh`byuUAX5KXxyFNCJ@yf96=ufQw4+)98xp{wAbmu^$8 zi*TXjp^fZuRc;aaa-%%|+jEQ}mfma&#EN&oD)M&>`X8%^u%WZD1+c~L;$-??ye?W* z8`#-H_*i6L84NS%#Z^j{W*FRNfs$3G0EM)a^e+&w%u^?{6CjZ0OUP2jd%=Indd%q} zvow+$LOheijF31G|5=kV^A5|`QCS2XL@zWTHgIQUzfP0 zF1yii3%^i&s&Fwv88RHB%8_)9E1Jztb78Qds;jqOq|GiSL4`Jw!#)<0mr>?$ zd@SdyN^?vACcUh)@X#ONZ^PUG7jCwqSM%CN%mxixYq>GMff<6&j2=p^ISXfdluA1x z%(P%%jeU7|Or|>q!pYP?b4I35i|*FaxnnFJ**{ohb!LQ%L427bM{9Y@gakMG9kcoq zo@pyq3zWd;#;+@M;+^7{qz1q#td2uIow#go(01+J}4&eU=keUd#}Kv$3+a?nHjB z-(VA&gD?^+UKlKfdfQPkf%Krv@w10v9y=IeHJe_&7Y!H zvW5I?pQ=_D-wd;jij#pm~B!uMUaln*8AJ*W_R2z3|;A0;fTH%B3>wSmF z3+61yn!W{>81_!vix^F=I&!3~aMup0(-RRnMzC#fjG`11A0)`#RUTSY&NPhZy?GuSp0Rh#P1Tg`L}8a+6aYHJYK`J z$0qda?<1M}v$1kI#8q%{I8mhb+53={?UI{f0Z&WemUWMDZ(Ipa|CmVpB@wa|hu(2O z?2`c`5&xOx0!f6h<3BJ@vYL%DvO0>dEmEDNP9#=8dIf!w6;ii`zILimGWdYlJV~G( z^SDkgu0~sH=8ARAN76@tZzVHW@r8`vPyW#pPIee*8W4QP%hty%pB$%2UUPPQzK=Wn zpV;mxfzjHUI6XFKZW`WpJE1r)_Nl>rMgaq{;`W${Xufs!@&qg2lYY9B#@(z@#pU5c zW|dTpvT8mQ=+Q0pbuWy?{4_oH9O-QkXlo%(gC9*P(iPc?UfL zR@v_$@2bFjPTL=fU~sL+lyY%#ogSDi9kJh)cOs-IHRk-SC~=`-uVj&)71%K8p_R6~ zPjIOIyd!PsV$1O&zjS7I3*=$A<%*x>S%17}G|9f8`4(lyPZU{&qL~frFkj!<4zcnw zs)j|A)Wurgx$=>F*R_;o=urN!LH==%pa2rMMt^4Gb=PZ)hhqj%biztTpCs8ip!)5Q zqOu+n3<0vz)?Q({d?0vDX65EKq~~$-Y-mh+y=4zNxld?yOBq092~zFeR8LK~ded1(#tpc(7{HDq7_@%?5rmJ*eny znvXwZjWZ$~(PMWuVQo(*pY9ZeNGmBD(C@sNfGv882808(dpt`M!0X*P3o zvntar)tEcK;acUF`!)b(-}o{b>$-bys6L6NMrO=a`-Ig0MreHaf-Bs0!q zDLt_=U^B+9&ZatV*@L@t$#~vPzl5MG-!4>EKbgb@+6B&XsMTRys#Ev}G)ppv=q<~I zmc%raeI3gS9w13e9Kg~PV`AxzTk2*au?;&S`0_xYFR1Se-um|K{s`ZQfIqU{g!<1H zhM?-N`PoBY0p`)A$YRE~1$kW<^4TV*4n=s8dA?HfQ(FbZUAYc|2TM^pTTir_!JR5)4OHF_}3y#s?SwWlShO6 zpIv#m!Tr}xFvKQLXkZbk&qrao#$!<_A7XU=*#d*a7LAux5mPG>t(aN1EW-e3I&WeQcou{tDf^cx3NCAyxe59C-B6fVf( zIn>N!=uLDK$>=XTMmRzGB>Zj>{_`k?mk93^&*1-O%U+;9mF&N9boD3EKJIWd%9ZARTjLB!-J$Fgf>r|Nn9{X6zj zm_*I{R$0pR%*IrdIMY3x)?{k@)P7xLH=M)aGK0jghVzjD(CU_@+{8;bF4DgE#V(pUb3uqnmE}mXdPREyYK@EI~k?DR~Rffw`tq zDC3hn!dxmeu*66`oxa6(QPxPs3-mg7o77RZ4f^iJxjh&>4E&)jKUQ}2@nT=v_A5X- z=9SqV8l?+=)Rt#%s!P&M*Bz5n=vqF9;3JI}lI0>t&`lvVeNjzSS~Zr=7q6YF!qw4P zan62>BH+Hk&pIcTh<`{?^_P}bJs7J?Rvw2d%u*V?353b_#jx^iF0(@ZKKF4fNv~`W zSUcUCl3k|Va7f~We9(kxI1N6z)9SArhRqSE=kMgV_2#U)ik&hRLl3Jn!1|4Iyx!fC zVCoPAnhOmKsgi{P?y1(+MXF@ZHTeTMRDOuszx)$N0Jj=UiB>@&Ed8|K!n@8JTuz8^{I5&Q_`VR>;ahHxX871l?Tg7*`B zazgNmH_3Y3^Jo}AM~Fn9M%C{p)skP@n+_cl_yxBOZE6}#9Rd>l!%@^~S8M>}qDFle zqJf>;LfTKTBt!zv^HBos!X)HJSc0bT`5ayZTY)$KY{lW4Eq1$UP_w%ThC@d zP1|Wr1>qxxSi380JrjX_noO3WHcO*m2P^a^c2>e}(SR#6E?%1Ae4r#>E-wK?d|sShuD>_xsh?N$`FX)>0o+tUm!RAY+|db<-%F&yJV7Vlzrl|T)t*N!kaFSt&4+29Hv zn(%KF&F0o%1NP3HTThH)%sQ&MSC1{R>{tT>}%KRPZq5Tma>a%uwx9j>@?tf zw4t5jvW6CEhHjTIGhjO5kt(&DuJ03mfHU6G(+F<0nz~{n!7-U&&}qgj+b+5Wt0zxm z44Mdd4PDbvHYY{+87L-{R(8e&=gpv$~_=%R^(7QJyc2uV$8U z4&5R=>N;c8(LN4uY2@F!Q|V&fp4bpJ9OYJu&3V=FfJ%i;+^})_{n6pFIC*JXsWqt{ z>2`az&#w6y6$o34%m zaAs#ht|MbC+u?+i7&o}!PHjRgx|zoR*hbWp%#zqMA;F3-=~m=8*4&*eIV}UVWm0zO z%BTJIJI*y4MN2frk2hb$OyQB(skj+_%?BB{2R~uVPy(Q5SSIJVWM^$eB)^d~obMue zfj;~Ug48Qyl|*?n?|(M#5UWGO+66KD`7?8X)RCAIlMTt9=~@86D29cp-;G2({8Z}! zk!W@I2%hoR`xjAdBD%8oFbz;GEe}Z6N*+O~%Ahg`K{2FZkKXSeD2g92A(vndqP*>M6wM!NEy`ioL9p z{X(+hkp`0xvtM7y^{*e9F3Psfx;|_8G~Lpczc}fP!xd*7waOks1QDbcj8r@@2(*sk%M86?GL7S+h0UEV zfqxt@R0taD1A_4^EkJsDyko`Z?(pK~?coX302xMgIqhDdH!0E*of-Dv2cBs_9-7|m z4?Gbamw-eS>4g;|`=-K#6S_V*V5~m5#Sl7RtUgGf8oiuHe?;P#AZm>#xpwu1uHJLd z;7XCa&z>TgW7P9VDEIb`0$)0M26Jh4egKf|YTCOU!i1V34*TJ+By=UpBME9ljY8rwXBxN&(-nh}W%k z_E=-Io~-RwW7`GUbr|5As{pBm zF|`L8Lb2qi?gG1t*II47_^|nJ6Ivr3qt#nr{SN@z1^E6aocmv6Z9!XGJ7DV`xOzy% z!ycHb4@78=|CoSv0y!ck@=1KZy(|qfUVaNEey5u}DD$v0dmo zO|jtkABUJ?PFg8ar$*Naco3O#2M5_+VZCY<$`q;8(Z=Fsi1tYqSnE2;t^q|34aBsH z^A$owXIw?95?xj-ywfyub;wgCXnwd;LspK`0-A;K-3LjQm?u(}L-vlVa`6diucrYQ>sy=$;4JcTyI^6LLb&-((=b+`Xq{9t6Yf$ zq0(K4Z7jS2XZl+<1gkFlcU@FV{~Z|9Wh0!WLn;rh9he3-{6}_axy6G|@1yQ%-9hD8 zJ?`wY0dhgMDyXuB#eqJ&H`)WHk@ZzqN6iP@3vEp(O;OaTmC_2$8kXkWX*W~K`sJ*N zRo702=Bwo`cU9fXLY*T8vJoFzP5;_R-s&HX5$GWKvn$44kIU6IrIQ?QvhUiDztrtO zsChFMACI1XUkVn-x%h;cTtwt!pB3p?7`6&tjsBwILvC4ggdZbV?^1PaFVq@bX2$88 zjV{~W4!)^O2|0@NhdhH2Mu*1(#1SD;7BsmQ;SL};-rk9#tPR&sbmnA$)Z@yq)8wdm zWo%louG-%EqOY*;U|2G{)$hfZ@0v!oT)5SAqvj@4Fl?;}p|q?*mqn;|v=yF2D3_?% zWnQ&)6|Jsw9l7K${%+kltG$*}zad4hL&?!q0{Q)M6&v~e=Twc0(!Cy~!`0zw3&iXP zguMFH_bp?$iiul9jH@!zibwJJkITXEv8;|hY3arBMwo)07-o!d7XrY>-^Uy>&Ahqvmo zT_Boz5shIJ|0p-zZ!H?ee-X*C3tJxw7{29O=&~IherS!sgru-#56hNTo8BrJ$Q-uJjH}s;@ zOtKUEW$421MPWYyKNS{o-Xtvrn08=L?@IdAvC!o~vOIU2 zHc5ghRf>0w=>6;n6>A}(s-i~NDG)?aCL*1bGqUs6u9ML&2_iRwhS|Y)nXPQyf%16> zBa(H2;~cLwO#5kKlX7`WFH`5ga*>R(V$rOigg6}bn4&F}HT?lKewJ7Ovc%Oszrs*u z$WmETCvtV~qK_4lD^_^Sg2Li`SbfVw=^`1Lu|)6YBU=~dcTG1JB!)Q19AeZ~_R>V5 z8;DFYC?AhJkdJt$f$dky&S+X84>bD>CQ0dGdCK6Ck!1?R?;m{MdS9YEkfiu$Zi0MI zj1VE6B+yME?`k~g>rxFj1XC(GX$Xg!2S#s~^)Yij)PAxyF)eoo9N2gKWF33{x2L+` zG$y7LSntj%<}A2%sn@Tksz`)}5w2CR@#?HvBf< zZrFdWxf^ab`E!F`Vm{6GcZ64jgCRc}V{`{%gw(iymE>W$PWC7LHU<(8CeHZ*@JIdX zG_m38igN1y~^h-rx53$D0hbDnN2TLS2+`8$J7=HW0F&tO2;bqHecMk`J0k8nJQ7@UeBvV%9e594zqF#?%qBa>@&%Cyh? zTdRppQ|^O3+Qm*sH_At;@jEkF8Qtxn~ncQ{ejYkZ;X(+3|!)dewGzN2dHK8X`ofEp2c$hZv)> ze0gJyzimPDyv61=bbI^ZsiFQZ!5y>R;@J$9Es9OZ5@KC7ws{*rF(B9?wY@S!ZduQB z0IPD2wD{FsF3hccJNjqP`?d9|XW4iXw^+X1t<_KqSj!m|OcJdQW;9Tq&isC_Pk!+Z zx}fRXPLTntuiN_Yo$)yB7;9W|FpB+SLGO?4gR4+2IH!g_PBfW|>Hqk&#w@@UHCAw? z`~sHBAmM4p#p>k5Lem?HP&l27K~X3a3W*sCnJN_CBSe`Mh9VURm9G#%G9KVQA^Cw- zy9E-8e_iR9Zb0U+lr;c(&>Euc9i~XI4O9;c|F|^PSynVl=TW3A(#USTLIh5Y! z!h$a*B5Ns_sdCEPv?wa_8w&pVCdG#m6l<|`m`!jm(RWDE5I)5?@`v*F=1ah$Ormr= zeB$J`S==yfL1tGK#4UP{7aG(ok8(#CES)IeKA{KIQ+}7xR^hOvTZcnx4fawApQuy* z9pPjttnLz%2k;hkk3Gve>mu^`kFdGFuzlsKt&AM}%a<^O|LmoZG6YU8luXTlTRMP? zl>WlL{{ruo8fQ+(e>EG-{7-mzD{ifnXMIArsd&Z0#kqQ-Y!~?Ht(y~ z*v-FM514*ALoEwMWkZdiHPslP?QO^2QECCbBddln%*u^{>-3Zj4!xC~T4} zg`_IXt9Jd&5=kNIU)`l-yA0C=k4YIrMtx<~A!bJu7jSkJnSTCWGL!|+;g=riB6dqVre3PIc$FzHU@ z^&?5t{S~S|ssU5|(c%p>`L1bZbVJ&re+`Mq!ayTAviS82K9WF$C7S|d^m3f?F% zD0-PB50|?%Z+e#7J(aMdru4p`u|G|!4d~I}%SxUO)SI;^)LYh(mPrkAdJ^aj<&d95 zKeHv`OSop_%TpJyWC!bO1IVUS0bo~Dl<12-^m=vM7}`YbNJ9P$ymAC~AV?wR&Mqh# z&E?%G|Efxv&RvQFJZk)el_lL8nrt^5orVm>jgNmbj`w3n?HKZwqAEPS=d%h=0>oE} zD(PUK&^GB_uB)WqE^F;;x#^LBtw|5p*8C#imi9eBY0ym9cFO5@W{)XNF)K56I!eOh zbX|4)&fSTzgz_A)EF}N3KtKptvKyM=3#M}8bfLht+R&Wtjz8lmpf}L|Z9+7I9RP*L zGIy>H&;wjV%Pw_Sbvu1cp70}bhcOke5`1c{eWvP2KR|nQ98-P5`Vmx)K%eM}sb!%f z-$dvKRsDwAd%L7TtuIpk2G=yYWfb~`t0ifsJ%|lJ;m4_p_HAbf-Q#q}i7{6^wC92M zZxC4VyqvZ}s}93LgpXXN+eJi-1zA`YTtfw6@{?;~QPR(kKXT8hcnNE|!FiA77kvAM zDEFOZ$~D;rT@CuIOQ6N0aC-5w@`i0gpcTR#)b(a^t94xn1#JI{vKgOXB(+AgYjR1zh{IEaLSt^>%ACgXq{Ye-h*#EerPgl?iaE2H1WVYF_R9FWR(8Qas&HPEhJ}Y()5)xk)dG*b z2l&@#tkZ+D<9IuPR6DK{Rdb^;GSqGk*L?a+6@;8m1$v{Y-^;?%vBvy{{mNJbq*aG@ zRvrGRc>6jxT0uN#Oc%L*W3lAZ@~;qKsaX-*)ZhAXN%ROjA?1;ddVaN6;ZDf|WeH4M zcl42-c$0T;&R?fO?|5QSZgsM+o|{E^`4f+xS3q)NC+^ONoH%l5f7!&~Wj`ypgO}3@ zpwJ0Xydvg|#Td~A)laJ5^%7qI%CRHrc_Sdg9^wXEoLd%OM@w^q6p!{k86fuvvADxr zXT9n5hWG%C0AZ0>1YRj;){Ea1NuU2l5O#@4J5Yh-pb#kXVfmlHr;4M;|J!c{zCKQN zE{?|kO%s&0?fxH`ty)v$ywzEu7JZ=-m{l7YUz#&ixFtiWQD%3@{fNo3oWU^D+WDl~ z+3(wteu%#_b9b}C5V2JQ4}-~f?;~z=zmJ!XZ{5`Mk;8l8(Jt87u)!fJO6-_TWfv59 zErQM@Aw04Y7_r33okL70wQDgTRi*_UTCqK=g&ZM;O)5tGxR0XecfJxSrpRay?ljSS zj9Q))$k?bM3HZ6LtV>2TNn|eal!#X$Jdz^4HPeWaNrS+5k0;iwKUpRIH756Z)H!@q zGemr42b8dHy`G_hOsH7}K`dM)@b36@;aR{W3dJ2E%-CiRvu_o+ywOmeOi7%vFLcF} z{9}}~Rj-m4aA-Tc(9L@sEAq=S!04?0L}m|RXbLl{3WuUJMgl!Zivhey_A z_Y!-6AfK{b^LO(sPLi=}jg1&ZPcLi8#b-Mjwra>97^k{B1aa60eM_e*a5gL0>gr*_ zzR{?{>0rJ242-UVWL2@4F6X;3WJwX^V=2isLa&%jA)hnsW%UF{;2LLNnyw*`5w}l~ z?w;rhp|8>{P-cwly-WgE&;$DpvEFD0mAk%bM_(7olJU%5^3OjRrS5GNOb|_VikERD zBU1v`miw~Zr$fwq>>w&Cl zv2k@F)wS}Yr0Eq_SONslk<~fH=XG^=v6lXPw6*06VUMXVtr6zGBL_Pa7md_RVMrM= z{cu>N;mCMTv=zYujg(?+A<7njDtHxcs!H=n?2gcLgwz$bBh8a^EzzqETgepp*;&3A$n5udb{PL#0S)3R_1%K~G=GN%+GMvRYHM{x}Xl zlmC#7-9^t`ZwF~nBB>lb-e-`dl~;QfCp2+jJHWKM*tvX|y`d~|bzY{gLFsN)xw5*4 zVFF=FRd$KuF6w#SKB^HZmrZtTK_{?=8J3-Ew$d?upR!(y%Cbn!^Abajiki(tVzTWO z^vfD^4l@oRtr8uc0s}J8^qC0j-;;+LvQ5)!Vxi9a;*V9j z@^3_)r8>;{_p67g*N0k3cv|aOTuQ1dPb&wT#51}L65fp)4o8bn*dgp6Qe3_P)%c4m zB7~`QCFLiQw^+)#;m4wIBs^wehtV#|PUUQzP2`SCkArzti!d31K(a;1?i2W-YCBXLU3}*dO2Z;X^^n+LJFhNmFoiJ? zrIiLfV43dEH`zLw{mX2$o1&PHFx4iJoMJ~4BKGkrVarj&> zOO!AaeAl$as`dePC_A!8n>`ZiUKOJ88QvRHt+vu(ek4OL`0wm)Z|beC>G^`q$-Z>8 z2i~u_#dg3$!db_2hfG8^ssTc0D!DfIWbHiOe%{X%*c(AWecIh<$>LJ3nrGl!cUPgZVX2ac z(zREXXQWzL;QB&SFfq|6ZG&Cj9x8Z{P>Eb#y(HoMLL*FjWL2mmcBd3L^C*w%v6}8@ zi7#Y6-oAjHkyfwGE>oi!&fR^9PkURl{XLkVSFpjhIcVmZ(nD6zG|`rUGs6|GUx<@P zBFSZ;RBmD3FLMaWQvkIHKa}`MaYGmsV92sMIREn>%Ow61L>W{=DR1C=O#m#Wod2Pa zDi}HfCH*9_rp^|2CjS@-{L6s5^PR^h=2ni4yBG_e;9zFlEH!12vF@Vhtroi|L5;h8U>s9bqN&wkR|VF-B4yLmXOk2|9Dj(Wp&B z!XMS_cX92si~4-SYeJQy6irbi6wszAb+n&ZC#o=(r8fOQXuZb*kcCuRIgmUbE#*yhNl^yp_4c zvEM?Yu%TEvFi*LDhY;qqU9G~kW{leE0i^N$I^XUu!t94@zr6i*qD)Fjic>2)XGwnL zx{tPgxMC)M-y9ZetPT;UAcO5Q$glnCc`#lMk}(2e*xpyNZU)Ik<=Nd5+M4R)_K^Evnq3JdDsQEf-D1YM7=2Ow zv^=u?V7o9KVYC|`-5dB5-S+Ta;_(pQ3~Wh7Lrf&zpLI1HPQk|mmG?U0Z9?~9Tsgsv zPGLKoBiKY}tVD8`5c^s}@l^=zB0MZAoGdTG!jVCP0{7&aN|8;qWX;od^F*|;7^w#6 zpD(gu3A1eA1-delR}Au<5;oW9Y49%4#D@9}8}8vd=llBqrxHo!P@D@B=gXG@VB8Ji ze>l?0#)h`G|83%&@bvOdTA2A9=gB-Dxg}ZQ39=6*MS#r24kJjlhm!nhA;AD;fDoN& zl2~Ewx_228`6#`hhDg7xRa2=!e}5sZEkLMIw%BEJv9PGuP}!-a@3Z!@s8w#`qv*?g z+ z_LigS5l{RG);StH<7L2X=XL+KNpWZAxHo~97Sb$nL@Z>B>X;d~#>)Z0e>xZ|dGwvo z&GQ$ATiXpCe9|r*Q!&x7n8h`wPT6_2EDvz5{>V zCre8ou_rvG7Wo|bv0?L#r?6h}!p}5_vTFy-b^2L;^8GDf2{8L?@BIe-n;^x<0mT;z z9(s*%hl55L__@ScF2HpO4+IJxa!zrS z>LELduW}J@^|dzi@5*a#=-Zl)j_rHs-`BA&7uHk||Nh|#9_wae`xj!Vk z-IaIzBF#T&9=Z+&f4AJ&1pBEz#q|EB^j6>Tg8r9#GbyM5`rLTSNgsM2%6Sm09#= zKt+*JZ=Xm+6H#RqSybcYXIMmo@iOF?dF;_qDKknfQUFGx^{CIN$rMzoDn%taiopQR zh(jta`57M3BkGK53x5C%3av_m#Eg-sG>R&fR+&XQ;2=VcqD-klNwE=tCE6IFMyXY4 z5e?uHMW@Q&? zOt6kGlIEc1!0I9kDQa%oUee@>3o@{VIG77?H}#`f$Be4=T|k9~zy|58@HA5PBSAcc zTNG^y+0A>fuc1N;Cj$vb&<<13!jE=QcNXCe z8Fr*Z5xicF5T1!BGU#4Bf^!iE)NT*vC=e~LqaPb|wuDcFfi{zud4VuqE)wobxS9F^ z7?Bzv#9jJN+ZVxzv#oiE)Ay_3MFZg8J1+#yM&!S%iTAyC#_!wP619RBWKD7qo1c>MR)Art7hM=+N1(=*1p`?>oJ#X>o z1p$a`GOsk%9H_A69Q)^gq^=nigtr*F`rO6Q_>A2*tZd5S1S@iJ1B`xPvBqz?|CjEOovYmi&M*#+Q+ zOwOyG#ZKAVz#MLmVg&J7#>oo0i?fyYl%hhn&yB4?6oxx!$91|QTq}^~rb?lQd=B{s zyq7iaQb7nujXkr`Q|N}Z=VCGqGlJcfM2Ti4?`c#aD8K*YDAGV2P_QT0*0S z=n`z?KyKOo63|1tTAkHNIczs3t~2){ArZ1D9zfGdpC%4r$TE2<+tCoz?GYT#c)Y+TO0|?7?7E{A*0sN_aDT;D{xX zIXtt9fl|P`G|fj&`5ecjsBeg_uGNG1hr8_QFS*2kfPm!5Rz|Y;qawP*w$h=XPk%R? zPIH)Y!7O1iZ%A_4ag?WZp%xA%2mj*?4_l}q5z@Q5p#9n8^OZSUHN#~iBV@Krl)=Id z`^`ernX0TiXYk%JQOZ)8fIxt`9tf3y8Yr}zwE<8= zh^ISq`^*=_&ipUN-ZH3;D9qL-!QI_GxVyW%YjBr?I|MrfcZUGM-QC^Y;h+Zz!QI1U zzM1=DroO4VRbAE9fBHw)-o5tT@4MDREPi2;cS}u)hT>5&-lWEGkim3^jfxV@;?HhR z8$ob+)x2Sgh`(Gbr0a4?P#Z&bd{fpRl{?_tXETMuSF{}%4|z` z*er(0vh+37P*qhAejse)JYXKqkD)z^%Cp&lz_E8|$T!m@-*#cr63bVSc=O`n?pN8VZ4C0uEQGy(MxZf(q>bk0MlaEoZ%$!}Yp#Osxi(4Rq|b zfjDfaXAJdDpGA?aTKxX$48j&zxT4v+8YtD{+iu{AHL>9lX;s&jIV z2uqE7Jv5bIXo|^Rp>Pyu(xh-ffle~9FzTBgz=kYixtwWO9o{lw$$-2*y6)GHLfVW$4m;b;`|(v5Mx`B9r9kTsh&@O+Fr5A7)CV1-}DuC zrN87p34CcA>Zj%X9sTTu{80nmDSqpW338=mJp9i0q0Ootjq4W50d*q;!|==#8?<`s z%=dw#P_m2Ll)_i0KE_VI7&etNexN_|M^l}`-%>D`@SBRr2mRA1;}{Xq+z%;=-|CWe zlIEGJ^yp%i;&0;gO@`#F2Gz3ku6`T0GZ?Gy!?rQ-qF z`?BTgAQ~kalL#(3mK9HCI>gvbMFG$ zLs_o7J;$4vaSlc00fJLRC;tT0(Nh=uJ*h&N4CmJ$%Ttdv<53xUBUV9H)~r*_ZstXv zouIeqS^rvD(;5i@JVkyJQ?TZKERB3{xL|;>nC!G!0b1+%6@zW7j^)SDXjr=tyWi5S|WiA>o+_~ON$?p(ppmQ zW0(Dn#%!HzlUTCo)@~|RHMz93+?)YZ)gls{o{`wglup@GqTfmLMByxto91Ce5|s($ zOkPeqIJ#%5?QP19a-Hn@D_%w_aP;OoqysZsfT~3tZAL3(YFgH`3C;c9hNK2cABbM1 z(+-J{azRC@cvmPLCGS6EC?nEl!v<(5amv~9WLv#57?tL_7|!#RPj z9#whF$C-OE*8Bmjx0*8?3L>D+;|hQd&rG#2sk|PscvO&b$U8{Cdl52qjX!&P!+B?JFlvp|*EZ~A-^FFx^@^ux zST$mC0nKGh94=QpYU+CG%>|)hIBN1&AAhp!F`dYig(Sjuzi)t&bxY(KZt5!Dg*f=P z*k$$C{=?HQR3)Gj^#S2>4#D>y6i?(S1JSIgfk>5mv_uIk*UHcM4#D61wXO`-5PCzi z_q5uG15v#`flv+8T?^`EQX-J4{T%7n!@m{^24X@dP@943FG_uc93ZgVts)8W-<|^;_5pqYi`Pebg$LI zaLBOv-R)YiBxJ~Jh53c!9W%afs{d+i>7hF_&4pd#!Fpd%o5FhVnK$qG)*Y+xSK?j} zhCggXH;O4ZF)Jb^DbyPg5HvlCnY0F!Z)JEo5y#ws&M>`QP;o~kJ%e(cFIbN&=B{usIg72p)(ylm@bwvOtY;!=VaHkiva71! zbM@hc(xo_8E?~DrOO3!wrNVJ00ZxPlIuAoIZb3di$%$-r$&(mnZP)Yp2imVgFzz50 za-AA0+f#UcjEj%}v8N7K4Y{O?A%nJE%Iuo8U*g5@A(jLgrW9x|f?blkuwNxVnT2x3 z9}>G<^pAtzoGwCw&cqgoKZS7M;|!#EL7WtS&|ID%@S;Fq>_A z`Hf58H$wgT2O&X0$E=0}!0CmU=qA&~BtUCPfA{>+`f1ljioXXQ^WmYhr*=?g=!9T( z>}YP|ME_&igXnXBOhK$)me=KY`mhYe(q40ota;;C<>PNn2E?EvAGzgnsBu+9bsJ8LIVI@lDeomuzmo$Hwho$P zB5cu}-ScuqI4@(P971G$c<6@MpFt*i3g8>H{G-S?p<}&K;+o;@{}IdAZ#!};+_nSD?plqRFniL+>9zxF z-e5oS&0g~X%d3XXBg9k-X%gLIoG7tfD-M1JTPg682%hp~ZU!OO&$I%itR(T40eonO+Urdf4*2u~kCRpvaM=R7BjSoMd?u%eWlgDjlU9>q45_!R- z+$Go&HePam%iVuC_syTiX{WUPvrPGC(*;O$v-HC zCx{Oq?KKpBiWoCHU9xm?1P#C*#&XerqCkJ5*tg-2*+eM~fkd_~BeX4%qj?RX4aSzV z#vha{Y1FG2SX&$+`NoszgVI`6Iw6_EJFbfS%XuQ-(4z$d&UocHe@L5xz2DR$Q2JI= z`G+G873^yxQ)!Zq^n1=-7~M5oAGJC?M@VV>Ii#ujA*3WfA#3JQjNFg3pcE!JG09l+g;vS zPVppT3WLV3p?D-(WS#D!cw}_)!{YImx(EqhU_&MqQ5?ie9z|I`b|#l zu9P(wF?7}bs!Vy8wg@*3HHf7l_s=<0L!D{w_PgD>3=p(jt1=03xP2DkKEV9wn{?@n zHzkvOKg_Oh`600q``~d|G7)KL!W$ZRY89Y6o$ge8ajggD=Ui)W${cf3-w;Vk36%A6 z7CM~qCTA@c$p7If&b9^p$-N%~|KC#;!@mi^2+_2;K4hoIl>v z=Z9#W$K4I&ha)YeRS(ypj2ZCwmKW!nAG0__kGw-^+kX*W71DK<^M=Wa(>T&|skt7S zUP03nxl#OaKQpRW9_Y3XQx0-Nee!v4b>ttY_SM5pry@;FO-1S({bgynt_oqk=V=}J z;zucjyne?RQ(Qa1>&tw9Xi9Woium1YxNqGSrX#rF6a;vs^K=B9*|`ck#xT3gq-U?S zC7S;UZ@MHZ)Hr;$=`opvwWO82)!t<_N-tt<+v?BHS-tRJy&^&DCQKIfmVp@viDadN zmi#jL#Wyn88=vuoPx1HnJ_}!J%k2YB)`^JVc>q0Lb=x=_ARY5L+97*LV;rK35_?_Xx$UE8$Y3Go!2R@;v)xjg*Ji#yyj7omE}sy@Svj=OXv?I=ah8bP_mnE8JoVJ;-T z{Z8IJPEobPvjwJ>`Wwcjk8{rf*~F9GlKZ#HI#L3A`V&KkmM`%*34Hr4yM=7f*Ak{yOY&4gFv$*?Ow-Pt+i8(5QO* z_7~8o8tCEWRyo;G+RioD(b~?n*iqZQ?0D|vq_y%sUA?V%3Rc`&q0OK_{ZgX0LYwJZ zC8Q{$=a-qa5~|o(t->~4FFeh;`huiUdylp#Xius!>6>w4bK0nQTP>3jS5IQ8Zz43} zUUkpA=&}-600Wccd}MwFg780wFI#P!ZgzP^?XQ*qymst9AorhVhiu*wv^v{@da7Ms<3`Rq z30f~dA8K9v#J~@bv74w|PAqnyz#^B|`jK;5g4R4cknuuWca_U)=E(UmLF;!Lknw!m zyE^!RA$GHt%c=MCK^y!a5W87P(7Iy}GG1(ZFC97eh}|S4X#H4dd;ff`OzdVJr<1cC z=$`@j!8&%+fS|S42DH7>_RbZ)X7zj)8~UMY&IuN$9HP{bB{JLnpNSDc@AH|VpPBU8 zBoGk)0lNSH>p9YNwzqWpPxic=BjCSWkg2-x-kQt!LS9x$nMkNfNODvOW8csR6vai+ z(2{;aC8VIS=3_PeNf1o#A6LwQ7IR$Iw%@YDO^<5U=P|6?7YEoj+Yweax2$Xg)GyaC zZ#6|roj+gt-LfmvK=u0jTyDKyyj}*)^ltlHyvvZnvf^Go&BvU|*TH?i0sEe%Od?a; z45urw67#$y_Ujy+(2zPkPy%Zw77YB`mLoPT&d&qmE)&L$Zrn=+w+@H50u`Hvwka1o z4;TFoPwZXWRhaz_&fjnDe&C|Ms70Upd&^$<1>mK;3i~|Qp~gRz_Z1oo9*y!nbr|-L zr;jR>fiNkavm18hNPpez**+f`82cMQAw6HkkTzZu2fNgsJyxO4#twR2ccJdw?m@Hk zT#lk%{l*o!+oxs;EHY^t1zR;R;m8$FrG)S-6DPulZ^TJ9w3Z9Zn?FdgmzPv0dUO7= z(YNk4uG+nnb8n@7d7bvwC!ee5YG7KxiXQUIUMWtV_h!*iJee3;oDXai>S4-?a*E6x z9R)|aQxPi-wQCFp-*nPN-6~xx#;8-&6PMdmlxx7eB@B$9QcXL~OVTt8yOiLUUWNQU zNjEgY;At&APc6Rb;!HNi*(S+|G*$8GiX~C_Kru1dfGI(!1B_Vi9f~~Aef}%4GS(3tLB)zCSU8(R> z>xE9_9;L=N)e0eLZ>@@YyMxH2v15ZRe$y`I4jUSX+~&t%7gu|LSBojPtZU+s);eF( zoiDjzIaOJVwE>G~2-qpwefv=_J}`>6imr^U$`L-{6EBJ1r>IP+%N6bV&Xf^@weCx? zbeEKS5T_W3^fJtlZQtX*XiVnuj}V00f;^D;G|72aD8y;sCDn2`H8OR5vBsJ@XF*Qn z5BsAQoHQ{j@p~cr=w{HSnH%4tST=AXEyt<^HX4mgFET8!e$OQzF|Y)R7Y~^j5&TdP z-@tA3(u6yFo%yrjbFD#>Xwx+c>N!ICO>Z$bK%=cTF6>)%t7E0T?jH%L>yl<>-Ki#9b zhXeGvoEDr*)Dw+`Q`Z@C*R*uC$8bm2N|;olo+ei$Qx@Wc`fBW($`RI^6DHd+61#r1 z^p@TkpU>4S;e`K1-(jAXG#BE3f63}J_ssHGMTGt55vka)X^4(ZLVuIA$B1r`kEuhb z8v^cr$_&l}=c7 z1=|fxb~!T*$emwBO(J5&zk)4+!K+t(%$ev@N699fzvMZ>LL)gs`~%pOxd8ag$NSol zL47C<{B~7%Plb#h$q851?Qc@NKE%#&Zq7+=RxV8G3n4HyeW_L1q%%R1d#xFVGK;O4 zL0nhpRr@GP#&6SoJ1+3=oyNW)nRl5lZc`Q%cpTvFlO7YQ%$T#?vnGje5yo$WeInP~ zl$1NCx@040eMx?B220ZgA^P5&^GiK6v^NtsN@HgJb1}uN?{forMss5NNPh?YSGNgO zsHTwv=9}7`e)LR|RqjxcgPUI=KS9MF)-sD*0gNvmDLUz?I@aTrlWJ2Q2+8FzML-7jVlVW=hS1y^dhAhCa@dZ<>Wn4PCIakI}E+Q{8XMXf5y*=HEsZf5NWPoq`0l-*l7fsN2`Dji8 zfIP5x4G}efTHuYgt>@(y2>l`TJNsk#A!wSJ(6uE!~i}fp*i4F5X zO`;hW?5Vc#%ZLl1*n*+C4`Ltn8v?dDQ)U{lstnH*p5Jz(h*$`-@F zAuBxb0sn0vFswV{BI}uX$7yNJNRZfR{|{D<9oS&JbC*MM9Hm;rJAQBlD)y9Wf*(Kd zf@3Pn^ioT*F~}jTK6Q!P4>s9r-igNRn;#5VoKxx9I7R43pEA_h_$aFjSXfh#d{77l zf8`*W8){maRFKes3U}dXTGvXNp%Xka%FdTsYh1yLEfkTor$)D-bbdBL;vrd)hI{SG3Y2yzSM9A5W z$dws9zxfg2`l$h_monJqL16<1j^QlF+873ivQo(9x?5xxXBG*wa?60g7FZi_4D`aRCmqk9Hl}7zbac zOvXm#Br<%cqoAlU+a&gii&O_1;c!iV06kq+Zx8Fl115aF%H1Hwmo#J5JhElB&^G=J z>M$ev^ObHBx@Hg7tTc_Wb9ir)?UZe5+vZm6bQYe7X@&NdPZF;EmGu!mb7nS9MSZZ8 zl|+Su6y=atB097vTvjpGq9At21sMKJ!Z4Gdgs7t4184xL=?F-8#BmUT65J377NVMu z>0Y46oC^=V6M_Elv%_H zV}gAn*}n-EY*2EDmY%*Fkra&LfeGp2AtKAlE=9j(-f_$$L zP{0MA^zk_ySM;6HtE|xxJ?(k5%C!X^sQmfn#cCt)|E0Vau*E3CFh*>5<8+DOGPY1!oZL*f+sg1+k ziv|X;e@j+cb39mH?|20%rN5grdAYO97j?PFv?y1@kWNt0e$yxQhtU>m7{2EccDWEV z{2Y23S7Qq3nxLDD394-GDSNgol)ivbM1F}^Q4^-5>^h@_Cf_*qw;D8{DeI?H#n;ht zc}9(7S#giJavi<|o%Q98pFS67h8mZnk`z@n{19eNU|OqL=`+HX^;LZ<)J>~&U**P~ zFgbI4BkedxcP?yWY^rTaRypFx{4D7%k0AkKB|DmW^ikusbDn_={F@4`~G=X+;Eem35(-+Z(cB`LAW$~U~W{dCnzJ5mlJ>DySR6wiZ`PM8h%3nfcS`5F@pPVT?~Jd zS_F3C9{@R1!g4=je&_)0HQnZ_{`pFcEzjWo0!j0qu%`3?A5|25v?j` zadKFYRBxAiZ8Sock5D zv(mTCrUfW8Q{CH?uQPTT&V6V3FyE@^8&c;6$jy#-vPWsOjH9vi4U$4M>|a@KSjYu^ zzjH(p3jHkT2o;Qa-2|TfVr+HtBy`jE)39 z;p(a4)$-=S8%I~{%MUdDt5Yk_^wl~X!lm~Whp)3>L{Fz|i1^i468Y|w8|KL(G>!cR zxx|Nq=rb24{UcjnpCWihf}#D~2evNWx({i$P|G%hNDz^WE%T(*b`(x?ge*}@&AuEaxXvMikP{LF1 z!NF5O$-<#sBtbA?{juVpg3SIj#KIRbVH+^D7~5zy(d%PZN42DiGi*dv|E~V6HSfD) z-F@k+)!Ry#mbuR3Y|X9^=)becbNT+^+~ac@u=8m3E8yfU83ISG4Z`v%9qof8&h^cj z*LWx>+*|JFM?D#wnD_xao8(V_av%bm;?Eywt8i*$Lsxs>3em-F#IX?6sD@<8qr$(T zo28%?hT~BV>5`AYyHulr!!yW+B*|&uU2@Tq!!sy`G|5Zg*@Q$nzH)MiHo}Dm^aE^p z2@rr@Q06WJW3jLy0sRx(Vdym=tg~`TBn5Z;>jnCOnAmw;Ys+sN1zym8;8fTUbV`hB z5D6QG()m}%8IhjTr$854!9p1U^MB-|rnbBHi<6vA2~#>zX@_I_GGZwHQJR+(L_bBQ{#~CY zOWH^sV?!nC2B)#)2zPqO)A`G}R&%Sh=F<)?5kekX=RWr{EJV97x%P474r2Py+JTLD z?;b0_QmgVPtk@TQu%~fuDDOX|hu=<=HFXC$S-?ZI~Mse=>2*BEl5x zpq8q<#>iFc8Xwh%jq_u7nXb{pdsOH-IWjgXCNODSkOTv4uKfA*q^GxpA_J4vj;`4o zR?5>8o2#d&I(+fhA(kd(r;irNz#1@oAk0Ou%XW*RcKfes>UL4x0s;ps*vNn*KS(mr zQT#$_;s{M_)5zQ)apk=#FCO8Q*Dd_;gT8pTS=GZzLwjRysOsLA5WpI&xpGUUBfp>O zEvWp!aYVa(+sYx<(lfHW6<;94H+y{Gc#&QsS=;vfd{0$@gx)2AcmA2*G}i2F=G}r^ z-7rjcIaHhyl$mULV6o_Q8YP9AS+e$ZyQ$jauhQm*Hg`Y9=N%@0yJ`9J10_X`B;SwA zZZ~x)!RU|8w{^tw6X-mq;<1Vl{r$->c+Hsb*L!o*+ds?On|}Mq{nF$I4myw~It;qP zTZc1oEqnuhvBAT9;%Jb(TLz>lC>jmFWnAvR?do!qrX!=IQI^Fzpea_S7Bjx2+R`z! zlyP3eLoUoql85t6?)lkf$|8h1ha=k_u9@E1-j446+#!sQxf2s3)M7%Z4KtPtR>Fuz zW9cVgR6Cm*RNtKTT>4I-tvxK@8N+z3j5CqLn3VQT)3|g)!KPJ)8(Ci95Qz6z%Iu=r zmwm z-u-lL+L{t3D`8-nwT^j>S|@>oe0B!YX9XF;L=Q3#{nmygk9(bL=M791vzz&eUg7mt}@s_{Zk-m^h3J>`Qkz- z3V&f3aw>~f|60=-_Tad7eoi1MF=@-@DM*_u;ln7Lgf%Gh_sqo1t&Ta4LwQ zXu{)@>1%(y92$!b+8idQY5ZCiBAXfN`JsFGZ))Sa$HrxfLpy0NW30yq9JO9tvm1DK zn^0GbXnWS0WT=fNC%N~|#5dxz@nd9iDxoTOl*XA^XoPqGR)(+BkG`=!#WG)_9IHe) z;$mWh9qatUN`=Dx1`#+Btb)HVYF`f7vtr}SSX9e4Wd9H278mt)Se7@_J}&IDGd12K z`PZK)PbiyI>DZ+zQ(=0pslj`K3Y%DId)sblLYqcu1Di@|o;mJV{c@$k^CpSHT=zn0 zo_Xe2&&6qi^MY=I^Fjko1ocj7^1pMkrSAl0f0k9F`H^K@(t){a#ik7UTumm0KxtNI z5vNrE8ai_Nbr%rarZVUwSg?mQW83zll*@34gFt!z3opo%g=DYu=Sr2 z*Vk8tBOhaKT4i&?OPGpltFCkd&Y!X>9_bIQ>@%oBo2QXWb!kT3Zkm)vmRA9uErsPL+M@k6+DD4m+}T;B~99r#n( zspU#g@(^ab?#b`zdEPDEHaO7dL`OD58C~_7$&=T&sfsT-T^p}n(I~SBP#l5Pi3Vqe{ zdnwD=4NZ3iD5D4we%qRIit1|-2Byg3s7h_X`Sq}+f`pdC`1|1Abelp;3~2KHqM53_ z9<&!PbSfsjvZts%v>kS>yfiVlcC?ox0R!91N!gj+$C*+<6oznsjT;SxU^2aqmGJgG z9NOQ5JUuHRg1ky5O(zFBbygs{>t>Yo0KH~nkRolcja-^}$c#B2%>OUgZV6&M=?B$6 zuzOT3e4q5nZ!I;c=fW{RqGy5dz%`(b;{by^7`uRE^4V9S;hvt^h5P7-fM!(Z&=)V{ zl@ga#mq-mEl{PMvw(L5-*v@c;92wt%YL7n0!3WL@6lRyCzwln=kQYv%oRO_T>KR?3 zJ!ZEdM-?!zZK%AK7Q_z=V#c!tK^pGofH0l9A)T;nGa-oJXjc7PE>l~K=j6{OG^Akn>nxV9U;lyf~t~Oc01Th zUUqx;Wzc;+NgPl!oKtnwi9 z!Hbub6`ncd;4LMax1c&Dk|h}gVJ!QRN=fj~n#8?o3MrD6ZrBb! zfD>jQe0EEWZv}VP*_p5qX&bws6rPQh<4I#$*2s~6?|&r$ zFYUjw=gb?s#O=ef=dc;OcC zS%>K4gg#{=S^w6_nS5$TvQE;;0Y8N!9rM@n9$zcq&sfy*T3<2X&tTQ^;$GR~&uD($ zuS1ZmvvhKjpJI`$J9Kg|pQ@3pfjT)FPq|3eB|16XPu)nROAIq?Z(=}>NroBfH#H!~ zCWA8UKdQk@F!nL$KgPk#Q}_tW3CMTsAceoZVA64Wmi;{4fnr?N%;c-3-eOL|N!Bk6 zy0wyDvUF>Yc24!2O#&cK9jlxz0x(Yls+>)$8@Ffqr#iAday&xrtE-&N-w{^48Unda z{d9X1@3VD#WA8Whdb98Sbc24}!|Da4+>h#tOuR6jsxHn?d^4}Ng zy#MJ&EY@^X`OK6KY#|b=)uj6tLJ^|c#4>hG5rxwa%Sr7+W^BJm>|{Xy z00v3MvP6=;efjqd>|g+wB5r$D+$zw>F+0T}z!UWE-wUz{i5}#ZtQ4B`hUixfbYGz* zw(7wpwn;P;&v4Q_Q@q)BT#U!CBtm&5>OnN4^o0j);)_@A$Ye)}VQXBv z_F%}pXtGnEH$W~5wAYZ#<|5lHn*9oURg=|K7U`9DqX$jqCS~^tK#v=7)2{_vwF?aw z=>$a;56Dx&j)v`LRACtDrY+C28w{XE&*hJ`Y)?fvQPT7H#_EL#uxG z)tq2X{a9RoVlw9@n!##>2b2?o)Yz(}Co^KV`DhaLv~#$(Dy-L8!vBu+Br|bWUzI1W z79FJm9peB_{P1RG$Q`B4Up!v-D{ z6DD6&PJ3`DdTA>G>pek{-s?1SFJIEd%D0DE@PIRvqgNH7&JSai9;IH?OYTueh{KPS z;cr%K*(?>AO9QS?A%6K5pAJ0J7O*CpbR$0!3^5~)2C)=nSM)#hy993ePRp=ADO!zhP&UWW#?D~g{7pB?j?0Na_ZU}I_?2Z;y!*~r39qs zWjSPzC6GG}n%F@jaF*q0c{~jq>N|Z4BKcf{Jpy0si3SPUqSg3GE5q|o+~|J*uLFeLga zC#%rb)UdXeJ&_boReD&x;bt>=LK*-;61Au|?qOz5hp}>9!M*XzDtmu$OC3Up92cD@Z^Y$t8mAtnp7Pc~|aPa!riu^!ykOJi~4 zt1BD~B~-&li<~w61+CAW(&*9HwG?D$cTgU9tY#!p;4_c(A6v;T%p2_?IM2J>trFI5 z8up_i*O?|`#3#!KA= ze>-e+J62u3c^57?Y5>B7}NiXVK-R}p*OC>LbRIj$wsZ)6-Y8o_gQq2D>Db}N7w z%(fZQ#)En}ivW!8s=tOS%9>tati^)mp!tpzS##pAiJ+N)ZmDN53=SZC^rSO z5L>Km3Z&3(sq#|6O6<5o*=BC9yYB%{`*8a*F?PZ9&rh(I1Nl#lT^-Y`nL7mkT1qLB zV)rE1-0+oUW*nCS=J=vB5cwE zJGIX_^CYUjH~wQ*{Ks+PZLlU!(wDpl2*htMq7~BPKUlAgI%}MD6ROnjrb|wUVAh_5bPZzYH^|Zu?YE;Aa5b|C4%haQ@f7LeuC| zJ+Xuq)0_|wH6+on!+tX{F(L#GVGxMR4`C^8Y+UURHsm%*v@}i!=D#ucv(gAZLB6St z2y9YFRWfng9?#@1x?KuA&iH>^-xK~&>iS6;l4^>yr-l^IRm54vF4+wPBoiVjNNtv% zmKcGN2fgk!BoJ&*FN>}o&a4!4LF4GY1_|a7d#uZ(fyAa8(s!0X=$Z)48C0HtO*x(Au+td(}W;^zW0KjpK?UC5}t9vBBb_#Aa!N>V_e-xOxr zwCoagb42CtgPcB-i*+Nc`urT9prLRE!A;Y#N9{tlUtPeN;^@+m%mM>o)v2?Ja{CE- zr>Ez5YWo-L!6^@)8H02TM`YzjqfetzI2eR-Jv{lF8F|+0=kFdP-+SwtaIlkg@!lG# z<`8HGnX@e_gC^Rv=RiS)^Ep_P9c{BFr3Lq!O2G7kQ_>xie1F}H(cWJfyTXP12Vc*1 zx3DS>#nO&pCqH;L!c{d5W9akf76%2C#Y*F*u$6bHKD`Dc`fc^)drJzWx2OjoTJUs6*TPZ z$CWeB_UvjqrjKlTAd(uOF!djL?{pT5Ed=~il-Mpcwv({h-7Gc!x!#Mu@bo6Z1Wf-h zfWkTHC;Hu*tfW{rl8jyAjN~$hl<4t8br)Z6+8@SXI#BEnrlSPjXB5Iv{jUx7ebFkEa4*6TG{_O|(G;4b>td)z#`Gx!LLQEB z8ZzQW3%TC**RhG70a%cm zGwJoRTY zfC9Ua^!95S{>G|&mcyqX`}dgCcmm^=3EE;-310bci>#<|)?f3n;_5566Q3h0IU=dV z&ezq5Kk;|E*DPkQM}F4498hfav+3Ke_^!~rxom9#k^W0Gl0f18u#hdrB-@h))fC!H zSyqD{|AtOXRnvE>BB8=bbv9aj`aB&`k<}d2wi=50bzAlBQ%No@dx?6KyROBcL_1m0 zDDo6#ds_Fh0QHqE-OZH0TeoaF>rDY-^N89DV_qrNvflYte;Q+DPqJ+BsWVwisg1a< z9(BqtJBFi?+1FW5Qu9p5vlUR`RdA@Q6F?gxnikP`;+1)phZ!=7bSB}ZhXmRkb1+`Z zAvAi_p%>kct~p0-AMF~whTCVpftH~PV>;S61b5BguG}Q6a+!%B*eMp*ptHZ!651=8 zB=wFnPkFlnbk*8i?Pb{&+TR3p`zn5Nb>Ym%p`f?U)BMJChAP50L;fE3)5C#P(fhXh zr;%kPTo38b=tLx&>I_jUW%A~-pft|Tv z2d?&j3K{08baO#c5NgxaN|MggFF5QmoMi|weIiI>zE2eBzY-ztQ-0mO&nc?VcGxs$ zOVd5gNe1x#;)RTk&+W(kh%0f1dIm&nK)_SD5DIZ#(8Ng2M+^6nxX4Qf$&11kFky>) zrtmxk_6Nls)oO)RbHTx0&CwKD)F|QcMyM*`)rj%N$e1KrE&#$-c=s&ASZ>!%}>tPp&aBD1V6!ZFz_M@-9l{tYIHWF?MP__R$FBn3w(( zS9X*W{WE^dk7Uv%Sx%al|4^YvcaJ!=lE9PpuGw4R%k#U@+1i~ij9@hko&-5@3GG2L z*Bg#n@_1}i?;jLHG@e*}YziXyvm!GV9yP{wh`M-rG3BXsm*jV_j_j0E6T znic&<@4^Hl(p`eSbK>7s*br@WHPN+o%Zw(00W4SJ<93pEP8x4gtuEgjE~Y(i={oO7 zS{*b-HsfMUi)P@@IRh52y%am8Ho%zXs%yWqC51c zQC7=50j6xo+~kN~zXd3{Qf^t?}sJ`Piqd8(3b)W<7()^&0-R z2u3?2=snC|>@!UeX<@m8`i+R`K#Pe(2`I#a^cSbzKd`YY`AN)_hWQ6Uy^-vUZ#p}8 zRxf-gY>_+J^Z8s%-`7kp%S7qod!69pbqq@v8XOV0+!}@xjq?7!A|05hd@$i>ZBA&P zYq?aaS37FjjpkX){8>0*DxLRzvv4!-G!!_0_LQbvP!Q0ansb&YB$Y=pZ-{RqOs+1{ zf=-3AeTkcs6BWx@X>5N!{@(X1K$h zFo^bBw?%if;2gQit~?aqwgO2Um5LcX)v<)DqR;A}q6eEf@eyb9o@7s?@v^%`j}F@= zVZ#lw9g7bY#Q|Oo><%BjP#FMWTAT#TiAtXaU@Dk*f^P+%{g1h@8>{(U{4)-GA^-al)|i6L}Nnnnc@{Y(Kfp3)}_a3mrpp1wd7@BD!vP zy?%wmREF`Ax5_&rdDoY@)tuzdfUn-gMLj)U7=Lp3=J=+@Mvk4Sz)pb;>yLjo3_rZ; z3ipVdMiTo{fPr14qsabb#kO?QHg_+qaeDq#9;6CJ@`IaClJagYH{I0VZLwRmKDYF< zXo#*L6=hb#nYQgT({?=pYn%Y{O)SH<5g|2Jp`C}4aaSNB$rL*h3J*82p0(brgDO2|pjl7~xwYiIa5RT%-+brM$bmB1pF zpK$jKxFA$$kch)zJh&$_o@j8`2~s|$knlQZ10E*3Y7^$;XjM}uXJiF%83I4aS7#ST zEsoGo1S%gk>){C&*a}F5AERmIl8f5UX{@2YWh5Up2Z`INCoSC~tGB=xT$s^BNyvRx zaW%49A{%bl7$w%!w3P*~j?A;3qy53%G~H!XMd62S!ng*}o(XP^^cR2sNT$aj>aJY# zt+;~*JK|oS)JI`@7OuH^)UK=JE=~Tfg;j&eTig$>x=~uVdZ>8l15Mgoe`8zA=1t6)WcZ{yPR-rt1UKg#U!>RroGFj~AfZe))EsUx28}t! za|_VTGD;iK4-wu!z6Ym*2&^&`U0p+j`97P3lafV&q}e8|qFRv^IStb4y6dOxk}xSU ze)U8}4kAl`=Hy}Vi;L?90rg#v?e>g*hER_}Bw@FID+;V^7{*A7xdds^`4vWSw4jl_ zNWMD&uV3hhdD1zeGP;GS%Z=>B&k=(sL9=Nn=QP+-9q3glilsKIuj&y1%Vhcyjo@q@ zH462jr3^n?bm>c5fFM5eGGzW#)&&``2WUUb`ta3oxm;9BlQW9;IQmOR0d*gMgfTH8 zHPI>1@y@VMYQZy^GaBaw@_N2LuV0$9@R0Uz^DdX~=orj|h9O0Jr$hMaJY-msr1@Q` zzMMJ6WPDxByii@eP?K;lMx^8GPdOJ0S;cmKV+_kVliM!b&biH@1+ims@vQ`wnW=P(VOC`veBy(o`$}G+2B$oy zf&R`gFJ`%jNi@vR*+f$>b!xSQAp#N+fvHlg>S~tJ63BxrW0yf@vgC6xS#GyMRQ+xA zdYxTBGHsUTe4k%#@@>+!E8Q{fqs#LVsoUgrB?SFbcs%49y>f4mUuL_|(ze8uS=6;i zg{2~G4L2Oul=XG;=bbt|qqYI3zQa03pHtERO_lo~ZgP@SWSZT|;C|du+xp!YgFaRz zN|%ugL-7RF+EyT7LD1kt6$Ggns=LUEX!Ss=Ar2(^9qTJB^{D)_qtA5 zi4pQt-P1y{&uXgUQsfX}wGD$2yx4`x6>%Fs+0uQR-)8H0jrC;Bcb0=QZlkpiTS{p51eC!rU$$f*p5Iev{5P)seJgwT2uYz!?**Axsuc}fppMl=$g^uHpNh{37QLbk|ZJXAc*JSFKpmlulhIP6A>cZDiht-!u6qzR# z?UD$SNKsx~cwYU>T~dRRz1oPa>zSS{;AhAxuTOmlr%XoRGxM()=xQ0U14J(t3#Wou zVykaaggGJa0yN1!2uVL8vV6=JBf#b;wt33VG#E7q8A~sC%bBq)73S zogT-4-Z@?9SGyihbcz3h`mIXUKU9c(GJ$M?KTu;s%zi;oL@SttRxdbRs?14X@TNm4 za6%++AP)zcZ3GQ(oKFhcf($|+dJ;ukRi&KxhYjk-e%stn@yo#6vg zi_>-wh_^iQCw7=Ph`Je@0RPgT`a5(KD9XzB zekzEKsmnqLBCFVD^iL7;8ik^;^{{#fG&7ngCneCY*dq9hp@sd80h*J(KY&7S{kxed zQuR<{EoyNa)+Gy+CFuo(I{OWX5K?>7I zpSUH7ujz;)x-L)u3JO>b04!!TCx>M(>a#GQx}dN(##fI4-}IN@8|Gu-GvDW2vFN{B zklwYKlf;0UMCjpI&zOvOh7UOT)Z>jl#bYd6!BQ*MQ-g_<%?u){4zNL+s3i?Gw{55{E=`kARpW}D&Iet?A)A$Mh zCE6-xVQoStZ1>$n(8MuW(|#b?b#w`e1+Q z+fANW@yj;s^LHyzOE@6~;)2WgiD?-Pv$5ah%N!qZNP<^AMB6c_mjn@Cp-GyNj4R;8wV7;(nn{tD^te76k&%u4q8pVDM`TtkN_4k_ zZU%nu@9MzOo#R=(f{eHf-SKf^gd&zvi>iqao-R$ag}G2#D#aCjD}`14MBzuzlgQWc zc_tTdHI0YfA7&I$MH0`WTIYz;s!Oey*!W&lcP$)=2&1HD8$sRI#SUiff}^RHr=FGN zHL9C#%7=pP)>3@0^w(EN?tj4dn=N&6pE?B|lK<@Jbf{CS0T~A{$f(Ksn1#bdwhun2 z7^nA`-AjIJzMC>H1;sF-yWLfdK&-aCg7f%b?K1Fr$nc=#4#lIKT?2a<^jBFmR>7Y^ zbbD2~y+q&$YVlUgLaiPB)EXNEc-Hj3jxPR!;LfI6C1Kmh#r-LdGQvmZK&1Nw59aRc z{DiMg{l0HTac2((9m#{@XU|yC-JI@Xur4pMnV#Fry*T_Q48c6jYMYI4n>+4b@=br) zMBi_Y>d8-=X!&G^<@?{+gsAo3B+%6it({HC)GQn=oa`L_F_l$T$kTUm3FJEJKW8JtE9ck1iDolb(mX~RB=-QUeFK_+#VN9@4UguYge46zYBXPEBglh zmL-t&le_U$>)EdiI@_aCfsZCrbwYVE-OGH)`Jlq0OuFC=%>qhl8^N%4OlCPoy^ICW zTC(9C-|%Rb_9{!DqvpmMv%iHpxM~w@VCmS>G{o*trxg52J zdeImYzR@r-_Yt)Kx73n!)JT)q7BiTOElTo{+{=E8aFWz_z~chiRM?A6_`B#Ed8~(~O;@T5S3F#Q#c5R3`N*H`|8X`o5k1ngX{&SS)7Czxvrp zBu)T(+w7-LXaI~5XkU4ehIBPs6iN~#U^ELWCG<+B)(n;fC5!IJ^XFmBbbm5(q^LKN zA%b!cL2^n{1G> zKN}N|UeCyvv?tWpOEv0Fz2CzRmZQ_Fq}!5^q9<0^m>!XsAi;)Vf|v8pmutI$#3mPL z<{n|h%(7JQndECIxA*rLI}qA9i0^N_=Sf!VCT#a9*VLH-YK}~H6%oIbG1B43zlSdA zlzB{6DB-MWLxoSKGxT|m(&^SzYPqjXOli#0aN{{!NXcuQp`CmPouKfN^jHT2RZZsW zC;`q5E~=$^jp?(c?W&FJ{@+=hn2zhxF=~iymaf^_`@6kPJdD!4nD3zvg>>J;DoQ;k z6iHc|^ubUoTSaPJFLU5+)Cx#EeSqJs-BO6_aY9j~`kc=6M2i*R2`Z|EVMn`}@Hv3s zqF^C21v@5SSPDidD@dD=qbcggaJUT2=!8hhAkJ3J-?uSRATej`wrf$PrP@pTt&YvG z-@EF1fyJ7OyFz70^L+^=S}PqxR{;F4svW-{|CExJ9;Tg1TB^|K<|``7yYN}wC>K6c zccXpC{oFa$%Pb7;JYP}zog=O!8dUN$d$T_P>m_B`tC?uk-?QTNz+Ki`<@N^>;VPQa zk~V7Zu0UowBym%d?aO!3;X>h;JVsqwpxCF9! z@sVdJE@wM2u3D*CzgGQxASb^Kefa>j@CM6!m6o}xI{x*7YjTtRny#h}2@JmB6;38% zvbr}z5gt~Cq-k;qB;2kZjd~03wRH&N4A!Q{qa+|9IGgZ>a%p3OXIl7v0ksotec3%M zM{SIaAl0H>iL85Yxfg~srD83t_0tW>`&@Pe z|95EesU>1;C2I)$cOW6-_{Y+tto>I)lYfpu*8_xx14R_2tZ`jU)L3RAG*nU+gftLp zt8gthvo*7%tv^>VxPK=h>=+*N5B zYDo!5aPy`BFkB4eLOX;ZyKwAT5C*;WG)(ucAt{UOR!D_wyx^c6-}r?A#n)$Ip=_4k zs*^2TlzY0)(nA91EGd4ojB4~xH!0B3yT%CqE+qtT9mvfZ;sr$-ovqMh$1G9as>4)3RTh5Zm$Z^cj&|IITAj;vnaI;Y8 zfMbK%Q0=B_^fVG50G+S4wOU@#j9z>Kxu}GC*<6f*J%Pn75fYxZaBa!%e7e9xAYHx0 z?_Axl(Mju&H*3&HgQ-UXqG@r={_ZSkupMdu+Je=o&^{t%l>7rU53e36S~A) zAh!E;MsKxtMWl<|6KYPm=-hrvYYFO;7sWl1+nnmBT`jU5$#|CmJiT%)Zw;D~%XuV{m;Ac61`T>vB;P!t)7f{P!=`Hp!ccJAjv*- z9runxQZmzvGqY_LZ+O0vMS4eV#=jDZe67tkfH6!;Ok!0)?#*Kb))`mM|T zr0hqe1SqWO#~3&F8pjYRutB_VLHS3fTLBiHUr0N#0tpK_k5S{J`}225bb4P-a++%P zNkNkv#3sH|3X7Lkr! z=r;$3=A)J$D{Cw_OLTuLm*YV#k&sz8GW%n4*uk+_-S4t<7KHH;G88!cppLf4mgXLqX& zP443lJ4_&MsdCw^cn%_feR|)cdx6_H0^?lD578qsWZdOxbB~5;&92esUjk!KpU&|q zoB`|Xr_Y0h_@A}){uUS$F);xO+W{>;H58cG|KZ!H*|?&KqI(xstk~+9(|uj|zOaSt{0i^6!O_`w~W4c#p!N1O}Etw6M6nTt(vnFwYK~tb8GU>b-&WvTA#+dOa0iu z9w^*(I*x9 z3{ytFjQ!Mq9ypNYgNFai*H$)tr^59VJuOg7LNrshto&otRyy;wjb1zJNMh&*YG`-m z2?!Tqeo^6T@ZKl614RL)rL3HUlgn!0@P6Q34|9gqP&@JWjXh~XJ0K1r!W&$09TOuC zAzAs?LFxmcU@$q_mMns-9hDz~7)u9&)iLJ2lcOVgc*69=ubV9vbC;~xsD)wzIGD8} z)#7UDe{?rGn>vOzs;##{`feN-a*!f+)EduIPz<|ah_?qzrEw<=u-5n1!wi^)xd8$6R4;pl6OL*lr~^2o>HocJ&l zs2AOu%Us|+Eq2HT>CmsHd(|HodtDzldy#;2)_o0t5IjsNw;>@Ebvl`Cgy69gk+cMg zG%_$AGFTi`6JJc`%+>^WtapwY__`J5baa8Pgh>-VPv9S&k`B1_!vGmK_~En znh*-4x}n+4yxiXL3cW$JZ`?Oa@Xgg?2m?^b6_~)4Q{=vWwT~orqn4VEBu)v+6c9kg z7ycQ6`id@m#jqJcN~IFIZJKOQDgBccr4>c=a$e~Y+U6wZ(zuhThi)f8FeQ)CU$Z|X z05E~olhG|60Jxosl1y?~`xFoixv<;WYiyXUga_i~IaP9JF9C_FlzAlIS~9|<=k*J( z;qTQ;y2Rd`OSfR=0rHn!h1VQhxBu;Z$b@^JvgH$?_dd`6RH*qkYykdUgO}dopZKdP z$7Bo2gp3ci;~UOuQB2(f;iMvhf~q8vf8~F0MeyTs@z|R5nhln)JMR~#BwFEeYUV=u z;k$DONJ?%t@z}I7wzaOcL!FTodX{_wQ3)$rQm-s6cYNmr?~PENO6 zd++N8TfyYUCO*|O*(vvv)~^P#?~GX;o6XgdFJ}i6=;$^YRFOnV7%&lYOw|h?cvSE{ z+{bsN^F(c<`#TT{<~vCA^*&}$@?mW!vk;yCqR{oG!Rv9u;NL**22rSlAFwje;Yh^Gwj13977c zC{v9Cuh6(CzhP{?`g}{cZ{q_44N$X?ZQ0(OJIZ>=bnLu!M*8;tKH>UBZf`)XelH9Y zzaOWL*CMHC6^MDX>c(xfccQk8Z8OMX>a)l>wT5jM5CAvPY@UH-EwkKAHeGs@NOT5K zA>$8XmIJM#_sV?((LQa>pPQM(g4f0SGF$8tU=Cq&NsL}ubz^PXow{@W z5wG*+L%?#B^EQjTi&>VE0>fPP11%vQumdkP`bPA&I|7_1=S?FB&Q_AV19{t^VI#lR zKiUYg!P?5oW$9WX;2GB>>n`+7g z-fHb;?Trfspd-FYI_fbl{_5F5Jon*BqZVb@&Y+*6te7JxpL5e7&1$;5d14w!ZT~?c zA2KfWk=I;}wukemuH>QV9`iT6IYDx|5*VAqKyfuaBgd>0_NxDD^o zwmUk$OfD7h(JqZez3R&5T(xQ-{L-N+pvlAmu;D_e@$i8|R?nl>^2Cg-4uO9S6G@e}^hE(XbaURh3c zt2uR!afLKm`wH$=WL~y$bvNHt1nc#RV0Bcb9ZriOShf)g3i>c3=XfdUIS+mQVj<1j z{Y{XZcUZgQu6@!h;)nVnyNIb90du^rI8D5E$i1rn#!v&EZ2K!D-4lh@zVBCG+h;Ki0Lufzq!f-z`TRzagQJIIQS4*j(lyLCtLnC^!<&0_3&*oZ*no$8v z-?r4MSQ_{bUkKbq$TaZ6T099j&tTKm{p79QEVe-#R@eGV$U~0XlkQLU? z1gs;jBZoh}-+0}PAORS@_5XT>&T0>1dt$m6mPZX1=Ngk6H^SC*V3RX);FOf&KeE34 zzTAsDNyguIzp+Y|284sYK0<_L}pTKtcV648#&BoO2a-c$u zA>VV)0R1vV1H%$5Y;ij^hYdM>L-}7G^0gILYARvE+R*s? zkk{qhiPP#@dS+rK|HqT(%f$M`#rgR86PPa9)~ z>?q0`IA)vk5f$F-@qG27_@|UTDW-k_sTnJK?sHSD{N0v}^mwBs>ibk{X<3rF{l-Ba z2ez}_lkeQhb!45y{5`-JF6AYOnL1nLUK>XA10%UQ^aX^GBq!dj*nvJX*R!mUic)PJ z@$eqK4a}wnv)ZR1TM~>Z4w52}WN8C2H~x1k?P@)@^kcuMzOKaX7yv zN|xIV58c|Pr;FL?QHo{*Q_)#c!~;$EBAT^C`n`-xBlwf1>IX9Jl~gd^%DKNyMn!WCBq6{Ls$wmmJ@31S*)K6tXN>MC5}lVieaVRr?3Q! zfXhe=(l7Ebe>upVq+$=J)y#F?^G6fo%Gytvru=n(EL4<6-EfphUv%156yj7oj|HvU zsI9rG+kU0YF^92PxQ#})R%(?q(gN&(aEX~|1iTe(4>tW5pXM^X2P0{WUTtQ<_G`M+ z$#AEti{JEZx~^-wZ>{o(r zvSzcHHmT7W9N6#;@W18(r1S$9);~Fl-5avjWM#Yb@}wG<%uugY<}VPbA!8HJd06Br zRC#|q90w;|1dQ#4-Ctw6`ZQI{5V?g|Z~9u+kSL8CmK&&caR07@SoPK&gxcbf2ikLA znx^lCT@AbBq4!PjZY7!WRl)vnR$plHp?P9BwzT$-&=|;1QMV|=rELJhT_WQ|U~_Aj z8PXXYYyQ^gUoTj$2`ylZqKP@0g?1ze^Q6#59sh1Tsd!}=x9gRnQuACbS;k5VvrE-$ z$DAcac+lIMSFo%`E%0EJl>_auI9{sJ$ZA8|wdHA5C}~q_oPS)Dd?P|5)F27g}(yry25mz z5e=?$p?tajS!htM!s`oC-4wWJ?{^ZA%@zqLV0TVJL8u(_5A|J(8nvo;b;9B^4V@Yv7S7 zS9OD%qI@6G<3s1D^ZQ$nNyQ~2$;BE=*n}+A zA6;KAJjT8wNjKf8_f>jDMtV4N$HNrITy&-nq42OjD`jS|59ADECtE?Q*6f>_mxbCl z6--AYo>&Q>?`=8IT3v94np?r=37uKil2}_S13?(E+pD!CsKYDjA}Wd<`c-m6b(v$U z8?)u7sCQ>ToUN+9+C^^ylqD}8ZzYyvB`4I1pH-C<*7tA4?RLslU)2U3-FT8E6;&T& z^s{LtG?tD$iM`rSo}|5)k))NFjvO>{lCC;*66>Qm*bGU`W)tY@MR`@TbOthL!@1(A zOeRuARB1+1e`kKy3~g0MSaCx$ykCobYBo!g~0Go(7v{78X&G8@`%TjE?ZhmuWnQ)F{b-KA$J1ysQ?UXKk7=d<5Orxcl9(r(BEz;=jKy&I0^^bIUc=3SX4>oXk8rRetabt zZnof!vL|DFBjMpTjze0JK<9>NOkn&6KaZI**?rCCY_tlEd5|_>Q5p8JCX2+J;4H|5 z^pa#{F^fh#ReQ!HLUa>UaGgB}dQ7X$T4bGrQX@_G)E2%fIEkN|>4<$CJ&N_RON{Mo zmagbzZBxFX15g>w)g)abdjfk)I`#&qltrvlr4D|t!clzlEE#XtOK@xk-qS@W)RT5v z=>{!h z)v?mWooUU< zs5x)gwF5=w_XHmiCa%4O&otf!iwlubd3$=j+f#0lgJpUOOg`)+$GN{NkvnJWX2~`Z z+u8vmrhKsZ@o08GEzj~^Q&+2r+&u3WeN1w9lSb_OuUMP_V zHPJUG3#-?!=y&xZ_TB3B$4t~7NmWnvBASzFNzh@ZHNV({7}dD5)F0N1seOlbLfRuu z&Irq_qA=8)nKKQG$IuF#f^!KMoq}5sKR*WEyUi@uaO3F-uw@tr3`h=nVX|Q`uKCeW z8=fN>DvHoMIF<1yiitKS0Ll4oW?xmysYT8Ov;9IeTK4hfY#V>=S&R z2ORP;4oJxPH~G)qm3E(JR8yTvv+dL=8;@ph_nv@ct03`o_^KOyeNs14sO?0p)B6sg zRFlt(HT8XMm%0Ix5%X|YtHgw%QgZERS21XKR-CtuaL#C;G8tBy=<|mY;rK>IFA)rq zlda+4<7Bv&q*;`R;UshRC8O!uBdzi9!T?}wd|nmRkOwKH1TEK)bS7EFetG4!h{csG z9Xx&7FN8Om&M-2Q6YGTV`*ic!*<{6I-H$3_|7pCp7R^?ByKl+C2Tiv~NP{>8CnA1I z9G+d#+sO1sLq3Y`Q$m$}KWX~aR09LQ5(URx;SE%j<|=t_6M0R8zd>fzL^M7UdWeam zg>l*a<@N2yOfF}irRp^mG)>0b;;#rPUHB+kGpVm=?LKr3?+UR$S=?!~CSO!lHc!>_!wIurZQRm=E=l+mBhSA$ zs`TMz!%KB$diAy>rQBwr^GThdqm^5rUY@Lr$m$0`ACA9s+nv;08c*WW>>iC1E1qR& zCP&+{P#@v(Up1yC+`_%G$1CvrZJDA@5GjZksVH|6@N*YvsqV~BPjth^$SF1^4egzL zl6fhajX%llEFmPIj#c4N#VuASr&aTJ!4Q#}DMn1oo=5VNmK2y*%W1|3d zGz@Br`aMV5Jw%h(zeSemXs#ErZa2szL{o9NvyblQTpOJGFVcA3lS5{LmEP4Qvtx!& z4>AeptRBrc10^x)dP7tDJH0Knvm0wNEs^%A1wFtbI-56eJlD=3lUKzBsf&W;aIqp& zw+qpOt;DUhV30>sNg<`KNvXBmJ~MrIin{#9?KvWjx(V1jU>Eee+@GqI%|uz8KeZHw zj45;04$rZ(dw3wrblS(!^ot=IsHnOf`dbHWq^rgdT-c<6D5+!O$l-PBpC1Z&;~3ZN zo<^c0qB6DP4~jmBVK`_O=X%D7{lWTBrC&o-o~m{vYU;RvJBmh8-*{8A5eyK>Jt zv|Wt!@~*{YO|Q9pNE9_z9{q;BG&?7q2rXSqV)7DGz0Co$>-fP$W+~k=Ci7cT)z>j&5tV9~k0-{;zW~G&cCLH3qSB8#-tFZywhu9@VQ|#aPJe^kYH%N}s zCcOHA)644$xsF|jm3lqII(2Wcj#Y<)b5nWD*(B%r$E@0V0~N!I!>Lkr5YL)J8*P}w z=o2-K$bB6>(=dF8!lfC%Se7Ke9huq|4}-AYYUH?yjQ@t(A=S11k}s!~s?Esx`rQ`u zJ|~V|B>09Kqr6fujrW4Srn4v6*D50MMDAiOR$wa(6dKMuCIRts;on9wDdC ze{XPcnA-<)SbAIpH7<;5%JRXfR?-~1@mlihq2u;l`huUMEBF`@SeGZ(Xp%leR%kPO z=vofr5CG!EShKP_45{z{THp0v1xFB&o!i1-O-+L(^P3R7!YTZM8Tkd!&*ll5cc;V#C zxxk?~4#yh6o}#ub=t)e>B?G`T)eP64WgPaMWBhWX8mjkoC!f-OZh|0U84b_`HyexL zNPTGS*NWbzGVd818N+ozTbz)1OY zZ`z9EQxHR)AY_occJt3(QGv43C$B_I+Ho}hU8ryz_~ayz)6Uoo&ZBq?vR?(*mP(hz zLFmy`uLR2E18O9}Wi~+7pc~5>v=1&LZ9s>JzW)hSVHdUpAfOu_vk%&oGX=$nwI>pU zMa^0sfYvR8t_*ETlVV{+(K8OJY+OEtXk_NU0x@1`gyg@1Hf~*m)$<#{G%H0O0M|{2 z{(?H*)J=zZ4_@~zrC_j8k3tPq1}sH&_lv(K+@XLybPqo4Apqr*X&(SHBLko}&>HaY zm-MqhI|p|pODRA8nu1`!9-aq%Xbu<(-Gey<2T+D9Lm#RDio#f64(R}95x3BXo&c=S z2$(}uKyo-O^q~|$JS5xQ73LKGXv|gh9a^5(AJThM*7a z0rH_m(1#X)sn8>sLkIw4xH|NqDxfNi1Llw(U>ETKedq-s3cW-ff9Uq4;wMg#@A0JQ zBulvrc=CH>>Ujkpmmgq+K1??7Cij*Gbb;OexTf@fVoB)=?Lryf>UkA@O*eQ4JG7$C zi*zc@UzhsxH3d7xX1@`94o=2yCC1=6U_}m*B&Ek7__LOQ1$113E7)MrABU0>d0Ye# zzpvt-3TfJ!AqrqKaQP{MpBS3wpf1_37{*CsCQTA8)vw4Uk7}<%5cO*=?JrD(VWJ;0VKLB@xNTEIW zY@I&kY(EOz5iKSCzy{orCxvH!1KbfQ<;MXS`0dw}ko`RHTkMpf1514QDY`ws4}VJn>UzfQ|4^j38~m}yL>?CcI8Qb~n=+-4PrifKp^r1q znQv%bup)Dpb(gy=qK^9+AUYtDr}P16so!0j9Row`XM{s5*p2Tr4p_q~%1TA;%N(Yd)6}Ba|skS+{-LB$&cFS$obnp36g2+Q=A@YK` zAztZh2X#mICWA0S=^?a3x`ADZZC7-!`MQE!LT(~?etCqxQrwR3p6E97n*z}X?S%Iv zc!azn*lz2#1M32Ng??nZa@@x1CIa;Vf5o{v>edJI0ec0zis|kG?E-yAdt}`9AsBZa z0E9gdH$kd_{Q!OW@&XF=1qw6-Gz3H(L|k2U?7I8ho)3o;f)k7rE%brpT90h=lZrmp z(bv)M<%^Y{bGK+WdH0*|nD3ZhnQxgN6e!ZEB0UlqVe}bzHOw-s4CtD#g#l&?+JXZq z5Unx>gbXc62wKE1K0oqU%g{mJ^$?9m(T9ee|GO1 z$Y=Ku9vPn!Uz@NGp%Wyw7v%pV!;R$M)%$c6FN{jAGrx%7Y{ z{$k8d!h~q59ixThtQFK3=HjDeR8eZ1m=GR7x&Ea2as}p!^NBWDgtl4b$!Vg2pkD%= z<0jhBj+tm5a-}x64G(yNOrKS-WjL_O zEzrsvi?U-X0NasCF1RDm#@K)UIlj=q`j;)jWmDOc&s(>udyw6zfk5rj2(D;jsKa_g2pdVQU8^Ih@9lVca$O{Q{q_VklwDocZ51df&C zcLWFL_?yA?fr^g~&O0mS@zer8uHPuR`gMUrVoPicJ5*qO(EiCuM$Mp6n@i@2Brn*5 zCu`6Y07*`%Bx(nM&X6pG6?bo1j}3$sLvPy9FaHK~!;Kz3h%@iN`!6x*<%!T|Oj%SK z@;@~9zcp3vX@+v$d}%}f3~LFg*ZATETJ&lM)&;@^;ykxYH_JT!1V}eaHH!`B>^&VX z|BGS&RVkf~QClzmnX5$o-0%GLKNz;4v9XDxfPFJg^*6cCOeJv9{YwB%CcQvSN0dvz)^5$0ZAjv!?@#= zW7cibe&3MK2NaM%4Ay>K>6+F%o{3`572^m%8_fmLq&mJAZ9qpw1@}W;P;^-oM->B1 zLkVUbt^Nfz7$T<{1%gYklo~tnIt1L^xuQ|AQk{I?X~`-{=7Gs))L+gbf-7k+X|!;J zCsPxqWT7F^qd}>PL225^xum^viJVM*>GHGzR#(cUhdae+UYhJ^h#E_=SuEs{KHg%2;r_+pUVx@Pa6la4iwWEhu zI#e{=KBS@f%Bw&OV@fmnU`iUBn?kdqT5#rFIf7dXM{Ul~dplN%juHP9ZOVoKKrSI7 ziO0|Kp=WwdRkhoey3DmbytsOHRS>Y4r&_jIq;;PiP)yX0RS7sn}7W!tl`ern|r!60JvT&CVx*J}T9k6Ft zK^7i!lQV4@8rYcepu^E3>{u&`hER1zMEx=(LAT9c(r|O_Yj=w-gjR*Oe)PLQOEzOO z2iZkFqBWymn@wYFmZrL0BcNWQV7+#dzGk1yBWkc-dL9=Y%u=HlqU3%1idD7$C)RTT z&AXFUXAnb?+Z%>Uz2xUepHNn8=(dc>{w`sH&(}+mhImCJ!4Ekx9`KXQCTw%`2m4ck zf&N}k99#k*p2f4biG7Ha?(ryJkdnr80ycw@g{+SJo>qdMhM+Rlz&4@xab~Ykasp~Y z?ljheCHe=m4C4G2$>#8PqZiIM+!ExBu#lW@;AUvV*swh@Bgpv&AJ7BE`gtftVo_p7 zSi)kYl3ut~#2wu0+sKKCxSDB&*#*H2bML0+!d^2HW}1yWq`Sw)dQWjJy`)puf@i+mSFcR@Yoz>VVfWYM`ZtNz*QUX5Ne}X#yb(TW zBV$iysjrUSI(r`@`D2e5Q9i_%9}ABktnUSK6}=Y>?}>6ePpY{$RyenKQ9fnLp+1!K zRU>;3xF3Rh-%imFzn$p}`OG{zXI5@^JzqVh^S)O)(tijyUT5p+G2U%XKk0jFiG64n zF?cI4z9tRY{dkI0kCj`x_T?qKqZ!PXI=KT&r09NERVF0ZO<;(DcQ;SFIdzJdEr<&X z0k$PUwcqio<-yCEoH&JyF3w|TUZ`+!(+_#Nz2YF@WS-^GBo@teSe91~mUvoBCLn&B zop;M5R6+C2OFP~Ry^*$vOMuO1)s#}+XHr$hI<~}38u(DcV#aaa0Rpt)05j>RQ83dq%g-Z4zo04`;=$G(+_y@6Z^)heIv4pYQJ+`NHjBAV zaC}a^h6Wwdi(;iC+m02COlxUFvw>lQJ$~Wrd8+4Mw1W%fV!lj>Zwh z-Rq}e$68!+VqPX`Zd{7vEy{PTXCn6z^Os$jzFroHTIe z#1Dfo%9X&5pq9+YW1C$F`cn(EGc)^&f4shgc1mrPFv<}B^4q+ca<+wISq%@?rP1&e z8>=0`&$efNz;Eie8kTL0f%xe->|A9(aU%6Le_w8$HF&$31`D0GpK4?n<9uvrqHqyg z*>-JSIW=aGMx+RLtf?@dk%wgcOl`zSp$e=&R!uOxWREumpNyS3d6M<8l$l0&N5L9G zS=W#T)zj8Jvo3K&Ie@UXEf-i_(`2O%Shc=RRF5g)X@dMkT(TBLN0ecs@2F~3t=%7C zasDK6wG!A}K-tIx-fc!fehf6T!;C+LF+U}FC@nQ!-q-D5qH~s{Nw@za@N>d4972n6 z$U804$}+gn)F>|6puEyu-xYu;EzdPq-pj=kXMBDElV)*VV_R6$5NocUqudD+ET9gc zSTxyq+D;CP3mvJHu+#Y7NET#b6Ur^&0R6E5GNV>gI_c~bKpW?%eTk`8F=cS!b(m6=edBjVqFIg>LdtB50cP0=qqI2&F-Z^3 zc2@DY=;?WDXytRwjZ~wfkOj?zqzN1hCFF^$Ta{fhN0z6Wx3U1B*NlO-E&>?|=DAIj z=&xUl!EGQ)+~v>9V$%!ScYNSXnVG*9L>{(PcUz(Od{>H?P|GlOoYn{yAbL&m>Nf%C z#2i0lq?P0I$RPEUgJkyByH_@$QY>6>%Cq4Z%1G4?5x6$3u8T2El#!EJ7CZ21QL$%s z$=%FCRf{{!+|c5tHI1i`FKm3GpBS*+`M+|6P;U`nwt5OYrvd3&V@hZeD`cgNgC%>u z7qNA#*lz{~il~qzwB#+v5Wo0d4Ol`kD8q%!+%#*&^i(6HaGt`Ch)FBkxdK)#IYM(Q z*#j1LPBga!<>CBzz$3QvfRio}TDsVGYhV?JawoZ2r@GU@0#@VCt=!9VxCqX|99Pcx z$JrPWJxhTei-AAdkoiJ9@GTF&FU*)5WUq8G;hw?{2>l>`2QDDjfam3^73z$uqMe*Z z2L|kuFtcof_rc5yP#7Pv5Ta1(qVT{Q=iDseJ9f+N^=_=nj&UvL&@=@&C~# z`c*qFwu(36NU@Gg%y_0{O3;jn&bAo4cnvD&bW-j}10r$Aow$yNFlwQZT&3>eQM#NZ5D~XepdWV0)Qp z4KKN>Qw)jvKm-h`{_0vNl>cNxnU=KG`7FTWE}s^8olc4|9X28p9w%`G=`Y?m$43ll^P0g5cO zWc|j1`Y#5-<`>5l2CY;D)Vxe+2la~`&~t-B1%s@imA(LS<229eW7)Eoh{D4@=+-#z zgKG^-4X{0M1=K>eO9>MALi_Kx#*m}X z12!z^&QTcv^fT3tTsf8yJ`l4|2|TSrE~`{bi){7{1+21yn_>f_`6PRgZRNx(j-zoB zY)h^V%VG@6g4|LSHsy1k_}@&&$#=Acsnilnkx#X&s+<%EwvFyiKkE{6Esy0P-=g0Q ziWe*qcDEAHNP|mNt(0|yTIdWjb5B(|a&f_9#Tt4P@Z9}#*-Iv53i2GPAno>~QwlR_ z?G1}57!1-fjX;A+%vnd0PLAZzJ{9k8P=YNB)kESbn+VKkaH1Rm&FNKuPsz4rr`fB1 zga%oVDU1(I#BK$8BihC>V<^IE&igCVJUDa_g-x4c>7ZegQ@-F;NqGUb_~5Zv_JFD# zD+EGD0{dzT7leCn=ek_}9Deg+B$#t;bb~C`R}{z#`#PNGFlhjb&|D#@ zcbXoP&DE#wM>dUfS+4D#Y@dOT+I9?icSNz({qTCPLn|L}E=-IcX% zx=zKmE4Ei`+cqny*fuJ*ZQHhORczb#&eL!29&hilyT7a-Fvq&*ea;KVQPedp{}j&4 z*OM9KE-#57aiUM@B`6u(f)Z;7eb4+LA;go1XH@nXeU)kl&sWXWsGlLR%g)~6Fxbp4 zOnN9g4cL`*eu`rgZ-M_I-gBRfkzGA3KDpPRFulL zI)6wH&+5H9ij^j9FqUlTfUKa-DV@iWn6A+;gYp(Xr{g-;ao55c8ssl@;Gs39kum$p zPfR?sfeTCAM=8TONU#4^NBD6ey&?Y5Su5^&6&@#jU~ZOl?weX1yT94;Sifg`UQtZ$ z=rkh@w73KHURxjN%H+(cJeYPGOZhF!w%!kLT~y|x-3 z8peg>B(6Z_duw54)7-?Y?RT1c#Hddb$13(IR4TaX5|<;fC5m>E;~FCblNyh`!*NRY z%?))rXg!6RKsyLeWN*1a+%cnGXfhYP5$^6B&>P}$jA2SVNFNQ#%0ulsjU~;=n?TkyiJ{Wb9?grfa+5|eVx6iV z^@@!A;}u*T0qe(peHZ=v*Wv>4AC8o$0ap^T;rkY9HaFe`P>Z7j& ze`mw}GCj}yaX7=~ViZ^Byg*YK@??*B?AvOmscO7r7QM{Z_pC!3^=KCJDJiYZHaZ#R zIWk3D4W%w8HhI%J((dv*W_2g68mp2v6oli=&VrjXKfiW&;?j=V0(+gww84LKX}Lh; zEg0}_OPB|x?6e3Qxam^+&?U5rcd%}v$8A-ac^E~rHiW}TuEV``+3wo%!8Qia zo4Sn^5_^p4S5HU}pZJYCS!;7jcC2H3&vt2!r4f()oPG+$z{%?-4vpOmEoq%j1yo3psQVQ>#daEK|dx`Y3qGR$?y9eh~c;4~BwkGLZ#lAVU9Ec`1 zdROXl4(6{VXBko~;+X5(Y}KnEn@zqOR=giGU7vZ+N`s4(nOumROgfWdH}AJ5`3oVl zFoik|md~@^F0Dx=)~~P1_No+~Kg);p5IDXPMIM+f&<)bPz>CmYd@esdw5gMsyZBtn z_nEFysjkv|hcSz1{Y!mLr>{Ewr^;GOtF?vG9d?3h zb!p|M;#NjXM?+-ZI}p_++#x(ZI_Z}M*p;a%@BI|{15glM0dE-*5kMv`fI!mC9?7_? zTReF3Y41MZ7ym%` zh|tNiz0xd@TUeBm`#6vhx5VnNSuXW^G&DYmvXXrXNop!y!Poc%jdcVTfR`_0__M^= zY5u`+X5{8?$s5w*Z2QT@6|}59=B5Mo$o}H@j++nW+>&gv!xx3)VhQhQAT;hf_N0a= z=NTh)d+g2-k;)tfSJvO5aLl4;GbB!2lLfTO!dj&fnx;_hYIE&BHVZLZQ5sbv;By+n zH^n;d;UaV)AD!nMIVAwT@R)@*v(2FsaepR^C4Q;qG$wTkZS|jTiJg&1a)H$0MZPj~ z7S19tyQi9li@IU>&LNz1(*q;URg3oBM=&8r*G9IwY*a%E!^YUY`T*&QpF>TStW;Xvx5#i;o2BTh5 zVAYWWa|F>5W>e%?uIRZvli%WZNrkQMF&Hxg{s{4oFj58T%bt;Au!q+45#@srp;-VY z%sf5ukq)KG8N7%70H*QUN+@k$qtwbLwi&(id10O`{t{}3;91I>2J|)zzI)}6qaNWG zV=b?x!fo3wE>TtsgBmUZP4bEcev79PmgOfnL`C{MZ}j8W!w(JX01$+qPx`e_^UCfs zhK6Y(%PZ9{WKtoortnFxROr_h4cM~dHs^yLp<4x|*dMI=quu#@%TiS$>2Yr1(Ml39 z`(5{#lZh;o9F}Dx;Z62PJO)+2I~LY7#PB|1KLf7(|MJ|MxNoSIfR%(YpV$5<4OT88 zR(6g!+(`hzJY>SnkwhKCa-cJ%p~B4xM9bpER*R*R+?Bg39wdSQn||h*o{a-BM1A8y zkMJ_dqNZz#)ixk@yV0M}hV-H+7GcdxYRLda2JDQxTwi!RzcM=Rv@3%|q@AEz`GXD* z7Q%{uC4L6Y!qkHJ6#MYgn^P=VQx_n=l6UG~4&nU-12CWKErU2;_%>vSN<8Ky%x&d) zp8JY^@e0LTk;bKo!i2DB-jf}eI^K0t95HV{%%tBp!EGD-?5RTfp`FV3M+#vE6X=-)a>~I<;V|^Wc0sb$0g|bO>=W$DC>a+PIIcGN9yqCfF@bje6CLK*)e(JPW zie`l#8nC27-~(^4^n1@*AePLeK4rQ(t9eJ7^kYB*2wDofakAg6Bv1_wVh4=XPeOEH zMI_aRj5=x4xg23?X$z9F#uadqU{yP856ee~(L3}5vy3UinlvX(^rAA8T$ik+XzX_{?^<{l~PdK^Q;dcuN^A&MKX6j7*jZFzN>aj(Q zNz6Aul^#V`6G~B7<#k}ivezYnT;_J5CUi!-FRbz$Lah_~!&Njp%w-3Tt7E$6JFu%i zSOqJT$W1t9%^_&oNm-=`Ov!IVoUh%*)T<2r>*>?8S2?(P-&1M1hgf~?C_ZGy7M9=8 zvzo4H@*I)y9)}K05-ItL%_793#$)W@j|^OdgEni3{WIzQ%)iw5m*t14*r{kQs6#{I zN}J@VI>-;9N0$hDkvU6tSP*~4M$zCKWj6xTG_TZ+>z#|^?h2`XFft3+s< zCZwVbU2&*HoH5I)3mK{ixK0@s+&p)#JY%7q-B}i zJ|md(33Q-I%A+>xL3?;1*XPq&qla^ZcTYxmnSMZTEehKaM`p(|xvRpQZh<4m^ONk5 z1oO92C5>}M34s++^>A9S2JIbtNU9O<_)XG{E)C@!1q*yAjB6%Gy8c7$(Jwp%BpFWc z{-dA>y==pdVDcfWpMnfV-b|L#tnf2MxfZ65v-FnO{uvB!!CI4tx#cSfjx6yxhKtW4 zkI&Ll%jr>ycQb>`?c#=SFb9vg*ib4q+{d20I?yqqk2+~WKgtzW{$yBE zOnamWPN}ejcjQsBCrsF^l#bRx@+3K4)3(*5{`ebHX|40oCuv^cm%^8HkC6ye3))I@^#_ShT8T5~Tj!O&DpNr7type{cC?kq*kUNS>5b=zhL z(u$A{f;5<1*ZdZh>Of$9Gp})kBl5Q>4cd~Ld^ZvXl_Mr{9SShSg-;9tam71t0gbC~ z!Aar<$@Sm`2u4)&T37$s&@}*dE(BRl^kksv42VNge=9zx5P;2J@WFab-)fnQ!l;wz z6hUFug`fAvzTXfv1dFHu+vWwL`*9GJ4#G};i#{KJ40QFzol};Mi5+=>A$?{2W9Bocf?`bG8?58eh3}4SI3VK@Af% zr}EJ-y%|$ZJ5y~&LB%1t1YFS%d06dm3%I%{hT^vo+<3!-Ik+Orka^)` zJ*a{=rijHX;%1g~J8N0U6+2)_wGZAx9E#7%Zr|d|mhwy57uE(;IjJI&low>q487P1 zIY5^!pj(F3$IBEYu3Y+aQ_Yg$XmIp`4636XTe3^`gfw_IDahvSr=%{y9y_hMZ(HXV zR=r7RVMrD8m~xH;Rj^~C7|McrTG?&yn5$+9nPTE?dmVf^j;LxX*JO>XNDWuIm8fUI z1dfo3ci(OLHdN&SpGEpO*dV#BX2_9`aiTOORaPCOf?)DD5(2NN;Sbv zNFiMgEK6kFf@6gj8Cpqr4K7m&rq$ho<#3d2?{d8HFLI4o*FF>9{%K&G+NNJYuo(co zok~`|Lw%Syjp1}8qR80(q_r69Jt>;5IZ4FyaDz&)fv#ucbxRv?)?JXuybTIZdWGJ> z0KtB+2xzI^4=f1uXls&^9~%Cy`|BZheL{BQiiw?=4uvTitEy+B>%h>I6`ZWECr5=>a8-e33gV(&p2 zVRyXH53L_SGTgE*4X9{uOz*tim(w^o(Jc28dzrK%pVHmB=W>8V8<6`OaFiB-m4x}P zGswZERS(=jiR{0?=XZkG#Yr)GD*OjjwZqhuJh zxo!o~fXB=sh$7dE{Ur&m*s-|>7*Xew{9-z@Y<~JQzeJ~Tn&A)grocj~Sc|-Q<_Fk5 zSmG0xmwugR;>BmHwDOxm?BEx;v={I~6V<&ttP;aR@!tnnNbyaow~X4HM%sQ>$D%X8 zPh+USsN;xTq4IJDok|$1gpy)57fdlOo9YK<2&zW@Xm2Xz3RGcu%%i{EdcAB4G2;SX zQdiG>ygE2u{TRDFo1W`L!@7=t^CH^$jKCOq0>%9^cw?e2jG$TlCQaL}2fASa{R%>K z%!}l>f$FFOs)HfGCyOAM(5UF+Ub{u@_b%_>>fTJS6%@&S40-G93a{uQ5OWnofd8kq z4S(qI2bEHGf&Bv{OPQEixqAeiidO^5gYyGNo3>Y0mAoDhQS`jksRZPs0%u6zW;OVC zMU|2>w@Z+GK~2VY10mM3fLn>gX~HtJ0Ks}0SwaWVd16xvN2G=_=m8XzIw&(mP_IG6 zo|VTO+YKRc7G&Kb%#Z_jzta;<_?8TEnwM#}4UXIbi4WkFMAu+~a?`G?%{@?k3+qR- zfr!O8S!|v3-=}W75QZ0`o-HHOe~1@0rru!`;ip9B2~OBcFQ+(XW+$a?mMEJ zu(PLOnjQH{*MaeC-KZnOsbMLvYjH21uA?~~VR%Q$6YKP8*NgNu_~sL3vw4Oxz>D`e z*|L$hzqqeKO5_?Z_|b#A>+9d>MgPn=<>~*__WTC4kbRGXF#HdrApejPg>0-Hja(f` z6pbvuQD~0;V9@^6jZRej7xPjJeM~GdY%BYZEq@4l9$O?v#87}~UeKH0Pl{42#jx9E zZcJnZJnq;0i26Vfi1<^Zqser)dYihphmX%6+n9yS>`h9J=LJZ^RZ$&Lz(PdFYyI|_ zxy!fjSql$%MtTuJi~KE3XBskN=~+KV>}(|`O`0qqj$jWY4W9(keX0Ej*XnfKnA}gr zzXTj{4D4yEsWY0ctrDljt19RXFAIbgeo104RZ)Kw5eErL4EC3Jj60ayPk3}a^&ys1 za7p-R#&d?js?Bl<>n=hr{T*_0V#bv8k*6=X>Wtk=Om5Pg$tcHz8>pv`5gy|QZWH9d zeS>ZEv6LZRz#K=io^HE2pc#hT`ruX3Ax2Lz>S&?+6@T+rwo(RLy=ID(LS99N|1$*lVS#uH| zv)#Tq33MlJ+5C+O-%Jp+aXWVL^9GDITbKfW2=Hzq-HW|^bo+hEb-Ahe@j-D?9`gtk zgJvd0@WF5rrx5he;KRSzLOVk_!&D)kBhi4W!Bin>Q!i7y3bM#-w`sbklDjXHyT)nf z^%7^%Y)K<&132LPilMW}T-&y>{C<+!6m4tx0b=$8Yo@#r^pu6hj|2X)YF*<9@S%Fv z<`jcYqj`pIyZNPqvcbSZW&zkKTz$6#{j@;!s9a;W$3gYzo}qig!2n2}h1-RGd<4(_ zep+D7P+gMOjJ;|wU81()w(Z+B+wcN!pkkn2Vz$NG2YzahQP8@Su8G?z+Y5f}pu=F* zr8^d_TCd3^2Y9+#Qq{KAo)vf3PK^!eOXI`f2sVcH4@3F5(wdeY_G)#cnG>*0B+wi@ zOv_`;gc+0jFr(7BY^iuM0!?0Ca-e!UXt~x!Wl_(okC-*>Oy~#?^c?v*u5AW4RR_Pw zR0rCB@Lb%u(jcE2wEZ$FVQU~oKh^9a{kwp61@$1jr=!(I%o9r|0ksLrCdqiSDemIZ z0PPZ^a$s(dwU_lGq z(Q27{5NJSPuftv^%BfCf0w#Wgo*PD!Y)yea(L<>_u%;&3r_Po?aH<>rt1?QN^>?85 zY|QaVlw_gOaQ*6%=x^M)E~*aRS(IM9YQUPpblBx{+sw1+NCi?ypjlXN_)zoque@A! zM}Nw)A)L3c6zv1oe0I>p)+Tanrc@rr0wpe@P+J#AxSph}7B@ki+y%sJQ{z)p0lDT1 z*(d^;a+KF!WEz;0gd!Np-Bw&&mD7hb{!wN@fwXf04Qh2!+&1$}?)q4_?WL`1#y(Aa zq#H9#rB$;#R;Ai<_F9~zQWR^+(obX#Z3F1@D&J2*`cs{iZFw#@QD}75bNtazblOmh zQ}LxMgI96na9f}<>q-``z`j%(e{q2=^TOl~oa|CegA97K3wJrwC|_YZcz)EAH!Iv2 zx^$ht4p3-%d-5Vrm%N@|gAJ|}XzT1sk}hEcO-i0ZS8CX(XkvTt(0O!=oJbNB_X0|S zo;Whjk6u>=*=~V+gQkL;~cBjqPTI+ z@2hM)DvrU=3HsK8B02{y~9qsD&^T|y?4Y8%p!1IR3@1Yg_&9fh! zxUL<*LNpEjaf$;qiV97Bfc5&Oq|O~tv~yK2g}}Xr-#x+WLzb=)lQ3A;m3ndTLv-@m z65tgcyv=;3yE(;0ulQxJ)|;jaD0W+x8a}B>&MN4~B-7F8uFy@v{KNKr%`H<9oO+jL zN9FA_26N?y!s$-gE=om@i*z94YooM4Ng&-z^8Vwu!Aq~#7*MCRJlgb`d@d-BC^ZuEo0e|C1jw2RDGU(Y3<{HW3F?9j5#uN{4K<(DfQe!EE7 znD~oBg{s(34CX@^?mu=ec>q0Bi}_)&?nhbwam{+xycPgUYM#^9%(Fg=rDGOk8{Gkd zg(zcCn}^$Y)oebREx_CIiZ=R=c-lA4b}O@N$is$=R9ixOrDxsVnwfjfE-(aT)fkoS&T)KOQp!JVf%F5)22 z?q$uZf(!*z%PBG;9_lV4`nX`I*P8+qFXbn~T2V5`Xc&`GA}*==(%IB6-l$4#Azcek z{h_=zSVz}fsoZY91|M&*78XUdKw00eT{bW6$`Vx93ybZcYGBGR{u^@~+d(~4f1(7_ zsr$xiP01_p_9KnEC^amD#o@2jG({!mSCgk6lt#|Mn`8=0IcB4j!>Oxktwh<*71o%M zO^s*@@+Xm&EUb{`bQ1}^n8LIN?G0s6OO{T_?Jy}ir_`zCg^07;j&Q7bQjH=zYcqvK zLVhpHm3>jgK3zjA-saIk`U^T>YpmJr*%{yy^5pT-?-`uT*XN=+74g$q_mg7ln4Uo~2pmrqId{9YK z9(*EcQLe}(7Ju^2PO2I^LLDtOeP8T63^An-zUIItl7HJS;H_4-Gg>G*C$Sf`feNpN zYL|Bv-EOwVt6a8${SGvZc6;WCwVx`3=9uJ1hG;IOID{Xym_v{1UDs)?la$#Z8mB0Vd>v2eiINeR6zE?qJD5&Fbya9(+|>(C2SuEwGPp@Ar>#@3=2t z(Kf~PFj@!nGYYgWoM*D%UXM@VwuSGPW-rusx572K@0V)t*&n{b?Z%#L)ob_dX$n{3 z>n%!GrfUJZOEIwpb@p}3dWS=S+$${ zG~jy{ci63kTRdx8cp=TMa+rwN{s+?A7Wmk4$}M&@S}8PIr+X)Msr!K3*_}>s-1sg9 zWz1UCSKuiI`&R>q>0JBNyg9Y1x+!@MXS&3n(}i_=AIf;S`AK8A`-}s0jmmgA9K)lH zpX{wm__fJX|am!OC81y&Vn zh(E_MEhP;0YEXl&bLhfea8ftaN9^&5$2U^uoZ>>9Blg=1QBJWSlA{hVV=G!tC|WAV z*8ivtiX{&hZKENc2VPY^f5E#2Z!jA=q|V+8$@9=`wVpJ?*>67gH)L}LAQ*JlPeL!7 z--)Ng&ux1j=CI;2Xf07Zx!_MiF^{=SLYTVep8+j^He%gceYf!vu-D)--DYFn4BQwe zDq>&-tdg`LK@L})1Ue{4hmjk+^E#KGB12YW*pt0X$Jyq=Oy869R+G z7u#oJYy^mt-X7E6Y9BB7&%RiBL~gq&9_RRGTzX<_Q}{3E9e;R?@RYmjvgq!LHDDnR z<3fi1%zcS)NNv0TcUYsqx$-9OKM^v~<=V=`mckDbvF^#o&4zpS#LqsucI(~kHuwH{ zpZ?roG24p#RS_+uakt;%viYYPEeN2#Tjs(6pEc|Timjt{rjU#As_6+cJOYp_-du1F zc?4?=Ct(+U$zfe9xX)o7E@%R643{ytl25l#EcaU_B}0QYzmq;4%ULv3gz?j?m*}ik zs=p-FlZU|!-Q{*To>;s7zDt|;3ZVS|`tERmfjCeF?bE>aLk8`{W7}&^_VV>|{8Kz9kR|YRBjU)XC#`q*au4(^9d%>i8IU?b>Po{qaDJr{JF&pY zALbAUZcWbGp1gA|<9$!S_kiG*Mra1o72Uh#CMgH_Y z42%t5abWETwk4?WtcTmSw_-=D(y^@{P==lbI>Q+M|35h8bR?x%IR zrm8Z$u07i6eZdO^mwI+si^UZ6okbn z7P|-vN07g0Qu^DAOMaZU^q0PfAs_09=KWru?ZWAct0P!;VJJ)%OWuwX9uboEpH#iD zFuOW!Icw&3Y)9r3(Vo8od<QQ!0UJoAZk9)W9R%cc4^q#7@wS6*VM43 zRyyIh!-T}N-1ZqsgqCX6eUVb_$Cx^M1x`O`3nrUJovor|WasIcMr&A?$haD$EbYr! zr^$#l<7J%9Cs|vywe;f^tO{kS<_uU{HP7lsTbhQ*su$X<7;_(%_cbe*$d=5OR`%Vk z{AH~FvaakKU%02Q=UFw57Bo$gy)}5JOPhhPa+JWW6jWI?kK$^sk_9(L$QU;wrLUJB z(A-iSbjyaHW9Ap)62D9Gisnn#z6_gU5T_sgY4M#sbgqU6jh@9L<9?E)EZv<+#gL}G zJBY?ej>UPJA%RDMg)ytuN7HLb<%S(J`(j4b2u6&1C?XB*afNG2M zk;vZG(>Lr2ntir-h5ibPx;?vR{z~#5u$j4gC+i%unbmsd;vDTZVe6jc{RCmVJ@AZcf8p7jC~n=_*>C6{bm3U{oKYBj;^Ka<5kmDS*p@jiKzP zN_+u&kuS~2!fFc2P0n0hO}(5ydb*lF$~f`)@_0h{bN5^a1d{!b_h(v0IwghqCNPu| zCE3FcY#vNoWO4vj=&No!vJmTXrAqw{H(CE`9mHazjFqNUG4n}>P>l@pHeL|gCrrPl z!K<~S=hxp1QdxTk!CNf>huC(Pia~HS`zR|GbQ-l1H%sPsm7UlpD>ddoz@l5{Vhwj4 z>Y1WFTZU~~)D*|1Qe=jhBE3kfOW@lB*G~&i2QHh89pzF_JwW_D zi95n9OL6O=IGtCc=(T)lhY%zv?k8DHm+5VrHbxdft{Eln)@>kh^RNxqB-u(iqpAF7 z#ZVA=$0;pKC=q53q;bnejdquEbUeQRj)jNcBn7Oo8ILor4yl@GSz=rx|@wxPb zC(=X{m1>GfPv&*X)>_TuWZqS^bv-9RLMA+>jLQO44r{2;G=L^);YO}gedH)Tf8o4i z-EFy4cKdh|u5oiVtR95nG7$@Yh22sV9N3NwKv9uq&E%LW@k(La&|b%a7-tfiip)sF zb;)qX@EAqcBHVV-TqQ^sIlIZsqT0(v19P@1JGO-~%vu2gLPuTuUY72v=kh2#3Jlz4(b;AozE8WcUCCsSvj0eIE^V_h$x_(c#IH!wXCDvt-%-A_!bmwGJe33;l`S|iB?_jDN zcj&EZ%;P&C?uVlgt|2`4@AS7si2?*Tlq_v5W=1|0DuvBNZS9-+XVVE2`SL}W90QEt z@Y@7#l&;J{4m2{nvN5eG+^E%f@G4Q;C1jw)wD-b4`w$OIxz;m-x)=J$Z52U|=C55e zT|tYPYf^)<&3ERp7@sPBA4*ab3zc*T(9zy^vIa;tHGJg+P8)`N4M*KX8_U0x`a|~* zV}>KlDW1F%7v9#32F~_!#dn*I)~`tr%kVD69WkE2lqca#^M4kRPMd^0y3o4)!1!%X zLl(JoEU|Pf;>V(SUbtAb$FP>)c4u>6^yHno?P0(hEZ28bypzZ#-F8Z~E;eoLj?Z*_ zTlhrZDdX;ASo8I7i%tKOln>od&j=uafRHeNfY|a-4Xn zvlIiqaY>nVR}JS>p?hhL`cR0@&j`< z0dkbjj+ohlM*(5eFo)y=dlF!J^4B6@dWzR*P-R(l^HuS)8wv4AGst}9nezr%OAQ8g zOy4peQ^^#jrbA;KA;CBGthXOR&CFtZ5sE`rYqmo)w3)0FMn0hlv94pOp(mvU4CO|1 zwe@LMtEJ0Gw~lJ4>>bhWT*Wf|l2wQ@w~?!(c599@lZ*gNSxw%K+o6v+k?9u#HZVsV6yx;&=(jcU4Q?o~DnjOv}9wu$1 z#g?O4YrUjk0%il`d0sHRc~(OU3ju$YZhtmfu0@~ zFKAEMvubr5rZAhQQ_bSxM>}yEFA~lmx&l7O(3i2o1qw7^%$RV~5n&_|V;T(W2zfI7}x5MI)SvsH*P?tymD@E~?z}Rg%SQR)>YV-KU8e?SpZ3PUI z6Edg^oN3xv#&(g&A%e!v!;74(;*crx9wY$>8B39FfK@b4slFzxejQ5Gmt`ueA2gIa zod>kkx}xu2W4yHyXpN)2S(+TWw6v?o>a@RGMN(HpcS7fy>T*Z^^bIj8yi8uWB-j!q z+Bxp!P)2o0Y~v&5DW3<9>~~Zt49}tm00zs8Rqk`t27%m38xB)t*@FbwsUCmH_cp6| zLTstr@NnnvGEbE}6Vg$(RP2hmYW7B_ez44$9*8O^)2)`zT$SqV3|pxxqmR|n5Mo$S zWeuW%sj1NHrUiU&e#;CZXDQW2WS6u=9a*HxGr%i%{K3$Gl9~>8W6DmglhzrOdT#a8 z6UQvYtH;v9IKDGzM<~kRn5424vO3Vt15UmUo$u)NP@aq$WF!U$>sG#@o(SRTR=Cl6 zmmR#ifq-haA^z!H-BwzUv0)$mN29?=Xo0PBfiRiUnv(pYvolPQbSR7tji%{Ahrk?5 zVbi?*>_nsRhfb2@3hI~^(e`oQ@Y{Sm$G3L9s z?MG~3(|LmP;&Bw|gzl39E^%hv6qW>^HQOSEKWtxnS+|kSJgtz!djtOby~d5%yaHSV zXkenJ-%%fYn_>aXnVkZ|B${11y9uJdxYHYD`Gv0*8|CRNI&5<%iI67oUnoMx4Ivcz#%TDoM(|Y&ap7o4a$8j9 z)U9Vjq=Gd(K2yIb#~JU>NxMc7r%*?8x2Fg3=Q}=+-e0%c5)pOF6=`O`G^~i1lY*Pe|N}CJ**>*f8RG`*i*cxY*Sjf(Qv6!8lUpvft=`8!5 zKt~Q1FgiGHSI^J=|-xBf4b!8F#Vbg^FTbaff zVH=1vWJLfc??*64G$=PK)ZXSDr0#fek4r*@Un|Qijh~m4sd-On*Re{#9s8mpxZf8) z$+KJo=-0uC-j&VRiH%4`-g96eR0kdPYgfgwe8UwLk((Q#FVnNVZ-%_AEaY(P^?pWA zsr&v&FO~h~9|<{lK(z*kNxglWX;}u76O=iKxP}9WK7;n4W-ifosVRzel8W#?i)p;p z3}rKV$=O)xS>MkavNE@kHA*M@SK|)SpTN^GBpN0snsT%*2YBo;g7z!fNnAYS zxx6+wN`iCs!=5>@7CBAe@Q&$$C%<~(!Os4WED6|SzRUb3vk;lMBU^yF(xKuAz|Eps zFc~Skz0*&Ui?4c$-X^OTa+SrHZ3=)x&gwa4WFG51F@u)CY!Y9ZX$`4r!N}S_OM$&7rGt{_YEYBxSahM$FTXZ^D$Ga*M+%c?762{hd{ zV$Xlyhi5n*C!K>A-F#q}-P`Yq&F)cRuG#hjtE$=IopBf!r(4o(>*yIIitpf=Y3ze# zIK8*%{|z19(Pw(y-(f@ayO&M%zlIKLYa7RZZ(v8twnKkQ8#5x)m=IXhS1fWP)zmdZ z2`Kym3m34WQxGo+lArC+z5n2o>RJv6I)<;TY^3U6MW4a-z#<9t%n&-FXwW6h6;dcw z!U`pCcVh(DRwmFSwbF}X_qlR&SUSaCt7q?PCCu{Se#=%CeV7q*hP zK6s^R{5nPw?Is$ibq2#%QeJbTQ|@yamL4M2e;%d|=w(ZkB495GPYVD~F@j$O+;6XL zHdjF5(=OF(;^&_6{6E<%azwl3^!tm7@cWDEyAAyR73cmddpX)z{bvJTWz7La1@kMD zY-7@h5E0QFDLsiuLJ1qJx}Y)%yQYOXZD7@1p!r0$&8a?pMJf>Mi^9{)#8A{L7DD$K z#?ezn*UnQPr@Ou#&MeJRkSqSm^QisXe!C|2^>NSir(7Oce9s?L)<9yw327Wk_(mU= zRMzJDEuF!0AT>QhLxR9h>;2vima(|`w>sKQXowtEfZOCuhDea7Dr==^uC;aPTA0Or z?g}$pbQ7~qBXpgH8qRmIq?IM;nDK<6z;IGtZR4=Wd?7(5(|S%@Mo)<*@~+qD@5-Vj zkyO-*VT>xQP-BwQV3SH)COeLc7z1-Z&AOsx$PcGRM>T7=HzBIIYBeoE9)LeJD=lub)p6=s8)n;n7HdmRd z=2b_9mDb^tt_cl1u4k7j<)Y--;GjX2tjlw3oHmP2dDZ5O>(Ji&paXYUfD$|Hj*2wn zMBw!3tYy}Pb?jMHkxePOOPhd@R@+)$RgDB26X_0&C#61mvKg4P5i^$%ahw7p{6j!M z^)(Ay8?ioivMDjQ8a-o@8YYy+emjZ#%;^?=27(SGZ0e`e(O$JRL&$}akIAQO3g%3p zJ~qVO3vGWP;n}EdnV1m0aGhr*v}Ac9cEGFs8X(~%2Wj`>cazQ=vR zXnDXbCFWT&-fAJCM5ydsG6mcx2E<;bR5t+01&;V00g~y(s|Nps*^eoMoI|mDmnL)i z85^^oW7_|Znr+s81RLLIOSVQdxrKB0_^?eD!e}x$ZxQebSY8C%B|jh<+&z~J|M zLX!FO->e`1e5OHeFwFd;zg7D7miO%0uMK4#8{}_KDo6uN z!m8$Zu&R}cGR1P_EP3-{3V1xW9~5bU99e&xFX1>%>PF>ba>_IH{phhjUk|=f2UB6< z>Yy^7@#Q+llkH_H44;qJ*N<(eb8f~Ku3(}|jTW1oAN}W1F+{6IuDaHsWhU*tT-DQi zF@8JrF~;~%TT0Hvwr(WAC(PjQA#1$JJZ}Vwf{z)J!G!fmMJ=Cu&@SnmVSz$Q^DTR`k#l$3;P^J}J&6d{60p#lEtvN1UbL*u%UtS?LNMDdNmkma6L$dZ2DnjPA z%y`;MI=$IbEuU1bb*XKgzxw0(ps7&56K%gcby6&*`-P^UbW4S-Ly;bvKx1uf((2s3}cxs^PLuW5s68O=XSM zzw}@Uq{_j6JrLWY+GO#^40q(#*n+{lX61=!vm{8V7XfAxWH^gWr&XJ~2ow8E!sbyj zvSE+LpbjILq*7Jd^zvJNwKyGeim*{k&2&Yji*C@a<2hKR+!bkD$)iD*uTknpQH4|N zty)HzOSz_uet`dX)j_MbM|Je~MYi%?Ncdmxb77lro1Wu;Xk7pG$E;Mna6l2k{IbHf zmvAovH2levFGwP>`W`&>sFjrLF`!8s(0uNhc&MAV?mRcXEL{GAHfexm-!m}^0(=TY zP3uO*%`l@60ouF09^HIa+D*Q=KR|krFO3+2qM1f^;%UgPOMR#7#CPwQlOC*3((6R^ z3ToqqlCiy6!Z_CHYs|OFgoYIANP%+;e6?UpU$ZWPGJ3V+<2=f%F5cEnT9lPGoktaD zyM70In=K^?md)XMOlMt4s*`A;D}qvkTQlubRoQ!+gF5%DX@kS+cj@`(uUe7jcM@w~ zCYq1cHvU|^NqTjH*N@PlvZN34nF&x%=tReBM$N=}iiqHUJaDXR*+ zVDCV|RR5iFnY@MA1(`5x%f&Zi=*h<=KX%&a>|&A3wTl1yDsZZhO@7q2s2lw)6XSuR zyQ|H<-kPO6eG&R(Qq^ZsBZvD9{8GN2ZJ?>ae};HLptZ}BIqB3GP}{m!8oY{PaR6uk zFc%M;3xUm;Q60>PXw^mLG$sB3N2!AN0L-%4x7`$*EA_d2*#&r?YvVP|rsxWS<_X?H z=2XMlpjKCI;D4iE(VVWd=6(YX>;rxV?M|3tXg2i z?cD##`epk0Ws~MzN9KvAT)0P-O|@XF9QFDp|2eQy?KM(yK((EG6~9Yim|VK{US6s{D%KX9kDm*tfxYPLCk zt^y)p26D$NBmcpQbH!11j#nW=&lOfxyW{I8@*qgWyWjxmW-f~u&B#Zn@+BbRC~Jp% zHcW@GBOHc5fX*OD*<6@OBrXKb+<{rTwv}_kwCepzeA6RBDZUg0b5Jjqm*1FiZ4fF_ z3<|%;)A3kDa{M4mY;m$GYMHYiA|O8Y(`k3HY5s1I+?Co_OrZyG@D;T`?dS&IDVNWW z#4v$>x|jea7#o zS=JL(&SX&(5I<$L+8Su=DR;qaC?vJ$%Kb2CFp$E58%Y8Yr6We?s%%G`n^!i7ctetX z$asH{g(;xm_XIOeS;B-hpgj&}a2!o|-c4q3czA!lK7#Z>qUXyqU<<%l*S1t{%c2sl zNA7|SskaXk4H{H{ePF85m;RC@Ftu)hE&>yr&(cAaKcX48HJ09Rf>YZz7;;{0?9icS zy{{{`I!$V@w9?(0dt2ruMRX&CtB+X-58^j5j(28OYF=z4osM1P#A3EOFvWe$twTGG zVoy?Y-O^CNMtM!6yB0eX#|$SnzU;;)ddioIFUwLZsFFbl;oB6^5MkNcV$421@kdIi zW-)Vl0G#H$Sz_yb&Fbyxkq+X*CV7o6oI$lrB0qHo1 z(g@E`p;Qr9yq(K$F~paL#)&rV3|h3y;->oa2CrWHDXOQv#(ayHOReRh4l?`)`IuMz zuUcVeUlbm$u_s5erfhW)85AIG^N^;sAGM&gIeEXW(8DO0VDe_YvwwZQ+y_8s!~Q7M zy+;$VDhi1}mr)Gy3 z&8wwsm*G%-qFkL{v=_y9XO9K_1LBm^uuP4!qd3FH)dpMOpv)(%f`4~p0VQOC9q6=x z_=#_8?N^48Iv^3YoPZv++O89URz*st*olsJ-t?_vYs*RUt;V^Wx#f`MYUae9;GWz{ zUV1&}NU)HNBm~}?dXBTDzDEy3);C51X5MwEInQ!N==Qr^0XgFj+&~V|#ZLmkM;wFm z8nG>=3A4>TD!UooDXMf1msi_t8s6CtrYWcTZj03EUEuspG7~h4)ByF2bh8yc-8CNo zTh%Y$yn^KU@GIIU2X#i;44mWb2scgxm)H_9G|2{55xb4B@(ZE2O{(B9 zz2m>7&iirxJocqy zG_tohGL+W0GBDKtFMZj6@&hxKFRcHS6~`4D8cnsd%2i8m71|ezLXiXwu$eLaz#+{H z3>aysGmTS8>tR-#=kSM)5&k)@n;Hscfn>;i6CY^~)13|`-CxhIa6PR*C-KKA_6syb0Jx6Gh$Ey|~6Jj=i`#lxJkrFUf8ByL>(1p%t&e&nY zRR}XzbC#Ty?R<(ocao5vL5_W3jPmaZy6P5l&uLTA-Dy1cXxMpcXxMphXM+B zcP})!2X}XOcL*8?4Bb8Rt=spT+f(aZwW@x+|IXR_oc&mtk>%P(bB@0u;dGt%);4lC z6z`zosO)!wmwA;bw0e^m&sBb>i$G@a&G-0SLvq2ht5yA+$AG6wR#?NIS`4h?5@EHdF*LI9bDEK4opF(kyQ03~97d4viyqN(3W< z_t`*PWz-;nlsQ<>kwl|oTm1F&hY|bHAerb^pEKBPkd;_NAq>uz;OZ1h!K>NX=*r@K z65}l)G}tYvyAhV>NZr$R!$6&Dw-DTF1tUsq9tYM+QAKV+(!D5!F07osxIn+$=BS}J z)-=WT)*zzQUU98le}PX|KCSKSvIW9D`)PpKHmqL;F=>|MOuL|V9FN?PfYwgxqWP%eb}E%$IU+E%kyL^fE}%}9o2!O+g9<2o$2vp z-oLNb=w~%&{qJACh{66pn#(`a-2d;WQ;xch2F_>H9)gs4;t%?gF!mA*Ad<^hWoq?x z#YXtvV4?-ml7+Q1MlSe}q>QT3@9Q1}Z?Ny*49>Ob+h>|||C~h$^!(aWe#(GLX5|Q^ zo!YX<^LzBW<_dU!9e4P$HSGQ)L=(pN3l>)1m?m^z)S|!VO-4_>u@>x%lf2>i-*k~u z0*+h3p=0JdW+2>Lu({=9VKh9%N2bUK&Zb0EvcDHF=PEqyx8R4(=0l3Ghi~g*96WcJW_2H}iRrcf}= z1BHMadU6p;XzXDZz-p3QzCDI0MzS_Tw0ph>`u&l~JZ(AHU+rn5K}NUE$N|ST8j}8$ zt4*4s3CI0|ltIU4xfJH?0;D7+)y*m;{Y_|QDVNe?xVD+dkX{rt0%oZSb*nq(2~nLg z^>acO8z89xu(Sj3)viIvxbLDgEe< zff_FkzmdX$l8TEb)qUm^hw)7xcJr2lnYM4+nwk&z;`h-*VL~r1L9j!DOjI@2rv8F* z>lgax=1eh?R8&#{5gcXO=;n>|4|`4-6e1VsyID{jPpA(x{hkR0THa0#jm^;Nw#yRh z6tXT^(Uc(;iSn0YsMtCG=pC1AKO2A!xKLO z{=Htcxa^KZeRkHFPtgwBe>x1ynb{eeIse}aR<*{e$3Momo9r1 z;Ay3*>&V}2VcDC6m0UC7P8UXHvolc70I*l*1bZQ!s53>w79Zewz7Mztyu%pNv-=(v zrI(}`wD;-ZuYONk4hBaZFNfRL0pD&gyCVxwG>lhbDw$~au!r^c#>a_kqCCkUkZSL` zHokq8@y*(mNzJ3e?U)UgI?!f}%8odMQl+;L8?Yf}MQV@u7Q$G12I8adGmV(Q)OrAb zeo*VpxD95{nT{*dBBA%kzBC0pYq1}88q<_QWf2X;Vfx1?;F!}VFx8LM>o!7-9>Z@2 zfvc%Z3^XPr*e%W^&C}e0vS&>B(j?bQLC1;9(pnm(dW$gwV#Md$j7Phz(p9ocrIi#E zTy=u-^0RCr=ZzqmhQYT)>O9emPYVZ)t4V!!u>XB>;MjI~Hu#GZimE;=;EBR?j6kjO(`aiqStm8lqFxv#xbqPz3HS?2o2jK~R_6PERO4t9aGYy~~3 z*x&53n|ld6gXDYUXo}jCr}cWQQtnQ+_KkB{3uqs^Z}nCiu0RK0p4~NOv%tD@T(c7@ z2Y3UCh}OZnIM^)KdK)3DtQrRiMjOYo+>IJmK%`y$pWh9S0mu2d(gyg%pp+w{X-3vz zNc0NLIq}&E%41c1FQu)Z;Wjkn$Gy5xwoalwIcSGnJLu~jKIrRxuuSL}cZUAlJM|n| zkhVHgTQCpul%a0lyr+0y_;UmUfy{HLgTUdq#d9`4;URMQGTksFw84%HU_fMw1F)~g z^-MvoGdiPn*x62~;U>n(U74+c(fB}3ji!3o*>WXo#<{k#W6G(Rq&T1fM}qhfWVnK+ zcmfyaP&>8xm8s#OjuX4~f&GS$?opkjusJ>_a!tT_?|Z*#Cz6*NxeJfnQF87qT$D6t z4wL6ww>cCKrHDaH#zj15QY)VZedw-awbfZY_hfgEg=vA{HQ_P1B9kyp8E6?a&@I5P zcCJm@u(6B#GbV=o(h0(}30-#are&;w^+u;Vp*)FKsR*9zOgr%Up6sGR$^ZTiglBv| zCy+1CS^(>FQmgtsIzZ5BTc(4jG46*g4lXwT65bht2khn^UtNdmQYcB|5EStnj&}dA zqPRyFdcHqSsE&0KNE8{kI6Z`AXuISEn`;vqgh7^v8@5N5Gq3>&voMN7F zlCb?&tpXV9D=LtC(AsUIB;VWGM|QX~uZmtesL3+m$Zo>-zUlRfw+|MJj_FsMi8>oE3WtxSI zV5%xAsSdzH#=9W&#p4+l`?y5eY@zc0WSSXZMMlME7tWG_oY=gZgE?+a6=Es+P_=)w{GB(YsQgR=S~=l zRMQ~Wx|dd{zv%1YQEcqJ-^HJ1R!`v-j86CIP;y$%^QaS|)(CXh`F4c}V}V|G}mG2hN`ot?z$6kf!#FjJuh`q~UQ1&Rk8WHdRO<4;r=nnPm zv$~(a{Xb5l@}E)9rvLZio}!_nfue!>E}uA>0T;5N^G&T-86F8Lq6D>SFt~YS@>@xv zI*%;Er_pn9MsLaWU)ZjiE%jGJf(vc8>cX1#vm(U5uzu&YCrR#kL<8|Sw}%&gQ>(8$ zJ^{xUIiuUt@4nv*F>3(k*>U;M7za1qxcob<Il8JTN9;+o7W;S~|+= zHT6*2hRSKG8E^WAcGw;3k9p38c%N&c@%)OrlvJ4&pOHfu)S%?16CDQ=1I2-)AmV4c2&bX9F$=<^mUmRkaTB_$`%4S7WQGDV3~(Db{;ribA~UkaVQ2@e-3C z{g6ysCGzlA2E%a^L-Q?IJ@2Oq9?jSyT}waN7dlzpdT>PBIakM) zaxe3(OD+o2A*(Dt@x59OnnesrS{xICCR}kT#9mzP@Y%FH!wVqEJxl>UuL64s7v3q% zJX+ggzIU2J>jm&~nHqVXaKywF!R^PDIDOmm9_6{B!CiE5XUHyKThghyc?R4Z{dKOO zumk1G>#)NY(j}pZ21?FN)eAB9YF?vJTaNR!9C>T?lZx24VbD^G1ZaKB+VTdF%S^d1 zp<>W_RU&2V2J#w6a4fkt?xNu-ak=h5g^x07C#3&w&T!TRdW^a<8yo2%H(f)!;u#W6 z>n{HcqQlcm>~4=VlmTps8?wM*4Z#T=oWQd z4##qr^+!|pSD@xllSLteES>8HPUo-UEP^DqF<4P5P^JNTk{6 z02$=T+8AQUi@ha8iR}*@vV3MrUVIh_B?ufY@`bccj;m057?mXDENrr}OUQHL1T|Zg z4V@Lbfe+=`TwMJQxSmH68nDC_#LM^ns`i5XFs5Ot)Ge<10wIy?;H3P5ATsupuH@fi zmbjdfHzUKp{G%7{45_U6%(RX((!XaJB0fK~nEVv#d5sWo!dfB4E!^!6?1n=#NLmQ> zlSA%v%Dm_Qo`+ICHal=j9zMmi7w6#Q>Ky%T`e2IE&T?!c-N->&8ht*<9`x zvK;m=xF6e!?yq~O@GCU2PL;sbWQ8YPwS0iHVBFZZE)BHupSUyo%z|HK|Lmh8BaFX5^Nksg^p4|_RMI~_5mP}+KG$e44sx#`Abb!U~SI2FmIXN+|mI)`kZLefY$2Dc^bdjkusvq zfLu1k54x=ORv;5ibK35FdeCg6b!j0qg++SBO4CCjEXXQd-8FlKTO)ckdx4wAB;^Hk z8tFDYLn3jSUO;7^{^zqUlpU8Ywy|D@J~zy4{?5&L7DP)j=oK4ZwSY+L7M|E z<%y0+xAYnUC}-~1W9`Wwig9FKD?LLCR!2h-<{NRpqF9~L#gu0#plDiqkFsB!kQ&58%&i%n$DZT$IjZ*1v+9*57sjvF&129z zSRyqSfqK=WC{ zmPt~#HRwu_8_O?5y>m9vqkd46D|vXCPj|HR5uWtMqNy;oSxOM%Xu7qeGR!N`bfRs4 z@wFQEI?gE>G!Ap<;%v#c!R7aI5<893nUxZ+spT0s>N;3JzlWDo7%UyG831_2V0KI< z>|_v}Rw{SWvmsD=o zq4*Xk#fNrSoI*p>QJVzXT zZE}JNepL}FR2_&1<(NIjEt(4@U|>C}^7Cspl`y(4E^(Cis+*dgtcohd-?H`Z9D$`8 zNkr~gSQgYUnp<0qlRuHO(o~Lne!AgE0{YQ10}YW%evzqFh3+br2wwt~rzwYb(f1>p z=(o{o7Rq7&_$s0ucDme*AOR8_>4eqRf@M=%5BZbnl;){2po8tU62>VjR%!n#{*ZGO zfn8lu>vS#dcnp=G!F$ECAwxopoYN(n+(X;YcxllYb*~G){4)G#Dz~CfbL$WC!$TuZ z=XN{n=@t4DZwZbD{^uy(sx&x~`=V^o@A;I#GWP!dvFUXUXRXBj1$w2nQ6n6T6~YRT zESA(L>K#axMi4#&4Rz+_njkOV707r@4#GiNfCYs=`Uang5nz8`jH4tK!t4E_#VY9< zE(=zqK>RdaqQ11C;FK+Hf5)iT%aB0KH(pM|evg1%+|f6~o@TSC2+gqj{nXGCi-J!t z#2EgWWW4+Xr`tfiNy^utor0B$KCUd12vC502d_YcHZDL(6#(4Y8T%03z|AfqWfg9dagcWA^l>r{LV)7}cgBODlWrrERUpHHAO3br@| zhv)R`^>ySMpp?5xYlr_a{M*Xk%DPvk*yR&m{VxNYWKc)7Whhe zr{+Fk3dm1Xk^Mil1!XfQH>*!DQO?2C&DKox^QXx_G)4a+4&?j;R}?~hFHN@B0hQzj z2Eu4JVT?Kd7N?06ib&T^n)`u_oOJ0og8A};P<&xbh_8Y~443bq1-+I2p(+Ho>eV+ZuZuPZT* z|FExs`-w&b=QF#2JM4&=h$A*$Q`xS@!p=6W)WX^uR?+{B@_0e8OFx|`?#CMA zv}H#z;&d5PrI1^WvD4`F0|W&KF1PmUMYZ4aE~YBZ=h+6(=J6|6YAn(@EAyI_fpdEx z$r5{Y7Iv&xgZSIN=octYX{0XwhIKAa-5Qrx)UuQ&U=&sN(zC3wKxx*XoX(ptxB6TD zv))SQnl$CDPi)Lhl1y&ShKe~lnQ!+F@RQhd73#I(N9y@gEQDpDUsjj}GSTFQTWe8j zF|m|VaAm5+`4Ds3B5=eiGx@?KV&4ePXu!BNbexH|&jkE8oXRntFyLpb#9IQ3HR~5h z<+H|fcU%ttKIZrB4p5+TE>{4_iI~;AD&Qi+A;lL-{4dwFX8GYGiSrqngHIlSyLaX$ zJCUdKveg2ZSn zPHjdIhFeO$&pNkCcLJEgk>*5-6~IYU8*t5q6Mp^sxN+FLICA0>d!P8E)bRhOU83UV zXy$BUYvkhcA5i!lb-Pd6D60RJd6L{*a_VW`(0*J`qx*K2m2X5kBp1nt`=!u zSzDh=I!|r0T#qIAQ}TPh>lnUlv-BYmPczm2d`U#Xy98U|gpf;SGp%$HbZRG@SZ%MA zj=XA@jNMIh3C|qiaIS7>-AWZ-9^T&b($6GL?n=+ZV0X#@<(qLG7hx))U8)1(R#F}t zW)z61sT0@IEd~2VqGpxVl;36FE>}8aJ5t+J+L3s?DYm$soP8iZ!=xAD(%tul0pEY$ zITjKt6PynW-* zy~IOg1)rzP<|@2;%eGN?X{}kr2D8Ug)AQhKP_y}W6#x8gE6`)9MYg{zaP6_CwnD>^ zjB!banA*8THVx8)WxFaZE5+v!{4TAA*Dy7k0zOa4h2sEF{Td`gU;XlDL+B#K#|I-T ztLC?zClk6p5d{Mhs^lwBEqKoH+cKjD>AaNF_Jngb+>p}b&i8t(&lMRo|9aknDp_gP zZtKYt;&H1F!M0I=JMF#U(V#jmIl4p7zpRMSdKaLRLf=;)QMuWXyP`IQ=fxA}{Mn5$ zH>cG^MDiIO3&EZJti|LwZpijA#A&BZgs)v?JIEN5u7B%u)h$%RY|qs#$I01N2Y|uf ze>?RVp29tf<%g0m{ef}H!zN2miz)BC9+W8A@O@N;RKXb|IB(w5Z(Be5fO9J_*oD<5 zK~*Xiy$oLjwTwWBYKm9*LPA^RhCD`VYx*z1^9p*dtqgPYZ!EMngZS|cI(=w@_$@p6 zD2Gk7BsAL)Qa?{I;-F;&VO=qeAc@E`edzazzc3F`!x0z(!FQ14j}z|kD?zDpb9EsL zPZ1XL#dGqs+Al=KgNDODDC(y358nqOH(XF%0(PkPbKu#C)hTrFXTKVWSl2Wx z3kwA2iOnPMiz<g@I3xObbHT8^k{sC*90Nx3D%w1S3k!l8ttrWvAwLEn=@ z1ABiEMj|QB%9lxTyS5Ks;Tsxmcnsn_{f#oHNR+Fj{na>z_^@r@QQ3H}Fphlf(_l(2 z+xJ6;e?sDy&vnN&{qd&9c9!q^Q#b*bX4q43E1t)9r6@lk+M%4Ug;y{I7}8MdjC}&2 z&{@K_ndgo7AJ|Mo6;9A7LZS0}U_y;XvcX8AXyktxUo4$?ZV9gdm*M;2<6)6M+;{Us zMUW9#Mnv45XVi41y%{f!ixjqwGA3LH)E0|yc2h_{lMAXsWDlKb|3?!(6V#?3O4_tXp6rDuhT+QZTg&^_NTkDA@2s4s~9eBJmD%nT8_N_FHW5+KaW0r4GJ!m ziA-+Fsr%VV4yS*Rqtn_M(7lc(J0fiDeAE#e{3e z;gQhNJ9-Mdi8Wm|$RxNyqii>|;~BwW#c6vdCvYEx-=iZ0aVv9qL=%mVN8@`R#8(tZh;Q=G*1+}NLX=uOjtHEg935m<-;&o3OiC* z3cKmiWw&r5=Ot>@(H6Ja(GaYop7_INtR`WhX`j?uia>TOkEBoPfgSaS+or0s&vDmm?J+ZmsxEah z*`vT|@N^j=slOgD)OD}P0cVG}P}Ig(YORS7q^ub>yN9>i_k*aD2UVogJ+(0F(hw?rMqYplWByu&;jdxw2jU(Qqfx^c3(Jt1p+0q8XNxBO-UW!PDe(gUH zQn0%WNH76mZUYkY?V522f{HmSf{&boBsWmNVMRbpusN0#jntgdCgIz5`fVQR2S2vR zZ0S*IM9T~xBGztg*!iMWQ}(&yl$&JWE)ptYp{yb=R4r5?Ig5tEr|*zUHESAzMG{n5 zl)4h9Ek=xgVp*Y_g(AA$mJiI9%MDW%LtJv62v-rTAyOGBKw-y#L9F=1>brdQ2l<+C zd+!NXC~t&#=fDUH1DplAelLc&Z+EDRZyQDh!XB3%t;ZR9pY8J-rXnt%O-;wu{sDxXb#O_X1e9}+L zpLL1yH5Z7*#ozKK{`X+9``-W!x&SopAy(Tu?Jnaymsrm0kv^zxNeF(-L#_?4*o`0zDbK8DJx<5!GmcA9A-ys_D z#3TuXHxY=mh8>lS<;egd+tn^;`mf+fePCisAbkd(*w@^;E8p@IxNC&J%S3`izG%$1 zMvDu?jCXxyo%3zv4P=a=BXubHnRD-fln6#@AnTiWub+QAc-Qma8eD#<*oWNbb#eWZ zEA&5kp8iAmpyJ@>Y-09LWJcD($kfc4l<9wKA5?S{{&`cZ)?1@*Y>WW5z*Jr|_k7!8 zR70_1Rf6RYeBPON<*{m6y<+zv+O^1t5dTNIGcLjXq^EflsvU|UhmZNu1=se=lv(%3 z`zNDKk9SP4SB`2-OLj*c{kLYk@c1tK6{V(XK215J?3Igz4Ss0foi?%I2uHhbUY3$2 z&k_3dGUlAoFhZjmz`FnUdTHbN4JBj8Mw8yOuhEnv~68MOl7ybC@;XxY!JF5& zkd20Is5F&7#mZ>Z?H9xfm*bJ#f*(&kyY}n8^A|8Q zs&&0}{0RGWO|4fCf06@h@hsE{P^T@@@rzidWC3sM!D}NTKe@7Jz`K}8TT%X~CS@`) zH%aL|n!ew&D|Xu5FBiyRhg`FSSg# zq$)qj3lV!Zc?P-24kVD&!sM7&x)o}G2YU1*XRw5%XZbrtwRB3Hq^bas*mf9$B*m>g zRA!8vKk`W}N>7Q&4l?~d%vW+Ij91-3Hf@#GSe*0%y#!Yn2n7g;PkQ=)n{eAue@r@l zB8D!X$*})PmGl2woBvvwO`2Z5x@u_e_RVRs`k5tmb6r-M&GS;3r=^n0(pmJZGsR6* z)pkO@8D4Uwq%2OU^wjhbgFqg_C}6O(8tT|wv0eN(YO#$UgYZw_N0<@DFB`=w*1{e_%-(@){ioH}b9VCCJ$NSFrwD6{fo0PQTZ?ijA+rGpGU3ialOxoryaBm9K1;H8xSn66 zmo$sT**t^gqN#85eae#+^~!#AmfF!=GNdX_Q-}vNldcv4+t$L87y)t7%xZl_55J}t zX>TDVWT$a+lJQVb7ok=NsYCm7@Yy?o4I5Fc@8@Dc`l8qi8Zb4ie8)M=_xg8@RUn43)HS zkgs7m3f|P8fhhK7<0VU1WXI6S%S5*+PO~Kv!_usnEEtE@^v#Kg$<>v%2zSzBJM%PNa8G`8aOSZQG3w%|44ZJp4l}C-g!HYk zpuDB%=?_pxt=>*GHdNl3vs+Ebi(!yl7+Wo)v-e+_H@dCd<- zfi{Krnc&T_xiI0%NemkcyT)6Ryaw~TzHV(c^P&9P-06%(^0FD0r&6znc?Vt*1_SJU z`*J=-2FrneR^kJK4Kf@`*-c{Pc%4dSd>^P`l&(^wifAw4q&qBV5zFUv5az~i{utfqe{Q)4VgpOSB>x@r8B4%4zpHb@yO{Dj+Cvo z0`17Rf?F~@acUTy$hPc^1!~xD`E^b9Vpa+mtyWaj$u;o^2V6+pGfzKQlVLrTr@x=MIaJM#?f@sbs(?uIFj6p56(Gw6E$=m)GZ zR<#Uli#_!X^KxeuwQ^|{wTj!)%zSce(3&cnbz%j1B~@uAsC1Ulqy$MtPJ_j{Rdva- z(twnnZCnqB2%wS+@uu~K zg^xs<-A@g*;+UhP2YnnhC3S>rn;21VyC|S{xutcy8hCdzu+ZJWl!N7A!!;OQufwtt zNu!fz5btl~^VpVtg*Ho#BU16m*>y|5ZyFtDktt>Z+yP9KMB)^4FHdGX>N`taWhZRL z#h-T995#MV-VNDjL(<;LqM31@D^VLOn+8^)_HX79HSTUUPd%BY@(~@M;XQzjw zjcpHWeN`RKc_7qz-nM9gZ`56rXcwWY2%rS-ocs>xBnMOwJTvPQjdDNYc%#W`LsR}n| ztj4v$TAE_QlB$R3r_+t2Z|wS*+0#L>+*;!lt#!{Ac<>D~vG8BuRHfXYw6ltMZI9B$ zjI29TOwb+Lr!(aP7@U5kjkOpjCOhD2I3h;LHKR*JwbeY+J>msni*9XtOJJ-M^Zmm9 z+FU9ULTKx6_95@_L5&)ZPx-)B4w;}35o`n%r>_n9lBH}2!c)c&_uf1qivI#=B@YZy zZ3^6O>jn&=l;aEUHWQ)RRWd^;@kS(uP~rt!=c3 zxJ($dnb|&srdLckxop~xeBp$6&W${x-4JJ6=_`Wy%kBpXf(dcQ;>8l~5UIgpGD*5O z=DctBE8`_&X~#BJ_fUU27p*DY=2y-@VP2^;Zy1$D(S=HDRuDGrq06qWZc$jR8)s># zgFN(h6`uNr1H)q*a*gX|#RNr}ER_tTV%BLAfvihuJ*r~{zqk{5w}Ec!Z%GnWvIQga z{x9Zc!hWRiAX0F(!bxgHU+}{~(0FJ&J>4)t8UC+a+(VaKJ6kg0N^27EDR&C|bS{R|qXXSZD;|eE{sR2*k$Qp}FF$mQ zE3w{yw~yN)ZLF64&{CwoPwuz&57;YqDUXhKg`U?k+}K}6ItxsCq0>byQ8{$;Bhhii zRPe1ETFy+TVe z|e?8M}8Z>U7c(YK8n9Hv{vKfw5$FbS35&4Vn_Vj%1>iq+NFw zu$uS|`aX&qsY?z>Wp@6ZHBaQE?(Ckrn4FC2Y$y94MSG}l*w3i4U@h50&ZyXy$JM=D z@|c=m%?#xRrJ{L8!;k1Ductyli$j#tO*2%CmCTV-QK&>vodnkE#JdN zSTrc%X+C1t-RH$R{1bx#2 z5P#}!o0hCs-|=vVyv25l_a*`_Zc>Q2BZ4*EC}Ss0wJFEW(sBGW$#I;$Di9Fx4#60s z`sM3D2o!OFA!{I0bPGv^A#nsHIaqpMW3UX2Ip&+ZPzwnPTtC+Q7zG;(1x%+YMN3-& zj=8fOFFnlUtQ<)Wwexx|nXx>Ft3@smx*dF^9rLp?>YyWU5sq<5V-ngD#5DS;9H)tO zXX_}P-qdCqzZFnO4hK*-MjTvPO^1;|95;N|r^O04zk3Jvv*7-NN`x5OXF+iyr5u|4 zXCUf4t5K4C0PE6^D&Mjl_f7q@;-Q_GA$;cVZaz#lJm|}~5MjmY&j@xuqe7csP;zOV zN%C|(Xg5bt@Jj>L?3E%!sJy#rE~o%ONA4wJo297EGz#b)lEy`b8459n7MUw_6oI1L zCl8s9(y2sdh}%+XEUosbv~n9uW@7E<^StzvDne=S)s}0f?=_Av=JLI0Xc|s(Vo)F; zf^ONAHQhXnx!S}ogb0olcBAF>Za&b9ma?_N7)d6a)~SvK6OiduVH*Te#S_z>tuB5` zodIqjbETzF5paPfsS6tF`zEc45Ec7%FFZ;D9^RG7bBs@Hwz7rv@x{m~PI|Vj9-i5M zrZRcSYZmi*s=_tR423Kn(yg-V)E~E8nYD!RqQvlZM480Dn+pA@_&FuNs?3_$fPf3e zMi7I(Q-A|Ch_c_jb<#JFf4bM$0?nwpjr5$juI@*ms!T~?( za^f+{ly(nTKfCO%7V1)S4FozdD!h9?>-F>IIP~7W0%F6pbsk0f%@_#x`dH|3f!9HQ z1M9;dy(8n;kY8i=1_zxN7{$k$qKE-WtY;+*UsdTDZu$II%lvg8fBv_8>paX)tq?gZq)%j^dlgVoSvNg57?X1kjuK9;rYl>c&^JQd5w44>q;iz zx0QM?Er8$=!NWkab`2oV>CFCVtB=aG~=o&-;2A>pxXY#iPSvtV<9t za~J;JsET^z8SbcAP43b82Tupeup`Ey@)KGOAe6lY(8aIWCXt*2X99T48;e2 zE7TZ%B7!0KL-lC%CFCH4@MS9ffGf9=J2!MbGmA%ld*INCmD)XckI^q_VAL_K&_(-q zQr!4&HP_K?q+~F)P}2_uSB+f^e%-LzqYT_y;+sXGP6_wu_OJXX=0V)=l7D@PV_i1N z+N`uQoPRQfKn9{j;-{lTUir=1PY|h}n`PAd9f;h$5zYF(NT9e%*CQN6WPNLc!0O|j zWcmi&FQAD3<(IDnY55gefzaSp&;&uT9{k5SDrMLpcKjDqG`+hPbso|-XnHubvfGiO z*0A)UypxiDB8*<0B=Ft|E6*t#%)efjs!=3}Tg+Wz_K_fPWa|1_5S z*PD`#&JO0E8`I7BQ?m7c2F5Y}XJCwxni@724BP{6l9ZvL(PV%QjfhAn&i@Gz_07Dh zF>~x#zX%KU2mnVs%VOyEj3mu0cOZYK+|RH&Bu;lt%5gcG$o}-#V*ceH@B*>JZC{o! z{XSM!HC~*IuwU!Pd-CO%_RdjGlZRt z+Oz{E`B%Ho88dqujqWrQ-!ybIB;B@;KFi;7`I(nfEr`kcOu8$SHPM3h(U#U-#}*^r zDu*?5PM-y3UGoU9`NY`-7BA-h83Wk17vr!NZr0y%Bpj%6KC7F^L4@-pSHDZ~*4~59 zy@@o;j3^0?j{C2{A z=^5PlO_!l|T*}IY*8fsIVYNd$lTWL`wm)f&)PWnU84fhM**0c`BhX+2HKDQTBx1PP z7Ti>wYbDV`@(ArS(NOpY@*u>xTk3eUpivfm9lw}YGMRLos|VX%U(y$|8tS7|yf()( zxks2cJORVOP0-8dz%I+n6kZaVgG-Dyb#}(M;G<$PwYpRq!aJ~BT*qJxMIogkm7k?M zeM>A3t@IVNh0K^!xc3u#JEce7UQfOID)R!Fm^Y(h4SSwbn5bq-Iz$6}!1n4?Vj5m+ z%}H0r>9rax3T7!DWR&*)_EQ(#Lh3wjc9%;`p{9%TSDKMn+t=0XN31TM8Dxiqhyc+G z%jP2L>2&{aaX5^HTTFWDPu_r*F2p(YV9rBIU$xL9b3GKj5Ig(l_`x+G@l7s?H-9(T+YS(Bb4%Pl1=W<4Ue#Ex?G zID`@+H~SpOVj4%Dg|L=;KA83BZ4aX1=iM917R9ltb!scjVf3;p*bBqv2gpZn zdX=Ah9V=&+L;gHor=oDBShBY;O5t%tRFn0=_>VP1)x{S1uUsHCkCtn`@ZOI zC5A%;9=_bVh>p2kuiNAtbwXNd(deP@@V4$zoEWAyJf2L~B)RoLFb>n3OxId&WawAR z+I{#^0C8i&Yo>;fX2q#NZee4Rdvsk-6_pnlFJA_k zX=N$*N;)H%-LU?&Jnkmb9_gG2PQhg8Jj^+i9kvgQTS*ZMHpK!C$&V>XoTXC;+VkB4 zSXJaDOIcV-8M||fb?gZus1&RH_6ySKb@ZQDFBk=4CV-jvDYazAN>P@+1Gu~!x-NU* zWrUa|O-J?{b{{ofKyIlkTp?WDOda3uMYNEULl2sW@o;-wG%2pWrg5w{w1u<*61N-q zSmubGnZg3EIw5{d^ei+*!)`MY@hW7Wa*h!eSq7E!vHlEo%d$_W>DXhT{F`8C^KX5D zm#Xb^iicv8-Jzx|zFM|v>9u$}d234k9{LnS;6&+K?-Y}!jAkVhbvYfI-3l3N?AH(v z;kBO4qRz!a<6g9^_N3+9KYT_>LzZql882p1Mj!P~y@zksacRyFIibHSJboI`Rn5OH z3hTYR4LAC;1q!C;vfG-_PNjN>@a)&5-A$gGw9xp7oXlWS*%0B)fRRKmu!`qzD7p{r zGYvob>UjKCL|-}$uUFLYhdT*p4vb%y*AsE?i!Hq~_d;|iGpzRK0dF!pvT|2eazOxN z;h{>|xxI&9mq}Mu6w|C;XLte4A;~ZYGksCeTr20BN>H_+#qS~(#vM`r6Bl}jO=}=$ zEeXm~w3*|?fB?465TyOZg*Z|>>F?nctDwr|hnsN7)miV@+)YUDA5Ohl^kvSO7E#kI zGguF6XRc%&=blMtWML7U+C^Zv!o!P=BcBfsxFU_50lyGSu7$>N3n{ZEXKCMPD7pwv zi-nBUXy1lM@{~9c?yG2{cswVIM%BJMJ>2om71HTz%>K%kZP?~M!FL2_YS$KFp??=# zr|+O%7ql?xO`p~3Y0JrFF?9Vc$?tzL_7y;JMA4QrxVt;SVQ>o`G{J+r!vG1cgHM77 zNpN>}cXtSb1r1IJ5ZrCBUX`W?s~7QtNT^o?tAV#_gvo7RP(C!G~}io z1%>NAY5Wgd3`=9xPD@))v-mM?)}(pLZ+kJc5BR@A>pha+J2)!fg>0$_I<8i0(99uL z^Vf!79!u^)6I^};L)a|JAF1<{t;#!NJl@r)Fug@2=hr?dL?iPjKh1$=wI}2k`VfYA z4g=cempx$b)ZZ=->4(G_MW^+0JFh~1YooGXNAc!@z84TK z50Qn8`kbvUY{@ZFC+((zQ-G~8^JwAWF5 zxyOjIBxscvMIR*cdV__QS=?z}}V{&|Z{+;IzC+r-Em8_bFIG#k`Vg&%n(y=o0uea zi-|<@twY~H5{dC3NY1slpQ&}VKhW6Dg7ijz>Dw_$d)@DTQ4d4?u%yhX*DP(q@0;QZ z|M2qGAzTa|sXP^f@ISBs2Y!`OL4(t8YADS_7I|C4I}Vd`@9`$*Md^o@+1*_G^4Rt@ z&>NKRoz`mq2Ia<0)?cAnmWag`s0*L<;iBxt>_ZpnQBSAlyFlHOrsgCp_`AumLmr=7 zH%)cdk~Tf}V{|-8GE4D zTV~oe^y}bzeurQ(U4=pdB`!MdapoX-W5c|@5)(S!! zlYOa8{2*%vZH}W-mp}M_Jo(C6cs_lU>uL)wdS^tFpwc+|xyZ{$*}dUHvwQROTeR=~ zKr_Y?XP)_`Rh$36A_T*8rbWEv?-S(SVPHn|S#7fR0nrwL#*&~uMhl^XkGL|3rNs(J zx`1%HUP}R2NB);jBS_D<+JXY>z)!81gid=U=yqH6Up+ta6Dr@`&P9_L))9SpA+Q^K z5lNRep!iJqbJlP^<~H{TVVJ##u;i#@#lM&RydBKNJTI{OMfV|hUO#jm>-TWaxIuN_ zawYjONKsDRpgaLgiz>Jt^G!TC`gasD*xMpF>`Sl!vTR{Qo?_p0Fee2{EON}lbfx*y zmn;et9FZ7uLn17{I`x6xxGLTO0^|YtR{eoqU?Z<=#n(QCp0F?59#&yR7-7;__GE9V zKk{T64%VuOC=F1cOo~Q7)Ef8wc&9ngI~Kz`fPp-KPc_fuf@1@#)$BXdG%m8()SgJS zNcy^!1W5|}!Y^YLHjfduxOGi|q9z_gzD$JWJZvO@mAOH|U#W4@>TO5%Hbw}nUttcx zU9Yg%bXa>OB=nt0`+pd4VQ}ID>zA0{u}T(OyuRLK{FQpKzgPsZTK;%hh2bC%l$?`e z54f({z^2R0TPbtqc`|#uhOtaA!XD0ium_$Xy{Q(#U$>O+>cjrSU|`c#<`6QoSswAP z?_GKjD?hHeG0UiouYVG|f(w13`Hgq57VPRIAJTP1gCVgF%- zuv+=PGv!C6fitRXVenG9`5k9qiKV&J8C778jol=lwt&ETCaz5o;FR4)H)lh!^+z^zu4w5e2f z*UUhdtAwkPwPw&anL0H#1|(c}WYZR7Lp#0(nzHo)jw>6O4!c2%1}^BvIrN7OVr!{v zd?I$D^t)4c#`U{1cPb3J!*(e3yW@6@47y`>#0m=rUgW=sE=8hS0y5cQ%xf}G-nI?P*)}87;o8t#ilyzV=oL{4d{qaKSP-kasv>R3YiQ&}+t7aBQ%YzjxIx$18 zl;rgnGN>zLV|0N|djg^!Tj^CWyjAEsHafv0LCBozcFd|A|*2^9FCX zgtbjAhjZ0(SX2#bgfrXRvXB`cjH-+8wf_M{P@`@qsVuPa<54})acdGrH=hjSlgv_K zL9!+WGhyfIUemZ-U`fJsJO1Q|ekApb-L*?Jxm60fr zGff#F12$e{thAJu$Ml(F6#I9`GH|c`^aL?2my>k+>0pCwbsYxU(-`BfG+8&-^#2r1Ja9xvOdP?$t@K}Wg6>*E)qoJQ+y4dDDWH= zjdO@TXkq|{KPe-2Ra)q&S|D+s79Cepm!H>&+UmDNDcB!yr3@x74#O5p69G7imhE06@ z_je%GarkJwvRfcSMEn^@93mva+xEqB%hQ3m&B@_I+EC14A$`tx9Ye|isjz{OJb$@5 zr9V$v!T`_|9tMgnT(C>z??Ic`zEWnN%b{Yl8PdniSWbAY3&LXbeO-j%Xen^zKx~lZ zil$=Yn^88y9wB@1AsRJBeV-$dS{EZZi$F9vsKM_;BB=|bqKaq7d$h0eU(M_yV3kLF z;l2;BRff?tagH6$r^>`8ofe8@FHQJIz68D`vfVo;dUd6;k5X(*HagYRi)u>bj19%M zUn6iqrt9gl9{7iUk<-<~Yip)|YyH-J{SF+ndKcQSmZq6s>efKjICsr4t7lP-Tqxyh z(=-BV*?Ql;h}hx21-mg$IYlj@ z4o+C>1b;NOpJp%TElpk^YfN4s*M5JEA~@|EOkp1^O=+JXonap(t=cFkGlAk*aE#J4 z9gB8M#9f8bq|qVWQ96*j=8-JzSDN5`fJX$Oj|(8l>&g@k_3{mw+$D7U67_LCZbS$# z^LJRAhqi}iJ04>thL6-ph5J$mU%$-pM6xBuU3G@_B2ni*dJvljLxH>UNs-DXE9$x- z9>s{ORIYoczeKH<%6l8?dVfvb+h0KrCgdm9@AY->R*NzQd|0?W_5N;8!|QTT(UdWn zk3s~k#!@`Fu35$pA}(yIJgs1>9@|W8<>U{9Y=Ty1N)0}lMy;=&ah4d*k^7>%DGasJ z8r)2!7;t>>Ch;cZZh5}oep$zuQ*uU!^f{45vITu!lb@aB0_}L4vzsF*O0TdiZGuT% zEH&1`_p+!wFE6h<+r=qDJ$n1_wlpuMe>#+qk7)PvuhKr%{!Mv8KC%PMnbM(&{y$sj z2HHUw6oL)%srxypX=gc_3dcgK{zwC>nzq>0HKZt2=;T;I8{(f9Fe6VQRjKyjcdR9S z1wyk;ArYh^4afl&!&RI$LSoTiu(tHuiESXoVY z=#>sAx3yKXyRiG$XV8CRb`#*#?o{dRsgLURiEU_n&S0i7vfy$jwY$NlaVKs*fyDnN zZ`#Z1 z;Uc*_{h3J8Vu}IjK}x@SVH>YS7^2bTP!g~Dwp^m&Ph*uN{)L|=Zj(U7h1j4KqjHeF zx@LES38YuRyq2HLwC7*CP0~MlBd}GUKJu=qM1!5ORea=~!NNQ8gPGpz``W`gUeFOS z#**sNQSjQ=-9}~Wr3fs)A_Ol=VV#bG&;V)&^LU5N#Qt}b%YrCgTcFS{e7}2jFcAHw z%Xn)^ANqZtKZSmAI=c0F2H>zmQpRf+S{DvJh@$ zjQ<7;&XAJa_##(^2GSk@Vnai_q=A0CmXb^KSR5$M^;mud2sZ?Sg25@huAx9#A(w>y z-lDg8yeLSe*nZ5B~1R)k<0^aV6Qq0_683$Zo>b z3$;BciyW)`CPFS1{Sv~P3Nc?YUXOaQe)l?JAhv9I#e5NSX|c4NOvGNT=5fJ2xfXem z6fcElF%7KedB9{Pm%`7}Yb`uV5N?_vS-3aX5H00tMU~jyqrdrEsF!jb7n^+KqnifquzK=o3vh zcVF@*s_6>t)=2gvdf-8lWO$ARmyt;ryIPml%ePrK5mPxGMe>DucR%(XP{t!Q{QMH3NlGnOlAOaTiv@Q>(z@7 zT^J@W)^(=saDq;)eMrY23?zFmbUH~D*m|F@l4kW~Lg z0Z3}-V)m$Ls^Jl#Cn#xWQ9m$a$6J50@AA&Vp8VmjyrHS4!aO8fHzoP$zfruu2#7|C zz6Q}Z#s2^Kb^j%Z&i9t_|BxiA($W2i=BY=5f(ZDTahTEGM7V`9pvU9A>!qXUx8R{- zhJM|%=4V9gXmI`h4v)L@wBV*TrR&HB*O9o&u&KFO*XaM*{5IyFH;ztf#l%i+Xp;RQ zFImE;0edgiFApypFfC{CJ2WV=hrJ+$oh3HKzFjc$eD2rSvOWUQh1{|ERAsGCcI>JZ z(!{DYTVMI*G`Zu`-lQ?b|9+Fk5RVck@U2jh(Oupx^TTdrp#dZ3n>6lt-8c~5m`ogq zVC?*j0Vtm7jRAW+q1-R#c$+sO8jR!eZt)+~NEADyIj|0R?2@D_(7`2#%Vn{xNop(h zGCJU1#lcaEDqkLmHXKAFuLmPen>S{au%JxBg6# zxK9EO^x9r7^sn0X+slUyk)~ozV+S$rllUfMsl*$WMO3a?QTJY+oGw6Y2T<&*74NdG~uAGYX&{aL-Ja*J!_cna(F51C<etq?%iRvVZbsoEtP8^}qC>Mx5jYXSG(XaY?$)C*V z*m3kBuGvl;nNYXGRz$?KkOZP-WQ_5-Lz`=g0xh)p>_BSWbX1ov{jA5R+K+jWtn`$P zWEMZs1ktVBz7y$|jE~i_y9Nu@^0H6TgS5&%YHKbK*0sVM5*g6m?|CS-2gLp(wy_$VC;RN>zsbo-Y9Xz^{-V%@L}oixY%A(69B-t0tVY++i_vZOpWx}TUtv|y zA1)jRV|`w*CKeT#I-i%w5^EGtU&|}9Dar_Gmqq;1e{euntEmgp5?Hl8JHXg2*VC(Q zl#?CmXJ@G0PBqd`Bf2fGS<&tP7Z2)>EZ@;G)?RS750e9*jr#aH)_dFP4kpxPp*U_I zFZ3{ibZQ?h`Aqn_Lg1{|nArs`Oi$|~2@)guSWSES+8>K!DsHHv&_m1Cw;55kg-$>B zM_(>@l6dReXZx0?Bqy;Jr|x`#hm5s3)q6K}D0;t*gB45A=G9J-c?e~Yyr!&|lw;b= zGy3Ya>qb{y_6O5n4Bi^O5%&t7q^B}$eFGwwR@YDxojgjKiujntlb0*Yf@&X2?0vHo z;Nv;D!-nS;Je{`y(w7E*F$r(VED2{P6X_-;yS{_~k=^GNxbzLnZB>qC6J0T81{H+9 zpl76L`GJPBbLlOMmfsbNrlS!hW8dZ$MKgaTiiJo#sg1iWI(Z*;t5S7(udEy=9(yun zRGx3$qE!$thb_i=9^R)BS#%3k)qY3v*o^>tywjPNB#k<~p`9>?S_*D+qlP2RqerYx-p%V|QTF949H776 zZW&9WUy-{0ISCaaZN4ZD`nBI&9%nUME6 zBC^L|wd(&sT@vB@1ZUrH8$6SYC;2vj)zi(c5CQp0Sf*ZpuMI5y>pxjj;hRC=)%s6Q zJF6ku-}1mp8(47#S*q!Jx92Yw&!n9-^*&wl+ahipipqroQ~b`4F3JlI?q7c1yNeYk z7Fm@lLPO8P|D0+zNw#Vje_Jf+nbKd;-(y@e&8LJy-fW%DTHcd}Oxz2HK8v+C@BB$J zQRmC}_Pc_5>mrsdFz>CpICO1M#|f0J(%?B-#g;(zEh9A?Ovsr?2aCM|*9Ug9o}?!- zI>A%l_9?eF(EU9?vV9Ce#pXL0%Un`8`3{G z0GS&M^zR#7yX%bXI3p+Y{&h-+P^*$!o%8`L5fPjnb;&|l5M1Z*oTwx%UP<1jLA+HTl$KbS1;XG-9x}^oxiSqP5auu)HQBnA3PX z8MkW=lK*(i<+;;);96A#;Q5u5ZwJ?~$If2hY5(0!>*`F}+1qQ#7cmUkXApBQ7cq8w z=5SUzQ)$-OAZAvfoVjF%&AXoKm>32cvY2Vulz=DFc@sW$A|UTjq~!b=7330AGw}c6 zT2o$C&mUZedURj>McYm9gduBHSa|K;*@^3ow&(l1-D0Eb3DqI0^Qw}AhU?K4IX#UFSW;-hT@dE3yA#KYVXVd@>YRYk8tSJ`+Jcj<3Jv{Ud*YNnYf8-f z+dE@6Q2#mfR(U$&WvNU$bi^=HsYcsmRQP2-Y+1fgh`pnLe?+q*`vx&L+mvEH^El`0u9H7r8&aZC|+l%|K~Oa`4NxDxg6TNzwP98xPPDdb(O+7*;k8Je=G zi95>so#u=H{Y=St2PhK%3(=GvX!@N}oNmO{w>N-bvkH=d`nOaa7n0^-*)xvl1}a1C zG)C~JT|Whi$jl>}(gOp@XwRWkw+FzQM1a&6b>DZf#n(_h#f(RT*K4=aoP9z+GhN*S zid-xrnlb<-Bf`Zu2O!$GkSe4*vhKe?+7UwE-b2RTnm?au&J@L~P&-W#9#OjYfjNB* zzP*%;y{DTUG-o2>HK?5$2#>(Pe?YU|DBs=;#@?LG5}Grn&Pvoy1B6H9z@vhvo0-bS!(E>3@Xx{=P;2eli8~ zK@LLSrYauyK@Y;-PHoR5J!|2av@@A>zw;y8b1|9Zzk5$sqMLHb_)I=rHoGGp8x-m~ z{LhVMxM$$)`+sgX+fo*pb?uoQ=NmkeZ%rmmAPmv&gO+v3>!-@ZR|t1kY@Gs$Q&DJjYr?HnZziY3G@AFqyQZNRZND--6BjNzA0v`Gud)6;r?y{b!Kr4WyLru?0?iu?R{Rz@zT?M!Tqmvw%JAyl;M?Zdi_e&{vSox`Gx)uOIzFT zHS_tkdZ&>uO(xo%uBptIW;*$g{!rY?+|aLb?+TciN9vLpMoha<|9D^E3Q|Nz1_GoK zU2lA4INvKL9G<`?oxwN4{0|RP-^~EbwYh(g!v31IlapI#HN^*Dc+=z zeTa1`@+kT1?7meM$m*Vwcj!6er6e-2zM0W}y4&nwfgz?hYSezH#>?2HI;$GPdnLbI zg+4pJvSKs)C$=$LNYVBCmTG&QPgk#J!%uPB4}bTgx3p)7{KsC`tS8I6o#KiM>?qr> zjlEUa!heQzA_wc@eHHsc=yUreOr)5Zd0)5Aq&>(sAX|I3C{>Q@KBk1fhBc1hHd(Q0 zLPANcUSe`OVN!t3m}x26!X<^uvxGlxILVz)Qg*l^BlgJ@c0$p4l-B%1!>|aK-a#E# z0E;seG0vhjkf4m+>f21GWn_I&X8t?jK5NK zEpdMBBSu;dpylco&E2s31py4?I$4tsRVC)JO6*3zDk8o|P9JQu2nbBsaRn@slBMfn zb*PA+%1=#+<7Y45>Ut|_qH4s}VrJ@CzMrx*$72*AUJDOr&V^H1XA>yE3)gltrI0|1Q@wJA#*cr-jS79#n5CSI`|9Jn zL2;?dJzS@s+emq)T40jn5{CJnJU4~LH;Lw1-zbBM3#ywg@QZQ(^AHa~aC9Ti%U98GSC)3fp#Vs-_%{z6*&wZD1U;JE{5Gv=slD_G6y+YP~`ha|oWsYZS=Z%UnUwfg2wvnM-maiDi zLgr*nlEApZcuz9cqS&4AkyrlEL-Psqzgimhuc6V0SK{dAD{=IHWN88d|7mGSiEHY! z=0w9eDFGfC@+kROXNVRU5j=m#aq)he_nI5UVMlKb{}TKN;@lVN6r7RjMRM+1kaHfC zL#NHE}xg(*T2u+9#3s__yOQ?=(*SyQU|UR>Q#r)#{{Beq6SX2 z9(AkM``~?x$l5Cu7tDan_0y6CfR$Jaia{9TYO;%-Icug5n5c0+u^qoTE2$Q|ccs&m zid$2bZ1H;wd7L$R<&ZM0Obj)zAuNfo)KCe{BPU*>z^QyoWJF&WW4M@WlX6BS*q4gf z#nXceqjiK$Hka96-?zQByHd=+^LHVOr=pS8g#0{jDlUz#Ek^lGB8u4NyJ0TTbV+-j zIbF3T+Zcu7!0lx4vd@5_;(n-(7|T|62tIiMgpMHuh1@FQXN1LT+LG3r;!jm_zr2b| ztJEmk)1t&;k4IBCYu}DDjuVXwD1WsHvmwzyS3D8p+(x*qGLMy2q_I@XjbSGesr+c& zBNNAPO-+XPgD|;BY`Zvp>d@zlxsUG8i}Y$@Btx~s_x+vkUdww21>pwCuXL29%hS|% z6}Yqc_}=gYbjdUE(&Ih1*o}F)g%zb2Ijd)kQ2S-ZU_$t2fBGl4^m`=k9xD}+$1C}M zTUTXHV$JiGF^FJd)Bnb%#7Cow>5*kP@pFx$G-N%21R^6^Bam(SVJ7fpO2C;}EN3$` zM6LW|ZK&EmAJH?NKD<*3sQ4g`D3_fap$~r_iSZml;PJbo08D?Gc_FjvDVL6<7MH2y zY2|-x1#A(NS zwk^{Cq#-GT@{&ujna4EC9otDAK*mTblZcZQKDkQ8_t!xDi$eAQ%Eez~@e$066i6uN zg)k)i_Al)ja>rk@b84wklhzONn7J5sv45JJsZ&~)Jj{yyi1OYO$QQ-NUGdgK1VVg! zG!Lqf^~Tohn7U-W3A@Tz-cP?jq!@mxQ-JR`zY$!x`fobw?++3YuG@eB2Nxs&^Z$Tz z`k&?l3qa4LlD~erGBsJ7?NHnp?+O8+>y7R*8;^kzDTNG1oe0oaVUv^KaM&b>zab+e z$N0^gKVS0E)=9sWLg-3aG1$M6vTs(ELN>pk(m)Ph0xI_YW$&NtuNefsm`UbzW}Q@* z-1)P2m>4;F5$WWHr=2vKnSpx z7@&t>jO-%VD+u65Y9-l}2P^_R$u@nVDS%bLnI@DHXo_;?2Gs*}5^OR<^?;oen_%c7 zpp#^i1iA=Z#c-kQJp`@;&J>`+z)tc_PJlBqgtB)92wCYN-1LHa13M`pg+~7Bvc*XPp}yXRR{W0YyzREKuN?i z1Lzb`9O=vl$_)_5I+KTT1O3T2IRIwJ5Twn|05jA(l->fscc3`VnJUy7=ui0?{M}>!9A>UEe2`sIS|IO?vC|Qj^-<0337AsN4h441C?C|_nJ5n=qcy9LGC5-AJ(zkeS zxxy);;k1-;+aYHewN_DE81dL3-?y2{Pk}mOB(TOw`oLYz^P_ePpC=^-ndL#7iiCRX zlRWhgtEuvpG|^~(hWvKf*e5*`V$y|jv7P3i(hflL=E^+)&*Q);tt(X$Z|Um=H_{Rl zp&%*mwU0_DOp60oFAbl3hGL*Oha-yIiGQM#;y_21iGq-`qOWD4b}*VDecxJnbd9hfaHWfzqoZ; zmG7GY!YZ#x2?btb=ff2WAql13X=Tn92S#ZaWyRel+8gwkdljdNp_g+j!1d7XP!+Ay zcjrc_aGK(Ew(?Wr`P;48hZzs=vHnsx(BMNUEzvw~-ZO17+A3!`tu*;<#6eg6kG_rX z-wsCPuDA1jb3n$$fifh;bLxE8fd->xIq{*D7;MFoy`n~P*U}nK(IBacKwT1?(j3D1 zaJlQtdY2+NayeFdySfkt92&aOwB4e$RvL ze7LFsqz%q#@YD>aHfWW3;sO*l!6h`FBrl$!yyce@2e@f)B&FxL(r2>Mpf}~}Kl>@? za=sl*S#PNxOxbJ+U6YRDh!i~&cs>#L)@W>P40iAxJTM(3l*|bABpK3v3<_5>4H^8Q ze?Z%7bPEt?C=1eldCfNgADE@WL9eSS7H7glx84R%;qa+eIJYMQ%}dPJ7s69G^GeJ9HIPFyC1@%r(`m9g7e}l`HYe>>{m_Lp#D)A;ZSI5D zT5%Q>BP&8i&2pKqI15yF`f2>UUT@A$|1oW;d*_2A)354p$|Du0;N?#St;vNlBV3Ny zwN*A*8E3Vz_9Z-y60>T=O|yTKNsc}KiZgIgmt?VhqWqhT<7j34T8@seNi~0g`;+Y$ zr>i5NwdEa=BYu-RqvOXF_7>1|bL28!*WBcg{=^o4F~_mEB{sIE*$U3w58Gw5SuY+( zmQ)lzm6f5!%bf*cyC0aP8G@w_%b}&c@_;D@q4{j?nVP6HFZ5Z0LC`Ymr_8|`-eJvr z1^BY!GELhxOFGjh!aL>o&uu7wxiG?P`((1Dx9I9r&0j6gK zA~+ps-Hu5G35+P|eYxkkNCh?L;DYfcDFi!=v6Ils_1HN*y6Yr>-j}TEm+or;1&+uXil(v~&XKCt`EjagY zz|hfP=sKoeM*T>4C4}vpabw{zw>AnC)C#^RmiC!F8S0?iiFJQRocI=7DHDZBtpj(% z2_g7h!mMPZ*)5_!27vElFms6n>!;~$fq$+PS8tE$ivwt6{vdup(^2zFwP(a^`6AYX z1n&kZdfP4XA9JWYl!Mm3D0Cw_UZKr)hvmFJDMr$rXs>YU4ePT^(=1Jj;R+iN`02r! z@7PFlTJ=-l$Ec0e%Ok*emqxuLTp1(#9urAW(GU{2bn2z0V& z=fdRfHw)e`UR|y}Nhy>%B&n5|J6MB+IF%+iwHiyw&EeYo!U~jGKPaS<%%!DAH=zER zhvQSho$sS1_07=G40cs;zMI+#!q=(dht~m=Rfn%g)w?x2|G|ZTqs;5m^4=R|ppbJ3c;Psj~ zqs4>I=vCcn@!uEKEnwDg-{MBz<{xz}f7Z%C&$N#cF+(qrJtYly8)vszZ90onNK14F zlzmzDZJtrxk^4`ZPs9QS`x#%98Ry?6@U)z!grpqW@;*6;Lm94s#m;emub2H&17kro zdgr;gf5vq?yW>2wp+c)+!m{zJ@2$*E+(i$J%dU~(KKb1yPOAF@5Gc{ScLZE5J>BCM z-z@8i!IQs6&pW5Ax$ym%ZlgJFYq-4IVdAs6zrWhND4eOy zGbfwi+O;^L?nyX6W?>dy#FzJ5xvI(*z)bI+LvUTW3V%awSR#om%9X$xK$A;{pZ(_S z)rlNTD?Lpp#f?O^usclQ&nmp#nD)^+;7klvHhe;!9&NFL=at=JJYf`@MTXn*h^@)? zG=|Ql%}v#@Tvu41i&K-d1!8y2nz5oU{;M6mCLN>xZmA%u)DyeU4TfK{TAs zp0asDc`B+8EO_Q?yXJb@$$!Rp3}Din0R@EyCY1Jk+ZPW0EZ25D0RIeqB!8_IPPEp4 zwYcR&h1ojSUY^(5*J_8leK>u(efP082+r5mTeJx zc-o)h9VHs=N*!+!&RgA+vVU42uo=H{bsGgYaXYj+P-km-2DV*uz){gl)p?&}dyy-Q zdyM9!-n0bf95Iw{NhIR2uM5Jv@}D&{UdSf8YQ+K(bb|C>-p-y{!CC%Xa8PkQGET(8 zpQ5n~1nOrMYi!{qGun7(@L=Yux#5IDP$Y{LkgmCg%kiT zsXmJ$LEtytp^i@%8whX!ZhD-OerTU%P|QJ1edd|%`7nTFvlb0>=SP7}6k-q@I)LFR zXQahFIJ0Ii?5%tF8ZYUq&CmmOhuI!3bde`Yz;^`mZ_^e}hZ}M_J;-N{0R0!y${^~6 z9-Kq77vO9VT2*#SKVkf%1bYOa6CnD(8$3ZJ9QWM|`D_r^_=daYi%fh_z7*4VPp*dQ zrmhE?>oxazO6xrbyoP5(LcV9Td?tvqrD`rHnD?xa@kQqaWi}|Pr^gC@YI9#RT8qp5 z((Cc3R*LUQUFC&;{Rw%Kw^(ZYs)z5%S>;7+_GQM`6GUPKmr8r)rYOXTdd%2=`7#uM zGW#-)1wq3qT~)wjyFn30QlMnw-=ydHzl{+*3WIw0eoUPi!(v?-MgW6|uyvd$i@?RU zst2{$)V)8pTPT?MLExpUgosnJ`IoQ1I?`N!tny-HI4Qkg^5cZ z-0#a@;wyzv+IdKk-Krd}fXYgml4lUH)=81iETqY9Ri&npH~lnv@duiaDMw4tTC2Q} zsqN}F%~Ge9v14Y{A4u8p|5gr}wp39%>xL0fc9lbMr0-s3g>BfQpbm9wRWR}#VHyWJ{BOOB;K5x}tP+!ee!60Od&o#5h7 zMc0%h*?dKLyS?}xPzqJ=``wI&E6^gYcA4jbsMwku9<44rw)~yBztCKq6}@2iVft?l z@D9|wmwJx(9GFdRWk_VNq~y@CCAYf^w&*G3Pj|^ky~*W6UCl zYnW_Ax*U3vmrI{Qhn)f0;9=*qbFsho68z7-(YDq$BKALqX?Je)-bFttI`xdjB?ghi z^o$-0&j#1eL%kR8D(oIror0*l=*RAFAuBP%QgYilZV!~xBa0L-1TjJ5$Ic3DFL5gy z<<*wMK|46C^8NP$WcpF$35F!8Usb>osvqPy_>KOOVC(#v7D-1(cW&lw$V&ThQp{TM z#7w9P2~tfj^Yz56z4Uoh5iF$s-XF~(?4b-=9eiQ-)2xixzfW{pP}}@ z3N8B!ESEJu*Zk&Wa~84SJ zsYw&M-vid{vaxEZa#PYc{@$VSRSsUVgR8;26yR#6JnQNLUfXO0K%`SPyEj#EVoBug zzS~32oQ~^`g=0Jv}iU?J;(VV&;;yvM1- zeCYYP2`2Vt25lv2PlifT(Sc5;{VEa8?ulvoi2QXZ;Nnz~NtMc=H2oL8x@Q|x_H;iD zVX{>)3P9N$OwvQwHo-;`M1q?F{0qbYe4J1%-QC6Nm4rS4Pyt-1ceGGafHBa8saFTq znVz=Td*7Us#%;~aw%U@${jEhJpVNBR6Xx&Q$x#plOb>sgWX-U0jejIYJ>n2|w)XF{K{Fl}8>r8lK7}gTCEA}@w zI#P%C(#zk6l)PZKL;ez{ZJX~>Lax?_g>geG|K7B0hZ>%eb}tF;&Pns;O>hao3!G2{ z2S%jPf_K+ok7)d-7Q0=r$I{gs!SMG|!?5pBy8)Ub*El6XGAFI;_ZN!O?oyA1r{LX^ z)O&ldWUmTAY0ADjJ`WbcuI;8Tv*}Z%U7*?tBW=1kmV@?;SW9wv-pxt9w*^0krcLo(*5nUv;`vAc)9WXC#;Da(gKT%Drc++5cY9JJ z<@pKKh%S3z-*}bHbuk+Zixx&Ig%Ec4(&l5i*55P*OdQ~GYMk744W%;iDuYSpER^x9 z)Mg9;J^O((hOA8)$=?iuq#6Vs*ZM(Om5k$foCyNRj(Mx+X5(+RXN*}4n?cQnYx$l( zK+Q&|{D*j8YNf}|)XawAA|J5mi?BK1cA zR2+uNj5E9b)|@sAt}szk5=Ttm8f1L?ETr0K1D2Ly%a@__Jmh`5YlKPw)TPl5kF!O0 zdyxX#1TlV}P~nL|v$%b(ovu_hdj=7up(m)~Cm(QW6yo{J&;URMlQO`_2r35%=2h-;Gqx9SCWPcr4@+|2%uh6`a?Qcm=r%0UKN!pNRy)8 zNXN;kIO?Z6f7RVD{IhND$s{-mF zX*WWE$~hw!uzrf{LEk}r?u8-uJGBrTeZ60(*cA-RL8+d^4b|WUL6|RnLp5;~Des}E zfq&k$CG-$zD)R_lgEsrvueQ(lk_UE)J9YK@7QwnduZ3uCkvEx6RA!`IQ#|@}HaPWd zECMUd7pkVsS;#5-H#9oM=`hueY!NDv^bu=mHa(y#08?Cm;DRrMVEfMwnmFWzjH2Y8 zfh~CIH3@|N2GOC9!I+XH`7a2GLjM%6%A4=d%b+ugsAMT&p(FL$3A`pPns7}6USl&n zO&av8y5&FQ0tY}1Xss8*Q?D(+YZH~3#)F<e zHvD}vu7lX&pYA-z07?Ze8mc8xo;QP{U-D8%NxYf)X4xnX>vYTelc|lSx*+D4Lkuh4 zS6U})5blqEE*>T*QNkb2`IrZBD5xkzZBu`WmAw3k!<;c$Rc%j8#z8gUn-;43y~KEY zTiu+TWqa+>FFB#C3C@-N7p(L6#T>OF50WlU*QEt)Qe*#{u&gIiTwI5Pp4Ont-RJJO zttf9CEZk@;N7AYu%sK|4zn77Sh`&8}vqlTK)px=#R&1w5#b!f2c;5%OrgNYC+8e^I zC)D^2zQ{RflxLQH>gG)uFC_SVU9GoL(lB&oT^69N#O}99f|Sfn$3|->xA=AL zaW&i0Ou5~6^d|bv*tm&t7!B^fRm;ZYykL1m&eFof9tyH>{|;Lyu9Dintv#oaf8=yr z%ksvJRRWb>H_o&)xe6qf?}eSd0*O52nsOZ}o1JNwb@ViqUwn|7XILG`srn*5=b71V zor9j2!f^drn-k1|69TzWWl!?-UWLwr@N&fnfW(-*M!8hZ)Xr4#!q0aLNCKX0j)7@n zg2eglnET@zEEsjw0g>WcD_U%qg(x;B7lrRpnH4aH{9j4oipsTpI26SB*SPT3zLx^L zL+t+rX+W00kP9=3#Q}LRKGM8%2Xl!r(7Gsu0h}}#F9jNmsX|3m6KAdT%?BKPrD`s& zlk8Mdd@MCOy=OQ#ollailljwNI*Sq|{;)HVhnQT9879w%!=`ir96d(!!l`L0|8ya? zYsu_u-ngDicM7pK@C-&e(X?%Pax|460dmNZTr!a%>e768IWOCa6X`-w8@k(qYHluB z>MQ%#`Mr-Q@z*TzDROBPYAkPcNLrMtbfHg?k*JA;c3~k^J72dg89>8}GvyZt)7L$lN(xOeFV`ARlpbxPJ;UR%*On8W(jPa$?9( ztnCMs-w$&q!_90>$ajFwKDYY-+MdGUU*6sd|Y$sjVqKxQX#M zrIMLMJ`qL(uT5A+(2$%KV#h^q4AJ_RX#Jb`YHMz6n#%EE_29z%^EeGW2|||7Ixh{u zQa@8S*R@MnH`lfWsAxf3hqOk0`yJ+KU*63g3f6~E5SrDI>r<0SqG;`0%Mh*qke1;| zGeC+(ems%%Hq;qW$@I%!CN(_32)LhAlRC4d;Tz*?(5+i&(lTH?QrHd0ANY9)| z<->=;DN$?T*Z19H`~_(^bv)gcC2G7%c}Yp6`QM?7kJ80u>fMwN*J$KqoGfXi(AMc* znD;7_vP|hoh%Jelk9fNP4aG;Xr>a+`pgR6t9YtsHV_lY`sWG31S|Sc?O%*3{XrK^v zW^pMMgx+YPK>1oGiQ=O#jFv}FS8V)&?09YH2J3f|6Q+scuoO@~B zE~X|^pgTlG@+PG++S5*_wM?QF9VT%1Mo25xCWexmZhjh%Zj2{~sRTYbsK^dpBsJA?{~;TiSuY&Ok{~Unlbp*-HtK>&-dPx>r8mMj zA#yx-LhPte(ZCaN6^D|pn1dK2Oz9F!#}mn9iZr-PVh{KtIUXd-YE)ZN5R#6ZDLIS0 zola#fbm`55=>j+TotY>#=CEq1=Id(`hA>;ra5d9FVLXK}1hm;IR2^swUS&59Qi#;+KzF9aKOVg8+sj)OMaOOaNHkrx6tvOVg zwLKW9bBN)@Y^qSCL}1beRq#jawTK%Guv+~Q7V(MWiM^?knOZ;N7dB+aTpnYhxP5#q zkuHdKE6Ir3Au*J9r4?dZqEi-H5~tJfw*Y!WJ1RiwmkTaQrYB1?r>EQ&SOv>wSvDE; z`NY1;Co24~Ff|bV!E9=uOQyYG$#7Wuuy3J={B@K%p)VJvr&9T(Gk3^oS3NrNsls#y z6WI(+A+_%G$_(xCt7|27-t$tfJ_)hHl8`C4nlMDtmJ)H-eB|jMpQ1`gc_KJl>g+^< z=E?y#Y5r_go}*o~kw%!PJUNniVf95>w=}D-Jk7B*+)VJ|$ve%y(`j^?2vRdsREChi zxR!)roUuxOh7Ur%LMX(BL)JDX$O4Vh z!HeBRI`Ykn)?WrymS9BbNYQQ+?RHT*#I=awDpa&PgxFOfQpZC<6rS82WpsLrMMq*W zk3Uh{_WE~Zco9m<)BIufmp5Zqd0-d#ieixeD;&EynK&?->dU7{5<}LPZSBM?d|y#o zw^mPu1bEqVi(@t_F335!)SjCv`g7UYLtT5`Jk#oUE#dYt1Kd#|I!KT^whgMk5VPtk zf4B@?ojuNPiaEz&p-@$7?a1$rbBXo&$Xx zNDWPB3Mx%$2zJ16nV}yihRm0Kr7WoUX3Kg&%OsI3Z>_iof^^Ip*;&niWxe7=KDRIH zUddWmNX(?hTr2RRv|W_Wr)r0=bB!zlXXrj5AVl;iTnR2a0nRXDOlp~-@$mxm0)I3v zf)+a|VU;G6P`Qd!_b$>YTxo>ZQ2`5%-q?b;$I4h5Y5`P@2-MGCeW^=IRt|3*5Y(Xk zc(KB$5bJ4_uo}+b_M_-a>L#e&P)Jjicr4x-8_EmxJQRY;T0BKsY?^HAN@0D8EV?H{ zYV9`eH_IIGF%CmKjCFGpUteh=|I*!}EmC)F%DLU8AuZ}k=m@HP#ZeBE)FhZkLi*30 z7fdz-_wHzJ?0~~~qRmL-*__L}NI5P{*1J+VU6LoGp~DH73JQVYIh`aJHgFfW^N=_8Vi#g)+v9vQ8DTT?6sqtJswUO!}#18i@rM!fUHAU37j}I?M_z;cGQ$0BJ zbNT*6a>CU(CxthTCpB-N1*4o)R}oT4!5pNP>-5OSAc0XYNx7c7pP0RXcmBQj5WeJ^QgdO5PkCa?va=Mt}b1uq}Msxd9V@|f#aY+2I)r{J@lNhTK=oDho z;KK@5s;1JLm-t5tI8=o!B-A!Z`*=E=7$k|3sSPZl0p^Q*Lw0OyBENS-)uo#n6K#oz z-!wIS77ivxX#kEz9MiR_9ep6_H>tE_!iPv*QKB6u`QXGNyT}O(bErwoscG9w zLqABT_C(uM!=CwEke>0XHt^6nVa;*Uf2qmG)7-bIlwE<&N~)O4kI^{Gpx+=Sl8Hi! z*NsRij289?v4b2$US2pWpPMEY+!VQTivvr+Z~@k{6NL#30^)&ON#j~jp{DEv5a@Gz zf3}!EP+9CK`%)J3*Xvy~$}p!tWXZI)1K=Gj%nZ&{mYvH5@pLre3j9ZZjVM$qWD*(P zi0qMkD&@>uS}C}krz@+JaWt{>5bP-P)i95$b@})P1#kCjqEmgiBr0MQdTxFFnk!P2 zejs)(5N!o3&-qkhZ$wG$P0LlWPGXPYWDdO&qO9^1>%*6|y=7zS&*%20+)K(DPg5g( zLyY12Ccsz!b!0SO4Tx@jyiO@uqbeF%>@1*JPowUX=wd ztjoi&)@LX=ED*oEu0hZcUw0vKU7iIzF^j2G(1L3q<_0=&?Q$^hr*XV>Bwt6mwA>$0 zS4~UNUWmA*fQ=a8>-_f&Dn$a(mAh*+L(`_~MQa)!2D>8fYcsSL z3f!VM664hV^`A$b&#g3ZffWm6_qP|FckLN(*Ux8-Ob4H37g3;8TbT*5qj&aBB&<4+I=$eT?BGEQT z){elF8*xhZrsb4|^l43M>d=wAd-4zSeYsO|JRy!GPg+vFOS3V(?n{t4uo?DK<^b%{ z?I|ZxmDu1VQIXgBMjKmxt{t$AjCWo&xoQBy>z_|gCh`YJb3ka{^b}VLQe&=d6J5Mo zu878vKJX@m1;wYxK4R(=4?w6iNO{MM!&ClxN?XbE-c> zSk&F-i_1Bn(2nOancO~NcUR}N6p4K{&LZf@w{RFs_sx+F=74Pe8OQSHbRl+4+EAB`!6ShDklS0UsPTTToL2A$G_ABI!QXZq#%o&(!puiDG4BI@6Q@zd;g} zp-Flgqb%O%ehSekUFk4ar^*G>yy&bTDn*X~l@isKWDd$?R*Zj9h>gs{60*;ty_BrR ziM3!qM(6g_IL%bqWND5LoseBAxyp<}3JI!J$@NkS0*U7n`$i_xg_1KoGqaq-UZf`V z-Aww2UCY>MvVq4M!^h)sJeS^v7+*f=5lUC`zLg8Gs@ori#)n;jYMqx&lV({~xG3Ry z&U0!EHK|lRbeooc7O2UEBn_N{IX+hq_&|21vYBO7$lM>8jR$zAy@BlRLZbz;d&+Cg zp*2*Obx>X`6ekje5x#l$s5((v8)U`LHb{((RhOJa+a&JeTXGz&^}`Lc*687Q!x6X0 zmp4BYlKJ#hur?dEYTi1TCN7pT)oO$|fvUA2-gcrQ9G5V29=47J$&xxs9lgJ18*b~8 zr2}MB)2mIs$7)!UiM`(S8=$BD8*aGJ9w|!B)iQk36QWC;3VK(RoOf*nE&yASq!pQ? z)9K8ZhcxckJ$ZIQj|;RiDyr8xb2~c0$9w_*9^|_O=7{kN!>qp9Th=uZA14N4_FI4x zVgu3RGir{*SX%SUJvZK+Ne|Ao@TQu7>AZI3rZ5vOdfR+*4#oT*YK3#dUEV&^LBke# zxXB{_54BF%9soySuWb#umqwikpX@9%H&<^$yf$J0j1SGlIc05{;&oBuWc-?$?_w2V z({n4MY1&VlO&E(0{cSURemXGsVxCimaeKmN$uPlLpIeGuadxw%I52C|6o;)2)UJg4 zP`!W7*_)<(NN5P(18LV|j z5zJyaO`58%7ECmUHDIflFWT;Oq2g1^r;7(%YZjdqiX1;*fLEc~1#_zR;xLC~DJBwU zVDY^R<)u{Y5G@qad$NuRyCghNrj(AC6Sf7invyiq%tWCNTd(P_Q+?Ul-0CbRTJ)ta zU$UT&4r`cvs)AdY%I79?#c~j_kPJGe&XW_Hm8MepM0TvNAlkMNv#Fg^I8UO0$+jAk zBE(jO9uef5$vGJc_>FDuc^>d_J$F^{mtIsB^7!T%A-1*gAc(r`v72w* z8$&ExJeN;Xzm>XSffs2n517h`_Ccb3Fp;z)o5&v+0;4&}cZ87SO*=c%TCji+A|GnX zCb~DKYe?G}W{jtspp6gu8Ty-d{)+kX;FsvjKmq5wVoA38yFE8GFovUgMsuNY3$-TA0ot#sLIIzn!;Q&4Kj1; z+6zfe-zjr1W{wjpA(u;I=`4f=ogQKQg{(SM<3FghUeP|324+c?m*YY=hY3b^(jY|T z14mj%5kM8WG=#AhM)Dw`Ke}uPt!t?H>9Z0XURrBrCK0$Vx?YxqmKC>Sp2-sR>71-BIQphG9!kwW%9Yh;`M_ok2C_ ziT2?%FFDJRt|mJ>>@-$y;)Wzm25u|!El#wLAkDYWIsMGh0Qn&mM6HX!ursm4^ERTl zEtyW{L$%@t<%@QY5VJVH`VcL|PT3&e z;h71STkez%hxNXEVrr^v2Vglo6B-%mDLdRx>$YS=Dk$1VIsqf)Er0G|9A;?vW@*rt z?ZsR_wvEu=?GvbOVG?x?*9ZI&k-9xuR<{>8+csQhS_C*BN(=JApTfy+DJ};f9d}_3 zMjp$Q6ysOmh}FWz110N{pFz6VeZ+0)0*j^YGZ82?0;F@{k@FSTKq!e}JrN#VQmR&=AZkvz&`vu3>HSt)HOna+v!BGTH*Vly`$t~6BB zHJo44xlrGet0X5BsHTeod=*i6Jf1F`n@>;DfI|rS;h_cXCILps-OdZjUOB4H`RYEo zE?`BbR9wKZWhKH64gV~r%Lhj-q+eY2LEqcVj}!^T+n|QBb4*#utLFs&IuBv&RcpbA zH+oxNqR?PF5mGejy6LRz(1DPnJb76-(zh5scL&-k;Uc3P2v+s!g{gzJsT++AR=|Fx zkx`mPbYMiZtY@!NNKmLxdN}-?5^GPBs@&RA_a(xuE%nD)wdN5iCR~a$%c=c0 z{)`)K=2EH{e`kZE-NWeVuBE5^RkRnELwvbttREC%3nan$8mjt*B#wh>6hG`=HVxiw zrL#k;p6W2WsWx>Z{V(mvrnt-bO_IIh9`_y3-JZ|h^KP?n_J<6jI{M(HP3&`4WsVkN z2T=m=iNe)-hn*%Dn605AQ3Rvvp6K$j^HQ#C$cDE)m!hgINR+XZ8>=nL7dS#rNDihA zJ8mnTKj2!gfIJxRtacn1euk(kEtP8dmm1`__b;Tqkeq^SkSmg^;G7}kUaX{7r#G|M zi7w~~krE|7RVy?&eSfdv{h{BbMn#7H)m8nFilcKgXnwY>k zjuN$@0rMkOBVHHB(v!}3$1ms)L{v5rbXL8$Y#Jo=+ZESbA}q|)=aZq4ai2{mLNx}z zBH_|6R#!gGN|iUdEUcX>!Zn}T)QvVT8Z#~*?isCG#bJEzP212|Dr7>J2y03!8%(Z?M8l~Gp8x$WrVR7_hvidiiRx{z6kg*U z_bD>$3Lvio+b46_LQ0+O?2|vt?;Yei@+qdHY;1)shtwNeJ|(r__9DY2#0N^JwQ&|2 ztz7@=ivHP%Z7F=fv}Gm=n@D@7|805x6K^)to6XLf&CZ)adb7oOGw8e-aGnOBq*u#F z@XW*{uK4g=a0X`!mC)zp>4VV#H(6_P1~7x)SajYv10!OhP-5OSK9vfYol0DY2&=^* zJGV*=hh$595%?wzTN5mqus(egwwe)ag!&W|<`DH?2l6STHG_Svt?2?3u!&mRr6o@@ zMpx<3rHUL48!q0o0o*yAMj%0AEWMwY9^MP=k&oIDKzPD@mGUgw2z@- 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
`UreOuv6~?O( z*jBX45S425Q?IcR?N*#~1V}qZMZfrpQ41CF0q;%npIr>pA;m>CsUfm74lVSga?fbU z9MlmdTL8Lg1=aJ-m{B{V9tv}r8y?07b=sd(ltvw+X24OIQ4ORX>iwq1q#l7Ij6naM{dNapJ>8vx9N}YX{v)xij4Ovm%Ej9%@fh}CsxqpT*vXj2` z!{mV?Yo{tzs-nzN%~VeeaTfWhY84?Wb4P}s3QdF91gYfxDov#k2;G^H;?NeQfMrI9 z<;AcdTYR{t@#SR!54pykF5cN`qAuP=>>5N`#Ndsfd^L!wM8VXm%M2gjP^hx9-ix#l_@{fLdF@3kE|`nQteEHfe5c?m)VL@nzS(r`@xo-m2wg zg|HsbuwKR})FA6i8@s2Lx6G9m$PG%`0$gw8fQYvxU(_>IEw=QPW& zncScF#5lUPX-)eQ5_|l;G*?Y^T=Nk^YRTT@L=O3s@ ze<^%8E{tGaxDkrtRKIeuNaJMrljWV2^c%)bsGn6 zgTFr$$}*#*(q`b=P_E-TaGg2$KM9PW_;Jjx6Zffye896%3&UK1p39HJrZFoV5sih5 zsMfL8l{K2W5_LusF^$&1EWKx@y$=Zf(A!dCSq1+s|Ji1UYF)&Lo5~{_Wx%pc11}od zK{Q$Yc8)h!BmqrDGvjGg2It+$M&pF`hspt6zr4h|$3+*#Lk>hbd-;gB5U zW5oK$wBOj@j1+eUjlLn3EE2X3hjEOCcGkGKhHR6gFt@&@gi2JNn^ESYC1j{U^(%^e z&&06Oh_~J9SXFjY3*?289==5FR=p#eVk?AvcU*khTa5D)hSdV zo&>N%Gp@182>*zwildqj5ecy|s(-k$AJ_tFv&Cq?UZ<#3f*wydJ84z)bt4lo4z?|_ zZUfXW34w30TCEP`{#80|0=&ClHHs_$C?66#w+-+O2dKrP;Uln<*6JznxEhzZT4Kci zL>D#bX1L@TSO}R~Ynwx|L444ONAIG&=T#yvk&Hpq2(9lsp^)_NwhIrv)^VnSyuz2q zYvIIyenIlMgo3^i&A3(?s^R|6@sBTm)qstibiSbjJL(M{T%QhoGztEG?e@i zPDKfEU~8(PL{ksN{&o&JPaxm<8!^^C?xOl%iwf3+0H44!!!%+tFXYe2ciM7A*>mT88xkgB<&7Ve9@vQ{5vyDa50jktPUO zk@kksSDsDONlb3-(mRPFdW;LXdhQv^MMmXE4m_!0d)~s zCm-w_%>GFug1T{syGsIMd6S4)Am~?Rqz!d>V6p^eV+)&3MhG5i-u1#0RgE~SI8s#PX;yA<_kVek zR*~dpg?Co~W^Jb`ntBHFNErKIk!R*44Ps(JgP@8zIZ(>a&qG z+Eb_p*}HJC8PQ4@jA8truAmw=hu=){!#!LqK4q^zHyn*;1m@O@8^l$fID%}UF;*Ozvlxm+$r=#B4azS?Dq7+7kj;N|kp{NiGvCRIi63tN2GkQmya&W%Enk75%!Dx{)#6q2(2Mf}KbH0#!?@)UD0PmrvoqEE&#@EE}uPW4Dy1Weu3$}$_< zjAzFR9zrcoz%!Qwv+2IkAK+?cqSh@yVV$J%K(gwek-7JCpb3-5Y!f}{pc)>U>c$lw z&eXWR6PjHz*eufsq|^E460DvB1g@kXQt`q_QYQZQbIK_vEoAgeX{b%Y-;Q(n(UWyJ zlVYsY-(cdiY0d9xjVaPSJV=7Mbi{G~tZ;CTPpy`4{q%WsG0BV98|0lPBoDi=BO_*I z&aONz3$upnfsuTQ7g2jYj2r;Zr5i&)s4=s<8qBTr0X8y*b2ZlWTQ#>C%pR6(9b=O% zr&@-3^~i+Gznm3f;1VBwDs(9^yG&k<)T(4QJ-ljeh5(e^t7HK#M5{iEUe;)h$wpOe ze4>{_s6O2$rhFkaWk@(v(Le5VYOoglOh6ZS3zT@Lfe3g-#W82gfjx)Jq<(|Y?uIR>I(AVlYA>W+HYB{n9`4Aof<7+RR*A=*b<6;W z8Wz5?am5`>qHgRUoYQ5&b)Um)qX~?OGxLbNi+Wj+04Yj*Fbp?2Pt$#$N$bNJloi9> zz)aAzB4+H0cJY_lqaFvQ7SJBJM~z?7j=96rjK(B|)SVh8ymk`0{H$fBz4 zB6hT#DL8Hy65f5~zW1P&ns^!c;msoO`_3X}?3cVKQ5c5k1v963jztN`c~Exlcq%jS z6Q{;T>5Zp-dRjk*OI`ZqXe2 zVnzmuhiKUJ>4WF!L^w!7viOficl6JDSk76_!9tkB*Ui9WTMHjvFZ=Q92Di zLJkUUMTubJlkhC;C)2@7#CPvAiTmA9Qb*qhP=|^z;l;(-Z}rIu1}3~I4-rqICyvu? zLjOp@@IB4BAljs?s*;!HtW_Y1hfUIodD?Ls!GKSy-FVft2C{8sdC(I(l>)`0ANzrk zrj4sXYAGK5Ay_fN&ESB#6ZC6gv&aVsMjfYR^(DUV;;_=UmL*GI38VHvg^3FxuxdGT zo}V%$SCq8Pm|1bTxCE^WD2hM8bXP+*lUdLFeC30n*^$f5viV37heH$8)|2b7Y&8{( zp8Pea#`1AWj38fJJ|`QqC~E?^dCHfktGeEsO~`ntO2k-sl@W)=e1U~6_|VD*Vky>` znUyVUYH3v=wX`0F4H4EcVOUIQy|DJ#JQZ%Zl&u$VV1o=PitkOO$R}%q#6LGUv|oy& zO;0Zf9EAviMGUUoKWp0DXzpfShmA?yZ*W|Yuf3)fhn0sPQG0lvp7P2x)5w!6RNALt z?sfm>Evo~dFWh&6nBMUoUa&q!E~8mH#OcVeS|Wo;tIt*3m{9=OM#&npGd%z^U3M<7&a6!ZPgMs` z>EcuxPO!Lv)6B1Os$@lLL{$#4{`{JuuH4be>QL6GuUW}{$ZY0C;MNO3kX{KVdi^1T zV@LkdW^v=+=r<@shkZY_j6=ba#E$z3dE;Y#?R)xI1{#+X_7$f}-a&$DVy!Lh^Q)say%(-&X;62q+9*Z0lzF5d^(%LBwRl^NG&Zn*n+U{e*!FbuuTJO6#hEZIRMQje{ zED!2l{Fb#r%Vc>{4wW|j;gqNUcxeZ$tD(PKY;abRZRgg+tip%8P4!Weu9gRR_tP>~ zX|I+BYX^NYURal3RP<^hcEWcGFt;~|7zN+)6<;eTe1A1t7}1PPvEAlTw2QP*G)LjT zBs0tM3_e+2@IKWH!)W9Whc!sE92twkL5L$~qrXZF#3M`;uI>`rsm{IX?%L}sU)q!I zWtBKihfugh@>}d1*3-<-cuwa>Y=wuc1~rlrAjetlGv2mRVH|9 zA=F(PEmwE1S#9=ZS9mC)dJQR8ycAO8VE8Ew<+9#l-bN2I|Mmkov0NRtWq7FZgyHIR zGR4M>&UfWku3#;n=v!_Mv z=V!)a+RuDx0UL4fk*U#|K?zC2?;SK$~VBR@^ zO+`AYTf`5aySCi-xdjF_)umd^i$EOuBEyZubCBd%MY({gVJ3+^WqihY*RxR?6937$ z_Li#SR~|afcxic2Le-D>w%W(NrE6&L7u2+hBuSMuDiC{E6-R{}6ciBvCWB%p524Ny zbetEYneK=0?BaF%^98NNDoP~>Qe_nx6+Tu8)5PXlWjTY$dK4G^Nd~-v1c>PO_OlK; zOyKEFierLtgdPW9-A`lUx^(E*g&RjaaNR7mrTc;YDf39-M0yz`p+}WiNO7^6TS)Pf zGcREj*B)DNa1Lzm+MMfb;Z;#uXf{ci0*JQYB^rR&qIqLhZoyTm=3ii%a9<=lG3b`V zor^Nj+M`6f)Wn)MHAbLS6Alp`4ZO7LNaG3bn-fXf70tv8L`&gBAZ>~^Hy!6cs$YNH zhJqRCTas_igQFj*(@$k)@L7ko*8&?1i#T%)CM3Ewx89o;$%19aio6JO$$cI)IY*lH zTuk}GMYj~O88hXS1_@4{qRO4)NILiBbddW_H6?JWcPyrVn0V|Og~S8;{%m8Ni6fVJ z%VOulsi!1lFhz2xCVQq;h+Xq=P@-v7`mM0m*HR1_U;mqZK+P=RD}&P+s-JU# zSn&D=H|S&+$}tz0(6>e_i{Pwb@iAkKZf?{ubT)3tSa}&>4PfR1t$qx+Gr5EYP9?wJ zH10WMjR_ttY|~OH_O)eYqK;TX-Y}2 z+hnDAnFu3=Du=OD?^8$CvwSZ-#qGI2s01PHCF(_H*~*S~hRs;ETtJF1N=kRWAQ2Jx z3!0QFfl=yRPPU&1>nChHSdvA^7f~_i5GCSe?8M6~UWOx$uxkSD#H~r`P#iqPLXb?m zu$6;LXx^vEd_X*r1jQ`K!^EZUMZwZTut8p)G#5~8`271Q&E>3Mhz3K2wt9A>@NL#S zEC~SaWD?0j_*)L2rWKW1IRBS&wt`Gy!GgO-IoKgpAp803-ViZI1Q3j$aL{Gr_Gr=a z7+ebnkT{u_f-gHZ3wvn*qa<~LtaJ%6*tF1RZkz`F?Mm$^@-v z{m2@d5|goOB(&veWplQ#wzq30<#%}vhkfBGAUIgKkF2uC6w)8{`jUuL5>yWb5zzzn z5ra%~z4R>cX{bI4t%5B3OZ1=CThUlaB*>F*F@fZKxb;48k7>&|9LnrTV@g=K- zLLXIvqmP#-dt)lh!5V~C+V&e3F=+r)mNCLV4rh*9Z;~M|^j%XL(%pY4SIJRu+$WGA zNf~7-HCWu2_J3CbFzGlQH5)GQ8u_cwngtAOl^*dfB`f=tIa`e88peO8pi7^UdU+=- zPhL#N4w=A`eeedhn-B`Ap5>mlh$qdfn8JO|OD$p&W}4(C(oAP`Uz4bex*98F@F4N5 zcV8U`JG9MZ5GFrF7`rX)bB-``aEZbwGqolg5m7hs>*H968;y{RSkSQz(u{1pm7Gnn zqzVf%fk&Ylg{jC&40P?%kA4AyoCHsC@wREvS!?_MC`C`xpAZtat$tDRofY|C9`lgqk^DU%qD{fbtyLDqcnv78W#Jl+)=rljWnndKAjS(rPyro(0DUwO`X15c%m= z^{``>(jInuRtudqm-8z2hnHvj8Mzm`Rwg$sE|3Kny4G{P@ za1rw6X8vzoqJnr{lyz2*E;XU&Q(f5;%L@&Wl(BV{Do0J4feeA0Lz@i7Q6bP*I8F&n zs_sq5K4g4K(09;HAo^D1`8^6)a_l1wY#XkIEQvpc{V|3x{jTfjPSPkPA_Dj$W;h?& zrU$iF$NiV#TlkW_Q$~#kzj?x@SPe$vxJ<~6 zR!$Kt!wX#2AXbv=i@)IOe~)^+TCV zk3t5#Husf58a5$Py2+>jh2=#9bp|vo6N}4*0y|Lu>*psX+-{2I@5l24G$CAot2N;kp$TBxd~5;{bNC3z|?ISQ=FKs`Y?tR1np878@9r9AOz_6+Nn)7S(#3Q&?rv5 zPy(w_i12bax}0XAt@dArVjU(?tj{0g^W$!^j3dbh&da!gbmJx^MENiu@@Lcq?u6z9 z@_}YTwz=$&m?D@1o?u=|IMuK>wHVipga2uywwsH^q&-CZKYc!|kuhCWw!Bk|VR`pTE+>Q!MHm zGR}Yh>*n)!45xm=(r`t_d25PgWlLtFJ<3!)%tSqvnYfV4Rjl3!FOqGef&hy(hs-80 z?oWu*5s_F-5=y9yGr6f2HNewA+g4^HRHlP)xDGNeoHtzNJg~e97rO;N)|{K{UT)cu zDP7%^sW!5j`H%Y0w1^nACTS?3K@84yG-D^e9WbV#x*VQQKQ>z!qjwG$iqhnXy3PTL z!^jCzNI=e>GiEMH;iXIAjLPWM)pxNd)QN_D zXj19A^!bu%jl^vuPD!gprnbq(_>MfkYnn9ITH7C+$ z8bk3jFUO*2nHs^RDyL!+>}d>^K~)M|xE-vBm;;9TXw0cNut2Mf`i_}h;|%ZYz?p96 zfuCX2UO=%fAe8K5Y5bIiQRrLNCxSz2WC-bL3|o-EGscEA6OJElPF4?!i5t84+rpRP zW^6W#WBn-Uwu(${aZTx-`Hg9~&IEHqKsI(f2f<1JMS+Z5jdGCAoOM4~C;V^JD#U<7 z10vH9-)J6LP*qY+rz;Xl!6*@by_Xf{K{%JD>TtN66R7w`7_Jfav?anZ3kc-gUfB>6 z-m>pFJ)_tkgO@;rBFm;Jo=@(boA(v-cHC`xqXY zwCkKoQeIgtG-jCM|J5OM&BzWfAS?Wm@RVU&{Sh2wYp1Bu6%hJz{v8?U8T^Z2!q=c(a@r-a^k3H{o(YW^*RxbN1nsSWt-dbQ{OoL(JDVWyEXQe zx4Mpz9U?vBSIwz~0Dr3W>(-LNYCKS$}mB zaRiTap{pt3+B{GTjLU^nzWw6xtvGEu?q_%~u^;$k0Ig1!z#FytU2D|CKx@z6!D}|8 zIDs!_B7u??FeuA{?5y&fX6OwkI#1I|Ya_}A`X%dloJsq9-E7_zx*flY?)qy1)=~9@PxvY#4 z>lfFT{XSQfw4JF67U@n*A2o10`2TwPqNT$arWy$zOdBH|C+TfSaWnHgr+7y)= zuH4`%S;Z1db`KK`_C@R>aj5|~D`5`UTjm|uCtt;G}4Xr`H{vGKKIxN@s-=}X-CC2U^U!ql9O&uUxi z0@PYo236+`J3&GApqM*>s6FUTFP|l=t&Tf8k==OEw*sgG0Vuz8O;mSbRWvGw@&-@O zP4LN|U=ReKzK#lX7kq>h7KBr{`*g{q*bhLoM1<-)Ww75Es;Ha|ek-Lb%P2Z8=HnV3 z%9V-Yw$)6cm$&+GS}DeARlJMQbj9MKSJvP=Fa?F}CF45)!g%WnhR+8f?P@nX>c%}B zl%^oi%`XYE91iJE!l+#OEOmD&MW`x-u2#-Y`g0&PM>8*pUW7+8`HA$OdKJE?qW{GS z0{fxF?)!-t`rsVh@DD(LU~^>i!(tk~7k_}aM)yEv-n>bFxsOeVnFwO5G(gT`y7l3y zNa|`h3nOzelL7yuw`F_n&^-Qi_ibmn?yLi~Qg8Q=4GtGFG3U1(GS7_S+wayW9^VD- zK;V_kYZr_>3YZ=7?8iHoY>KjMuIT*@^wyXTP{ALJrAL2ClGv*rf2z2kPWeO@{fEn8 zO3lY9v5=Vu>0mWDWS;GmXJ7BiJLE2abBpkla*?qgE|bGo%NZMU10_dli&=w`HNn)P zo*y-GzU>Oz+9LRAHrr{Z*ba(=m0bK=7qNGfX{z#4bSDoMM8ma!TV|@!J~Y1!3Re=D zZOO>^DT5x$ZQztOQxN^dpKI#YN^iwYLt2y>old36b88D}9oQC6&4btUbo)V9nQ1WD zm-Ew!iLD>W-czn;zCosTA+!8>Z59#SJQpn&`=leAc**V$SP3tH!F5z|*FLcS8~oIp z5N`iE&+vvmdhc40@f!%-l%Mt=VPC`tYS}^VfYldx*1$J>+D+}A)R%s1tRE=*t^FRG zw=ZJNzB%%+v{3JcP=b&PIv{%M4bPo9R#{G%p zy;5}b0eN^2>a8*D1{->V&A|^bUB>iR z<8>P)X5z=kSvAC+7u1JvVp3+N=Gw>gU@)Uo)RL&$1UhZxzzkQCv0XZ;C^R?vn2Fde zJNMOCs6_TGnjausG$7E4fULe=^8=^l>ILXYOZE7lpY~cNTGM zoz&|2g{%mtmHlKIGa4u5fUIfJ__o5D_D)`iY25jD6p6G=u3!Uii9_sEDSA-S%Sln2 zMplBMWAC=%HBPGCqWM)I;o2Tz%yatf)SoBrv(3neHcgrpVp-iGOj{i9B*!pesV}qKKQClD= zsgY>LWR7eWiW^GsnY_12O3uUWW;)V7+% zru1N^XMq!Ct$2c0{xq$@pEdTHHBpUZ=PsYVprF)6Nt zD{xG%TK#+EfdF!@oH00mY}>A`c{r}`Fq}2P9utY;r#-1QGP@)WKjh{M>*s|*v5Gv zn<4YkoG`!pebzft*)}{ND5W8eIsz1Zn&T+~ju4Yk_KX&IVORT!`hC(Vx%9c9uI}r? z*;|aJ0B#;pAY;6n14wU?cqa%mWuRvbZV`T`(9=+#;R!!leNPCT9{WMx!3RDTl&bIn zkJx*xn{Arn3{JeCbOmO-|8efXOZSg_!|D-%bG=8=vRT8E{)QyrZ!|M0pAT3V`hcoe+iu?s zxHl|EteRa0ge#WNwjdG6+O{F7xtfLM0XnbUWl{2eb!9TCI#cdVH57cd%=SpR*}?Qy zzzj$&$3`M*@`UPDBGo@9_UF-j=ieDR@Hh{~gvOcOm?H1iX5SAIemRCu@*`hd^Pm2= zGC8If;7N61a4m1Mv+d=uzJ@DH$679b`);5zf4j-BVFX;-(`Gsa9ahNi%52*~dIp4S zRd7ZD2nCubAnZ4+PD*v6>#qiQno2cWM+W1EH)fddL5hf;EINo7vK9DfkSOiQ?{K!K+c zU7FMl_;DCp3fzD@w^IU%CX~z{CSZrJIOqPLm&y}Kx?SlSrzc9tWNuHhZqu`4BuaVN z+m(Mlo})7F*It@X<16d0jZz$w7o6itlje#)UKMILu^* zd@wXzH3UB(?8!c zv{=2QWvgkttYvLWn7wdQt)4zL+8YRa=d(YFQgbfoX%`wg>K7YYjMHz6cl9NXxK`!^AMb_g3Ht1netP6 z$cSmsWPy!$5>>s|`c?kOLPi)VD|3Z=T6U}o8l7R2O`X$+Q>%{&s1ti$w z_sf|Ke>K6|!1EFP0U0I6CQ=16c~^uFG13D#V=!tKpcnACugam_iGUg5*Z*ilXoGg$ zOL5D$N%J(Ic8knI#uqv=bbLdi_f7l-S{q?ME645cwl4UB@i#`Sn@0$|4`A!+_w z3IeyJIq^6feLk54Ze?QbnptMly6h116EYh zd>29WCey5V5V3q$Nd%MZb9wd->gMgU1>YgG#>7|B8yf zDXKqZvTp3r=_jnDC#kK23)V)i*uAJvg|e_5Wr!xM^q zhK96q1}Z_@nG(w=S{ZaJ!BPTT>(n^0xl)vOK{~(7=YdwJfLmoPgLkN9Z(31;)lI(j3l$Jh2_QCnJ;9Fj5aIm644l#@YD zsU!y7txl1X+X7K3Q*8p*=4bOqBzM!f4f&@J#G|B4 zj8;aP2lWrV9jGxis=8&;55)b_I}_zpO>q^66G$-i!?PO1fXY@lDwihW(yV1h$|Kd! z-|_Ux;A380!pUwo46tN^;6JDu6z9Q1$_SumAF)uO^r zN9a)&3;xbV%cOqbdK_egHDuDO*$fEYiU{5U=NuQqs<3Gw3yNWd## zP5C1xpA1VCPz%$@qg*y!TpLeq7S|_wCnSFqJN`T7pc@=%$R9Qs&_@DJd`*PHRjG}% zd0DJf3QSZlh9j>9pdNKrB>cA63x!k7H*=!th{x?n!1W?h{|zPvh|{`*k(P-~zA+u{ zlZ{AMQ|Xsy;ZP#^T_`{Xsy?%ra_TsmIhV*@!HS8wgJ<5&IXyZgJD7Rs*oj>2`>m(K&zMH-#9T2*Y^th<8G9t#m;Rk&RUVUJqEu_83YUI0StU!=eDe{+%dY!JWO( zq4a?B_*dL1TLQreq`u~q{$VW@RjokkT}3%+gMe__DO!Y6&QGPcWSJLaVTL*71l&P$ zfnrqNr0|B0$)}9gGR0Cb#bnK;DfaesgNtdhUwqGt!|LA1KDEs8u1D|h6~vu-6V1PN zSeVxeCl`p8Q4T1KkAe|a#9fbJb4yXY;V+_kbnG%o-6oA6I;H z>bJb1r;re|rFNFuSzH^!)1zcuvk@>?Wh2VH3VPoIE*U*XJ!v}I)JNbPO??(r<@0wO zr&K8Lh7qU9?PEIRS19!cjTvugl_vCOJLJ}=^+v=5K<>e09pd7_T{#h{gn?+|BY5=g z1=EMY<~Xz(){;)~Yyy7xfH*WFS9YRuev`T!gt}!zpDs)YUZ%G3o4ERH$1kSL6E~*L zvHENcrsuojodozM1%(>J_XTaIZ%`Qsvh+T74>)06aab@EB{PlW2I_6^FkW! zQ2~_}&Z;2F9bl!rnV+9AkTjwLXk$1f?q{z_aLh|Vql=ftO6R)m%4z^-GmG>`?b!2? z?SH(6j)HP8O$Z*8G_=?kDxydbEW{Z3@3p*`u{K4eB z3P84)zcvr5&oQ-uL%ij+q4X5?GeE2lLubzleD=z+(+F7^>4i1leMG-$V83~jzj#L1 z`wsdfIP~&B=G%!`Q7X-pA3jI3u^+U?-kuper|tBEw{E=kLEMI{_e0hDj$RpgAz*~4 zmETL*cEbKK;6RBkcUfdG`xjx$di^@{ZeA*py<^d*|NzM*QDHkzq0W2bAbQK=Q2rKl)JF z($>Mo^uOU@rvH=IUabl3t+KSlZ_YH93HJ+_0t_lDIy_N^Ej&D)=*MJ3i-H8Ig-7vg ztk;BMQU;{2+OArw*y^vHYi+TbiVl39cq=Xw(o1#mD1JKpSkaP+qCJD zAfLNwUe_HHZRYw{oj2Dbh)8p{AIIK+-LZK*(kV1d!t)YFZyi4QLnX|#wzZK%9Q9V} zTpC>+a-YA$3!5wvr5&we-Er=>aG1Hf`{{OBT#B*NPPaBO)`uN(2b^|*y`)Wle)Wu` zt)}$jwiC9PV{4Zbi_h5!Sj-yWn*+WZ%Y3W^`$>xvy(mE)6{-R__k1Ixa zqi(XIZo21WSu^`0#s2qpWz!p7zxoz|UG}IJcJRQ&CFY_{?cjWKv2SPg1>*IqWSG7E zg@S$Pl?-#|q5yW&8+HfKBA$ z53w2daj(XJ7K-U8Y#^bPpCCF|P+t_#Bv90af4{UhjTCABx~$ z;K+g#LH`(*DIBPTLA1PW;d*BWM7??|=M z&sxxZGrqb$Jm| zcz2S-kz~K;e1I;&qQeNjNTc0ui;Fy4fUE)A3QS=;z%pTrvmOe}Sq0`-9vw}4w1qTP zgX(W_gDe^W>$)w%lf@T$A|^j$T7pN&{gNVd6ze=O8CK0sximkiRAyHrqbIdD+~Fvr5>+fTi#%tJw zJREJV&wPyxM2&h!)J+>*>_m##A}$we$=`hZWG7blBkPj}Aj)F91n>m(lgU0RX#{d_ z#DUn*3f2ow@uxw8lF~w8a*mPyD4YWqGWWuhXl7|h#3=VFgjl%72;Wtr}I%9-N zM*8>(txy5!TkU;VRK%)VE5C@$yY)B`0_cG0P4LZd?pXH~g}*L0(Dh@HSR<~2jM-`9 zC*%74HF^H*Fa$J)dVjusRgGPQB-$Sn>B#2Q8r(jF0gDaF%5(+^(_FdhdE_|)FNBho z3g0^3Oo(gdsfjCf&RUA2Xg0i63+<_}bW_mYE?b1suu)=Y24ph?UI)f0?l)JL8 zr*mbA0Dg8*%B9k$Oxg4$(k|`(^ua=E&Di+}*?dJkmLK@p?sx9U%*%rx)ne7d&!-=} zT>dXP1Xma?5W=mxrQ#=+MelkaNpyPI*T=fy&p}D$_n;p{PCD0kjqPR0lfP@+V;geE z8Rqxeww=42-(Vuzw(nhiCFE#RWVntVy+ENw@1>6RZ6n0iyKdPAHgGPO`YrvD={oxy zFj;7hY}J>*7ZS(tvvGy{{Mjiu-Fr9Cq1n8OYXGd}I(}|zCiX=`l@3yZ)%_a*jLoAy zb*|@PxEmTN(;cVq*@PIr1on|bG=HE-mJ-UqmHmHj1i`3evlCoC$*F;)>xAf#XP8sf#5sF(6D=)3qvKNE3ED9dMidDz1^Fz;G-${&Azv<`Q1`Q_%$ zz5;!QSbhL;vMO_`RlccJPA&nGE^#y7nVtcpw158$L%RAV=FXo2+7(JGi`R&!R!(uU zYl^LR(IKPaarmq6nLn!JR_54UxJ2TlK!g2=lKF*a#*gp&L1{ZBW=K^Ds8-A?xH`%3 zOb_~9s$qp9#c1tVybcn=Thr@GH5W_=ub?Y}Gi{pC8f06RC(>epUal zX7H9+>ycw$pyTQi_7hS{7jUCiQZG8^>JsyteP#d1o9;Q>IsNeRRV}rTK>eVB3)xOI z7S1mbEBh^L4CIx)BM?1(^6vUY+EczD{!l;tdr}zGr_FF?R#;vAPtEJZ>?-ttn(@xn?3kvOw=Ay&1lq$>OVh z?Ea#gy>sxK9*a9^Mv^(zk~SW|zFg{*CqIEEDQSYXCXYjtXjY1EN02xKq?GHD#GtW- z5o5!8N+nVL8INZw#jW(|%8n<*-2n5*lI@YA3C*74kwQJr5ChjB+%0rBzIT1X&>yax zlAdG#CacMa?azu9d_=lfHvjcbW*RO&$ruS?oLR9DW1-Tu(bo=|5PQ|MLRm>wHN#L0 zxcETgDAP!PHLcc9B|{ADbxKE%I5y=-B8DV9DR!`FW@VU%`-sS(PxpVs9^aQaya`9! z5Btv2Ph;P#-Kzie2;@hZ}zI9M@ zgvJ?2sH-mkKU%w%tfqH%yg<2UQyx-H*TX^VQEIdmLq9}abXHY!&QjzO;7$9yt>Q?! z9h-p{^Ag{Lk;zG(7dW!{9KA_G;sD0RDdC))emlxAEs-}BxLGGHi-qvRYTULAd&5Y0 z;$v?jp>R$oXGd%Vqc>pv0%|vh)Gc#?Tcdwbr?k-QJ!#n?nZU))Dd(PBtR zHDelsbFP!rm^f=Turb^(4KutA=jqj~%PKZK$L#HJ4JXV=>3a*a_+Um%i@9^4TEs6d zF@oR`W!To`)mIJLhb!nezmc<#?frD$^R8U%WTwTtM2#4FO;usqJ3jLQ5m7n)Rtkqt zLyYk!_2Ixd6EqxKwGj&xvNT|bY&82>A7Wgv<~$S#l!wW@OZtbk&ZkdcnY~mZM18jN z0A3&CA3;u2d)9pTvfwWCjM?G?mRZUBA}0wah3*nQHJ)hatM};9_p^SgQ^Yja>|rnk zJDaL7CBiUtOV@Rt$=Ve?%V&y`dy|h=-k@I!Zh5Rd_EJF+WE*-NthVr{E;tAy3Vd5) zEykA!B;7doIA*zb_zv5mmArELa|M3%J5^oPe|mLwRj>EK-Oqbp_0yQ5 z6}d$%pOU4;8~46i&5$3kA1LH@`^eXw(BSu#c4XZZJ*Em^&?PeFy+7G%DVB#<+- zrMVi@1D*uF7&h*p6DD2pz2cn#tU*`hx(1IwIqDBeIv)vF@RbG~-;-D3O17h5{`jRi zVa*Yeg0n(?`0VOs?Qe>YKF&FMj{)yeu8Ny@5AR;^Np|t6+FxPvjS*C(l#lLIu8S?b zExPJ)eQG>4&O#W*#qCKT)RN>@Cg;vCc{^utQQHPfaZ15|}o6c7K`XiAphn1l=I}AWE`tT$B(=bpr6dd+V_%JGcuPwfG(0zAlnABag%%elqM&Bh3gedX zIAx)w;)2~tz044$@)S`|k6Y2fY3Oy+j9fIM6c4HW<68dZrq~ygsUKBTiw!+S!UOjZ z7`^Z@`ULR37$xgO*_@+Lr8Qonke!ySRc^rHIp?e$jD3GQNE&C0^B0`Z;IsbBL-~N( z^#G268IFqF5DRvQ*M6Z2_(S0NV1eA6z@X%N&ipY6fibKRLmtMpA~mFe#D7Y-Y*qtJ zg6f!Cl~&H4pnhD4q@+4fBliyvczYvtrD(aPY>90`e3x`%XjO>t6Z3=uuz2p;>hf%a zb1+$x$7-xdx60;mLBKe}^VJ9(Cq>5jpzey`~n%}AO5@B_(|#J6SDWoxWZp3^%@gb z?oUO03|e07@Hm=a2k+KF1WZp=F<(2JY8V?YcN+Wn4pYb`ndBkEw};Q57>5+R*;VtB z!`RDyaziX$l%Y0>!_;bajd1T%LmP487SP^ufs-0AD7E22IU8VIc;06%uHF$u`4FXq zlMI}{!_(;NhYPC0)g=19sYCTFoh?=A*oomab*>X38P1%aR9=>{$@RmX=`*K=t(XDc zd=Oi?Nje!@H9Q*G)yGm!g&>>_r?hj)TFd%HF2_vE+0et43=sRcPR?{_K{Jv(F=dT% z%^d;9jzqLaNG@FQCDT5rwm{>G=yC;i`J&5<(&%T4E-+_xqbn=E74&|V;WYh)dWWa8 zxhj1UNrwfu25k|djjkc|5%^cQwV8fep}x>Sj*FB59#0gxD2YFcGZ+F}sN>SuQxYPe zvo}_~xndD_{;{71oPn`v>e#T9IqM2~z4=*~K)Y=}t?PjI*$vz7U#2-af}J{Kq zGrJ>l*blVHGV4J6)$j}(J-s*?Biptk$hak|0Ir`c(G%u=&&|H@ZTd@G26JV6Tl_tH zkM(Ed29Di4VBy4%6U@S$=jlsCGcY@N7$O) z_1_G=9{sK@b)l2A4^`(g3x&Z?)EkzpacY}(7U;Gwx~-JwRLyU=dd2!11q5;sqq$ux z?lNZMKJkPq)m@;NIV02?YNQ1LB@Mr#rjGn^nxWIA!j=_CdyI=+EkyeYV3;{{I%CJ;#+9{&C4Aixp@HIr1 z)^#hog$^-8W?yBC{m0wFMBDX`%wW4zb83i2V-fKUa!?GS|84<9AHHAR1jI(|w`D-R zFbxxkx;E-bRP%1G*cGef;jtEj?Do43$V)o-p$jaxY(w^yw0b5^;Et^1sU@3NmZ}E7 zfj$W{`&#K^AJHwn0h*la^sj0)$_fs!h;G$wB*}~l&-L7tQFpRR(Li5K5bm{>LQuCN zfD5TEH8X-%R1mM{@dp&gc&V4x!gVr?-+-saeC_9C8UZYo2o$=^$B#;RUJowTjxI;G{~#6`P30P1JExBI##Oi zM?KRZ?So;)eh_du;w@)LLZ4dBScmElPd+dj%v+-zg{6X{kqYxih^ho=ghpF`ThQiLp%*xQCMO zsYC$Y$O!tNHiPM_LVzs=zX`|=RY3BfM4`kkBUT52uxOld^Yi@EVo~SRmI*jqI#jL1 z!8|(7XpFcG3n3=bPy)IyhzaU z7Xt#2Vept#8w zUa8udIvX20n2LHBn>zd-Kf3?$p-WAilmlf%4f}G+R1{G}m64HQVN?ucfOR+8!qkz~ z88g-YV@45}HYMsyt4>Pi^Mhc*Ln{JvBvdzN(fMs=_x|JH0j?kX5q8XQHG>xe4MP>; zM2ad?hgoIj9m)T2C!>1qoHS9z@VsX>p?e-7sPIsl>}p|LKtz`hQ@QNoJRts!C+U|S zsq?tPAAqdBcp=8zVM2K$`@R&_7xFU8kmw;0vw39hc{UjnK5g<^IdyEj zDZ<#~3}I?KAQR0*H;RsOCc)k60c$O~wmviCZ$I?PxY`LGg~1RqE^4Es>(IKE!^ZGz zlDqZ8A3$b)fif&tR=ZE&7Wmkf)LPqu+l9#s%753s`(OEgzk&Vu;r*?D|8LOW@7Lk- zU&=QrBO58>f4qm%v={YFF~re6X{f4cr3lHf^{`)ZgNa6QEQkfDLIq?%30NXqFBl>l zMQtb2YymHQ$?Togule3|{CfWT^1QRJrB%)K4zWa<@wR;Pm2U3*Ox;?yw)MyT{aQzAViA#@{ADA5R2q9qE=8t6n+VzQ85!(E8=Fo|gii3QSQ zOe}{=S!mbE4!MD|r(ITZWu}u#v|%XCVVN=%f2y*Q?$nHQS6H_tbmm*PN)pGdESF=R za`A|UE5v+_0eE%~T*q%(WCp8&2@QI4LZ#kij5>=$NvShYGDvCmSi1pYB&p4&}@62q!IZn4p zrh5SgePYgt-at!vu{u-{8V3+-iU&wW3!=?FZC|!#8X7k!xs0Vfr`}z|X=$tzPGw(N z9=11L^?HYu{A50Ebt<4gd!HUc$Tn zdeq!7vT(&R=uk0~q=WF2r~EA#UQLdh1dPCYHeFYF(2;6w$^L8g(%d=Tuxp%PSU3pE z6Z5W@K*JmHd7~0`R%N7F4iwQ^NsbInbyXdYmGFoK`kX>K6NDS~mi;^j?NJ!9z6`>G z_elpAiWhw+2RW^Y@hFW^Xu0F`ko*v6*oXG7d5S z9N?NWjLir0#lgEcw;XV&iUwLCPDIS`rre+joA3FWaNzeRhtyNzKyX93G>2~bmj_-; zmb#!F{;n8)kkA!g-uVeAAKDU5DIU@bOv(E6Uy`O=>`>LcHX6;t37|MtlE;k_<;V^zrI>k<4Yx8MM*QWh@tiGQ+p7gu2-%9 z>FJKRbqFaHG_6hIq`vd5LYdZJY7I1C)Rc;ri2Rto=Z+9w^3KpQt|+EQy!`tKQgcqe zn>O~jP5Rfu@86njcZdD zy&}h9$Dlv*O5J()l&^{t564$-PG2{Bd4G!G>5iQ3?Shlv$|2hce!|nC!S3jckAF-C zbB8#?)2R~gY(eI1_3?HXdkhYgGL?Hk=yV{FKjlF_ftFn;3#2ByrznzN5+H1d>QKcu znFe(i0ypFHblw#!Lt$Z@0Ta(oQgPta*Pp+Bgq%^z`o)WE4T zc1^k>&L!NIeq~%RmyA!qC*u`-qO&XSQviOEV63#@4EH-bs%0EU)|5%XRxM zh)i*r(GO`(i)X@cQ56$sWwyzt&eVhL&8AM0Y_#<))tkZ~Rq*o=n+EHbP-Ngj)%mi^ z&^DvH=;G>Ah%sl~=Uih@l(xM_+gYln*Uo+@_~5BV*Yh(Qk+LE5V5RCL<7)Op)tPN% z)ikxany&EL=3w(7kJglgG1NxXL5AK4dawOI7nam22ivUJ#Yfs*m@C)=Yeim9?pUYwW=DM)Mr;1tM_m z5a&kikXA5QW7P!!8;#IeM@C3Cy}>BH{a+tq)(<_`S(c!tpzx5h`*%;dM25|2$&u4( zS65#n)MeS~GckP^njw|K+ZJ9h)6 zZ{-0fEQAprA}wQy*=q`8l(E1ZW&k>2jKn0^Bg6&a4W`olED6BFlH_I_JiHAO<;c%) z)=5%os?KF%{em_}AZ~0u@7!(Ci_$oO7f+1#pN^@zQrB@cbxEqOOOfb&{4o9}yBK1a z(W{@4l}UL!qr@k_W?7#jpZ;eBRwk4qv-a~gV)JP^@;c1KmQBOVUUv?6Pt$_ zuey*Q=D`7ULW1Bwh4rTY5EF4|x6jC%1?pH9@{i=$4&e^y;2uZn zF^t~G;g)T{9uw}wI$c4TU3p|+^^cwpD51gSF+ty0j1`>I$Z}%FCruU2FgF`>gUU9;_aoFN zHW=FhlPhhJlytYl$S(0QPC0en6Aa6!9|KHVF}gHZWe)q3(*LHi1iLT*FCyaP*xw{u zWhVI@6ANuFTaA6&QV+r$Uq6GSGW*e$&difIqE%K?f!BV9#YjfSGl3Suts2UxL8rT| z8pIKoBCC?|+Mu=*t}s;z@^h7I__)xkh53My=sdjDJ-}~~ZbQwsN!_8gDrhWO((J6% z>Jk2&kDBPD;dHc`1+UpUj;=M96649)0+(>05z25H;=k#7?k-us%Z4`*? zX1Rtk3@DHA?y=&jeerABoEVymh`KNw}VB`L9n~4wzKaurEy%{B1k-CerN^ z&#i=tj~~#Qm%Qq)!7VD{aAca*ePv{twm{La$Tm2E(_1&p3A|NT!L~XYTIBQcN7*TUw&}J#;ebN{ zt49D0t2>0g)hFO{$sMdWaVOjmJ2>ReFz3~rPlQXzDAX0`j?6!%*WDqDSl3(zWHia# zLH0KkT|OUM!Z%fM^iIO)J-$CYO^!STypnBFNpgfzCKsrJ$e#oX*w9}dAy#NwBsm4- z0$Gy(xUPWZ43}rGjKP@GN~eRgJYp<_{L;(%?y_NL%#Re9LRpWih_C-+97n6UwuAG# zCAowCe_bJ*|7B60s%HJ2P%-`Fnx>DN5G4W^w4`b&X(XjPJSz>fVFW7!jRL7^qr#`r z+ro6s+=E}~aj)IPKGx_z6sWbt%U|cQ{F4u!^R`XLOQ45Ml5!^Bcus!vT(7@>T-*Eq zz#QEM<8q{o1n8wQX7;1%MISW9P($DESs`1>kUPLJ8*ya|AUT8Hbj7eISq$~WoJ;mZ zCdGhbMFU2gw;WwIS!otCS#f(6=O+OdY{FC9OgR@7F=P;?J1nl=WyCgkVRvMcJ2Vz* zn!Ed|vdAMTg4XF+Dy?M0bF>haVKJBsWJ60B)tfk7 zzln2<#!_fv)$JTN5$_BXk>!{GZ9@5YIU#2*mnI-`%fTC*zat=$*8Qf@V=P}cmNb6WTWeFW|*QmQ^ z;o$W2V3Re@@vi}9lZIhiCPzxL{EO2WOyMu!6$}PkxYD0XS*ONm3bi(ybCTxg#2mt=`IR)lJlvX%c8o5;jhsSBGr2DAhNIz-zG3D0$0N0RSrRGH0ias;z83^RS({ zZDp9~eMvx3r7BH$k^bB^ZFS8a?Uu@1tR6Ud;}pZ3R<216g$6ht<@G-))jr2`po()v zQ2T9M5bIW&qxe8Krs@s;im3xQj*%0tF5ovLTGq~Wevz$q+l_4y@IuimrI3=sI3TN% z?2IXgn?{FU6xE2ss}NjEAGx_p4cMj!Um%1WEZ!Pu&qayqkpVAKh;!`KXKf^1y()Fb zJbj4D7VnQ0tAHMN;nF1Mh>(tlheI^BHJ?6vXc$GW*>f?^Mq^bAC;T+Aq2#C=Vf=k; z+w1FlX*la zKj8F29`R0Lz#+TK$Q1NS`D(FskPQU9g4mOuo&rV0b799|c5pe+eWOl`xw2+?ut(UT zEGifi;QEI^G`s+n_-~$YYA}r_jEDyGU~3Wg4e&3ba7@U=kolrw=ZX4kB2}0$2^c?3 zSe$Eiw(G45<`J`m`DD5dFI}bOk+h?aANwB(zoMS;ktZKa^j8yH!%Z->BKESwkaPgs z6}0L#pKJCwY1V5$p&O8(%6FgmI>S}Y7*pOPPQHZxy0_Hw3clsPi0i?C*>ZmfeZW&b zc;8p@FGW=G7sw~di8Dw+#>~^eKO(%EIFZZwxBLn+Cg%wcn2~wKaM=o{;SE6jfSzcs z!n@;%r%*p53di_;nSRq$UVz^dgu*hBD2VW`ZhnJ97Ib0^{WlNOP9o;B**D;+bohTz z5@r;MtJuDC^!0ZXqWE7Z3IDC2=lD-WL`BygSrC=?XUF8Imc>Vve(L@Ts3`ho6)QFY zscq4~V__?TFHRaX4PEo})xz4>VKj6s0ptIr7|#d+Y%K^K$FH`$Pqv!L&9DBw&Bp8h zU~5PX3hVKrDK9Ke5W+@S)Cj6H79AzQs|IX9yH7LBDVj0Bzb}3ygcp&UNStX?hU&X} zB1QxlwgO*lCm*$nhTZ~~j}{X19^&#HoO`ax9K-O)8LR=Jk+cLQC@a5r8V_Lf3-viw zJf`O;|=KxXDPzoSquT=iri;Z@_BL4=KiR;QCR_Ht6a;sIfhrU=j#Nc z!)bKn+z6#ZGY*^Rf(CXgRxFF%U#z7VW2q}6ll}MO(__IrzK2|6^utb^th_&JS)}g8 z5sSIQJvVdk0W~_D!Zq(`xAaIi*u1Qu#CLeBl)YH2=8nO{B8f#N_uZkQ z8_FT{7N$tRfMU?^{&lsp!A{P;l>?<(UtAwI%q)+@S@m4Y(LK=kvj#y-- zkY$FRq!#U^BoCnnSK0@R|K3anffeQQeG@v=;D7ue`(OWi4u($7rcR`c|J6vUYpbKE zqkhU^Vj+(r4_HU)Y$yy)|9G5`rTl{qCK6n@V#N&*=SY|u&V)9ywx)mF*_{jTy>Hcx zR@#L07xiy}R^hH5ih^fgk=uBY+F?k0r^8o9_~jOE2ux+%1Pube2yV4++#emUN9#gQ`x zug^R`pnSLlgabmktF^Co@)~BPRd)|EscsoSr8{H&sq*HlwLhJAI!nCDWZfTK?CM08 z*EGtk0cYY8zQ$+3)S^3*Q8#%`q2w*KaPL)$@a=8{P$v`lZB3Pq*W~qHLXq>4Lo3mU zahtDW@#|YBJl?>n$>n?jg!f12k1w)k>9B zlJZ1aAdFhQ=wvPJ(@~aKSm~NlTAZ7ECm}UCKjXPZB`ZaLQyOAhP8Qb8I=L@vsWsLC zJzFdy2y2OcQjtpwqqDQFIjA18*zBbp+E{hOB2?zRZLs=;aAWiRAy>h?G#1$%F~{Z` zO{eNzxMA%jYP2)z3si*jjj)a=iY~ll{2lA2>)EE_Gdb5+9#i=oSNImWx-8$;)VGvY zdx7!Oo7Ow*IQJPt&GtO4{p;hOm^5PY_ZB(t=q%Ght`M~R_RurBjlt=B<7w43m#(vQ z8ul{*9`x*sy28qi_U3;rT4VCItxwUYgfA$)pa8hKRF`qk)@$L$1zOcTWFa24!Cal? zOVYl~gR%i}s~3-i^dr4ICiSPK+9y*oVloD`v!p3I37X`v4kDACtg6o1R4zdiBaHP2 zlO4nvzioz&1Cek!I6%d7-b$TH!I}mg-xXw}eRd_NWEpqei+g?T%>{a$|Jd+J4? z7R8NuCIvb~sCyEEFnAtFV||h=Tz97R#rDY5M~1DBHi5Q1HspVGe@R>xq>PIz zrN~Go4s;MZNNR=)@)Vjh6lT0oc#oYVdw~CR!K*0dm^m;lGC9m1 z8Dqbt(ks@`FMkqS;uWFxj$lIz{z`bga+xbUC_RWv|D137)`H4{Pmgs2Rl`&?a(-oM zjT9#6+*5M`sBy@mS+rbSQH@nZy3r|l1od)fAR%Fz_f&o5ui4-)_tMeRf9*yp_wxwH z3*|&IP4V?;!rk388;CJ}pU0Gqkxy*&o^W?2X6=osW^>DK!M)0e>H|XiNs+eg z|59sJooWQWZk^X6`q!dQJ~FnIreOAyFvcs^o_6ifX$E`-<{57&SJ1$w41((#oU2P;@99dj$;nGp)v?3~YwB zQ;*TSD|;nPvG%YHPMpi`s!*A&C+g_Pl9LwiY!<5Ld*mEX8zW-YS+&4ll554OwZ+gA z+<2($_%O*D`>_isNd`I50V5=!?}9s--5x77QshCD^vn6%Jk8xy^fi>+RC-8wzaq0g z_2$PlR=1d#=HX~s8|UnLnDSI@w^OpjAO^?ofUJQ@>)+=l6I!mQjK*26xO1Fd7d!}` z&gESfX|li0a?8Jx4f!j_r0g`IT}U(ZVEslI++&Cj%vV|j5IfLsp#l|8l~cgQxV>TS zmpatl;ku>%if?Vd>g-v8IdA`p;W)l*V&L+E$*Dt{Wv1^{XZ2k*rqV>B3dZI`#)Q#D z&WMJhULs?WQihSiLzCEhY#bx7vC%G*&sZ8{FH@~Uiw=t~R^C=Chf6sZ-z*5*jU`D_ z8Di}G6&(ju=T#G*t+`HHSw`>53$S5FID*1ulAT$EbT5~Lhu0+0e!)uZVQv4Lxq?@T zEq3jPk)fh@!3-N`*^PFFdaeDL)?Eoi!J`M3#DVx!9WeXU9l-ij9#CUqv$ z@kDLT*8*!v!r5J8_HyDFbygga>YJ&>+6e>i2LKin&^lfNG!_Py`LQ&hj{|a>e`b~o ziNSuwtM!BaoF%AfIO>c)&wZ6q;REYxs(N`+Igj#sF2<|Qp=1y^In|R0?AZQiNku4bGPsqx5%RBsx5aBCM8DoZ6H8C# zTG3I~aEz@XPxR|?j;z<#pma&<0>fq0j~DEkr})VF*B;)HZiO+n!7g0GBFr1*eh7bK zxCqXW{b}^a&TxVqLy05wIva#ruyE1pM#GoEy+1zk2Tq2~kl8GBgZj@4;E!%@k0=*E zgz;Z_d@hc!7@nVh3@-A2)d3+^8TydxTb)YH$6a%KqpYd+q+*gO9&o!o*i_;Z z+||X>hF;Lf*~Q7w*hR(3(9QJ!7Jn@N5qoO2Abn6pG5qNxIZ+^fN}v&?C?HGyXe5LR zE3;4_jlkR1L3i?4k9Nn%dZm2a>DFj;r-?xGzW(h#vg7@+qoe6|Vs?ByI7olJeL3y@ z^_89JduQhMEl^#(ffz&=B{5}0HBTC1vxPXwaiShv9nA$ePdc)175}`7^mc4y$9QvS zq{n!3WQ>pg=;R21jgA(h8`NQOG<0Unj|SIaaYh0f5n(_&AsrQk!iGmd+kQ8_>Z6_Z8?qM@I~GaA!uG=;HSFCFtVz_buq+fcqMB zaR>VzbaH$9B6M>5`zCa9zMM{R_lAltE`<4?fUrAcvce)RGkG z1(=X!6K7Hdb|^y+AG*E*P8{p{w3>F2Tj7yNj)tu^6LM{>wG8Qj8V4u29c|MAG&f(~ z#V?T@2`6Hq%Q+!H^$Br`Oko=o+5^()PHAoFeB5j!cCpL1p%jgc=!w=X)K^}o#vn9t z49$;HD;lJWEBCe**7Js;A_?N88hsPd%?m_5iNX?o5*s1}Q*!G>)l@s?-Y!2P-|ASc znV^l%MhO%UG*Jwq`&sKq1wLEJH9J``BRS#_YQza_Vpv@2b#(Gm+YJz2`(eXqy-K+>wD|ev&SVE4gl&hRBj~#7o zdoK9+F`}M`?r~?q$zMx~OKR#UhP5eN!|q2=Q?D{&JQ-rak1ZqaaYQ|t;)YM|!4)pB z_FNj|7DNavED{+Da;aZ2vdV@RR~oma-EAAUOJiX-$RV=ttB8NB1Kw>*gBQ_-E7%?b z8a^QUO^wd8qHLW+pstj#d4NT|IH7+QZm=nPu$?iWTya1%pc`n=_E4z>o3EMj-cC^l zEn>}ggt!g{p;A4k5ZkEMoWm?mSQLBdmc)}#)mTE(7oC8=gBavab{3Vmei%A24A0>k z8g&;kA=#$IQ5>^~J|k+*TS{vkux;D>gw=|i{`!D6Y=YM?BSuJTdaV$`Bf4k+?q%L+ za=mxt%K54jhA``ql8EJyo6R)ew#6G@O+nhu&D$vh7)J zg4wdZJcdkf*_@`al}1RmX@YUKneaq(5^Rkuwx838hod0AyGO3 z(ijpdc>^I0`kM%$;e0_hK0dGU+>f{;!CL;Ns*lh$T~bJjgH$CG^RTGM$xk+cH0o`pT|Z{IxYz zvhE2n&liRHN@O&A7mZEm>zs{d7Sc4diB*Hlr3(>vgs6l6nft=4WsUFCIo2@H-WzWo zz#EE_qpxcw>*P{;#{M7cXQxY~=dzxy>2hNoZMv0}Z z%VrALm3q>=2=XVBLM*6mMQT(X@RSmSSmH!oXrGJ;W%RWx;v?4|k3$Gqdo~#QTIL;y z^lcoC-se_N6}2{wl!d5bz%SY{hO{rDOLtW4oTS&X8ob5x#!igZ)IWA++ohsw;U}?6 zo3*3t6=n~&T{I~AK}*GwMu0fn+Fo%gvc(66zelO zw=G>3=0Gm1!(6q?hiV2TJ)AVHbd_F^c2}lL{wkX_b;7ZvYaV>nDRU^DH7$G2l$)cy zfv9t{)v*ZoY~*InxOMzEiz{?>4>}h}M`#@%d?@ICTgF;uU45t-2r^zm=`!7{Aa6;H zJtva4EvW_7CGR3Jv0T2;`dik${y5-Ts@=-s&9thg{Yw2hO4g2M*a*ygd8hYuw4ead+X5_4jvsxRjRTF?kS zdA?pj0eJy#Ay@#nL2o26^nXQ)%NBO71VeAPf?){2AtIj;VDU5N(C966nEbvnApeg! zCSUX{qYu8m!8391@QtZYD6sk&dua6*F3f&k8?gJw9kVz3n$d@V-{6_ZZ}`UeJp@?( zj5Xx?%sE7VBORs>Tm|$E*kk#H?=ruG_?tYVd`{h9yr%-~pSg$lZ{)-LfxCdO0e`Gd zbua*=G3$^NZhr&$quCl(Hxtch93Z+eWnlM)AXtCT9b$J70kATV4&V!j$LayUX7a$& zGrj@v%d zPy~a~7$JsY{=~_`n)(?%V6%p2=t1Nw*Dc`qFC+@FaKi&MG%5;_>Q2TaM0Ry($}(;g zy?mFTnyJ$p5dwRw07@9J_~Wu)mKk1%z*b5a(VpX7eB9r?6t^}KT}gK|I^IGYRfX=9Ct4nQS@D?95ywW&l?X7VzkYVya9tVAg?elFCu8v3DCE;~ z(m~WrVfn3#Zkl&Xbyty7yrdP}uE6Dhj75cchWD=$^9(2BMptgOfo@Heas>9>MXUl( zO5e{K4-mN*KKgdb*1`9Wbp=JnWt4|cFCoRB=FNTetvy1N?MlYFtX4}DA?u;Pmul=T zd`-2y%wM(Vvy!KL9%+9qm$BlwDr2k$o9AL#X)q-DKGpp00EMG3upr~-ZSr$kDMt!T znLUg&?8)yA#YU6}1oCJ15IH?{)Q@=}8&3&d|^T^A*umktptgz$m7k{?-#~8*rBX~Z9 zM`t8#S6sFS8xSvk>qw6szw?CjusWL?|I^7Jj^7<#2b;h+*i9{Q3ZyV=ojBRV+!h!5H z=>)BVOESe%&vJH?GYp~b#0=*fnY?dvp2|bw6`aX(VT*eZ)RoI3&ua9hc_&#~-K>-R z`ETDe<)E`ynv-!5PwWfrCKj};D*)Rr3HlSki~w)r4^&n;&IBu*NRz*@qhGX<#aGkq6yabnMp8s=#$Y z2Za_GF2Xfq2Ll?5$VNOzir7%7DQjR19e4!;ilkv7vL_HIfNEp~YKjcXiwdR%BqRZK z3={xGuY^vKhPqcl)Bs4wK+&K<(F6-GgK^TJXJ8?+K21r8Llo2|x z;2MkUX$1B=IuvCIh(ibNz<`nujEjNEAwlK9m{&mC5EC_YIzi2_5Idv^?AdV0%N7U; z6~qVizc>UU1;T=ZQtu(N2Ff9W`l5l+g-*sm>Cho+K&w|myQCpjs33M=*D(@1)C%lz zbtuXfM286Qg9m2snUR3DLxHIcTv~x_$3XQ^B6`5?sG^*b5iy8xf?Z=FdFT_~fcVt> zYRC-fR{#%`-%}<5&4&j2j$>A!zrXK~2G$!?9}DF}g7}Q?r+~7iBUB(JxF`NR9??Gw z8u%UdSOfWxL5EPmJ_Gn;q29?5pK(5wQBJ9e78@Z7z=)hjkKq)Q#R4@oBs2DXwOo_=9=s-cxz=0Ia6tF5%us;+~OLK5mLIxe_ z0_9x93_bVUU&JAx>>iswP#WvEc{ZWvbvE|1_p}mOQ5L)v5;w` z%(Fs~Qwj%)QWjN`3sfMZm1OQDgOpGxVBTZ3#}vZAqyNFir4|AZ(2umYl){*ZHBwwV zQURtW4HN`>D@-XH28ug_wk8zCL86{mqvHxD44R2`4$ZJ8+XjmA zeN?Q8X9LB!p-QF{=z$`dQA?(j%OFwButie}lweWGJ$R-R$$=sfM{%r)$AO|X4&gAR z@89!mNro|TYNV`pGy_bf0w`R_e(z>Pc9-1Ojcuk8w(k->_!tw)gx>e<$Jq758pugv zUC6)wI0e6xq$k`K+91j$-4=T_SO`zx3%XA%i6{33JzJE>mwUxsI7#Y_xF0F0C-fyd zJD1!QcvV|yPwb7p-!35_{2)9VmEf0q)m)fO^b5VOF4_I{0e^-d=`Zr?yr7@(8F&9w z;!pO0bLKDc1^c@JjJ`is!kuth!ky?hWr`vFLiYTVU&&iCzK|Eiy6os(LvIUUHOa4_ z<>KqVclYmIvo>LXA3uySfBc~TU$n&k=?Q3?Ozm9Moh)7c%V4Bb9oj=>8SV4h zhCY)=0y9GZnb;5kO9+*vm1Z7MFdTjWG$;Z#d5RPZ*KD5#q^4D?QcW9ezN$cUn9LP2 z6h!AodeumjZf)zz_tuiuD{Wfv#jlpOAxpBX{& z+Z98s*mbT$VXZF|e_a1*lmorkbu`Y+4j|aofRIHF#XNRNz1gB#^zm^27{u%oGuxw@ z=AVQ;ZD;}z+AY^iAiLlr2QP~nG_xQ}J;OD+m9uo29MZ$Ow=n}-7w$aqxkOLIJv&BY zy&dy0yZe4n-v;pDryL9aTPrfGUh~wzjvq75rs>tCLB29EVYx_nlEd00tBQ{rH1_n+ zamjNqW`B%mQD5;?!k6#7WyzbrF%EqXePEu2TB6Tk*B4}^09ZtfCRG~Hv{GQohNDGc zgEs#?$xmO@D%ktSp2t+}SSYlE6{|Uq7dt%2yhuw63=xtv@N6K_K!N+CWsP2foTza= zXVK!YYDy3bg5s!kyTp1m+{K$SN(L(gWxC1&G6&Fq?QyX#binUCgB@_kLv?+)j(H>q50~`g(Qad+JMVplJ^J%b$ z2Qzwj|JZG*T-gyNvo#`!H&5=Ut;O3F{6s1dwPm6d&aW@9tdnxP$Uu98a8Zk5qu8*E z-9V6P&F6E=pgVMk*IL!NWBW#Js2Rri)$aCeK*T{bWAc9Eyf8H!+$yWHfj3hsVm2HO zMY*)FFpCmq6t(9ggHeSzMqZjF*G79SUExtmu|ZUuQcS3t;#W4!OkP$*`6l#CnpM&O z=7HLl_qBSv`IEoc*DyB~@rvx?O-|TJCP<_M6aFP)@wlCyOR{dkakBZHmR6EsSX}`x z(pALpqNJR`jAIGW%tzS}AeL6hPyzwP@2abVjx+f#{&p5=m1Q#3`Zd$_PzBo?%>ScN zrDJ&^dug?tBoobDcl^=7nS-+NLH;xyywHG3VuDTlKoVlmG9)vb@i(W{QM$JvPp$?0 zY@9?vkMEs9(%KRu0gb+J-;eJ(9+#;>8@%#^bC{tnwDaaOUh2dMWoOC_mv^u?m!5$9 zzaB_$t~8@BTzW}?`M?t{zd-%JH^y-pwkW^#&z@QJ)G{2F)~gRxa>@?aJA_7g64jvk z)o-*H%DjWmQ6l%O)=irX1GGnx8?HF0o)>Sb1juT3fvk9aqvu@T`Rng<@{f1s9RqPheXnEYVo;!lbySXvC~ctVs69NZ#yJ=D z2b|E;-%qMFq#tX+g$nsy?DdwO9ht5BG@3-)DJ?gaPF;0bt46PsYiToV0QFK>d{CK2V#V!u=%8`GNHCaAbRut}Jj9cR&V z^AOi-v3B(L-LC^yh}w~Et2^3_+AboDJGrB`E3yx$KR#?U{Kr~EI(R2QhqE-!p8Px4 z5aqwfsw?Efnz}f(i03KXqaf=Bf{kE4W@s<2tRwze2`(#eU`d7ucM1)YaHh%`k=A>= z_FNtpgOLMGD3+Q{g*i8wy<5baw&#j@Q!bUgTBL!A6Xs(yTz|4hqG#)|oPIdJxGDC{ zTg;;578xp0tkW6rTT|!5u4ln=f6L;ky?6?$@dIrpkQbZ4H7gn3;MqVGkSt)y3vn+- zqpFSEpivGqCNedaELB=Vb0vKl{n6&Y({ng*D?n)1nZg4=m$mwsVn4h`wR^+L=IR6C zX?>xat9v~j^oS4JwlK*SBRs6pFu{_l?Z7iL(5WL=a(9|Posg#3L?W>cl5NhA1&~B? zk>F|9)+Xi`#BNK_Gb75KqgSerWC?z!S#f_g(iN=vUb;I!&qHumpe+j7ClZ=8$nOXK ze&}m#a>zFH+JMt&sU7ZwB&u%iQ8=0J>FJOeL4b1?M4QYS^s6;n?};r%nY7{-0$`#c zzP%`wCss@qNRvpoHNCc?ySH-zxwE9>sl*ry6vB{VcPJKs*&+xXk>TBvMU2Hx`}tOB zN50d~8wxr&1R*w~)3ism6=azEK3sk|Pq48~ zd*Knqlm7hc*~!whZQe$EmqH=qST7?r*5jEOrRr1(2$2UbciI1)Eny8+h@ zobv|ARbv9X;pz@(f&(*F#Pekb8wK(`HP)Xvu^nptb#aD)QCTf&uIo_BT6PI~#211C zONzzPztGZBa77Ba0*q?adpuQ{4s7|8Iqo>#K&<6|@<}(eCBAX5$}B|kCb{g>#Td7# zTn~DLKCHmcqPZ6|ps+?_yMLB%!f>mXkdAuAR z#;1zWP(?_!5;m=>9u{pyUZs=SZ*M8oYF^Vj|JNl&y2@sZ%&p7Rmy^h4;}woIwR*1G zq^|r`pThG{8vQ2&sPlW=w1(dt1B7*x{VQ_g<9(14Tk8%L12&O)!nzRkh2@UZcL1&K zhyrj7g;`3IR*C%bE1@7})QKgo*HM&Ai+Z!l6si0+ISA*EAGR6PU~A?x7h}@^20T*7 zcT5+kWo(~!e?&SsY6Qb3vF74m1c%NG<_`D))3}@gO~P}ka^gc&*Mb0}Hn^9VQ5E;z z&ZF*uDfUf}$D_s2^>v?WCJ$EkqLHcxl;#qvmQ}8*-|*DQI@Y)oR;gLEzp_?|dg|FM z{N-HZ9zg0R&!H0K>$Hx;GeO#i4(_bD9djhVqQ|jhI#Ghx49{@w*_)Qf zl4Nsr3;8JO|Ar<>R86O_25_mm(v6lLxw;kGT!?W7Q{;-A-Ep1DA`t4)s$`4v)=xH0 zWw)b{n<#W=O!J6aWNK8oVmILby^wMvA3W+A4%FB7^uQ^8u4P5PHiD_tiGT7;eG9;7 zMDUb=%-{>p;3C-L+rh+Faa-6C)Z&)3#5k(~vZjW&tMGl6 zZvB6xy<>D`QMWBxF>1#~#kQS_ZKG02c5Log6;y27wr$%sDmE&{&3C@T+wM8H3;X+N z$tuR1PDxjEL`bxPuj%)Do+|gW>8yML`hG&Y z;E+7TT~Rfm0~dSdwiE(YFo%&V?!k~oT)XJXR~W|^rFw?3E#UVn|K(Q1Ce5a@o7r4L zj=b($3=eZ?N*ijgb(|{weVqpHO93x#1!=!Uxsf8W$I5}e_YTdN)%4q z9)h*Ep=&5>at&B&UK}!kZ`}*pfN?RtX|h9aVHTYp$rfi|)jQ*Vlr#@N#WkDS0(t{dgl%oDjDa>{R>sz!1&%t<%E|aY^pbN-==?Tyh3CB- zjm*#x(0PJC$VdVpgH^@KX=WAK@*7mqkR)wP8Y)>lMu)JgUP)Yquey=8FS}WHDL{&% z$aASw_kXXiTZS~8J3T+{aJu>D$C#?Whl%O1*8q2UL~@7|!czg_4JxNfOS2w+z|90{ ztDzS!6p-5%rP0g+!Gi~1^oT0}cSbP~Ad{y3;#bzlc>g=peLc#2c2!v#l0GXahiFzr z0|sJ^^ocl1#G^k!bv2Uoc#Zn7&inBsMxVb5uBj{Ee&X?{(R?hh zur+0~ci~RbIMrD}^|IU}-p!TsoiVU&c+Q8^&4DP2O3`w*SjaW<*}!XOe-)=R6^rB^H zSL#~n8K_W%6Gj92zo3xLF>}!GWFkI=OZIWpx$O6HrQEDs-vZ%dnE$%Us4fNnlzq0; zDZ_mE!t;N(%0z5!96>-EkhqmC5F}+|V*AfEmZA33aZMDR|CEth+_`uzpS;<#9_&wZ z#jikRNm#jc-1S!HVma*XoI3k>BY!#^a}3z=t8R#oUoejl?N?t|qgj~xx-ETFgW6(u zTjg2N2lL*VJs!?lOr!pIJotQ|b(6Pe4Uu6|D0W-A=?tE_>Ga^0x~cRT5OC8SX)3?{ zDAiiLZak^7&{R8SM_9tH^;n0qhE_KnriarQHVzEesw!8}xaBZ6ucg*$!?@?P>Kfz3 z4~N&b1FULEy96zn8M|Hcn zVr#aoV^o^{R_0QVSHX(pcY!pC5!!0pcOQQUeZnRROE_TI+Dq))urBMUv3wxQk-bA) z!Z{#-`5lka10!r6!$C_JW9ED3nVP|D=((e{)cogM5m9(0rgP#YOuGB7P-1_QAge#R z>)maNKvHPIBAa)*K*F+j!-UH`ayBY2H0;fM_*-uVN0fTeGPAU>ha4P!9XWZ7038&wvyE2&p>YkZ`g1@b3$H)A}^A>%|!iNwu~`EsA-; z<&{3AGh)d?HY%^~(ZiYM)vn@VSZIzf>L&9QnY@|nk{|>p+dA8HKRpD!EOR=phCVy` zWZ`)nUxRbm+)s0-OHs@g(qYmM7cAXX)BV->}T-V4> zv$(t8q_2vFC-_H4`)oca)w1^)P)aJ#b=?}#Xo@k3x#W1>=Q%4 z;?r9pzjAVSLpbc^ilr?j&@JjI-+2cafutX<@xB0idK2vgbqjYIg|@hVvPI_eWQpUHK9`qE2!`*SNWu;pefrSW#!VMy#~1oT~gq z8CT_BPSSFuOIoouRS!4?vG39`UFvqYCzp;;ORQ)gU;ZluSW23r{-aT@;j>Xr;Qt*2 z{L2<21Ohpj8#safZLd+YRzMR%_HL|l8N!H80*6AcIcZR<3?Ai$7mK4nFtbQ~oG9Sm z8Mc?G{4wD3JH&6MpKv>hW%8$<86F;(!20Cb*%|B9C1a)mp`hDWJiCS- zex_BR`#zWYruQXN`y4}$KD(1~>I7`RG6UyN?AWA`K_V^0Dl6^;1whTB?`eQby}?ej z&vxta(VEUxyO$1{*beUNkpmm=6_tARE{0VuC6)ipZoU=%*SJZn0hVvYH26!!b7_k_ z92bkCcp0%l7s&3pO=vy04lBj#abYWTBHR6h2O~Nl3!9bV!-}o}pGgsA=HF>Fs^!Is z(^vzgq3q?Xbb4(Wxyy>zfGPfYP4qOH#%RM~UG_YH%+fhV;EqY^OcD~Ktl%U$ot&z>4 ziF2;fQknywixG{EDbLx#VPnr^6f#MpGoM4ZO;e$n1P!~I$Rdd7B7|dhvB}nCH@0Pox@e!8-^Ho|gZ}&GM?KZ*EI3_^} zZ6Yclk~${vyxi60y~+-Fp+BHS*p?)@MJVTmfUpimF+(Ut$!{%&j=py>VUlGL8bX*; zFbg(_ior)3$v;j;9Qn`oD*s5&$jYR(;Xb1V=QBOy`oD`BDI3uLDK*=$nPbBKx73Wq zd+?HK8Z#uA5H3VrOc6s#6~UZ-p)z`X+~TM4WIUJQ`i-dF?`=PrZSHM$wV_Ji0 z(V8d$!mP{3xV@>Z@7~~cSP6V|I;le}3~QQA_6fu)andI|{%R-j9`o#e);-nBiPBd6 zyk02IZd*me4!l>*TYEWdd-l__A~+~*1)w=BUg2Yc2g6i5ZRwEeom(nY(dH$@UfJ&4dmPOm9mgrOw$z+yY*q@5RVtQc zejKT90RTqnp^@)$#axTUiEE^tLqD_1d|%oHP%|l9?l*-3C0Hr-HQzcqt6Y#QUai!x zwO#5~t!>m!YjayCrBNIa~prvjXVfLEiBrHiJOCyaW= z!i)%H{uxAw{6I`rx>xj8V>+(nJ|`GX#abnk=G#lt(4BXmR1;UOJ!c&C!uc3rKkcsepinmU2KlRiJ^D@xO!ezjbUDsK}^%!ty)CO0^)WkS<9mi8#WP+Kj(AJxUD? zCXx&l;wHtIAT%d$GjEtfo#z{Rhuw6Ty4w}ZV;-Fcha~`Pl!kBISwX?Y=gsNi{q^q~ z%rBhvGg0UyUVEqk=kOKmCT5^vY|eS)T|wtiG(%(sv!e|9VBo@?h?*()28v$3yjRjSX)&^#Uu*8mS|+T2_{;L=Q*a2sve ziGO@9S#>0Mr9FZyQ$_1X$g>)f?Q+p=ujuYoO)bOul)JvNDhDB+~ zO||x=W|}LCeH77im|W&q0{za(>@q8jpjx9W9ugnRp9d5ZD!vF$ddu9i%VEXXV>l#N zIkV59&vKtJ$a2GIeW%L4X)Leeia5Q<=aPH*UaEy-m2P^&z2K8 z#M2f2Y(ZqCDL5a{_rK%Oc?>C8O!P=)+$%2xYtzX3r0&Ri;-u_GS1m(4=g?CuMDKH> zY$$FxM&hqChdsqgF&?b(`4t}|b0ZcNI09Aje~`f(rw=MBkEtrlv{JsckZ-gV8q7u* zOycLqm=|2XE8?0mr%$`4PY=t7`vh2f`6X;uD?jlhx&8r`0qXK-?p_zLa)w&{tEWe8NPzjcBA*Ko-0&&r(sEK?NWzkC7y z{|W~cptbR5BRtT`k@;T&MhawXE$;AnQOp%&{7DQN{j-{BP(OD>6T|*EC!Zi+%1c0Y zgUkMf!6-$neg*ZMdxwyh1BF_u$eUtWYhFd+a&4os)76WE;2(Jf<|JhXH;cy>@+ zfiTZ}Epq>KzIk!$v3Wo8et-C&@I|LL)tBtMAdCw#9pp750F=rI6Dh3x!S<{Kw=WFlERu<7%p&tDI%*JW|dk2M); zY5!fP4-uT;NYg;F!fvaCSM3K^<`3`bU8trD=5;#vck#}Xg~!4T#G4~xN85Ng z?ADBpktW}5*J#Aq@=PF0)$6xCA(n-Sb@BCL(KP~Cfnzmug>ZG;Y1x)fC$F>rk$a%gS;gm2-Y%xl-{u&c0>uNHuc)>n+k~haoQGQ5Xv(tx zG`!IykpW0~1F-hh(ahIb>>(*Z>l{k4agMHiqFTAk?dH3P#mkXzDwkGk=JU;rQ$B-s z+(dgc2x_MZA)yxH7(t^Q%1I^~u9{bil@?hX*W4GQI|NYnui-pw9W+?#IQ0$TA!Dvd zz`s82zW_@x@i>7Z9tS*~`pq|439NJojWR6wZktr_W4zWmjH#OlWEL1kE-@Kg%=}nO2OtAM~sJc0H>;3-?={SBcZt zD(Hzxo|}0Le>_*-9bsOg>h6jqXC3|DA@`vu(aF;XrEnEy&M?_TB@s;(zJrfN`oMhJ zY+G>2?FGLYy#^ES!2$NDb@LinH_lx%R7Hp_F!E(dq_R;86NQvZyCjrJQzpL+5`GhP zSg}Pp*AEUz^axK5>{^L6!VNc_j>uxNcnwDzWDLDF)96Dq#67&mtkKM2#d$WiCpOZ7 z$JuK+Gd#j2K#in&WB!DTQ-+M|ZyEMFqM4mta?7fl!wGQ0msBqdD7oV3(C3XesMX$r zuNS0bvEo7C#eA%BL9Lpn`efYF&Hfq$7mv#gJ1)i|Nkj0~(^r|eTa;^p?*QW8B)VdL zn|{>zQ6o#h(A=zSQy}~vn?md>Tk7N2|A3RH&*be+jm^d56HZz`TaW1eCpZx`HntP7 zwKM-m3n{T77Mx4dt@_IunAaA9BQV?a>AiWNf*pnKqlO7k5`($r`m2Yac5mCcWH*RdFNn z;Qq)jyO8Yhv_cz7Rttur+vz2nm>y#1ni~B-0Mm+WlbhB6kF#4{pWnxGkqsir_J{Pv zQ0&UH@^|i*u+X;i`dF~v*o6Y7*MG=O@dO$lkd@!~(7HG0T2w8twKa%Gk3tC7hMF3a z6ILoEPHW+bMu%1AJJ+2J@dQcS!K$dGBjbAW*}+<~Qw7uwoX6o~4^3J|8c(G`b)u1| zqACZdRLTS_&>*&^K61-X5CP`L$j9G*Gp@B`PZgW+c-uPuSV|8FcnR291%C8JGwmKS zv@J(Gx0*8q-n7PWs8QP9%9*40tRV-PtpvFEl(tO$b1M8k){03WtFw z*p2ZUI#R!aRKVctbA)=sRz()e^25|ooDQ++CHF}PL9PTu24DpcWP!zA#j=(TN}(~R zB9P{Eb#kGuTK=CY~6*xERRS4qU9f ze=%-XVtV+vLt%b?aL{51sPH>LiJ17%I%_XoZlPILt_%L%^P$vPN|JOWiK^rAS7b>o==l4W-#D8A{8brJqyX zc&s6zkXM%dkr$N3-!Iae5+lvQ9J;oL-oK1tKDb=?vwfwHQeo~;-D$FQaQU)>Xmd^p zrpKCW&}20|Ok#?yop)S*z)nMn8ZPr^FpVms~O zmn6;kS=!*Y=*m-8w%H3i**AA6%`51)Q`z3oV_L=y9Dy4~KW-*{uh1M^`4#R+H&u6L zfU~oro5)Nsq=i_`7b+GGwV(B0gLolT7-##lfAHada8&#k&Ch=d;(u);Y)E?1z!O6s z>Jl+r3{az@zm!%p8_OeCeU4Ss#uY3PR6~B5XGkNavvJ-t5s~rg+IlB`&p&+R8X|m! zc+KP6T*;5NGLhmC=khRhA;@ZRIhf2y>-u=QW&9HN$Q;<${7nMpqTl|j%kO5&{vX2a z^!;AMY{VS9^IBLW^pIEvD#78%KZLR28!_7Rh{lken!;bFA^Sqe*G9-dyc13J!ryBu zv;vJO;VXd@2V7GHXX!l|WC+O|bbT2>Y=TP5{$U;wGO-No)kHv86?!e+y4D#)SJ z3^}=~`ZUiX#^wy)B{nS%hW3V_T4)@P&YThK=drW9NzvaC3?fPG5kh6=Sw6C)jmJi7 zE7NE*i}5S$wivY-CK%W}1#*A~OYMEU*ZE zea~ReNlt(&c2;cp#i7Cl`ao&_wByj(!6&P4vcMNOoMe(y z6QtIcmk(OgRIj3e99H#Jy-efm+exV733f8{#Ig(L`msH?5&Zx-)0&xO8sLC>M0i09 zRFHDO_mHm@Uo{$!&E8b`wM)g!e~o{Wk(=A`Y_4sxF|7lMg|V0>uS?~JGI}(*IEd>U zlsWgzJoyo`WHr9Yn&>PGvfn1!th)7$(mA?}$Y*`sIBv|dRLc)02fz&vUWP(G>- zm_F(ZU_B}iXgq2Ua6YOJFb%2#^c4rt8Ok)fb3@A{OSpo!A>{?S$x4P)B*5McTv%C4b_i z_%q_!DXmMk?408gk=paUTFJe1M+vn_GN+%GDA>VBSmcG8ZLSJn{MkwpqPbq!M%^7M8^1A#Y-XcW= zUKD>C7cUdg1QMrb-^bk%LCYi83BE(py;d+k1l!fKMeVj%a|X5u|Qj6_F_ zs!=Y5p#AhdOBVSbnv-0*N%pHE_L7zEZ(H518l53&x@+{YLq{zU>hPumU3=Nvm?1md zG+9xDOv3wA6Y<#&ex4_M30TFl%G`2)>r{2~Zpdl$3U6R#%V(OAj$iW3&h-8RDF1O( zHET{SW$}5AF!R~|DDyvMJC5e2Hb9V*gE6DZzx`AEr~k#jaEz*McEhNOnh3VCF%_~c5$23MNEd;Z8}Sf}R-X7YsbX;*seHAXEEXwp{!;AJ=%fT2OX)pK zwgl~H!~J;W+`j>h4JA)yahcj<=33+&`ycH~7fvYr(kvw=^l;{WAH{`*l#52#3$hcV zOyXqV`$nrw`nIM*w?Sxg7Rud0Y-tLdsw-9jHKEXFVM88!5PSDp4+!HSh&)4HfmiB! z?M1E*5PQEzALCto{iPSh0dTDW zfKS_nIM&z`B{_Y~u~woNQF<2P;nC&D7o#)ezRw{W%k+U?T8-KvMFQb$t_~0i$w`B> zaIlytfx3fZ%~X3BI;g1{#yW$RsJfK+twU0yuBbuA?Y?2_2a3EqqEJb3Oo}M5hh3q6 zC4%ho(^JCGOfdT^7-!>E?5}(FXe;7ti#&BGRMAmkC&+)9%H`c2O{;n~!m5GUD_Aw= zw9NZuFnsNiCK5iT?NO{VCm(TJ1%G=Z?-|Qd#F*sZ7ElhS^pMthA@_HUw^CoAgx&ZU zHN=QMS=_wTsWCwuH>z-Jb1cI4YDiFT+@EpbAGAS=PhPb^Y^t2pI_>%Nq<#7Dcxf4> z9>Cu%ULLqa5YSl;wJtv3gxtgKQpAOdUx}Xq^sDQqAtt(D4SURxF-1PnEdHLJX!WA| zX?Ox$C1?kOcc$;g0MKXCEO&fynZDE&beh)d_%apLXqr6qE&9tdzRfeZ?lYI}aB zbM-!De)d}Mv_kOQ z6qiVsexmmP`7?sXu}iyx&o;#K+{HfFg9<)w6?-aeG+aeJRWp zMdXh8g<ad)vK|ii|fcw?D`~qSfpiRnf98g9dB>? zCYFu9`Z)7xQi8q1>a2Z#SwcfM+BxdNwPbmZ_Wzx;!}mPS?)`*v=1+rn@&9j-{(nI8 zgh)AQ2qEO(dgOZ`7I~sD{2sB|8e)bGSU)0I6bdXeO0hMOxo^)BR~8I>zk*OrhSfo(7~!PGE`kf&KPgeMmDb*`e|5BlQ`>Vid+O1=V2zlO zFTj7)`aQhk{sq#aHv>t#(R-+hMq-89G(CwMhjtGvhVp9R-fQZE0O(x16g6qz+(aWD z8Sr%7G9fc$O;SE3pp6Pk3J`+=hUaRK>b%~gstM2naD2_V-~Q97;p0CznzpXwxX(9q z@mcEg{x@$)-Q3X}Wa~gC?qp;559@nTV?!&T1Mu_Gzux)hMwg(xCfz59%%9g#quroe zy7=Oc1aCmlk439H4IheCEar*2*j!J2TxrFj>pcM@&>e+JsaCzHX+@NB#$9K6IDWSF zGDiCgfsQ-bL6K;j?=i;~sUE%}gs*HO=MEeQ`N5q`{s)a(_+FFR91$EPkcAAtGTlPO ziQkLTk_3LERk3sSBE%JWEChaFVU8!xhDddxqq#z63KI^ZfKp-eN0tj$GN}>6TPZ3B zOKkPR8%l!1b)j;r$@-mq6eJ;p>FtffE#|AraUMJd(xjp!BrbB+fA zqbq6Hei}b-Tw~#-_8Pmwa6?|e2BK4wDW!{X9qn)1IghpdiY@noQ}t?P*5{gIOG4!F!tfiV64hBYj_5nbK2Q%O@&j-SzFu-A-)b9Ec=K#=agBU`N{u+8`Pq zs`|%_~Bsk6iDa-X;NYqV~(d4LtpGO(E~I`i(K;h^unnh~&WN|o6PxDAqq zdmB8+q~r@U<&~(sNKJeZOTX%7_0W;pPqlsem84L2l8{FLC7V`O`IIwJ|pD*p6V$tiHdm3_C!&4l~BH%wtuAuIO zr;ld`dPrK;D02Leb@y%*b?Xc;L#Zk5-$pwg4}9Z(Y3Onp4WFBLM^Cs zv(k_vKJ4c*3 zwQiDlq|x>9TTP+=XVm=%p1{*KWEOp%HcWr&@&3DG!+*(?g>8-8#GF6%cmKeVe*#Fy z=L};VwU&=gEr+-f>(xRz=~dyP0vQ&m)L8^ctfN@rY^ks2Dsh;j6XVuAMl5Ki6a`{B z$g0ke4RoPIG*IYPF@^D8oW!8eInkFMJimKQJ|J-slRTxezxB&ej%V+&%dOllDd6pz44a)_f#8n#OW$UtBxj#TRe zVz89Q?^xkX#_Z_eq(ZcZYFL82el1;DxH(qnYf<;Pv7|Hh@!%|uRSQL0#jAw@(%3qQ z1aBXOeHTiv#c=g>M$;hbqu1DMkQBe!#_V`Ld6f+&jqXH#VZY(fvQ9dmWZCSojaB){ z0vg+t2kVpx90TVIV_b4=8q_eef;7WeN_2=C-%*Ynwt_BBd4L6W++66o(U)E)fLWl_ z{DnFKkAyoyV7W`0+u~v<6QxEPel@{~t<9L_qQA`+7l4b-dNB7)7>5C`zz-6Dxv zGJ92z*ebl{m43{!2az2!;TFlz>#b+cMD5S%){WI)s-)X zV^*nsaDE$+;X5`2x%inNO{#@O5==X+ex_=&x%UOH2CiVT*-7v{QvWyt$2Vd3{&hpZ z;B3VkM2Hb?bD{C=a*Vv6EPh(NG=QSdq$ow!myE*Zw^1*NuX7$@YzfFiiu0jp!nHr# zikImZtIE%O-i@ExD(ES1CVbW%fL7^P6L^q84G(Tq;gT?3!*9t`o)k~KZnn8U6En># z-zBE3 zlIWCHO7WReVM2o6M<+UAzY12=?Fag+Ll;^H1C{KC1|vu$l8QzxZX$AdF$g*uSmfnW zjw9SYC7X-nlf``ak0gq0!skl!2I=u$Bk`gC#YF-i4P zxMA__&v3YVdSD-{ed#Yk$Sak#lWxS$ z8*w0PF&y7FwZlo=&IMHa=!%@pI%L|#$s=xW1ciY>v_&0x6iqvYY@!6QS z6fK}oJx9r&ih-?xo$!mw*d$5rn&~(rxQjO!HZmu7dX%RF7rwK~>?_TR!t&Lu+0G~r}3+96XQ2UjhXkF66>e6>aQm)ZlKmx*ug7ml;Z4r^nl z4>HGXuw@TJFP6?IVqsjIPgHAqT_{ChaH{|rCVA00j<8a-&iFlIs=|FkH zy@wxz@WaX-)ahu|rMnUm1>)nhCV8|+n4xIUw+MUfoVY8D72SsbrG^Jkmn% zwZv!#p}m(ctEFl;Vf#vC>WW3^Mbi3JvDxNNi^fIaq8R*!yVOXg$s+!^jRwVk0gS8c zHg^mI%T(V@43~OInLQp#NR`7QBV@2-r5;str@~>&j>c!rZk5eR84{LXSN-1Uzs@CQ z(oWcEr(jPq;P@}coY2zhy62kM7&R~z<%O1=#cdYH_^@HJdPFR@GM&t--p{OHqNRna zwla0GI>}XM(5f%Dj^xx2Nv0&qcxsYrTIlC-N3-RS#n^Oigy$8c!QSAuNnL_kx=JS9 ziA7L#-A}u!VL+s*Icm1eFndlPH}}(O{$+j4FV2+uPB>|~%2g9oYy|^1t@2GXc}(Lq zRjr`b#LW=0I(1p!GpW7EX1h3PaEtn0UN%c~xe(D`w;p%!I`;LY$>!Jn;Lumj0dX_FAf{CKT*d$>n7I&9CWBOn z8i?*Oq+B$5-ECOo=$^}CBwQbKYEV1%kHe_zA*fdoo{q^N&@=Cw$X<|>cL3qS6WXg( z2I1b2SZ`N%ch>9^6XDDg+&f!nbWN!^NW_rpXREDL^S$_D8taX<`I|ml;q$d4&uSAJ z3?owxlKq{&x-S7fq)d-wZvX&Iv-r*iT5T4NVfIPtJA6~W!LY$|vV2o7hbzmQcG1?f ztrCQhMWfS5zP%tkXW=nvNH58NG#1Itnv-{E)aff5(&;NeAgpl6!bH@0>ey4*`xW4m zX#R%B_zrr<_t8CMyuwO387g&fu;>`=mCMLFWDEgxmfvbTLb-XynD3WhD9Ad7buK3c zi#~{o*%zej=kCrbax*&ClKA8q=~q)?B78k&JU%m&J9Jpwzc$Y3n%tV@?#7)<_p^d$ zlr!aBZ*S~T2IWK>vyyx!a0vB%zGKv$AxPoPL_mzAkU3at6jDXW`?Qa zhzl9?jVUw7=AWj7>4S~q@d@fmlS#FF>aI6g+e9(_vTer04zG$4v@H?{P9mjosPW`s z=5yPwdpPIHFxPv%A$0WW_3rv^o^+IaRCj?|$o+8tqZV6>MagtjL16E=80gW0fbU+a zWGP=rvt(5s`Xag>|5atF-}B3L(ixgu1%x#(?=ZBpX0gMuGF_Rc%9x`2jf6gOB z%;jgaZcU7OHV*>igWbPyqKZ9=mGp*34waJ%(H0lY2%mJyL4~NczA>6xh@AUBMoKmx zT_YsPE**={n{y4aJTDBjISRR(TS87b)xT6Yn*x@VXUgRM?i68kfOvAG(N#UT^#sL> z9asEcgtd+rUHjL&jHCYA3w5#=L5mSdLo75@R8;m?awmj^lq)Exot#Pot!s_YJs9|G zeM{-3cd3VZTje8sy4j@a_dUT(o_p}N-Q$Bfl_zD-D)3juTM|_;ITxWqU>)S&*>hiG z$*m)Ng2hei(PSqKPpAIMojR)Qd<*wv4olN8zAzunH{JL$t;spxDgm+d^Gj%9I+Ew8 z^`AYwU41-h+-bFZ3+j0G+)uwwNk?kA7dYa`QL{p}8qKcX!F(Wu_Zhd{r*l_^o>#>2 zgjvb5tr@N|oQ}HG8K}=6I`OZA6Wo{X`2v%^K9)0eUzzhfdfq**?=Wu+#OBd?#PM+{ z6qcB82*~w@U+A8ql%<9pLnUC)GZb-W83C(=SJFq85L_s{296mV_o@`E7!s(I7Mv#shLRq7q;*4GY_Z z|8z)e*mOPGqYHJbA0|OiB{`@mS4RC|y5pC#BHSbdynrNOZ|k_BObq(SLkDv#5HrW)hd$s+<9)J7cE5fa9@m zvq^IjM?zxH4$q$=+ze1{2-d6sBAf6t?Uz48o|^%ZRRDIxnR>+t#*3h~$h|OiO%vEX31u5QzQ|QRQ?J1(9;QPyF`pHI$P$>BO z16L)ds9MS!g6;pz?hY4-L9s+>Zxri@tCJ9CX@>;7YN1*vq2h8oA;@D$AEA)vn7`Al z7>yk%g_GoAcf-Qv*pE>Obn|P7B2Q5lI(==OxNEqBd(Mw|@)SfdQC%*72B<;&fOZaX zzr58FQg(qXVo<#1oin^^MzH?vPCLN{ST1$!xFJt)}mM59Oa}Wwbhl#L{R*VUs zvD=Ohsc-H|>}ww`o(N9(01O%X)9AZ5W-JSgREmS>WYPrdUAeUo?>7ARHCV4YldSKe ztgO)x@T*bGA8CFBwUIp2V>`^0R(r1ib%KUxI`!S0EPqB#Nru{xDn-daGj%ge9X9_W zVW_|?63cf=x!f#cd*z+&(rm_TKUB=lqwWb5`0dF_tWh5OvZa#QFy9~t9}hBcYYxtTJJ zWFS>Cp|tK8Jb3|!=Gb&+;dZ-K--->|4;wUI7m1^4!zpR>vjUcTW2F>pWX72#M=NdT z&Yk5uPV-XF^P0YHbtBlS*fFMw(^6Y5anpl&Qp`q&)P8sMi5U907>5$SPJZW>J9@6% zfjuxhd#B=e>s3jvK?gnQx{@j>LDt6|y%#C3(yJF0k|zTrH;&no1Ba*Inug z;k<8t8OoZ@hEH5>;yWCx4B1B3!kMkBUn1g6t%sCt^nBOr^vG7Rw7wVxpu|1i)bB!z zEkU6Ohc5{8Dg}hem>;1dfn`rcyPJgi|5m`2;4J;j4Tcbcfdc^9$xX#QRBJ z-89PQ0~~0iPf|=tStXdoFPdeFJK;K+%@@y170`~FWc@Kz#e6BXtFEvqFdi^9ga^!U zTL%?ml9*lK7OD;3m(eZ(X0%V68WIkH*txgFDNdBa2omORC=10CU$hRvi{tKF;6{HS z9%#(S>I&7ll4lHA68&nBN8o}8e~T)8Fq!du*5m3>t1&tu1g0pFCM+L{)8L=Vm4H=j z8T~E20as`eKkO90WtTp`M55fQ-l*Ew~ToAf< z!}N1nO~oOcApoM!A*}&-qVZ=#GOK6>0=sMb5!cVNoNw%@waXK{l@GT$aapB4nI?-G zjR;CF{A>+ity>1)ctJNw2na*ww4@q89nu;85)FXx)xps#)j#z{w!fu$%@}Np)YjqD zF0wf;av3fP3C=F}$EXL}j(IL}Te#QS1a1#{J)E8;FCi12n&lULswIG5agd^Jag_#=G^Lfg^iZeEF;CaDDh=VL2bNog^$58D#VPwe-eHi`$=@ zfH4OKg_~C8EKj@oj*?&TlHaMDI)KWWoi3PtE@Iz1c?bJHE&7cZI;BqThkG@AKStz- z&FeghEw+Drw8P_`A|b`CsI?qo|IN%^5AcZwb@I}BhyQ8TSm0*bfh5cHV>_ zJKExrIJGYb8HS+H?G={&#;`t|(B-T_yq3eE5$ks^TV~|=d5*ZLEx~=MTj1y>E&fQQ zA}5pjeIWWO+ldH)%pX?}PSzm1Yiu))AQ{RAo!DVWv^rSfmM5e6-ynSr*UH#Kk7Dr6 z)U%sK7kmHIKhu4$nIx{h%!lt!8O$-7CGA zO_!9pNr~4}Yz_F+xk*|SKWZ7xNExc5vIzUOlFWycl99=h_VYC;wlwr@d*Z$4@HPAT z%nB!GH9JPO&oui&@qS!r?Tx~NdTw`&iX_7_Z2p%f8HN+03T}y6^@`qJ5O&eh<34op zAftvLySEdJ>RynqT*5Y{HA4wCij3uAe>vIZ8QK^R@zli0Qz@>T1r+jffNC{31%>0+ z__PddZzSz^!MLX=s=x067CTc+6mDJ_NEd4BvRZIXSIo2!~Ajk>F|mB?o~IWzwXF4v+v7JmbNs7+DK z)tkBex2WVbm?yzpQ2oo9Fq9|CsuY~t{vBy&fcF_~p3ITN-DNV~^{Yzr=@jz^R%RP> z7c~Fl6DmW`t@HQ`aY{z=maejR8E-ylK!IdvYYL}#98(2`}Ga{Hu9<5 z{Pqe{jS+A_I4CDZ&S-VRbQ%BX4@dA_MDi6MLZOaSfq%4Xu^I(IEh#FY40#<|;DvL6 zyXmk#CS2gXAdP*@R)na-gaq>^%N^;;_a}tz(PfrS9a#Osm!KrDI1}E(tszYY9G_EF zv~D_Cxv*tDx70i43+Ydx8*KA2@=)=f`Zhmfrw^R$#r6OAm;wAy+a0^+!ocxbL0T?8 z>IT~w%x7g@qQ|5~#<7;@lsZF)#+knHP|)|j#2F-2g5Y?94$<$-5o^9B_xI;e%yrk| z+)jKJqf@+tU8oLs+oc##t5*0A#mYbQxy|s6WfY&Jo#iK)^WTjI_>V!!|H{y*{`l-W zM1K$bxzdPPyl>K>E=HeJ>#9)-tFCB(K>|&}hj0#w>0+9;Q4#Xdi)F}0A+G1SC@RZ# zt%T+i$DEm>KA>3ExRT;HnSPsLJDHxsxV7m6X$N0t57OuIl@He(rqtILMaW}bwjc=G z5d~20{c+PiHbvzMbHtqd(X0Gfp}gdi8ttQ+$dE5Fq?v$Z>x++ujvU9cBZ zB$^6~1m(g7inJ_N`iB8bV$^Y3r6bXsEH+DFFRs**=#FFc4lYQL@C!J@3rTvS(-V{- z#gc+@h}hRyCc5Ev=W8a72QXy#vK!a~TXf9*w+-1AQ*xi5A% z8ensG&f}R^nJV@-?J#L(!isc?k1;o)3`Dg<0V5KAgG1<)tjCw z3`e5i=uv4$Ni1yAPCN0Z0GT$YSp9_t9eS+fo&hTLner@o;Jd&XQ3#w)p>?Eb0^10( zE&hUQqV!^P9cE!{v!4N~2D3GgBE+&;{@cp)BaU)c?PP^bu!AU`?aycN!P941h(NCj zWhTDKiutL*YiqUiUZ0YQ`ik>9*{+1HJ861{^}r9lsH0?KpjJac%-qCCnHn)I;Hkn( zuSyK~>4mkQwjQVc{)49DiYPol0w5}gaq`wO;uacAC2to6ZV;n4q<=JGEkO}=LE#I^ zDV26S#{A2X*Bng8?~;*S2`N*c3NuT)EUt&UVRTMx?guiK0w8oj;kgGjF0B?N4LaX| z5#&Y zvP1E&@cd+hQuu{cotexM^LNl8;VmHZ^9U-ZB_b+Y1A1w8k%`l|avzL5#RmOmQhL7}|AhYiZGq_T zN;mV*^u2g&bhcpPvqj%KWHSNJ|J42ek2upq)`Hml#5VO$b=ZGrn}D~FT&m_y0U0n7Y?gp+p0LJc*V9+v2EM7 zZQHhO+qRulbhGz9xBdH^+xGb%W?L`wX{{x?58tc-MX$jj^1{gJ%O5It^>nd zAh=q-6-1Ijbb*2pVg-Go`f7u)sEjbPmRiw()=BI>)F*aaIDxM-wCy9zY38%kI2(JI z&F~=87Ks3v4ALtX0|yG+hmMTz4_DJ$%|*Ok&1W`EHJ!+8{^ z+$mfUyCP+p_Ow|F*+_S3qww!UK*KN&DRtH;-Bcl@Gu!35{rUp3qcsopm8cQGnDAmf zr--<+ZTzlAcsK~zny^+b^&X8ZPhyN5Vv%_VLjcX;TkBG*>STh(I$gOS?hdKIFuj<} z=;7aU=Bv%7(~;WPr94xzy$+>(rtaXW?9WYZEOVX5AFAWf*`MRJJ&*NUIq~+(fE{xjc`ke0E~O^h8Kh0Hc|CLSJbR7uMNU zX3#GP2TgHfxhCKz%xF~lV9XgOA1XX1DF+ozf6utnN9vg@R=>_7@pgRD?lgnJI-t40 zo~@?UKz&JvUs~qHz|Ew9*x0r~#4U2&H93i(HiKFgiYx}CGgob-6&ic0-Y7oFpTkZY zDewR~EkCJ~SXirchKNME)Ky~(iM^5}SL9P!P;$mOH<%ld8Yy;@l!~{TjtSwXs{I2s zSMHy`#42ZuAnn{m(N~)Rd^*KILt+LMisI z=~-|U>Yz3AHG5@byK>HH5;g%)Z#T~bjXz0edoPq7e%@%v|L`0mbU-|WhxzG6ByCK2 z0}buF-4y`Y2ZMb0O=Sxceh`gl5VrL_&>(4t?-dA8jG$+^3$}q8`Us%it@D=Nfy#uc z+QXWPNBxx@mT!)LIY9UCVVVDc0@r$aCf+zeK>ptnd4m4{1;qY2{QcK)RH+8(@#it~ z>*LG5BYAY389}EHl}=}bk4Y~i$a z1H^%PO)o?M?YRLtDV+OQZ5-MHJ6jE8GMA6>z`c(>gWM7n;*E^Cf&9Qfgu<<_Q23ma zlloX6K7>3|lbAog!2G@Ao~8emh;SYvKbq9sDq3Y79hq;uLub1Wk!7KNjpZM^T%K=u z^ovC6gXUX*r_7;;wGWy~b@Y7br>TsmbPcX=FAX?*G^pS_%@W;0{Ps>x{)!aY-k%NU zbPG-QK0bQuc*5tDC;5sg=Gi+M-gp;8=2;x@{%exSS`nUY`(>~7^&9^Z@%Q;2ddFQ@ zwBKH`$D88T8!DiCY0%o`u$lAKTIxNDto!(gS?krGR?C?0JvN=k_TJd)GZ_9o(9k1E z#QJ8|e&byc`Pk7dDcJgK&$_L}Czk{#jz4}*#FmqzvqCI6+DdOLxDZ>XAXvr)bH`N5 zY=dXWot=alntg-I3om6x7~B9jX0HopoJnM0{HUmW44{P_EA$a^ z&JQmV~KQJmxn>#5Mj2>~)wB)2b2P^){*)1m$W>$dib| z>_=>lA5_YvrukBXBXpTbH+=`s23m;_^|F(*#%fdV89052^vv)l=0JvkFY*<=N05^V2gkYx64Gw#O7FbYIZ*E+ZZ#lnK zrASEjOI&k4qN0nnDilMp2IVfIfO@{1i@q`)`# z4@s*_@@PBiRzzfkYu(Qqx>jx>`&FS|Ba@h1ynR+H%+U z(aQqLrT759X38{!^}vIU@w{6TGw=?nM?ad2aG>;G$dMxK!6?ZdG1;J9Oi51!OL-iP z04Qgbc8;yOv z$#W}^eLj#Ru_>-`Y{<5EJqU&>@@Y>WO${z304-`kNbdWlOj{U+)SjZmPq)G38`W~{M;7sq|2sINz-|Ic`K$l{g69%U9=m1 zXM|PMeIPq<{C+&joM2nBd~2hq0da>b70IvZ5+q?mk+{Y7!AreZJXni}>QJ<$f9&eq zHK3F7(#JDQpFyPIg(1g-A>*|ibFd*Ow{d3LGVb4aPtw&&BNbm#(GoLY3LssImYkz=r1n)ZpeWa(BNahvXCu&pI^Eht+uS$IW;+(*>0a^ss@5p6 zfKm6W8a+1t9m?vgv1+DjO^qa-QZ6n!nvtS2HXVkN3wpb-B}%zw-tE4m+U}OhRm*ZP z1beQPuh4vP0D~$0C`e7?h@`s>QlVGzou32RYFHm1~w&yZH6w@Xodp3%ph(YH(+lu)YogNFB}|rl=9;6 zp^CZi$1V!|_rW28c@d|mjAG;~yt~i4XKc59% zQc{gSR5_#A?>+;h(>oo-Ddry_33}oK63NQi_QI&ZCP$z+^}Fs)#@+N!<4kNY!p?=q zQ>{&O$Y-%taSuJ3W(K-rb}=59|DyYUiXZDHVfL8!k9LNb{rCv=GMMR}eTLaZoXhbp zzQ*Q9PviUIaC#CSav6-Zb8fdH18WKj#Z<@IispJ*s5_ zq0_FIaGMr@l_z4sG9W!`86s?pTCaz}K;6dN8V>(}RTm8p6P^yc3x1xbSixW*xOaK1 z!9;@nz=$T3+yVy~5ug3iC7Mlh_?4FoiwFY#0PQx2pzagxB{q%uKw0!a6*2JG@noL$ z8C1NUQs9k}O*gn_je4^e8l+^egI;W^hU2u%fD-w5`*TQK$ji{0X_jMv zo4_%r4&JH=fO=ZK!4*@IVLrlT_xoTt8aENpS3Su*fME2X+&Z1J&G1_{>8L%Wj&i+J z8Z7QJ-OD&6WCxzHH39R%-ROlWAg5zA29gu-;j0BO zK$4O88cVkFtIKKH@Kwpa?qrCXM)3D2+t^J|(}$2(ssU z$XAx#G`3<1{Ujc2kjqt6Uf=vZ+ec*Yh%L%((97Vt;lxHJ(#3L>kn~u{0i00BJ$LL}v>@sT}KazLA{kn>%M$Qg!|f0-G@eK;=e5#XiON4_K?I>ZJKbgB|jL5iWqH z+{Hg#$?7G(Sz~zeOmRQdNm6BW&ZxdY#L;OMUP99;r}G^NnvL(Cl-=|z#YV2!;&Vhc z;qXuFis2ETiL{DtMO(r(glbLB{fOuNpz}cjM7Pn{@ml}zk#h70Wu49i(r_ywPB+W+ ztpqs2mH#@$bTE8t0p4y%9{DsaitBF<9V&GEwqrbw$~x1D$!;WI z8)(cc^BODncNM!IBD&>*QM$$rDejY-vXKjEe1tE8tI3JWAnt`fSPZY-*6f`5dxT-u z!TZSi*vm4-qE3cnH*1Ar07w7!5;!@bGBa^I=1x^fWBWt(@S-S(Jd%{i)9C=FHyrNQ zyd1O4kh#*`>22SwHk)@-#JgTCNm!u#toJ*OAc<$e7^$$SBXS&i? z$6~k6<96L&YxNK3iqH14GYQM{SF@*tI_2%TEy~DPsxrT zyncK%Ph566s{5z$1#!8&K8Y}!GiU}8VxshZ2kbp*MLq=o8>tc1NFZX9xpZs=n^aYn z!U4BfmOj+l_RQZIWE!Gob2QDK5xVL8U1b_%e$fma6fy>_MrJSIo7=TXHLLN0E9WDLvhwg1?>QDHqBMIn@unNTNgg>$;n>e;g znc`}(=aJku?HB<7zq-2&Y(wx>Lx-Za;-99+S%OsS@ zAwP!7@67wUrjz?~&;ZfA1!zkW!M1>&1^vKT4jo;21ld=Uqb>3S9ioSw8Xs)a!NiOP z8Pjzc;FRm7m6{+DkUX~63u2Io78;dIN5V^OF#<^r)kT|1rSk&8sEE-t(8yscFlDNc zizYQPAMoD60%?)YXV5BCP|hx#t3P0q2_mK3E;z(e)OmzW^Nc!Rv&$|XZD~OI5-2@} z$Ua280>QRGROT-|PH|#9E-4JWWNs`rv68a1TEj$!UrN3GtO=E~F2V^@FkKAbTf za>CF;#!VwjiXQcVHGi9tQ=8?+IyQ(b%o9vqqQg3rxA?(@3A?Fi6dt^?oSa~dR6wzx z<+mB$7)%aid9xfX%T=AL4Y3Gm zl6kh9J)BkiX`*IIx=2Nfal*)Ya1?nqMPI8YE$qKw%awC)`%&WeiX026bFO1evgA|W z7~>Xm6yoR*s3Jop$ib{{8F^9wH!HPCB&a?>x<<${Z&eH1Wy#TSYWV`E)_y zp31iT(xbQmjiw0_9^}N<=u`>fTtn%1urrJ>lSLlC9qy3&dA(m(&#z%z$nGvmIn9m? zxto|DQJJavpm>po9yzyok(X}AfM*K?e2f;Xo@F|Fk}sxLXyVjjXkQ3UJgS{cQ|^H{ zqY`wEKu|@$KfqiuzTA{uGzftv!ERQ6$UdK7jtlKX!SQEGH9^H&OCcD@PKwkgrLGef zIAHP2^9JF%Sn1Ze$pRa}ZhgS)*Sh0rMG{&#v#j$%1Oz=Fs?9u33Kh?^DE4jr29k4p zShf`PRs9WK5vbbFhI3sW(sG(4k8stl(+;CqIgU-1kLh6CKI$g?4i$$83ha%V5eJ58 z>8Q#+bz%ZPeKtCtQ&F)_Qb~h6O&3>-FxaLaOgYDlc+30xknS_U1NzHA*tV7m%Kvh&u z=&&G%k%=P9^p$1#y}7RO@e!o^jlR1HBL;e{^{+bJ7bgv_IwvbAA1*o%UopOf^372s)wHy*D_=Ly&Mn_9 zU(%_2LdYeBdCDkqtm%9Fd_p|yu{+LR2#oaxO+#EGmzH}*o|r33Q@fc@+BHT~Z*+lu z?|?h-E)c&ZQ<=_26yC~lvYpC3AY8vw`!l9P3IR?MdEWrm#7x`0K6JcHGvG8pa%GLc zf%3&+DfB@&RX=(SvQMZWCou6<^t2Y$#z>_hb!GQF(eI$C+1egUg=Jwm6+4u4TWC82 ztNf6FBtlLc)f!?+LNyY@SRy%9;A)3PlTd%kt}2#JzfF6*>N?B`1~gU6DVa}`5xIAH zahGTVau9S*KcesI#uz{|X?Kqx%erns5TThw=O(0`_1CGKBBFB4tokRYx_yNbEikAA zS(P<0P~fp91z9Ey@GhJ=;iNX%SPK=;YMxM6_=PosoI2#968rZT) z2K}8l|5r0#c|e-AshWG`R}kWwZAj5V5-qimZLPwVOqUv7XQyYxixsivmbBs>TyW{P zrHI&1-8Z%@@;(Qo<5DPB_@n+kBL###g%edrt;$l*7|yF;nne-PbyJG&(g)lTs{X9E z(hJthGVDUF&@zZ&b18hiNS5^c{e@=&%oLF;xqHMl&yX>UB=737w28qJLUE(tWvh{@ z^`23fv<*4ojyrZ0_pFcG0>2!m<#$cP{EvLeCKBC-#WVAAOQt02fCuG%mPo32g)qI! zl_lWkle+${Ovui4yrZ*>hoAA`X!#8u>lne@B8FUwmTvO5RD{ z&0j5gZ;q^dl1P^b8q-n`Yzm1~bWF!CtQ@Yc_z6w19{Z_y3%3p_Eil zZu$!~(%D;hN_A%7&Ec=lc~>2kzUUu%QdC@mbK%fxW}dc{*TS z1B&IgY1#()H-$G)#XBtSF+X9i8VY>a^aLVtS-&g0{(4ugZS~tMF$17DOy2H;O0?V{6jVw~rf^UqWqLjkt zUV}{N|71RydR~Q?_`8B`v%HikFKsvhNGgqj!W2nZ>kRY1*4h((!_#pBf^f;otpiHJ z>xmPgG3*+);G^fQqBNMkFX>^K zp0R3!@7pe4_#mG|b}ns13>bx1Nga@QvhOzF0z{k1=80W+i+IGq4%{{dJ#!m)Q&6+Y zDBSL$m%!~V{?&46W(dO@r!8B-+nC}4Tbfu-4Fg~TsgGeF;NKS>^^3bh6%=ye#PJYu zrxn+ zZI!G$UU`ulGfsyO5H3PfuwAXs;>>cob7jZjg|2L-*zvwJ@LemO$kEoXH)etCYE|~mdCIqgkX@Bo0e&}+qfHxy6$v(dJPWZe63!Qf$3we7( zet<7NrH$aW@%t-1|I_^)pIyi~I|sh}R%GEN-|U^9`LhMsU5=`HQzi zv+cOb2@+V13s7L#v8bD`ipLj|BKiJ_6vO$?X^3gX~i4GGV}{$ zP5#Ta!SEB=dt%geiJuE*DB3#$`cz4%Hi?uT>$(3D*Om+wV>2 zY#SBsz=#7=dWp|J6%LbV<{BJxsW~P-Lz}~&0uy2K-svN z6dyd6={_(I#a>%vGB6&%38|fjSU9KifTGYx5-m|Z80gU{6^9>|6;7{Y;f*)w}Eh?zm2Y?iXiwzl}U4gbmXar3JCKMW(5)}2Qf1)^@$LP7Zw%xCvujRXc(5O(4a!^(#_ zU6?Rc86~R&{WaO{8{@R%LUy@nZ_=IRm~bQte2mDyS(%p`j#?+QBw1Es8s=sLO>uEg z7U=y|&n_;Fi_JG^RjZgX^iOsuYqVqelRNlKYC+Rwk zdw^m*f!+Jv!Ix~(2^SqcPVt~kzQYx3ANUYN|Vx#1Gf69}{ z=`$pLXjF4FVPy&qyMGSEUai#iQ$*DszXZPq1oQ!4gSSInRQr&tFF+M)_if=K+K-o`K>fmrp%mZQPY=y}B7a-RulRwj!${ucCB9Re|)L zQf`j@D;g5Lu+L{EwU|f?qkoWm-f;An?zPT~jbS(|gyn#Yo7#_3meAVXLa*8O`&tG5 z=PBv%nCbWkNUch|LL`(W$l=Hz#zQOO%Aw59)zh3ubn}DMzbrm-AI;AR=1JkK$S8tQ zcWZ7Vuruu+`t@$yUAt$EEc#<7?O77D7Mw3Ue_MRLNd`<7;U)8$jGFbw@(>5t0LeJf zI2hX^;_gP3(0Y(B$9v&(>8iI7IRuzjJ%j)ErTs`cPE7jt4rKc_0R2w{!+$RAzmYjJ zmClv06;VE=kr-NGQ2Gjms0YQ@H3&>4I+tfZCV#RrGKOpPy~LCu)ST>S8s2H|hPstsrBgL=U$1X5oi>h31UUW_e zgXWwNEM9ebCR|m*VU{=;>ZiP?Qt7Dt5m(IeR{*JO1u z!_3m0x$yMB2}R+%9giEAdexASO(wJJXLWYPDL}efTCDciW*F9)i=CE9rB=3gmXs=E zNf&@77Pb3F4$vrHuQhw&Sg40U);kyhJGk_^svoqGCLQ|(`30l!J2a**3?mB=q(3Y) zLcYGvonsU?9OQS5;s~dxFuY`N^6Xy76db<^ez*jR+J<#5Xk1+zEt}gOl8XFoU=?+C z6*h-(L=w}Dy(yP5LK5B5UDo>9VH8(diQI#v^heRW1&IYyn8O<2AmU7uzUy=i<@!9& z)p%Fxq!*;8WM7_$D|NV45d6k+tkp~MyC<{}>fHSGu~16RXJw8!i@GtFco)xKU8Vig zBSj+J>&R`gsq64@Y`Pl+CU{FhkZXsee?V>C;jyjB!j5(1!Ov8!3IkrNXDi--1vX5C z{t0JuH1WlESdj5V&2V{XlCocP4&E(&yq;EFN`2Alv?DOT1GY{>e27&mpN;jR8+={q z$p!hY{EZ=qo=8_>==gnX^Q9!!{WFwZ{2 zb~#XF97_88)dRceiV=n_t=*t3=unnoylgQ?*`M%5zbF6B=S`SJIbKy zNHgp504rwo=#%?1+D(mux5$zato{-(TV7)Yx)F&1)fd!aZTd+?){=SQCgEm-)q$)d-g(1CIRt?jP;Bjx7 z5f~Xn>s%gQLO4!t-hkzwcS49V>+>z>;NKz?xrYs%QBXi1A{Q#qGW_2MMs5<~pQ?g4 zXj@k!e)e00XPUmy{q9;X%dyWEr~${%^w)(9RFr&MNdB9A#-4mXxr%`|5YB(HO1Wcs zf+->D>wf-h39(PuP}~^A$wYSeq(&J`ZtzBp`usP-`ah10emXkDs&Ak-`3>~A{x_ia zPo|pKKZBg~4F&8B|9N`I8yXqf8CvQ8SCUJnlBK4BhRa{fV{AMUQe!C0R~}8Q;_glr%beP}=++hMykd*tCeRnw zSMa7KAI3vWEt3h@Ucvk@5%=_M487%#ZD-P=!}CHHjORJ8nR@#Y?rI|Rg*H;k0mQi$ zl?4^<%a5)+j7MTp-*-U1f5%OM4tjYm+(pycg64X&Gj3y5Blbv-6SJ=>R>Zu+x@Da@ zy8}*X;r&Xpr+GTSA=a-@$p! zdaQV-H21ae>i)$cH~E7yp!+CDOJTFA^(SW01ZtPb9!Qg<M9dkNHll9Ea}qz`GqA~7=l9xN#>g=F&6Y~^ zVvnb9-^x7#D~CY=Q}BpcyOI>*F3#I zL&6ra%2c)kyI3_hs`&8x+XnVwHU})H=c|aMj|sC_w=FNsyyTy`qSSK1cAQg+IO~{* z_zs`&gGbWKCs9(-E+q{QC^-!`KNZ_o$h~es+ar_A-TMu=xK{U(qq3B-MLL-$y=y$$ zT}y(wt=@K&bml3%aHc8zVUSbg+mXtjekQ@3;5rcN2V+yGgL~GZ12^_yc2)40t-=Xb zO~>~Pnpog|ybFYz5p#8hp$&#}$RyQ0VPP*iIUc>$(MnS|a}?VRpS*Xw3I$=^L{1R; zq@E!#r?=fPhPTLZjH*pCF&_v=wm$I0K3oV&_&bYu2BrkB8E^r7^`WyvJAH%2-}g*)Ell11Z#epQ8BqAfJziEUqeqc*MI9*Wbf_hSV#v8oib(k= z3c0X9xTm3~4!WsLks#cN zBJ!6ppdN1_&Yz&`anw2vQc!pxZN}6qgv(kn?B7|+t6FJVk@Avbsl3cEv%1ACG(C1g z6=0m$z&)&GSS(F3hCpKyun&V->`H_SFE#44!n)d+aj-fZD=K46Gd(aigZUJS)5oAT z|DkQU1ZZunvN&r%Lf3PXs%7eR5BD+_A%00_-=tAVs%w_SU=~{L+DGf3{ItPSEYJ~X z3yzduuttG7wzx`wQ3O~{u2hv7`H!pULbQyoo6Z2h{;sKQupT`D675>j;SkF|~7^(ekhGcu17 zd!==RpIOg7%ZtB$aIm&dE{AiC!|$$RbvN_>&X0gLs=h5VIg&B2sL>j&PG-3is>7o2 zq{=JyZ!+{4L#Q%a587j*VX#1@ydT~tw{)Pbx>h*?fQWRtoKbfU(F-gH@G)pqFu{tH z-x~@Tgv0#C2s&*k6wQo1)oK)0AY%&aGB$UzQltYx6jEkRPa3l}Cz8>Xqf!Ot3ev3@ zw1E?#?+9(i@Q}%3h`^(XQkRn2-b;$9f=CsFQLG5xWz<8K(NEzEi(-x+F{<~g-0g~h z4LbiS=P4bKS+H-9wT`W4Vctwe4hr3}B;E%WxJn=s$($<$!zjW=;?-Urtk9`iSPkuLS{&36g*~D{Joo;V5@7HD>MN4#zj?hoi8snpLqddJs zH59Kd$v)AIm|+#)!19POi7H~4?v+2?-gygD!Ka;Z5w;LBhY&>-ET9}Rp_xS*#%=*? zNuV3oFvCGE7!{;)Im$-BFL)I{Q+`om_$hQ6KNjbuPxftH2=~Og-}v2Y|F83`N&A#Uj`EF zsRT(uH5~=lfZxMNB*~KB?k;)>Xbe)9t(r8kggD40kx=?qJA$V5$S}|8504kn(GgZUuEzg zz@Ox|Rww>z184@i*UPQXS&a4W_HX9^pJx^H6x0-C7pfXw2!ps1E7BUt71o(CrtY!< z(cg@h7opW^nWm->@P{&vWsJv=`-hm8>t4V5Pcd6jY0q!Ef;=nE}ms49d3j+=E1D7 zw94jBS|FgpsGG0svP(cMRkU437@VMJK*|H){ZG(iid&JDzT~pqJc0rybK9RHC92LU zWE>L=4-WPPT}{mMmlfZtR@;XI=r{ZH^pyvd+4TZ{sdGgny{`LGV{i??}l?ru1^IBdre- zbo>vuMLCsoaOCg*9{yWA#Pq-6Off4fL%V;1&42p<{?#xHlG`ItX| z!GW)V3EC=)!of^Fenrw8M)>B)Y+4<5Q&pRZVGS-F?f<2!pM(S;WO+n;q#{Dw90lY7r z2pX25kT_7CUCPUoj!hoy+gmvBVJMkhn{3lUW+2mkGIw0^7Keks(eB567}<3Icgygf zC~Ah!RA(K724GcleTv^f;GJcFXSm{EdpPy91d)ObbKgJNLS?50phdm2E32{I`u~qJ zJp3BN>g;=g%f8iF|H*FfpBwC-c}7aQcBcPG{`%MAM*f#H>#E^lP$yR})enMFN1F}` z3aUg7iMU8l4-(92-ig|Oo}+;A*BzC{8}vraj3B}6Kq0huNOT}Q7Baw$)7c0l4(jwM zYnbb}ecM@@bQoY8nOm-t$ZRiG zj;%Jj5T0NZbIi7UZeL`y-ZmNY^?QNq8-19WkzRBK!(L%9duCZb9r=^WT z023-e{b4Yp$`~R&oXZj95Uv)gHc%TTy}6?pbmdS~Bh5@{T2iK!L`l*<5M$QXx=A+? zrd8}0m6xB0zrD=jA+M|M+<{tBiDVm0doZq8?VP3jKnCOMP&9%-r55R?+N}@qP;!EY zY++D9N3##M$G|>2IAaecqUQ5rn(EJBL`OyJTX0h~{;eEkm>UV;8^<(_q!Kvp<$xDI zcS}t$QeQqTBPu1d)E78L4VrbhUr7hp(p?KY;`!>IC+4@rdZd0<@d9b;MI1PWip3e~ zi8?&R;1iX{z{W}CCQosPDG|uwBTQfU8ooIZ7~XS0bQB~6%sY^l0FV6yONTW>$rZ0(}`V`4d9^&B0wGrgHh)O*Ky6?ez!7B{TWji;clzPl9jT>-RFj3^iZ@V2AAB>i@q@(@N!a_3wy~FW7)gk^l*cnYlOcbsJOv z2R%!2s01kH87M^4^2J}NL4%XDS61XEjaynC@;uw`-1r{1t6j3?US8a9BCoDS^@W71 z7W~}zHOCVZt?x$@Z1$vI?{9C~K(ssMU%E1`fO0(&T6xM?Q!j7 zJlA7~&m_*;cGvbEGhwzRl`W8o`hmp#gcu3*lQS8%Q8)rFQxj(JOuF;~Mz8`Z3ThRIv<Hop_slQW^L8G}YB`?+!O*}_xJ&Zr+a@4`Nz8()YapaV z8*>$^R?K&T1{IWlI9O4eyNU(wF}xL^n-bM-SjTjMkDH$}Kcwh6zH~9rffEDX`jufr z%7CdGe^&eA^E+&h9dcN7W%A7x0J2GPjXc*>;cmx7zQo&{-~?!+LPlD2z9{Am;;`J+ z*tTrX@4IMqu_2L=8biUa(9S?OphSRChEzj^4ioRyaAxH;IH6J3%CmQrY|}M*f8MD~ zR(~vtFATOdHCNP)Dr&xuWxKbga>U`zZanV%mYwN*`@PiH`4FlTcZ z8xW!~E|KmfS`VxZEKh7t)uPRmtX4Y`0E7=?LoP#;gAQyaQ}^%QVNp@gL~(9f&aD)r zZr*V;{jtRPEc+C3E|fEeL53|$%9>vK^8C@Lk(43eFiWk1X)6#PiWXbwpv4}Ui!Gu@ zr(;(!e1lklVPPRVdwoi&%PQ+-DTT(wMMZtOqYIPYg3wReba1?n!5!N#a(7R`o&Tv| z-8y`OkaWKV%_JOcqu8%B4Snik2T z9fzg=)MraGfKnIFSzS%`a^lAdq>JI-t-fH;xZo_wUKiWAPi+TZr1;Mwn>+{aTu2eq z1~Gl<22UH47Y*%_>OvV;sHXsTdZl6LR*kxpk$6nuR8M9WE){| zWM?#6ruGe>$v_P&1c8TV5xag?5pTCCK;4K~U&n$ZwVwcn|q#bfOQ*O?d$fE6; z_j5QVp{aXo4VBuwh@au3<@58)VrqV2@m?p^4F#V*cE=PiGmHV4O^Bx#+rkNG>Y>}h z7l9vIkjdWRbYF0p@9sGJJUi64!<)>@MaMFKJ4424=ag^Cj$g};RWZNa(bnS`Hh7X& z76S;v}w}=|iov(_!WGQ%MaZqu6$uHh(1RlHMPd7WcTJ>2zzoEe) zbcQQE%w*qa{qpk{d_(9Z%nKGkqzqz86bzUGL`pPlNk2f%+E(l`=K(^y#2Uhn>Pfn@ zPe8Fa1fj(bc>eqKAe}8J3GurFvGCo2_+O`2(uU4_R@PRome!8;|E(?gFC>$xq^|s5 zW0!b`7zs?Zi@=7wBaqM~HB0|LISYOil|HhVaX?)cozttw7ay1Fsjr8F@I1gS(9v-xy(S==E;6z7LRMxjJfUzA zRGR^Dq^PZh`jo!F8`68Ak|!W)Yx9vy-Jgnl^eRb%H z3Q?{KCb?u>V3Nh7+Lg#T%JnjTYU%n*NU#TDc8vy~0m5L8k3de-HoK{Sygeacr<6-pfX%lIytKkY;{e+&M|G zq)tkW?+L{9{T;fqt0Mr}R*N?Ld&Z4-diJV80@1b4uptSU?5*-D*>$)j$Z)5^&bx>Q zSlUl3U4a#0{KXD$(%hudxG!U3W^AKV)<{vKjf(9&SkOl;9Lo!OV}iL~N;~k{i4${o zxEj*EP#FISKwr`{ro!2*MLsaV=L;$j5ho$Pm)=Plw3G;pm;Fdk_JYwE5c%Pzi}ek( zeoXCy^rxZ){wY3&F^FNNiv}0sE~-k>qcYS%9LK_oDRv-zRHV$FF79Hvfy6U=N!T45 zZBNs^34ydQZaUO4e~BQpzc66Pu#$&2tI&^5btBC)725AmL$yoPCGm6#ftz0Ze8`ZI zX;L_++JJ6PG?O#-uhd3#^tsY7arfEPW97FDvoCfP!Ml}b#$B$jgqdBgmdd>$^(=5d zxySt_jthgOqc#|tO}Leh#eT3bze^DYH{gWu zmc@jqA@Alm_Ku2sWG$N3oj?8#TJ%ar8-HcC38K|o&P(1Iv^*Lfhms{qPJuEjE6=JY z8oz-_EFT;yn-@z|=Hn@_yf9dcKYP1Fu~&G3Ea)YyEq#mCfc0?Y0UKJVVI60%7RlXP ziUp(g;Dn?)ws;wm8b=Aj_7ZQF{A{%kqvv1SEtbp!>rYwzyNf>MWl=0=a5kiip2Z>xQPzZ63?Q1 z+r;Q00s-;7w>407>U%iQYeRL6k4+{Ap!-x*bsxk z=;h^*4n$NUB{B!g2%}i{|1kEB;h9C-vT!;b_ZvIu*tTukwr!`wH@4Fq+vwP~ZQC|( z_C4o(XP^B&``-I^{he!$IYw2DDhG<*2r&->HGPz5Ff~Ed9#o)wl>wG*R!|w>%Lt3P z5U_4450qAFgq$heJ<9*U5ZZs5kK`>~IVrz5X$Ts`1Z|7h4l!S=RmwIW#I1x%sceXiPip1XyK`pEi5TYe$B{~w+U=)pYY?dVFyIzmAY!k9QSgvVK7?M zQmD+v9BZbN9!8iP>3H3^R2bD^ThLhpNFY-Q;_K?ZVJ*s}ROzc6Vf6gf)wjTuDd@{W zvr**6EM0rsiuaHj#uj*+KGa#4K9S)4@+wp5kU8AX*M z&3FAgN?m5A(U#G30Tej}u|fIEyK%Psj$rOL;t!I?&nR%>XI7t|MbufH1~us}!Wm}i z){+t<xp6_rg9)1pPFfhFr6WMLTbC}~#0Y;iKzbl8oAYOo<L|MSR4L&UksZPsC?Q+f)+0WyH5@0ibsrtmKL9z||MJuTH*qVzcXSOnm+TQw zEB##$&9%zq>V9ttF}d#+Ew{RW>VW^sid<=;rYYXiC&Eb|yx1SEMHpXQ+fazd_H@(h zJqd5`hQge!(EZn4pgY1{qGwe2QV~8m;HwP@SiEWpV7nsiO&07yT2+bpfZ6C-?IRf6 z?KpEEQ@etbC3i>f1>6UNi2W7iELmL}e?)1EVFkzA64Ld~-!ig&gyuD2Z+li28Z?wK z`t@$}vF?_5Ak-brct**))?^DbG^eG3WjLcP9jfjO(u&Hl`Mww$G>PHKD(e8O;2jLk z0nRLDmlV*1EX7Tg1KPp(%%EW6(Bz4=pi$%UEER+C0k7dEiGL_a8ta)I7ay-~W^i^toGvk{mL@xBfQ5qlJFmIbj}{%@)2MDX3YA zl&j3umcUlohTTUIvLqFxacO<~c?(mD{oDR;8ZJ7>4@EQ8H zDx@qAA$DP>ko$_{rZ9|iMkEf0Woe_uuaRT)3!nU*;%C{Y1q}yU&w5h`eP~#yhdNI# z>`=uI@=r|vs+1|gHoX@qdUuTz(Hbf_zzyH>wYzGwPG zf({K@Hu-Fc4IPeJ^@d@GdaHok+2P8MBTd!h@}v6lv-$W#M1WHG@_#;q;tWE=A4$y| zaC3 ztA-1E2+Z|{YAx>YgINPb(-YleD> zi`haTMb%cbCkmk56KbM3)N&*bq9=K@3!$gUx)h!YYjxbutb6AUKr|*|k9c()0$%Y? zxb4|%QFaLul=fJ{1c5$yT%^cM=Iw~Sq znQYX(*d|Sh<|XSe1929>XoeNDqhZYC)bd2rAf|pZY0a<$>x|#CYYcl%)k_2hOh2tr zIejCuUA^G+5TdCppZm3hl4Do?fCDSQV@~C6+I|Z2$~z_3SXVnGPY)V*EvWq=X~M)LCrW z!8GM$+Et`%VME>9{WCe9Fk3fVv3yq%zK==5@F9(ElC`Gb_>*D@RSZ1^R)fSKOkZ46 zULs}@&RW;q$yK4BJF_@+*9i61L0$y3FdWI)q`W6@wIJT+*(t4!^B8&*Il6zmM&AG& zq_>i@CE!rLJOZ_s8YX1>O7dwb(&ALkuyT+`0Vx^X%bJT}uz{}7EFtb#H`=ka~}f?dgyQy)vV z&oxqKJzRkUY^Kh%Y!7B~;n>)FDYl4v=C`5&>hg)U>z8canJ!QlXbM})7j}uUR*Y49 z5$W_DEo$<;ds>~IC+VPNr)knwIM4h7731$0p21j|l68_}yvL&Ec*3$7HmUn0m5Ix( zi9pH8=*A!BvWQZ4KoRnzlSpyq7$L=A|7x^5RC(_qa4zjxpJ8kiz5VaBoHx88D!oSK zb8|?7l%zt18{oo)y`T{4euP@)Va(AQDz`>b zt)TAJmBAw1QX$;39|&<@c$FAU5ad2#JrfJ;7Ez!S15S zzLqF4!LPMji~FqV9l^EP{tBD@_G49d%be&3SLyI&eu-!wEg|6+e_=;!<_m}**f9jk zIcb4j7q1PB&zxa1`ffqk~V<<^c&=RiQ18uShhv!&W6y= zM(L9Rjz}Xl`Z)oCyD*fyP~S&V=)1CN%1OQ$#aYuNbvxCsj&NwaF#=->8iZ5?6>CJu zmYzD*zHPYYdWCoFoWq2pJoq5O374Um0K)br-WNgg9qe?DalY)!e~KpkM=iz1TA-}@ z;t$(k{x5K?l8gNpk7V~HSaNhR`U+K&uywUF`ro3Z|3C>UR4<*7MKF9>p_@nHsgoi6 z2~n{v;|qDL1aiXW6~Cj+ZZT)TF-@@l<(a%2F`?TySHbMOf$WCVzYD~B?!qf*^MbCa zNy#_Yzc*)Uvi4g;&lK3?R4y_~Ka!pLL3TI{1=DEn^78FG zSBeB1xRN2v*lKC*3_YCt46Y#bBoVL@BtjxeH?EVm6MznG2XMvstg>O91OuR{HR^+D z7BPhhR$`3@U8yIFTXjCEjC9@%&cb(ag$xtW=%wdv3|ImlI`+>gvoH!#6aEhJXth5k zLVM^dIj8&<2$>OymY}0AI8{LM^krE4Il;9S>6p#s6RJe!Ptf14I@J(F+{lZI$5=0* zHi{ z4(oDt{sD3yc+T62|iB?=( zD7#w0Q=NikRlXN8<|CCFBv3BZX$6dS5dk;LxU9&C%6Pad*Lbeh^m;E%eQv-$PUYi7 z`ATzub{LLs#Xs;ABE+RwR6123MU_M}MbIr=K=FYmty(yf&2N0SoWP1Kc}kaASnk;t ztFjA;l1qDyYZC{!dTK@X-~Bq)3cD|sM&u}en_@1R=P0C*&jb#dz!?$X zN6~5yvo+INdYu&JgxFk&!G6YQZ?(^f%)K+@;A;noRgbfF7Hdc0v8y?q4gDYJ5Wwv; z&m2vTCQ`syFxNtHylqqurtcqNUr+Q9f@uq0!aRVQSV#bBi^q{Oa!z@1CFwJ6JBfsB zoOCR=7!!l^>?(4f#P>bP2r0U5$^FC7Bs~GT6kYs}NYWj4xk-z9a<&*_+8F5zPE*Gp zsRTYHR??mDe)i6YDWUvPC@$1Lbq1)nwV?;k@EE1drv+#n0pu9QHf_PdM;mnlxH;ti=N5eDxrl_|?3S+|wV{Aaisz3T4)BzqYejGsT z?ORd}{{^z+6%7H>oQrH!!)D>dN!YyviWYy{F8q=N-;9IHxHr}`&wrWWy>Cb8{Jz*^ z?5|2d^?&N9{vmG{a5QtVF|l?2k9(h_@SnMiEDRf$Zaxa#0jZFiktpH9^1?YZGh_x) zQfu)S$<{mji{4k?-^d-1yss344+xl?OwA^jnd~mm5tgEs44$t#UT!T8l`PkN;P@+ zJ!>c8RP(oVA`JBFCI{i{Ipf7Cw*d>GN5jZViiiUF&LWFUrs$0_$=lB=`seJ}3&X(b z*F)S6$$fBNIx}A^!KIg9d9dz4o8LuHd1PDNRZz0qkdTqa5@&S%4 z{kooa329UxX(M1>?~t{|WYVXHs2>ZjUN@-dPk3BahJm;yUB|Y6k)J{@{Y1I=I>_`d z+?VnHc98!F$@~9_0y31X{^8=w^W7zdQW6ypDjXEGHQY8g3{P8dCyz9>RALnY2|3OrK~soW%+eGbnb_))|L72$Od;&AUh$^X)ai{(&&6dKFj}85!5L5t`gU4d<8- zH;TG5ag|LOpKx6sQ-+pld6eDNlb461Im#R-F)Xsh%68Fvbhy)vZn>{ho3qh5s=uL7 zW7L=z1?2Q*dFLBjVGEbFHE#Yx*_xbkoy$fK>+kzwjrFL8T)&x2Wbot@o`loN6;`o3 z!w;oejF-rRTokfScaa|F&G|{sd zPEepf64aX)lv26B`BU)Dl3&HhlUJLdP}uu7F?VTt8YCD4*-b312bfLHwFsHwq=`#!3O+ zbt{5WeB=->-@ty@ATET7!ZQUFWM)L zLCENWvH!xP{ik9O@y{+H)ebk_ChBw#jS(SjkOShbH$%5sg14(gpO;J1Pf_HJ!yOoO z7<46ry;yi9b(KC9h?4SDNr`$DCwe;u>x`f23Z_ZU3e!ci8mn`6$Fx9BrFyDD-c++t z&*eZd7M;JXm^;^p)>XSM~KVU*g!dODz%? z1nBu!#P5y!7s8JT#%qVQc3%RR1zNdLL-uw71wUg;!v5f{RxS zQkx}8=F6QX5qhYE-zJ+Dh&=^xU+JQwl*yk*@nz!YpIGN zGFKPXro%mw9!V8h=W9z3t&SO;J?l?Ryv`e?On~+a#i<-F^1D7^Hfs%8ttnIC0YUBN zaaWWN5>xyLZtguoMrqn<$C0v_NV4yhJAK-+G*0HnwQq}Wy0gj$9N(d;IzIfn$4btE zf~?3T$pupMvxNjPoka+-eI%X~M>G*^3wih1R8@?YOR@lg_jj)30*&OUROF^~G{Nqq)06m?p1K*>?V2hG zW`e3FBaRZY7?Z}_7Q1RU!sy*Iw(^6*tM>GJc0>7tQCCyft-I$OwK)ty);*(TYT3}4 zH7WihFq9^;+CWyhj*iru@#~LIrmFqEHj}bD@?exq_X%=jLWvkM!rhz^;(;kax>wwn zk*Dflj)Kqc6Y}r~n>Y_a9G74@8iDgD z?*?rre_4wM2o~?hw0n`pwENA6nXFW$w|er(+s}HGJMv5~03nPn+ZWvb+63$uVsr(6 zzQ%g!+L%okcw+i0I0xy~{2oq_E%e#O3Es54N_|kZT~2A=H@C$poHN4U))P&4*MVi_ zh&fWWTiE*;{AYUt(x0u3FCw)S=ZLHfZ;?Q;rL$gS;h#TkpD|4S_6G~uZ(($+#;@0% z2oQPJ;P@!p^7+&IvHrmy#RobdX&BpM7U}OWABf*Vi(Yd~Y)Sp;Ut|xtv=~I1;F8BB zHD8Gp3nzy3YKAyreBjEq9R!^*zE>!>vj?$5dV(CLjjJf-5KiySUk!)(gt-N8ooq8K-eoZUzfjDc8~;LJ;A z2OXgPh*FXG=m4ah3BA+aCYQnZHLu;`%Gvf6T}DPhmPf+u+oHZ}HC3bMASgB8ZficU z$+F9o{e2amM9YRUb;+WL(u(BQH}lCYfZSvIpZaiX{@9TxXSTI46RZSE=1S{(_mAoZAIHMk6yBvY7-^hiExebiJH8`no5^)*VqyaN*^o0HFUN_? zw~6M%%+_DL?>4yn=)T2*Oc3o$(FC;ZJN@(%?ha@~pc5Vp;kVjO{#LX%gJqz)$oL)v zhGXB;s-QpQxiJn;gV5(w@A&WaM*^jAiNW*Rrm}yCdtGw^=o-3E^r45`?Uy4+wYLc> zVN%iTqjEGCSY{a+YYVUeTDwX!CKTz3bz#Lhkz}aL;SUFZ7R*wJd1#qhMqtmB_J*fk zamY`~3W<5^W8)>v#i+32arRrubTtrKlk z{B_o`tqT$a2tDojM1N^ZX$D|^4Y7sgZVNR@2#L`CW%SHA2h-f4*a%$$#w>Gxbs&{- z<1ZCvEy+BEo_i?{=^}LZGpI^!gG`vbCxyrS!byo<)J*z zWG@pa$W>7(PE_hd;;NBAq}Wl(0?c&P z>QV_h0n@7!%+Y0N;P`&VBAjNtcZr_l>O88w-`Z>Ch~!Gice*je{zO<7nR=3iZWGDn zD)j_ULNm!F=>omTTDt=V@{JeoRe@3YdT7Qa8X}b^*k}90{S$ z-2GH!rOWJ02IKwWk=DY>>RV3s>`?p_;h-yEDB2l51c0UN6y5p|fXXr{l|^GK1yyCpUX&PS~7 z`2U8+TtolzmW)gPF0mrPq%Ui=kZ)Mx$UnW8Z;K?#PeLY~F-6d+#}gjQX#1a{KveA^6d z8Z7aNcm>duS+HJxJG;0#Qr4i78X7Sq1rIUfs5lM9GApStN7zDu z<`Fbjnh#A+?=e+AUD4Suk2n9(*pE^L)Xlu@^?uFu!9Q5kFK~xs*6=rhk`x)Lbzm8h z`q#{sREiv8b*)_=eM>0UbXJ!&cjY6M$|b{%onV3P&pADGvah5&C3|?)7PE?q&O8!8 z0+~@y&vXLi0Q>SF%7Geh@cSZUIdI%;*rIM`+an6C-#(kor6Vr4uled)J8)aq9)UE` zlC9kc+3*ABcc>cQ`0%!}yB@U4HYdfnhC$y2p%Q2XXXq$&qm!D33=67o8DL}%>OvBp@RvLA2$9TfQIeqIZiNAg{ z_|>3o&GfBR!YvD59c_Rqq?G+vhJPd94Wupba2zp&* zOn%g~=Hs#2N^MWf`S7>x5K{$Fnsk%8oMF%p?A!Ij^)NDX45-#so%35K_d*_A-=-!z zoJ5c%%%>%C*4o>pK3aY3)=*y3SvEn?#xzm%V%Zc~tjAOfGThv~i+DYFxFsEjO$z@Q z+RMQFyi<%ud#I3-pY8d2KNk6pknM%}h2&^n#P><_EMIW?rDT^&W?aWx{&@EH-pWla zU7lEkkMZ&Au)b*fx+m);jF+eCW=)v^>stoiLQQ65qL_%+9&BodqXz6PT0w9)m|HIjdls zE=)iFX~6gg+9+HnuUPUWs1E)bGXC9wp=4oWZ~YbMV`Fb&ZQ}S}-Z1D;2A;+{Ea4w?-NHT?+SgG&-F4yVDZ!m`D0h)yg;VhBY3>75J&Fcswjmx z)oB-*PPLmHo6W|*K3+b)cY(4h9rfunOBai8XNGePl{UplpehZy7+`sTgMSe(rkIIF z7sli8-+J|*{qUUHaaW>kp$2QlrqDK!gc#Qv1Fho-w9q~>ZNt;+Cr|x-yLeWSJ^Ni_ zevTqs#GR{maCcgJxi*bd$G5^|yDS~!0D}a?ffu#s8dayZ^EZti^8`|M8mVi`+Z{Cb zg$x81jOKJalQ*y-kp3bkdo1L}hoSX`Fm%POd*-Dq=MX~2kKq1<0Tj0}E0ydi{D5Q0 zq48cC-#Oz7RpE8E3Sdk}M4PFJ3HhhYq_eQTmzS1m6P+bYIM%_Hybu-}v$MeLRFnv8 z%$0gKGOXOSbay<^q?}xRz8ZMgcgg9ZKP0?v-_%&tS%NFigi_8l)(F~JK$|5r>BNj> za);@uxkOrNDv-e(%&x3Y52#QJoH{gRf=kIG=w+mCu$`eY%(D3&u8B4|tg>Hd0nR+v zZ)ohf30&@fU~mr3bw<)W?U?90~UPw#P|&}kMsX7;vT^8YE`h()rb_F85e zA*qh+B3VMzs1?8WqUD1ioUmvK_}+Z84@V>Mva9AI-VGsi{mW{Aehn_bp~Xls+mJAM zc!&YayeKW-i(K^6*7(Q4v&VkcAZcPqiO5XcJ4o{>AexEzHVcu;I}oXt>zEU_`9dT>8=i0&7;Bn`VQTal zgvkNVB>0tti5C$|Ei*bEx=FKE$Rn_L#10{jRlt^E{-(_Bmnm(4OF~Vo_|FsdKYn=V zWb94J|I3<&vz?u_6P>udqn)Xek)w;@mnxcA$>1NC;6D*Ic^SE$uMDdn!IC96+l}^H z`hf!Jfed_l+H3&a&U}@ji@~HkEg;YJNykcSpTff5#paX$y;HkkLj~N{-A5aj73T zfm9&i7IvdGZJA($jJOrwUeLg0-Q7!S;#tNTuEoD?fvM`QSu9+nQ*E6mQ49`yHd}W8 zFQ6w8McM=7S0UO!`S+mbKfHJTyA(11S2e0ov(iEqL-BQ}xGu9wG44qtX>Nj=YB>=^`8w#Jd&Kg-J; zmtF^B%TBT%-cPT1Ai!%Oj0*KI{P2|Cqb@3M_ayi#kt@!Ptw5xlFB?Yr=h&UVfe;qn z@>>ELfs|?jg5IgUd>1ZOVuDfpNi3kLj0wu>f;CV{VpcPz%v4f}ii4{o2cyN{EAg&{ z$;6cHKGL)wI1z)~7bI(yg5l{!HFKn83MDg53S7vyHFs)0D>bE|D8tmWkV`L(*HER- z;5$SYS`!CmN^Thm87saf+n!DsyQ=6-rg3=54qGBg1Cx2A07ixpRa9D%){_g@9eWTo z15DvHxO%MjC~?f>5E;#iAi$x!hEf~O7K^1X*N7H0M_`^=y+)vx6lBI%>Y~CLEWqpm zw9J%M>ePWKg#bHoEV^R#n&X2%9L%?mtHy+aE1|S>`Qh%NP$Zy>xUGMR!n@y|6iKdrQ4Rf!ARfDgX6zXOVS}#FX3Sbem z{6PT;;+?elP5WLRz~q8O-`SE=GSL*Y+KngDOf|GiKlUzntquZkbU>vSFzKZuQ85;J zj@0r?D=u+Ho$HNwX~|*dhzKYkX)Z8!Tw#RQNw~ROe}@sQY!kGOc@ee{4nIvzuUfkH zKBk=;xRz>DrcLLNnm2XUeP0cydczkh8ZcOqXDY0~K4oJjvne#T-nGFzKT32!xEyjv zzZ`Cgcr@|$gD3GO-Zk%Lx%hnrUa8|lhQYaDt zFP~FF>M%YkThaoMQR&BDq0ZNgRQ`@vb%CQ(A;*qUcA^aXz}L=tVbVNZls#8uSq8$` z)Q3e&j{j0?Piz=l)wNnVOCo!eCGnP7uVypDfE)3UpKr0@%*FU095 zDlFf$(s$gVny?4_7xd^RtP5J_ZSt)NcC3DExmI)Mngc{fHARN8S@sQ!uy~zX#{v+vI(bdd(zVx$_8PUKsu$aQFmEa zLf%r_hSTqR(tBOCLH#Yj9Wzo#FyPf>BCViqX=$uJ8$rLyZC&wCxxyG{of34q)U(p% z{q}t@tRlr2Z&)DSA^uQ87oS7a@y{M=d;+nVIImDGtes&AMI?@A2QthqZb!FI?JlzC z2Q$jYzZfe8-%Sz2eQ`}vU($5)e*-%FkJ8KdpV)%RR~IOc@FA;(rJ0);0hJr7URTEt z<6cm&Nky)%FIvs_lv0?+DxFT>oZS%|lgqR*fXC|xzu#086>cF#!944xLU|Oz)3Q2!?G0Sp& z!T04UfRd+;xomeio@I-ri8xnT9R48v@1sgyHA5&=@j!#>V$72UExNLWOzi~;Mr~$P z4Z7jvfyYexzCZ&6Hh@q)OpEm@rOmF6PJaqT!!*(O2T?rEICB%wPvzzU`6~)6q>}<~ z*dfc&w9=j5P$DI&RGG0JrWuBuR_Nvq0We+P816X)-T<;9*WviJCtijP5t9y=#RizQ zP@f7r{0vbY^UT&A6u@m0*{Ks+LlL23iX+I^SeKk=@E^m*L_J zaG4lXoRkMx9o}{-@yer;5p5v~T|}y#7|;v5)|V1+Rf>eMC(*4&lTdv)Q#$0zL~Q#| z32HrN3cMM&m1oU9JXX0M(|UM(vK29MWE!>>T2Y*|3{Y7dxqDDMI#UL+2u;gqF_mhS zDlt)kIuhdUmV}Z+2#1n~x&v7dPu*z^Vc3KtcNkrO7yE+ga;Jqiy9EjE3vFztb zjMNXJn1ORf2Mm#KT)VpCjGN5A7o6EVF(BIL)4`i17H7?E-%4Rn3bYO-%fjB@-!CdE z`)jAo0-`61ccM)y;V-d}Lglx!AQl+f!cs;}?@m3HB3qS&#Iu3(o3`;$I5Mp-C zj(H?5B_WuPdRCX>CW0}Zx4J20xr6l7D^(rbGRz{I>DQkC(S28Cu~EQVHaD_=9qICzQq&1K?z&MEol3DhDyP6U;5_F(?{? zbQ$q_h2Oy?5-!pYNvFmEFp7&fD79`uOMRv6Jr;!X8Hk5t=)Z9w>&Ce4Q4A ztd4r6E{KvPC7w&X73RkRO_)+;phFl~^mYyY%lc}t(683d479^IR!v6fZv=3MJ-rvf zsjMYoj7ghT!lJFRq$1|vCU|Cf)Op3-`NpB-i;UV)vr5b(EH zner;_mt*c=o(;L}396Ch!JwINDKy9x(K{jc3d-)4k>}^{1 zx@fS-hRSc%Ue)>#PO^j=*`CyGxX=()>G|rP>*d_jYo+yMTU?l%LP^)W6C)UdnO_rv zv9de~E>-|A{_mP}N=*#bGk0AKfx-bhzYb|nCy5aj-sq5_=m2pcMqvnuzWAM~UKcz- zEu`EJq;zX8J6S&!aITPB{cU}S-l?wT0Smyb4=~#>$?#dAEdLum>UQJflq`Fmm+xM~w9XO+LDY?1-bb zggghtA<>TvRN zgXo+FW(s9)OwMo%?rO*2?MVMZza@59HJ4}fJqC$s=+0}3;#$ukONMgXfakd^;z%CG z<-h2+PLeus(zGz>fW4i?9kW0QpqImiWQqvJAA5`r>SL3RNKd-eKRc%Im5o}1tG)+o6Z_H&)F}OX1gU6ulEcWm%J9GE) z-i!{o@uvyC8<`Kcg8XJsyWIhG(}-C~x?&@VsM_W#*0iR!@CfeX-Ze!b%_{eUJKfhZ zv!4c{UC;|qWF>gxIk&OzJG~fh`d16>ys_4k)n!D7*%W(QOqf^Rs z8;;SOII-p&5kjTtnS%o(7k`^D?d-2V35tVm7@+WKnjEeHl1 z?2J(-c+DXs3r5u{aI`8rWFM|=b47ns5Z~qQ_AUqjbyjf!N+Ub7h(Vk-$dMAnRXV$b zZ1>*1D9aFL3lZ=r%9ss_d~MnI#+Oz7+z7)YB5MYsKg0c^o{JlY%j~5|g3Nd*b=IcK z=3X$o$96a0wo;}U(_aV(RzfRuSm{Fkv6*LT0A~gGF}SfFIpTReiKkK25~Dx8fD<&IXX81Auc%&vIb|AO`Oa8As4XZnHl zLbAj6G3{se|NU`iZ0-xK=4mj(T-r`?ZfE=p$FT{KtF8rtCqx)I_$a{czU$?Z9#|9g zt$bt9H>ix~EDD!3?~^ge8V;YyXRlr%pEYt#TKK@hhj|mZ_t}=@pw%xXB#iRie~jLp zE3Ozc99rK7PxXN5OS#`7_=B+&7Kz>C+h z{{^QMsJ;F|9z^`C%%9b`L9`D121y?iDx4f_@-y2n40(|?ih?x^7J|!XkGn?9jC1c71ue zNYB^(dsq7tae!>`j)Nq39eD%(Iv8$0^+AxaE)Z7)c}4-SQ-_chh7U&+4j3b+HtxTJ zM7-;bHbizDw~vk4qDLdd7}Tga*9JSh?j^_V-4zt|%y1Xv&P0hRGUmqVXL^c@{K1%+ zbZrD=VPHwwZOGFywN;-iDFS4g>N5eX(3Ob_(+l^D4PBgv@MVB9V$@!=1sWE-RR>Nb zv!x}!RZFW^CZ~v9uY;*ADtR3>b>x*i#Ml&#ER)fCC)zS2&f~9dJpL4u{1?!#V|pIn5wY$?PL6w0UkxhTV%Ir(Sjf#DmJ&?=&o zQWVAfLjM{vP?#Cu#kEjZ8zjbLo1CE#rC97M@K&6-a41&LK*#PZZt@my?R70zBAUsUf8wgMqD=XtFzr4~bw4T+ z9Rtp1{3jldOZpUvR;OXpr)$ksaM!34e*MUw?{XGaow^7Ur#lSMFXQUZp+OeSRU^F%{Crt zqty@~^%NMux1$37tH+A*qkPMyjbN7!<|Dk5!W}sUwJq2UV!i_XVCIgb+I7-*KvhP>H@Xd z$x@q5295S=**c?^roP=219`?T>2-+`4q$@Ih>(WC)$G?%qANO#jQk`ayDZQ67Oo%7 z)vL{<{JW^#IoA1C+uoiWCo4%~XIZzF1**oME#;&!qr<-_8fI&o$A&^=ryQ3sd9|W8 z2I|c2E7ld!72Hc7SOBWUT=scq&rtFG;zFmjv~@Pi2~B5$mwpZjOzOA^tp*|4mZU>+ zk9t;K4b9B;d&9$Jsf)E*Eh3xXls)l*PmBwTNcY_A()0o7oA>s+`bE3-f*166%h9N8 z9w#m^ZIuN`rS!iZg3fwthn}VJPa8oMZ9C*hCV8Ie#4qI&Hq$a{1@-)62AQ+gJf~tzVkNc_gD=p$qc*mdZ4lUIBTGjaD=rq9v&VgC zms}_Bf4FI^%bg^qWHch60B$VuJ{XEr(!BSzunzEA$Ai0QQzqf^qYw@i-)U%e9^Aw% zLDu3lL0<}1g+JHD;~!)_y%4?ynQQ+(>VQcW*A>C9xupyUvw~_Dy8-b$!X&j3w~Ebu zbRj14xiduH>VR&&OHf`rtRpSIC`g-vsdl5XwkEGZBwvA=ing12;gz%Fi;{MRfogf+ zB`UdcokPh(*XOD@za}YS%d4#$f>U_iMug>O9(d4#*4NtT&!rtRJ?6 zvc{ttZ4#iI;>Fe3=Ua7rvg>VTky!Ntezru)U-=Bpi)nVz5M*^77bd4aw; zhoXH_-dti?uwx=F!pJn!6=V<}`!9aMu}9Tf6OGy(;w8RwFFRM$jkYwqgQ&ZwX*&tfHyx;cu z2H1V)_2#eSBj?~#j>LJY4v0bdw)>gX2Dx_@36BRm^${~XIEd;k*ew8_g>NTMifs*}@%<$>Ih5^uLAtjty&+A325r&EW>4EH6}V1Y-PS^HHSYS&jcgNU?37iy zoelAL(MYTQgPx+>mMTBE(TYK+a#pyJS&;vnQHEy$b799JV{{~7Rkk>Sx&ngIMDB zn}4p>iK@vzx%?+Umxk#Q-~}&+{JIUVJ#uFeXV@?pc#N^V?gBEi2B>)O;<_~*Hdq-( z5P@g9pez;6mVGCa7Cni66i)#Gbd(r~OYRO6%WIv#A@gkfqh19)=b$SB=%DNL^m}9} z?;bG&3r)!k?=ELB9c@~Jf>4p0h}48O&SycH_HAG8SB|H%YQOA)Z|ZE$Z?1e)3DIV% z7>s%TFe&U|6`*X_P6L7~nhf9`jviwB5qvd7m{g0+yL~7x#2Uk^w#q0wfUoK~j9T~0 z3@{aP7~9`6%#($q?JPT9+MzIh8xbd(sPz)K$IhZ8kv^(%1a?`XNJ_hWQaY$2j< zcDt!~!*8lxf9EdUV(>4cEuJZUBJeHWA#2j;m%DaCmD`~}mD|Nk{zX;#I|&ieG6_`D zvv3D}rc!Sw$j@B)nu>c=qCQ;DNVwnb>QCLBhkv^F1F<#H+FV?91+5c5+c^$H?}Cxy z?vFWzyd4N@rRi`rMEqbnZ2$PTP?tl6ehnDc zk6*N{*}GO`?mB?f1NX9H?lNhD&x3`{A4Jx7;3QJaBH^agI2eBrg>AY7r12WEyn10V)0j~#=(59H1`7$yS8EpL{$qxNL`5w{|-I8=c}ZQ1?gPGia%SYm%!*A*d)y+v7$5$ z7rEs!Hr*06a~zV5H_cBOS;NWR)bSh_@T+VSJ2kB^ZmCbNaT`rCh6ODbCnkGFJ(-fi z(;<7#?&hPoNv35ud|B3eeI$tWp2GyW9bNF>SiJE!4?r#fPjMoy7F=Prl_Nr+i*-MQj_w zmy|UAgy`y(`%#I1FFCQYy~Br4T;YX~`v9d4Z@=H8;_W&7omH0VSvzs3hqChi{qPmVdhD z&%oHWHG-C)L^&?Ho!I>j9iZs+!RTt6zcl0L5`%hbikv|lbMq2>xLrurT`d2Lv$qV2 zv(45<6CgMQcXxN!K;!Q2?ydnsaCd^cJHg%E9fCVF?rx{wnb~{K%&9qNe_#LT>VoR3 z{&8RHUe8*WI7nZ}JTrw^JL2;Lb4@T?j|K>oXt%fn7GOCt2T;Cr2*1LOO?Fos-1uTO z&>uCor{%{^n2bJDg(qGN?B5Dw`!v&xg_!c+8J~&Lt{0w@-x~OowiPZYpQj!Raq@e_ zXC&WB&;XQwpjAtpY*}3iJ^G`QUP2_3)vy8dF)H{#YGS2JrAf&yo6{eMq0xf6rPT6} zP>N$>NXdMn^we-=(c4a#ZjlD+hoQI^r!v0Sg3saKD|eGJVKrK3pz5sPbg#$I%f3raYYO< zMHhP(DGP7aJHUKp&C_TWO}z{s3$#apWw)gAC~3o6btDZKHjw@1SC{@fVe*K`TQ4Gg zPN5mY4<0%xdRGaJ+CpKv*Z?!oUwe?6C1{UC($5X&M7PkqMn|X%t%5cyJS`qEkD`Bf z?9^V)ysN2^A*{8}lU3jKV57bi{IywfrufKapz_IOQUAA`5+uGRcw6QbMZB376j4{an<^?<`~LCESkdr5Qt=uTOA?OpWpOP(w=|`|>G$f#0}K zb}59xZgu>L2r?}sCd58&lm0Q|K6$u9uL;Wg0>m#NdJpgh4t?v!v^&TrByLB zemKNh{J3T$FlsX5Qp+gf6pkZO=0f6cTA|>GkaLLwBx7Jn!|}8uNY9VOZo%<#ymMcH z;;!G-Ss(-GbcTYiq`cfOxZA`Z&UY_--yh++bzkv>1hd4Le-=WsXe7l(kpvbpWrZC| zL0k}H4qBmE55tYMQgue|aCe3ik*{#{KJxf~jf)_P(n-ARL>_3$dz3?CB%?xsMG1%3 zo#bfRO@QZQDQftoG?CX>sd73WZRF4=Q;BUlP|Hp$%U|5wlW7^O9kx}K?-@Kz*TAAP z=>X6!ON32pC_{6py)}_how*4g5bdyn4-;YavAs|wJ)nzz#V84G8dv@#fU6xQ)tB1UOqHLgF=A0%YS5-E3;f)IvdbBKy5sH9e#uMNKh-o}KcHO-1ND?bA_ zuRtbQcXq%t+k+}V*{|XwY?tl=eL9cx^_@qhq5>2Q-F2S1=z z01bJE0J{no{_DPu+2(!9S%cx!q6{E1^Q2y;=H{%?D;KfDjxWx>qGpjMzltM!qUrc< zrA->P_n4H+g&yG+Y&S*8n73SyrI+P;iLbaotam8j$^(>}FSm9)J1Hz47#dGSmtG)n zu7PqRw6#I)jcA^XAc}x<-+djniO?$m=t2NO( zPw{3UeDSdl&j{qYa!q`|dTx(BG^3H;23BaVT}?(ywNzl&KW|8y$PL-(CM^arbL;Ji zMzNSDSYzO8ednC~&I->q>*v;u4{}L3r{wTPX$$j1dR8#vu=r^>bSnd|j?($5Lm*zk zKS@-!xaaOkQrI66vH&jl2utu&u(d%=iUtPo8Yy=ZsVd6G*Lp0&%X zR0HB#L2bnIPku7At+FM!rb|T7gD9I8y-1aEdN<81I^Y&Zp5P~b&W(IvR@Y!qRuw?P z7z+A9)?&4x-n8RX{hP~Q^O%U!ed8^Rb``S6I>#3Y-1Bz_jnT_jP@NG1!?IC87HK9 zEM|)|^yC$~2W$8cs&Mwe2|ugcC))4=)EByzXMYuQ#g~pX_pzen!T`WAzP5 zRv5mKo$sJ3?uj_cqdk#@*@PM?qdkb$kmnNRQW=I2pR2Vbq#8HJ)t!dtX_|JFE#jN1 zv$t4MyC&m~DvdbWsyatBwpOo3+fB{2f3b6Nj}uD8G1*}g8cdQ*BL1~olvTYX)vBu^ z4H=a&qM2rOTEcJVB`4G_^Zmq(j z@1%i$%uU^8Lcw-nKZ$z#b6-o*qG@^KP}E?AEz3`kZ4rgA87m(w$F#1i{)Ed+8jK>B zVCHIJX?SiLy6Mo4sg};Tqh~5HAw3en>5f-+6BS>)_Ygh|tU9GkyJ^Jk*^%+Y(DClG zW6GbhbcX7qlOhMS7Ml?^`X=TfZj^>J)$*BUqeTRz0vvg~b1Ki7+^_N>k)$(KB)d5hwEka zf$*QUKSUjkFe+LrkF0fc=m%qTM9l`+TAwS@(hRxl1HD?c8%9wGMkR>pciB3VuAs{T z-M^(^-m`NOwsX=7_n5bwi2QTO43EnPeV1JYXRE)^Z4p zL+3}}&A%oqR;(;GZMZTJ+IA!6{~nTT#C8og7``X`1fQ?GgOa7fF6$0F5EOU+sO&kv zTo2OjLZa}JX4Y|ib1Nyo5YHqU{Sw%Dgf1(_W+$_IPFFDUAXJV&kfqz#+Y3Z zQb<(d<=|~^P8prtsU>En3^3_p(b0m7EeQT%$6&E3C2LgDTd4Tu)a>rsE`vR&lkqd-w1mSqh&dosKebkILMv2V$Zd&y@)K-BYW7=tAz@k-`YD%j z4zcqY;{8~73uEInqFWmu0=RY}_YIcl(j615iFp&#FGvmSh@9k7;0P1q{1IXY^c$~) zDXTDbv(B5VUwY%>0QGKYUm}F4!YmYvE`^Y!ShNx&9+{x`O#-Rdc05lIlW^}~)o}Bz zRkHFAIml*r-zYAr^`glg!nCRt7E_9Nr0?(+>=oV+7cP{1!i8<9vmXDAc*TYXZhrt; z$HIe1BL9a)i2LtNR;8M?I!NBN&RKwsT0}OGRQ87khn_4ktWX6xJPON}EiVYG=Id&@ zAqKoPOJ7=QL7`wN;dA}{)n6*{(fu;jbu&FdISfh*DZ!VkX4>tnz$x-I^UxOF)`Uss@gzDgZN*A@ z+mI!E)8zD4Pzh4!O!QuGw5@72P3L4=ojE0+)n1fc*ELiqdQr?MWfD;bl_1SoS=&mz zax1q~b%YgcMok({nzh9No{`f>K?t$E|%Et zl(#j%BJ2xwd<``t*B3y!Wsx>=v*+r;i>Oq`B*L5cMAGj;ht6~eKurYaI$K{a?gEaE zd9y%{XmCSr91K|2tDK+G5(Uo1e`{kmndz->?9}=m^BZk$Y#E>bE`7x zMT{59xp`e+^<(DNK|8WwKn7kwjZhyBCK&;+i&<@h%kBS!(uqO;_x@NwNZK2KOXPQtpy&mfdM!@K#GF(K@rAdFH^r89UUPY#!w@ZoH- zxohWyPef7&h775$w(~Lugt4TILaFL^hpu?+Ja$R!zPCy1seQYOnz6hVJiPprFvC?f z`}n!$C3dhyBV$q%IeWTU$e0`B^V>js9LeGF5HmQ8htPM!aX!Ec>!TLXMhAx-b6GKlAD}zR+nXbx#MUq}Vu> zCa|2k1dp!?UERFL$lj0rxC)@?gb)N7h##eM910wAA5MC| zUT=TuMhkcBXTkmP4;Af%b|1%91;jzjJ3xoBJMu}CzoGhTUr5z<}uh|@p(4@;QUMaIc>$L_Ms$MAje z;s}~maLFYOfGai4Jk+Dt5yqZNbLF5-hZFzpGp6?hCT}Md+|t(FPmke0rYRPH+DqjS zC9G!w9zY(Q$6XA1Cu%`uUIek8haS6I;L)Ef*je7u@>zAq7@>70{saxy3bH9Fi;5bK zWF9gT$F`zsTRU7;A(8oYifftGZg}t^>6WHBxOe+H;WOt_|o1lx^U0!5O;JrUqMa<;)3 ztf(ZckpMUtH3d7weG_hzUjS2*2-vr*QUeZE%Xyg|&;~>nH};{wUjb*1&*j$>eQeJk3Mn};-{JdH&h<3) zp0{XC-18|zKY0cM!FG-w435UN1N9Oxg$eod*`UH+xWw?8)TuXNF;YllgL=&Dau=Ds3?{Hnv4VuSv`t3Gk2n_X6-t8n|aG4knk%0F^5?UwWlA-_ov_} zmpOGI5s{JipGs$>)%r*AQhOeMTK+?AH!D?VoX2;8!yCwR`Wd^87Ke|6lfxPuWIh!9V%w=pvR}{oAz*Nc})piR0%f=gRk+2Rg$o&+o+f}$-?3F=1Qg(sR($6zkp_=y^8*d zLylj*dJRq2ajN&w{~EUA@{_G-?%3AuWTwZA$L>oocH{-}5UkH*e&h;^*Kqw-3d_Lt z_@WHSno;`cP70Rim^p8kZ4y`_ni(9zBgR~mkI%^#$$Fa|iwszQcV@-%_56$__Fh8- zZ|a1@yZHQ7+}^48H^(` zP4ptN+ojhSZW4KVXF{%2|6PheF}~rb+TbWjR%Es^J@f~KGDOs^{ZB9;V=k~7X-Qvs zY;y~5d9|r;I`HOO$p{zZRS}cs6-7()n%LJOut+qDd|L&J6hv*5d7-Mge~Tv6WIPIury%IuHhZ78ofhbboy!u1V(n|z*scK4MnHQzc**~rDX|@?TBHVzz=!vOPV)r-}W0rE*cJ3 zNQsxR2PZC<+RdrBd->)J0JqkPYX>TDRoKWbLXDX3f}6{i&W8dNT=~S#7Bc`8W1w_q=Lz*Ovj84!@z7wL#Bk ztzWPn)rS#IKWKN+OIEo{&`tSh+BTI|AH|OM<40n-T=&4d#X6Nmrtn zu?n9EJfA0cnZumUJnRTdQ>31|Dx@S(B7c|~u&d@rdq zc1m~A3dw##O|{P=;T83}R?O&~XRduPlqve*8hJk++8Aaf^YTNcP3Z-9fRIV|1x{3W zzC@5D{TZfIA#4w8?;7iFH1zwIo06G#uw_fK$+ev5S=FWQdawan&3KeX12o01DU!MB zLs-Sej}TRXyW(83H)xuo&!l`oc%`j@Q7Z4;|1<0R3*bN{L5teIWWOMgi~R2Z{y#Zw zrp`hxAe4i6f~I7mb}mkye@A+u(wH5p5K=|s(z33(BML6K=M}UVte3%8hi_n_BFJ%c zn%f46FCMI1###RwZ2W)}g$OY-r^gR?$Q}tMj&yzl8&fdZMT<-?ZncdY8yN63zQi zHV~9`)g&;p(n3ihz_Bjl_wV98xVUHT;T@J|Gc+}+5Gb3XX!R(gz&3Bq=Tlv-p^41E zl&t1qFO0!&#LChJI(3!7vTiRz&s2K~pVU-_CkKk>x2I50du5ij%9xGfq*J<@*7h$D zq{)1Jr{Z>i{R(~NsE1jaBt`J_TFuLPrz6_@GaVT$5P1b`3=CPgcjE&aRc(N1Br%gh zo`N+~O#m|R<%34r;(WC4;#Kz(rw6UK9?n;57Th{^?5Tl$(MX-WyDdr}Q`O}WI*x_o zTBxAk&IC8k)gs=6hZiE_xtDXjz_l%Sxl$hiv|bcSr~4L4mP|5yR>M~qSCyYE5!Ad+b#9ZBP}A9|6&`Z_u^mAMD?=oOEJj%AOR#f z{!d0b|JHpeyMQ7SWDOlu{)(0RpF_d?j{~7BtqeL4?-XrF*C>1Bc~nO8flJ{)q5Py! z4RqR{IfvWo}{prT;T?iWUhfDec%q`gyy%vV@5W?EY4G{CBoP`CLqQFiGD@-P^) z5Cv7;-7NTvnwfdmBT7SNnR5#}Lc43V+uEWA%5p!7WSeEzpr&(&O-zBlgz#&tBw5j8 zzROBT_rmguZ_-2I-g;8vd-7Ai^r@NQmvti2u2+z^So(fe{}OjKgZ%|5xRa!^2v_yw zmZR;rF_X15p(HW>M_uK|vkhhN?I4tPeZ2~UW2fLuY}dfGxwu*LCO zwL1?7)y9{#TolwGf~hkrz@JtXZ|P4e4akjG{iJ|%EW_Z}^zMaP>e2JKTk=#r=NQf) zjE%GtnhDZS_=q7d%W|`X(ftV8cc^8eekR=e2ig1Hc#K;6Y4pPJb?tS)u3IQ=md`fTglwG zdmJ*NHI(#amIm%b7-x+Qzw>@F~@Zf`Kz=D2Rz zweWmoaIMx%k+0`i&FSCJ9*jUETcLIMf!MjX-+sv8`S3G_mXe~vWc=xxd3L=FFu@1y z_5JNlHFO!GojOISi|x95`{d^m{D>5!RUg0X^nqw%F zBB^av4%k|--T)cid6^E?UYanJKAjCWwK`Fj_CbEmwn=)+Wli}Jr^QW9`mwwRhu009 zW<0>Go~C0^8k*?<_8&SxK)FrGuuIX+iFbl|VP@uDIHO0|PN}~W#OAQ&MIU)OR~D~U z7%}r*BNdI*3I?};+~++}eh#J{2dCtMDGSNg_;HrR*&)YEq+y++(cj~h3#Yx;tyDBd z7>=B39N1Zlt62EwJ$1h7%lseau8r*N%_+F#0ayMy4b9iLsj9)4Ay(`7mF_a-MXd8I z{fV-N^p&bSv$yXW>gKK(Xf{*Su(pA@mQ!1c1N4K5c1k6`h@-7L*B3&!{R_bDU?c*uU;nwi-#~M-~ zjeR&XVArq4k2|>M1JazUMmx6-&g*WNNq;lZfXjJa^c(f_V4PHB*PIPXr;bJ)zUV-0 zsXHNx%#Vu1%9caK4)9(}pD0n!LSZTRrP+oN?er-j$k}_uXXsbcmY2;}$2b9D*=czQ z8FjD*dNv&4&>!FGt9@`!!6k|7kC`h;uka})1XLHgB{S}j^IAPDGDC9{7TM$kx+U+L zwuUf--+Ty>H-z^%jZk|7o54x1P$wi$0di-=#SR3R&`q>ko^cy?IDa<;9=vwj#Hdg$;6ab~{smg4!vKbr|1K|ejEOdfBr#i7cH5CFSOtH zpB#FgK%L8G`Rn!+=$3i_;qyO5p8oBY5-~M(0N6WN{>2n!`aAR*erYR%JbH#cw50%2 z^}izgL-Wv1MJ13$3)kSrw}|+kif6GISp_aXzbZtVufp2qW}*$c z)g~v@bvZBBrEmKF$OKCnD1!YmXL$SVt1+zQ@_3-+y$VMiY!8WBSk~`b*U1RYVyfo!+%Iswe;M}hom-3U{3K2xCl4NO{QV^N z^cdUrGMFnG-)TFh{uWU-W~y=`sJdbdAxf!YkEa$!Y_NIumd<<>li zxczEoPc)30I_JrNeB2Bb7J~|WLJ4w%BF*gVBoNIb+gQTYAi}VPbtzh+N;^1n$^C0g z^N}eU9n1L3MkU+{_B?-F6GHW=(o;js<+p>i++(he;g)&76)^qzpz@*)$IK%=qh} zzx`DatcR_dV?uBsnPh&QUIZ$6aumz;@brzk&J`O3G)~E*GmY!g&%w`S7N!wzuc81x zJdN(*s%~<>6Gy)c(*o3W^cs-osDD^FcpN@2Y3#xf#!@gmzuyb$GMP9Q$eJ?2HwYU> z3b^s->>^-9jqVi27<2OD*u~ybL?t2R2qMW9bq0|8l3e~W`z%s|5Qq4OCXcOCccGX5 z7W`LS!ORy1+UyOzAU)5 z3ttiV9$&8vTh=AiUH~2981kGO8xA!eGov$rw?lmQPs6jnfDst|{BsKwI3g(cpWR~q z9T@EYXw=f64DbffgE#AfCgYbHb(k2M$7CVL3?iBBBx(^@oPIGE)$vpovKR9@y2sl} z0XrTxyf`Q3zX=OV{PznQN!dtB1}4_`-sP~ZC3Pm+So7j+vh(SF;&eQnv+euSYH>Dc z-qEWJ_QGYoQ`tWMXiM4yno%o~7Ys8Xv?C~mx?eze9K}eV*bI(YJhVl49Mz~3=>JRr@Q;2dcwh;pQ4JCl>1q)OpO;R@6pEMg+3RzfH<7=y;cpfa~JL}8cjoIi%3dWXE+wfeIZvc|xLA5qX{S0^X zBKgi_cDn)7t;X-_gFTAv;VXUiDlU~#!A ztjXHxES{6=&g3EsM3>qdVk-yq^a!syye-8UuwY3^6bbniH+1eDUjcPWV=cpa)JM27 z>SP@nY4{RK=`I^5Ww#ZxiOlGtQ>+kvz(hF2aDs$_xQNOhk>EzHoR5}V7!j9YpfwGeeJlhNI}@EiHqJ%|fY)r_7Fx`mmkcj0BQtxNx0))<5ug7b zHf!39O)NTbqXzo6ypj;#oVsWAa6@N0Y#mv%>?Vx^)H} z`zILV$dSHnA;>KHtJ%u1gJ%!Qq>}M@{x6E!45nIEmr9L@ht}y+Yy)87N9Z1FfBazZ z(E}R)JSa2&xLZnH#L~*G#w_FKSI!e+bqmc#c)Yu+U)mvej@xE~cTM6JuCX|n`@9|VUM+Hl!I<(BR>)xVE3^2%(hc6PO2CtKN|q>JK}+gUs$AZX zb5_>Cd~r_b(YHCWaZZG#Vrn7)>Qy8-qq-;VY(tI6Z3=XDR6UXVXnmL8FIKoi&DO9| z7I^w}O2)I$0VjTU#Ou9wFz2icbF9gxW^Vb(LsLqb{LMvzo8LR(q7V_sEx_?!mYYE8 zdW8L=9W;1V>^689*(;f^p1g8}kQOb<0=_9FqnbW{O(M zTOw^>#D^oHfP4eEc>qn7dVcJ;teX;CD9*oD*^ z`C7Vkx_vA@vJWBBZK-)IK2@d<++N)!XLadW@1{&muwhpFguEjsozV=ZqI0l*bgQjZ zu9-oQBhzH1FD+Adt^h;FUy_>JPHPSn^GUphAUhrnglwuTU7pyi(hWzZPfioBx52+Y zi#5la7M(u*IZ4**$?t^?S+8P2FdgqWoGCubm1An=W;rq`ZgcKm&p&lBW~j_$9$}M$ zcwwhpU5RTK7xt`JZ@q}PNycj*z*K6ni=NVSY<=KaUWID6(W-dUzVLg(%exYA;?Tfz zFsNOY%wBnuceIS$Im`Fj>SHG=QRJ!Cr};U>p3@gSkQ-x0N^yO@WrfOi8Fhg6m#I9} zqS=?8+hh66Bq}1-zI}u6v?6!Gq6}rfH0uOU+T#n{@9w!J?efF23Hus}?g2@FZ#9Pv zr_Fi%oq0bp-^vlB8N>BXsE*S)LIydtlYDTZnP^XWWkuIJ03BJXDr`O{vmJhF24*3? zPgN!S9WVAUNpZg)$3pinsh{CZp1NT>oi)XxdoSF>M_GHlb5!POtpnxlT1mrB^W`ZF zHRx-TAy9%NB50}XXH|1+bDlHoZsLUlEb=N5{8WK%f(PW~CoE7cUvujY!!Gio!lF&) zwf7=aS~lc}*NFi#zSL!_Gbm8s+ClJvH*FJ{&YQBaJK`<2V*;L_}Q zbel2-??=2e{yB`$=2l|kHkfS?jIAGYM}XBW#;03IgT=mt1l#jLT;nfO)Q_3k{A0WlrgQn3yten*7N$@_ZyiF6j(+ zJ>GMFT($EwJ5t^`;Q1nwa?_q~mU1-szEbE`7r6BA(%pS~!8UDj z9E#f4T)^h&&J;Xq@4mH+aoZG(ta0$IDIWn!e~A>bQUG8Yk6AL?^h2AD#tNcyC_Higcn`ueqp$1`>4$ zr08+ZAKvDqC||#?Pq}unR8-OOq@p#{KM{t1!Wu6iDV88g`*q;?`KMz-rN8$~!0k#A zDkfnH`=8T?Vx(#b84Ul5g08qF?jF&blG)U4m5ZRf tSdfPHYmt8Wmw7`%@@IDbZTY|bzr`*fxWu#0V) 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%0DWH`g#X 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 e@b%#6-F?vw~Abo+Z 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

)jpMnO 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%0D=k-=?1qPWba% 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 z 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

o8$gA0OlP^kzCCK3Xk)82YHseLSzG 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 literal 0 HcmV?d00001 From d52b7b1fc2471d4e926c2be37c83a9b4706aaaae Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Fri, 5 Dec 2025 01:43:01 +0100 Subject: [PATCH 002/358] 1.0.1 version bump. --- dependencies.gradle | 3 ++- ...> standalone-JaninoLoader-1.0.1-ALPHA.jar} | Bin 1129766 -> 1136160 bytes 2 files changed, 2 insertions(+), 1 deletion(-) rename tools/{standalone-JaninoLoader-1.0.0-ALPHA.jar => standalone-JaninoLoader-1.0.1-ALPHA.jar} (94%) diff --git a/dependencies.gradle b/dependencies.gradle index 3748832d6..5f8f40b1e 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -38,7 +38,8 @@ dependencies { runtimeOnlyNonPublishable("com.github.GTNewHorizons:NotEnoughItems:2.5.4-GTNH:dev") runtimeOnlyNonPublishable("curse.maven:Nashorn-374413:2924946") - shadowImplementation(project.files("tools/standalone-JaninoLoader-1.0.0-ALPHA.jar")) + shadowImplementation(project.files("tools/standalone-JaninoLoader-1.0.1-ALPHA.jar")) + annotationProcessor(project.files("tools/standalone-JaninoLoader-1.0.1-ALPHA.jar")) if(embedMixin){ // Normal Jar will include built in Mixins diff --git a/tools/standalone-JaninoLoader-1.0.0-ALPHA.jar b/tools/standalone-JaninoLoader-1.0.1-ALPHA.jar similarity index 94% rename from tools/standalone-JaninoLoader-1.0.0-ALPHA.jar rename to tools/standalone-JaninoLoader-1.0.1-ALPHA.jar index 6b1b77aec1fa8f5719f128bd0b529d1683b3b9fe..cc5b1c843c7c87e4d67b8b78f13ced80ce1b17b8 100644 GIT binary patch delta 22787 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 z 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 Hum$`-hwE)n From f8dc8d9268dfab3efd726a2fe21a1f9f204d5a96 Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Tue, 9 Dec 2025 08:11:34 +0100 Subject: [PATCH 003/358] Basic NPC Wrapper --- src/main/java/noppes/npcs/EventHooks.java | 41 ++++++++- .../npcs/controllers/ScriptController.java | 6 ++ .../npcs/entity/EntityNPCInterface.java | 2 + .../janino/EntityUnloadListener.java | 77 +++++++++++++++++ .../wrapper/AbstractNPCJavaHandler.java | 60 +++++++++++++ .../somehussar/janino/wrapper/NPCWrapper.java | 84 +++++++++++++++++++ 6 files changed, 267 insertions(+), 3 deletions(-) create mode 100644 src/main/java/somehussar/janino/EntityUnloadListener.java create mode 100644 src/main/java/somehussar/janino/wrapper/AbstractNPCJavaHandler.java create mode 100644 src/main/java/somehussar/janino/wrapper/NPCWrapper.java diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index 9cb5b1669..ebb3933a1 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -266,6 +266,8 @@ public class EventHooks { noppes.npcs.scripted.event.NpcEvent.InitEvent event = new noppes.npcs.scripted.event.NpcEvent.InitEvent(npc.wrappedNPC); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.INIT, event); npc.script.callScript(EnumScriptType.INIT, event); + if (npc.janinoHandler != null) + npc.janinoHandler.onInitialize(event); NpcAPI.EVENT_BUS.post(event); npc.advanced.soulStoneInit = false; } @@ -277,6 +279,8 @@ public class EventHooks { noppes.npcs.scripted.event.NpcEvent.UpdateEvent event = new noppes.npcs.scripted.event.NpcEvent.UpdateEvent(npc.wrappedNPC); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.TICK, event); npc.script.callScript(EnumScriptType.TICK, event); + if (npc.janinoHandler != null) + npc.janinoHandler.onUpdate(event); NpcAPI.EVENT_BUS.post(event); } @@ -287,6 +291,8 @@ public class EventHooks { NpcEvent.DialogEvent event = new NpcEvent.DialogEvent(npc.wrappedNPC, player, dialogId, optionId, dialog); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.DIALOG, event); npc.script.callScript(EnumScriptType.DIALOG, event, "player", event.getPlayer(), "dialog", event.getDialogId(), "option", event.getOptionId(), "dialogObj", event.getDialog()); + if (npc.janinoHandler != null) + npc.janinoHandler.onDialog(event); return NpcAPI.EVENT_BUS.post(event); } @@ -298,7 +304,8 @@ public class EventHooks { NpcEvent.DialogClosedEvent event = new NpcEvent.DialogClosedEvent(npc.wrappedNPC, player, dialogId, optionId, dialog); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.DIALOG_CLOSE, event); npc.script.callScript(EnumScriptType.DIALOG_CLOSE, event, "player", event.getPlayer(), "dialog", event.getDialogId(), "option", event.getOptionId(), "dialogObj", event.getDialog()); - + if (npc.janinoHandler != null) + npc.janinoHandler.onDialogClosed(event); NpcAPI.EVENT_BUS.post(event); } @@ -320,6 +327,8 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ATTACK_MELEE, event); npc.script.callScript(EnumScriptType.ATTACK_MELEE, event, "target", event.target); npc.script.callScript(EnumScriptType.ATTACK, event, "target", event.target); + if (npc.janinoHandler != null) + npc.janinoHandler.onAttack(event); return NpcAPI.EVENT_BUS.post(event); } @@ -329,6 +338,8 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ATTACK_SWING, event); npc.script.callScript(EnumScriptType.ATTACK_SWING, event); + if (npc.janinoHandler != null) + npc.janinoHandler.onMeleeSwing(event); return NpcAPI.EVENT_BUS.post(event); } @@ -339,6 +350,8 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.RANGED_LAUNCHED, event); npc.script.callScript(EnumScriptType.RANGED_LAUNCHED, event, "target", event.target); npc.script.callScript(EnumScriptType.ATTACK, event, "target", event.target); + if (npc.janinoHandler != null) + npc.janinoHandler.onRangedAttack(event); return NpcAPI.EVENT_BUS.post(event); } @@ -349,6 +362,8 @@ public class EventHooks { NpcEvent.KilledEntityEvent event = new NpcEvent.KilledEntityEvent(npc.wrappedNPC, entity); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.KILLS, event); npc.script.callScript(EnumScriptType.KILLS, event, "target", entity); + if (npc.janinoHandler != null) + npc.janinoHandler.onKillEntity(event); NpcAPI.EVENT_BUS.post(event); } @@ -358,6 +373,8 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.TARGET, event); npc.script.callScript(EnumScriptType.TARGET, event); + if (npc.janinoHandler != null) + npc.janinoHandler.onTarget(event); return NpcAPI.EVENT_BUS.post(event); } @@ -368,6 +385,8 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.TARGET_LOST, event); npc.script.callScript(EnumScriptType.TARGET_LOST, event); + if (npc.janinoHandler != null) + npc.janinoHandler.onTargetLost(event); return NpcAPI.EVENT_BUS.post(event); } @@ -378,6 +397,8 @@ public class EventHooks { NpcEvent.CollideEvent event = new NpcEvent.CollideEvent(npc.wrappedNPC, entity); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.COLLIDE, event); npc.script.callScript(EnumScriptType.COLLIDE, event, "entity", entity); + if (npc.janinoHandler != null) + npc.janinoHandler.onCollide(event); NpcAPI.EVENT_BUS.post(event); } @@ -387,6 +408,8 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.DAMAGED, event); npc.script.callScript(EnumScriptType.DAMAGED, event); + if (npc.janinoHandler != null) + npc.janinoHandler.onDamaged(event); return NpcAPI.EVENT_BUS.post(event); } @@ -396,6 +419,8 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.KILLED, event); npc.script.callScript(EnumScriptType.KILLED, event); + if (npc.janinoHandler != null) + npc.janinoHandler.onNPCKilled(event); return NpcAPI.EVENT_BUS.post(event); } @@ -406,6 +431,8 @@ public class EventHooks { NpcEvent.TimerEvent event = new NpcEvent.TimerEvent(npc.wrappedNPC, id); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.TIMER, event); npc.script.callScript(EnumScriptType.TIMER, event); + if (npc.janinoHandler != null) + npc.janinoHandler.onTimer(event); NpcAPI.EVENT_BUS.post(event); } @@ -421,7 +448,11 @@ public class EventHooks { EntityLivingBase thrower = projectile.getThrower(); if (thrower != null) { if (thrower instanceof EntityNPCInterface) { - ((EntityNPCInterface) thrower).script.callScript(EnumScriptType.PROJECTILE_TICK, event); + EntityNPCInterface npc = (EntityNPCInterface) thrower; + + npc.script.callScript(EnumScriptType.PROJECTILE_TICK, event); + if (npc.janinoHandler != null) + npc.janinoHandler.onProjectileTick(event); } } @@ -439,7 +470,11 @@ public class EventHooks { EntityLivingBase thrower = projectile.getThrower(); if (thrower != null) { if (thrower instanceof EntityNPCInterface) { - ((EntityNPCInterface) thrower).script.callScript(EnumScriptType.PROJECTILE_IMPACT, event); + EntityNPCInterface npc = (EntityNPCInterface) thrower; + + npc.script.callScript(EnumScriptType.PROJECTILE_IMPACT, event); + if (npc.janinoHandler != null) + npc.janinoHandler.onProjectileImpact(event); } } diff --git a/src/main/java/noppes/npcs/controllers/ScriptController.java b/src/main/java/noppes/npcs/controllers/ScriptController.java index d4246440b..6fb4406c2 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptController.java @@ -21,6 +21,7 @@ import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.scripted.ScriptWorld; import noppes.npcs.util.JsonException; import noppes.npcs.util.NBTJsonUtil; +import somehussar.janino.EntityUnloadListener; import javax.script.ScriptContext; import javax.script.ScriptEngine; @@ -361,6 +362,11 @@ public class ScriptController { return list; } + @SubscribeEvent + public void addWorldAccess(WorldEvent.Load event) { + event.world.addWorldAccess(new EntityUnloadListener()); + } + @SubscribeEvent public void invoke(WorldEvent.Save event) { if (!shouldSave || event.world.isRemote || event.world != MinecraftServer.getServer().worldServers[0]) diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index c72aa7117..ddb090142 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -157,6 +157,7 @@ import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.entity.ScriptNpc; import noppes.npcs.scripted.event.NpcEvent; import noppes.npcs.util.GameProfileAlt; +import somehussar.janino.wrapper.NPCWrapper; import java.io.IOException; import java.util.ArrayList; @@ -188,6 +189,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti public float baseHeight = 1.8f; public float scaleX, scaleY, scaleZ; + public NPCWrapper janinoHandler; private boolean wasKilled = false; public RoleInterface roleInterface; public JobInterface jobInterface; diff --git a/src/main/java/somehussar/janino/EntityUnloadListener.java b/src/main/java/somehussar/janino/EntityUnloadListener.java new file mode 100644 index 000000000..afcbec5b8 --- /dev/null +++ b/src/main/java/somehussar/janino/EntityUnloadListener.java @@ -0,0 +1,77 @@ +package somehussar.janino; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.world.IWorldAccess; +import noppes.npcs.entity.EntityNPCInterface; + +public class EntityUnloadListener implements IWorldAccess { + @Override + public void markBlockForUpdate(int p_147586_1_, int p_147586_2_, int p_147586_3_) { + + } + + @Override + public void markBlockForRenderUpdate(int p_147588_1_, int p_147588_2_, int p_147588_3_) { + + } + + @Override + public void markBlockRangeForRenderUpdate(int p_147585_1_, int p_147585_2_, int p_147585_3_, int p_147585_4_, int p_147585_5_, int p_147585_6_) { + + } + + @Override + public void playSound(String soundName, double x, double y, double z, float volume, float pitch) { + + } + + @Override + public void playSoundToNearExcept(EntityPlayer p_85102_1_, String p_85102_2_, double p_85102_3_, double p_85102_5_, double p_85102_7_, float p_85102_9_, float p_85102_10_) { + + } + + @Override + public void spawnParticle(String p_72708_1_, double p_72708_2_, double p_72708_4_, double p_72708_6_, double p_72708_8_, double p_72708_10_, double p_72708_12_) { + + } + + @Override + public void onEntityCreate(Entity p_72703_1_) { + + } + + @Override + public void onEntityDestroy(Entity entity) { + if (entity instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) entity; + if (npc.janinoHandler != null) + npc.janinoHandler.prepareToUnload(); + } + } + + @Override + public void playRecord(String p_72702_1_, int p_72702_2_, int p_72702_3_, int p_72702_4_) { + + } + + @Override + public void broadcastSound(int p_82746_1_, int p_82746_2_, int p_82746_3_, int p_82746_4_, int p_82746_5_) { + + } + + @Override + public void playAuxSFX(EntityPlayer p_72706_1_, int p_72706_2_, int p_72706_3_, int p_72706_4_, int p_72706_5_, int p_72706_6_) { + + } + + @Override + public void destroyBlockPartially(int p_147587_1_, int p_147587_2_, int p_147587_3_, int p_147587_4_, int p_147587_5_) { + + } + + @Override + public void onStaticEntitiesChanged() { + + } +} diff --git a/src/main/java/somehussar/janino/wrapper/AbstractNPCJavaHandler.java b/src/main/java/somehussar/janino/wrapper/AbstractNPCJavaHandler.java new file mode 100644 index 000000000..37441c070 --- /dev/null +++ b/src/main/java/somehussar/janino/wrapper/AbstractNPCJavaHandler.java @@ -0,0 +1,60 @@ +package somehussar.janino.wrapper; + +import io.github.somehussar.janinoloader.annotations.PermissableScriptHandler; +import noppes.npcs.api.event.INpcEvent; +import noppes.npcs.scripted.event.NpcEvent; +import noppes.npcs.scripted.event.ProjectileEvent; + +public abstract class AbstractNPCJavaHandler { + + public void prepareToUnload() { + } + + public void onInitialize(INpcEvent.InitEvent event) { + } + + public void onUpdate(NpcEvent.UpdateEvent event) { + } + + public void onDialog(NpcEvent.DialogEvent event) { + } + + public void onProjectileTick(ProjectileEvent.UpdateEvent event) { + } + + public void onDialogClosed(NpcEvent.DialogClosedEvent event) { + } + + public void onAttack(NpcEvent.MeleeAttackEvent event) { + } + + public void onRangedAttack(NpcEvent.RangedLaunchedEvent event) { + } + + public void onMeleeSwing(NpcEvent.SwingEvent event) { + } + + public void onKillEntity(NpcEvent.KilledEntityEvent event) { + } + + public void onTarget(NpcEvent.TargetEvent event) { + } + + public void onTargetLost(NpcEvent.TargetLostEvent event) { + } + + public void onCollide(NpcEvent.CollideEvent event) { + } + + public void onDamaged(NpcEvent.DamagedEvent event) { + } + + public void onNPCKilled(NpcEvent.DiedEvent event) { + } + + public void onTimer(NpcEvent.TimerEvent event) { + } + + public void onProjectileImpact(ProjectileEvent.ImpactEvent event) { + } +} diff --git a/src/main/java/somehussar/janino/wrapper/NPCWrapper.java b/src/main/java/somehussar/janino/wrapper/NPCWrapper.java new file mode 100644 index 000000000..710f89734 --- /dev/null +++ b/src/main/java/somehussar/janino/wrapper/NPCWrapper.java @@ -0,0 +1,84 @@ +package somehussar.janino.wrapper; + +import io.github.somehussar.janinoloader.api.script.IScriptClassBody; +import org.codehaus.commons.compiler.Sandbox; + +public final class NPCWrapper { + + private final Sandbox sandbox; + private final IScriptClassBody scriptBody; + + public NPCWrapper(Sandbox sandbox, IScriptClassBody scriptBody) { + this.sandbox = sandbox; + this.scriptBody = scriptBody; + } + + public void prepareToUnload() { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().prepareToUnload(); return null; }); + } + + public void onInitialize(noppes.npcs.api.event.INpcEvent.InitEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onInitialize(event); return null; }); + } + + public void onUpdate(noppes.npcs.scripted.event.NpcEvent.UpdateEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onUpdate(event); return null; }); + } + + public void onDialog(noppes.npcs.scripted.event.NpcEvent.DialogEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onDialog(event); return null; }); + } + + public void onProjectileTick(noppes.npcs.scripted.event.ProjectileEvent.UpdateEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onProjectileTick(event); return null; }); + } + + public void onDialogClosed(noppes.npcs.scripted.event.NpcEvent.DialogClosedEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onDialogClosed(event); return null; }); + } + + public void onAttack(noppes.npcs.scripted.event.NpcEvent.MeleeAttackEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onAttack(event); return null; }); + } + + public void onRangedAttack(noppes.npcs.scripted.event.NpcEvent.RangedLaunchedEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onRangedAttack(event); return null; }); + } + + public void onMeleeSwing(noppes.npcs.scripted.event.NpcEvent.SwingEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onMeleeSwing(event); return null; }); + } + + public void onKillEntity(noppes.npcs.scripted.event.NpcEvent.KilledEntityEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onKillEntity(event); return null; }); + } + + public void onTarget(noppes.npcs.scripted.event.NpcEvent.TargetEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onTarget(event); return null; }); + } + + public void onTargetLost(noppes.npcs.scripted.event.NpcEvent.TargetLostEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onTargetLost(event); return null; }); + } + + public void onCollide(noppes.npcs.scripted.event.NpcEvent.CollideEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onCollide(event); return null; }); + } + + public void onDamaged(noppes.npcs.scripted.event.NpcEvent.DamagedEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onDamaged(event); return null; }); + } + + public void onNPCKilled(noppes.npcs.scripted.event.NpcEvent.DiedEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onNPCKilled(event); return null; }); + } + + public void onTimer(noppes.npcs.scripted.event.NpcEvent.TimerEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onTimer(event); return null; }); + } + + public void onProjectileImpact(noppes.npcs.scripted.event.ProjectileEvent.ImpactEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onProjectileImpact(event); return null; }); + } + +} From b4608ffe19672886424e752a381d7043e63940da Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Tue, 9 Dec 2025 08:15:12 +0100 Subject: [PATCH 004/358] Null out the loader anyway. --- src/main/java/somehussar/janino/EntityUnloadListener.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/somehussar/janino/EntityUnloadListener.java b/src/main/java/somehussar/janino/EntityUnloadListener.java index afcbec5b8..8e7d917bb 100644 --- a/src/main/java/somehussar/janino/EntityUnloadListener.java +++ b/src/main/java/somehussar/janino/EntityUnloadListener.java @@ -47,6 +47,7 @@ public class EntityUnloadListener implements IWorldAccess { EntityNPCInterface npc = (EntityNPCInterface) entity; if (npc.janinoHandler != null) npc.janinoHandler.prepareToUnload(); + npc.janinoHandler = null; } } From 4d825b6cf3be1920c9189e41db58175d8c797aef Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Sat, 13 Dec 2025 18:21:36 +0100 Subject: [PATCH 005/358] Null out the loader. --- src/main/java/noppes/npcs/CustomNpcs.java | 12 ++++++++ src/main/java/noppes/npcs/EventHooks.java | 2 ++ .../npcs/entity/EntityNPCInterface.java | 19 ++++++++++++ .../noppes/npcs/items/ItemNpcScripter.java | 1 + .../wrapper/AbstractNPCJavaHandler.java | 30 ++++++++++--------- .../somehussar/janino/wrapper/NPCWrapper.java | 6 +++- 6 files changed, 55 insertions(+), 15 deletions(-) diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 3cfcd18e4..482093705 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -14,6 +14,8 @@ import cpw.mods.fml.common.network.NetworkRegistry; import cpw.mods.fml.common.registry.EntityRegistry; import cpw.mods.fml.relauncher.Side; import foxz.command.CommandNoppes; +import io.github.somehussar.janinoloader.api.IDynamicCompiler; +import io.github.somehussar.janinoloader.api.IDynamicCompilerBuilder; import kamkeel.npcs.addon.AddonManager; import kamkeel.npcs.command.CommandKamkeel; import kamkeel.npcs.command.profile.CommandProfile; @@ -140,6 +142,16 @@ public class CustomNpcs { instance = this; } + private static IDynamicCompiler jls; + + public static IDynamicCompiler getDynamicCompiler() { + if (jls == null) { + jls = IDynamicCompilerBuilder.createBuilder().getCompiler(); + } + + return jls; + } + @EventHandler public void load(FMLPreInitializationEvent ev) { PacketHandler.Instance = new PacketHandler(); diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index ebb3933a1..4f6d2b909 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -316,6 +316,8 @@ public class EventHooks { NpcEvent.InteractEvent event = new NpcEvent.InteractEvent(npc.wrappedNPC, player); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.INTERACT, event); boolean result = npc.script.callScript(EnumScriptType.INTERACT, event, "player", player); + if (npc.janinoHandler != null) + npc.janinoHandler.onInteract(event); NpcAPI.EVENT_BUS.post(event); return result; } diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index ddb090142..10bb8c26c 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -4,6 +4,8 @@ import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import io.github.somehussar.janinoloader.api.script.IScriptBodyBuilder; +import io.github.somehussar.janinoloader.api.script.IScriptClassBody; import io.netty.buffer.ByteBuf; import kamkeel.npcs.addon.DBCAddon; import kamkeel.npcs.addon.client.DBCClient; @@ -157,9 +159,12 @@ import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.entity.ScriptNpc; import noppes.npcs.scripted.event.NpcEvent; import noppes.npcs.util.GameProfileAlt; +import org.codehaus.commons.compiler.Sandbox; +import somehussar.janino.wrapper.AbstractNPCJavaHandler; import somehussar.janino.wrapper.NPCWrapper; import java.io.IOException; +import java.security.Permissions; import java.util.ArrayList; import java.util.ConcurrentModificationException; import java.util.HashMap; @@ -247,6 +252,20 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti if (!this.isRemote() && this.wrappedNPC == null) { this.wrappedNPC = new ScriptNpc<>(this); } + if (!this.isRemote()) { + IScriptClassBody javaCompiler = IScriptBodyBuilder.getBuilder(AbstractNPCJavaHandler.class, CustomNpcs.getDynamicCompiler()) + .setDefaultImports("noppes.npcs.api.event.INpcEvent").build(); + javaCompiler.setScript("" + + "" + + "void onInteract(INpcEvent.InteractEvent event) {" + + " event.getNpc().say(\"Lol this is from a script!\");" + + "}" + + ""); + + Permissions permissions = new Permissions(); + Sandbox sandbox = new Sandbox(permissions); + this.janinoHandler = new NPCWrapper(sandbox, javaCompiler); + } } catch (Exception e) { e.printStackTrace(); } diff --git a/src/main/java/noppes/npcs/items/ItemNpcScripter.java b/src/main/java/noppes/npcs/items/ItemNpcScripter.java index 5c5734140..c3a88def0 100644 --- a/src/main/java/noppes/npcs/items/ItemNpcScripter.java +++ b/src/main/java/noppes/npcs/items/ItemNpcScripter.java @@ -41,6 +41,7 @@ public class ItemNpcScripter extends Item { } return itemStack; } + CustomNpcs.proxy.openGui(0, 0, 0, EnumGuiType.ScriptEvent, player); return itemStack; } diff --git a/src/main/java/somehussar/janino/wrapper/AbstractNPCJavaHandler.java b/src/main/java/somehussar/janino/wrapper/AbstractNPCJavaHandler.java index 37441c070..058219d48 100644 --- a/src/main/java/somehussar/janino/wrapper/AbstractNPCJavaHandler.java +++ b/src/main/java/somehussar/janino/wrapper/AbstractNPCJavaHandler.java @@ -2,7 +2,6 @@ package somehussar.janino.wrapper; import io.github.somehussar.janinoloader.annotations.PermissableScriptHandler; import noppes.npcs.api.event.INpcEvent; -import noppes.npcs.scripted.event.NpcEvent; import noppes.npcs.scripted.event.ProjectileEvent; public abstract class AbstractNPCJavaHandler { @@ -13,48 +12,51 @@ public abstract class AbstractNPCJavaHandler { public void onInitialize(INpcEvent.InitEvent event) { } - public void onUpdate(NpcEvent.UpdateEvent event) { + public void onUpdate(INpcEvent.UpdateEvent event) { } - public void onDialog(NpcEvent.DialogEvent event) { + public void onDialog(INpcEvent.DialogEvent event) { } public void onProjectileTick(ProjectileEvent.UpdateEvent event) { } - public void onDialogClosed(NpcEvent.DialogClosedEvent event) { + public void onDialogClosed(INpcEvent.DialogClosedEvent event) { } - public void onAttack(NpcEvent.MeleeAttackEvent event) { + public void onAttack(INpcEvent.MeleeAttackEvent event) { } - public void onRangedAttack(NpcEvent.RangedLaunchedEvent event) { + public void onRangedAttack(INpcEvent.RangedLaunchedEvent event) { } - public void onMeleeSwing(NpcEvent.SwingEvent event) { + public void onMeleeSwing(INpcEvent.SwingEvent event) { } - public void onKillEntity(NpcEvent.KilledEntityEvent event) { + public void onKillEntity(INpcEvent.KilledEntityEvent event) { } - public void onTarget(NpcEvent.TargetEvent event) { + public void onTarget(INpcEvent.TargetEvent event) { } - public void onTargetLost(NpcEvent.TargetLostEvent event) { + public void onTargetLost(INpcEvent.TargetLostEvent event) { } - public void onCollide(NpcEvent.CollideEvent event) { + public void onCollide(INpcEvent.CollideEvent event) { } - public void onDamaged(NpcEvent.DamagedEvent event) { + public void onDamaged(INpcEvent.DamagedEvent event) { } - public void onNPCKilled(NpcEvent.DiedEvent event) { + public void onNPCKilled(INpcEvent.DiedEvent event) { } - public void onTimer(NpcEvent.TimerEvent event) { + public void onTimer(INpcEvent.TimerEvent event) { } public void onProjectileImpact(ProjectileEvent.ImpactEvent event) { } + + public void onInteract(INpcEvent.InteractEvent event) { + } } diff --git a/src/main/java/somehussar/janino/wrapper/NPCWrapper.java b/src/main/java/somehussar/janino/wrapper/NPCWrapper.java index 710f89734..6158e2279 100644 --- a/src/main/java/somehussar/janino/wrapper/NPCWrapper.java +++ b/src/main/java/somehussar/janino/wrapper/NPCWrapper.java @@ -1,12 +1,13 @@ package somehussar.janino.wrapper; import io.github.somehussar.janinoloader.api.script.IScriptClassBody; +import noppes.npcs.scripted.event.NpcEvent; import org.codehaus.commons.compiler.Sandbox; public final class NPCWrapper { private final Sandbox sandbox; - private final IScriptClassBody scriptBody; + public final IScriptClassBody scriptBody; public NPCWrapper(Sandbox sandbox, IScriptClassBody scriptBody) { this.sandbox = sandbox; @@ -81,4 +82,7 @@ public final class NPCWrapper { sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onProjectileImpact(event); return null; }); } + public void onInteract(NpcEvent.InteractEvent event) { + sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onInteract(event); return null; }); + } } From 18ebc607b78fa6a8984eded58cac5ec6d4dd7a48 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 14 Dec 2025 21:41:21 +0200 Subject: [PATCH 006/358] Made ScriptController.languages final --- src/main/java/noppes/npcs/controllers/ScriptController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/controllers/ScriptController.java b/src/main/java/noppes/npcs/controllers/ScriptController.java index 6fb4406c2..c9351083e 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptController.java @@ -46,7 +46,7 @@ public class ScriptController { public static boolean HasStart = false; private final ScriptEngineManager manager; private ScriptEngineFactory nashornFactory; - public Map languages = new HashMap(); + public final Map languages = new HashMap(); public Map scripts = new HashMap(); public long lastLoaded = 0; public File dir; From 818c62bdf91945073b2dd7778705c21c0f953d04 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 14 Dec 2025 22:15:20 +0200 Subject: [PATCH 007/358] Added ScriptFilesPacket, which fetches all files of a given language --- .../kamkeel/npcs/network/PacketHandler.java | 11 +-- .../npcs/network/enums/EnumRequestPacket.java | 1 + .../request/script/ScriptFilesPacket.java | 81 +++++++++++++++++++ .../npcs/controllers/ScriptController.java | 2 +- 4 files changed, 85 insertions(+), 10 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 4dcf50bac..85d5c3f43 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -209,15 +209,7 @@ import kamkeel.npcs.network.packets.request.recipe.RecipesGetPacket; import kamkeel.npcs.network.packets.request.role.RoleCompanionUpdatePacket; import kamkeel.npcs.network.packets.request.role.RoleGetPacket; import kamkeel.npcs.network.packets.request.role.RoleSavePacket; -import kamkeel.npcs.network.packets.request.script.BlockScriptPacket; -import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; -import kamkeel.npcs.network.packets.request.script.EventScriptPacket; -import kamkeel.npcs.network.packets.request.script.ForgeScriptPacket; -import kamkeel.npcs.network.packets.request.script.GlobalNPCScriptPacket; -import kamkeel.npcs.network.packets.request.script.NPCScriptPacket; -import kamkeel.npcs.network.packets.request.script.PlayerScriptPacket; -import kamkeel.npcs.network.packets.request.script.RecipeScriptPacket; -import kamkeel.npcs.network.packets.request.script.ScriptInfoPacket; +import kamkeel.npcs.network.packets.request.script.*; import kamkeel.npcs.network.packets.request.script.item.ItemScriptErrorPacket; import kamkeel.npcs.network.packets.request.script.item.ItemScriptPacket; import kamkeel.npcs.network.packets.request.script.item.LinkedItemScriptPacket; @@ -297,6 +289,7 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new NPCScriptPacket()); REQUEST_PACKET.registerPacket(new PlayerScriptPacket()); REQUEST_PACKET.registerPacket(new ScriptInfoPacket()); + REQUEST_PACKET.registerPacket(new ScriptFilesPacket()); REQUEST_PACKET.registerPacket(new EffectScriptPacket()); // Cloner Packets diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index 0159fe719..0151734d3 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -172,6 +172,7 @@ public enum EnumRequestPacket { BlockScript, NPCScript, ScriptInfo, + ScriptFiles, EffectScript, DimensionTeleport, diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java new file mode 100644 index 000000000..7cea2467e --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java @@ -0,0 +1,81 @@ +package kamkeel.npcs.network.packets.request.script; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.*; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.NBTTagString; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.config.ConfigScript; +import noppes.npcs.controllers.ScriptController; + +import java.io.IOException; +import java.util.List; + +public final class ScriptFilesPacket extends AbstractPacket { + public static String packetName = "Request|ScriptLanguages"; + + private String lang; + + public ScriptFilesPacket() { + } + + public ScriptFilesPacket(String lang) { + this.lang = lang; + } + + @Override + public Enum getType() { + return EnumRequestPacket.ScriptFiles; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @Override + public CustomNpcsPermissions.Permission getPermission() { + return CustomNpcsPermissions.SCRIPT_GLOBAL; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, this.lang); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!ConfigScript.canScript(player, CustomNpcsPermissions.SCRIPT)) + return; + + lang = ByteBufUtils.readString(in); + + NBTTagCompound compound = new NBTTagCompound(); + List files = ScriptController.Instance.getScripts(lang); + + if (!files.isEmpty()) { + NBTTagList scripts = new NBTTagList(); + for (String script : files) + scripts.appendTag(new NBTTagString(script)); + compound.setTag("Scripts", scripts); + } + + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } + + public static void Get(String lang) { + PacketClient.sendClient(new ScriptFilesPacket(lang)); + } +} diff --git a/src/main/java/noppes/npcs/controllers/ScriptController.java b/src/main/java/noppes/npcs/controllers/ScriptController.java index c9351083e..ca6cce586 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptController.java @@ -349,7 +349,7 @@ public class ScriptController { return list; } - private List getScripts(String language) { + public List getScripts(String language) { List list = new ArrayList(); String ext = languages.get(language); if (ext == null) From 8a2ba6ae4eb85863a3be558d5fde877d6aaded34 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 14 Dec 2025 22:15:56 +0200 Subject: [PATCH 008/358] Added new Script language "Java" with extension type ".java" --- src/main/java/noppes/npcs/controllers/ScriptController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/noppes/npcs/controllers/ScriptController.java b/src/main/java/noppes/npcs/controllers/ScriptController.java index ca6cce586..210f71a76 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptController.java @@ -98,6 +98,7 @@ public class ScriptController { } catch (Exception ignored) { } } + languages.put("Java", ".java"); } private File forgeScriptsFile() { From a8b3c0b8d48a8ad7e286e5fc472c710cf436b3d8 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 15 Dec 2025 10:28:17 +0200 Subject: [PATCH 009/358] All external scripts of type "Java" are now synced to client on login/script reload --- .../kamkeel/npcs/command/ScriptCommand.java | 1 + .../request/script/ScriptFilesPacket.java | 58 ++++++++++++------- .../java/noppes/npcs/ServerTickHandler.java | 7 +-- .../npcs/controllers/ScriptController.java | 10 ++++ 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/src/main/java/kamkeel/npcs/command/ScriptCommand.java b/src/main/java/kamkeel/npcs/command/ScriptCommand.java index 41415cb81..61b380dbd 100644 --- a/src/main/java/kamkeel/npcs/command/ScriptCommand.java +++ b/src/main/java/kamkeel/npcs/command/ScriptCommand.java @@ -12,6 +12,7 @@ public class ScriptCommand extends CommandKamkeelBase { @SubCommand(desc = "Reload scripts data and folders.") public Boolean reload(ICommandSender sender, String args[]) { ScriptController.Instance.loadCategories(); + ScriptController.Instance.syncClientScripts(null); if (ScriptController.Instance.loadPlayerScripts()) sendResult(sender, "Reload player scripts successfully"); diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java index 7cea2467e..231eeecac 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java @@ -5,22 +5,20 @@ import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; import kamkeel.npcs.network.*; import kamkeel.npcs.network.enums.EnumRequestPacket; -import kamkeel.npcs.network.packets.data.large.GuiDataPacket; import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.nbt.NBTTagString; import noppes.npcs.CustomNpcsPermissions; -import noppes.npcs.config.ConfigScript; +import noppes.npcs.NBTTags; import noppes.npcs.controllers.ScriptController; import java.io.IOException; -import java.util.List; +import java.util.HashMap; +import java.util.Map; public final class ScriptFilesPacket extends AbstractPacket { - public static String packetName = "Request|ScriptLanguages"; + public static String packetName = "Request|ScriptFiles"; private String lang; @@ -38,7 +36,7 @@ public final class ScriptFilesPacket extends AbstractPacket { @Override public PacketChannel getChannel() { - return PacketHandler.REQUEST_PACKET; + return PacketHandler.DATA_PACKET; } @Override @@ -49,33 +47,49 @@ public final class ScriptFilesPacket extends AbstractPacket { @SideOnly(Side.CLIENT) @Override public void sendData(ByteBuf out) throws IOException { - ByteBufUtils.writeString(out, this.lang); + ByteBufUtils.writeNBT(out, getScriptsNbt(lang)); } @Override public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { - if (!(player instanceof EntityPlayerMP)) - return; + NBTTagCompound compound = ByteBufUtils.readNBT(in); - if (!ConfigScript.canScript(player, CustomNpcsPermissions.SCRIPT)) - return; + String lang = compound.getString("lang"); + String ext = compound.getString("ext"); + Map scripts = NBTTags.getStringStringMap(compound.getTagList("Scripts", 10)); - lang = ByteBufUtils.readString(in); + ScriptController cont = ScriptController.Instance; + if (!cont.languages.containsKey(lang) && ext != null) + cont.languages.put(lang, ext); + for (Map.Entry script : scripts.entrySet()) { + cont.scripts.put(script.getKey(), script.getValue()); + } + } + + public NBTTagCompound getScriptsNbt(String lang) { NBTTagCompound compound = new NBTTagCompound(); - List files = ScriptController.Instance.getScripts(lang); + compound.setString("lang", lang); - if (!files.isEmpty()) { - NBTTagList scripts = new NBTTagList(); - for (String script : files) - scripts.appendTag(new NBTTagString(script)); - compound.setTag("Scripts", scripts); + Map scriptss = new HashMap<>(); + String ext = ScriptController.Instance.languages.get(lang); + if (ext != null) { + for (Map.Entry script : ScriptController.Instance.scripts.entrySet()) { + if (script.getKey().endsWith(ext)) + scriptss.put(script.getKey(), script.getValue()); + } + compound.setString("ext", ext); } - GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + compound.setTag("Scripts", NBTTags.nbtStringStringMap(scriptss)); + return compound; } - public static void Get(String lang) { - PacketClient.sendClient(new ScriptFilesPacket(lang)); + public static void sendToAll(String lang) { + PacketHandler.Instance.sendToAll(new ScriptFilesPacket(lang)); + } + + public static void sendToPlayer(EntityPlayerMP player, String lang) { + PacketHandler.Instance.sendToPlayer(new ScriptFilesPacket(lang), player); } } diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index 62f53399b..9cbc331e2 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -7,15 +7,11 @@ import cpw.mods.fml.common.gameevent.TickEvent.Phase; import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.SyncController; import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.server.MinecraftServer; import net.minecraft.world.WorldServer; -import noppes.npcs.client.AnalyticsTracking; +import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.controllers.data.action.ActionManager; -import java.net.InetAddress; -import java.net.UnknownHostException; - public class ServerTickHandler { @SubscribeEvent @@ -64,6 +60,7 @@ public class ServerTickHandler { ProfileController.Instance.login(player); SyncController.beginLogin(player); SyncController.syncEffects(player); + ScriptController.Instance.syncClientScripts(player); } @SubscribeEvent diff --git a/src/main/java/noppes/npcs/controllers/ScriptController.java b/src/main/java/noppes/npcs/controllers/ScriptController.java index 210f71a76..b0787a282 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptController.java @@ -3,7 +3,9 @@ package noppes.npcs.controllers; import cpw.mods.fml.common.eventhandler.SubscribeEvent; import jdk.nashorn.api.scripting.ClassFilter; import jdk.nashorn.api.scripting.NashornScriptEngineFactory; +import kamkeel.npcs.network.packets.request.script.ScriptFilesPacket; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.nbt.NBTTagString; @@ -230,6 +232,14 @@ public class ScriptController { customNPCThread.execute(this::saveGlobalNpcScripts); } + public void syncClientScripts(EntityPlayerMP player) { + if (player != null) { + ScriptFilesPacket.sendToPlayer(player, "Java"); + } else { + ScriptFilesPacket.sendToAll("Java"); + } + } + public void loadCategories() { dir = new File(CustomNpcs.getWorldSaveDirectory(), "scripts"); if (!dir.exists()) From 4128bbff8bd63d1ce263236a41134af3efe7f94b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 15 Dec 2025 12:37:17 +0200 Subject: [PATCH 010/358] Called save() in SubGuiInterface.close --- src/main/java/noppes/npcs/client/gui/util/SubGuiInterface.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/SubGuiInterface.java b/src/main/java/noppes/npcs/client/gui/util/SubGuiInterface.java index 8b35c65dc..b10ba84bd 100644 --- a/src/main/java/noppes/npcs/client/gui/util/SubGuiInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/SubGuiInterface.java @@ -12,6 +12,8 @@ public class SubGuiInterface extends GuiNPCInterface { @Override public void close() { + save(); + if (parent instanceof ISubGuiListener) ((ISubGuiListener) parent).subGuiClosed(this); From 41722a37d75eda25f0f700a708954ec67d75af1b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 15 Dec 2025 12:38:27 +0200 Subject: [PATCH 011/358] Sended necessary Scripting configs to client --- .../packets/request/script/ScriptFilesPacket.java | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java index 231eeecac..f64550b65 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java @@ -11,6 +11,7 @@ import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcsPermissions; import noppes.npcs.NBTTags; +import noppes.npcs.config.ConfigScript; import noppes.npcs.controllers.ScriptController; import java.io.IOException; @@ -54,8 +55,8 @@ public final class ScriptFilesPacket extends AbstractPacket { public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { NBTTagCompound compound = ByteBufUtils.readNBT(in); - String lang = compound.getString("lang"); - String ext = compound.getString("ext"); + String lang = compound.getString("Lang"); + String ext = compound.getString("Ext"); Map scripts = NBTTags.getStringStringMap(compound.getTagList("Scripts", 10)); ScriptController cont = ScriptController.Instance; @@ -65,11 +66,14 @@ public final class ScriptFilesPacket extends AbstractPacket { for (Map.Entry script : scripts.entrySet()) { cont.scripts.put(script.getKey(), script.getValue()); } + + ConfigScript.ScriptingEnabled = compound.getBoolean("ScriptingEnabled"); + ConfigScript.RunLoadedScriptsFirst = compound.getBoolean("LoadedFirst"); } public NBTTagCompound getScriptsNbt(String lang) { NBTTagCompound compound = new NBTTagCompound(); - compound.setString("lang", lang); + compound.setString("Lang", lang); Map scriptss = new HashMap<>(); String ext = ScriptController.Instance.languages.get(lang); @@ -78,9 +82,12 @@ public final class ScriptFilesPacket extends AbstractPacket { if (script.getKey().endsWith(ext)) scriptss.put(script.getKey(), script.getValue()); } - compound.setString("ext", ext); + compound.setString("Ext", ext); } + //Send server configs to client + compound.setBoolean("ScriptingEnabled", ConfigScript.ScriptingEnabled); + compound.setBoolean("LoadedFirst", ConfigScript.RunLoadedScriptsFirst); compound.setTag("Scripts", NBTTags.nbtStringStringMap(scriptss)); return compound; } From a9bfa6c1209b5b5b12ae53559b04f3770e843928 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 15 Dec 2025 20:05:40 +0200 Subject: [PATCH 012/358] ScriptController.globalRevision, Incremented whenever external scripts loaded. --- .../network/packets/request/script/ScriptFilesPacket.java | 3 +++ .../java/noppes/npcs/controllers/ScriptController.java | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java index f64550b65..74bf8bd5e 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java @@ -67,8 +67,10 @@ public final class ScriptFilesPacket extends AbstractPacket { cont.scripts.put(script.getKey(), script.getValue()); } + //Set client side configs ConfigScript.ScriptingEnabled = compound.getBoolean("ScriptingEnabled"); ConfigScript.RunLoadedScriptsFirst = compound.getBoolean("LoadedFirst"); + ScriptController.Instance.globalRevision = compound.getInteger("GlobalRevision"); } public NBTTagCompound getScriptsNbt(String lang) { @@ -88,6 +90,7 @@ public final class ScriptFilesPacket extends AbstractPacket { //Send server configs to client compound.setBoolean("ScriptingEnabled", ConfigScript.ScriptingEnabled); compound.setBoolean("LoadedFirst", ConfigScript.RunLoadedScriptsFirst); + compound.setInteger("GlobalRevision", ScriptController.Instance.globalRevision); compound.setTag("Scripts", NBTTags.nbtStringStringMap(scriptss)); return compound; } diff --git a/src/main/java/noppes/npcs/controllers/ScriptController.java b/src/main/java/noppes/npcs/controllers/ScriptController.java index b0787a282..b5d21563c 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptController.java @@ -64,6 +64,12 @@ public class ScriptController { public GlobalNPCDataScript globalNpcScripts = new GlobalNPCDataScript((EntityNPCInterface) null); public long lastGlobalNpcUpdate = 0L; + /** + * Incremented whenever external scripts loaded. + */ + public int globalRevision; + + public ScriptController() { Instance = this; manager = new ScriptEngineManager(); @@ -257,6 +263,7 @@ public class ScriptController { loadDir(scriptDir, "", ext); } lastLoaded = System.currentTimeMillis(); + globalRevision++; } private void loadDir(File dir, String name, String ext) { From 24733892a0dae429eece24107d442768736a3e5b Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Mon, 15 Dec 2025 21:07:19 +0100 Subject: [PATCH 013/358] Remove my shitty janino-loader --- src/main/java/noppes/npcs/EventHooks.java | 66 ++++---------- .../npcs/entity/EntityNPCInterface.java | 21 ----- .../janino/EntityUnloadListener.java | 4 +- .../wrapper/AbstractNPCJavaHandler.java | 62 ------------- .../somehussar/janino/wrapper/NPCWrapper.java | 88 ------------------- 5 files changed, 17 insertions(+), 224 deletions(-) delete mode 100644 src/main/java/somehussar/janino/wrapper/AbstractNPCJavaHandler.java delete mode 100644 src/main/java/somehussar/janino/wrapper/NPCWrapper.java diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index 4f6d2b909..0dfac34ff 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -266,9 +266,7 @@ public class EventHooks { noppes.npcs.scripted.event.NpcEvent.InitEvent event = new noppes.npcs.scripted.event.NpcEvent.InitEvent(npc.wrappedNPC); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.INIT, event); npc.script.callScript(EnumScriptType.INIT, event); - if (npc.janinoHandler != null) - npc.janinoHandler.onInitialize(event); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); npc.advanced.soulStoneInit = false; } @@ -279,9 +277,7 @@ public class EventHooks { noppes.npcs.scripted.event.NpcEvent.UpdateEvent event = new noppes.npcs.scripted.event.NpcEvent.UpdateEvent(npc.wrappedNPC); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.TICK, event); npc.script.callScript(EnumScriptType.TICK, event); - if (npc.janinoHandler != null) - npc.janinoHandler.onUpdate(event); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCDialog(EntityNPCInterface npc, EntityPlayer player, int dialogId, int optionId, Dialog dialog) { @@ -291,8 +287,6 @@ public class EventHooks { NpcEvent.DialogEvent event = new NpcEvent.DialogEvent(npc.wrappedNPC, player, dialogId, optionId, dialog); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.DIALOG, event); npc.script.callScript(EnumScriptType.DIALOG, event, "player", event.getPlayer(), "dialog", event.getDialogId(), "option", event.getOptionId(), "dialogObj", event.getDialog()); - if (npc.janinoHandler != null) - npc.janinoHandler.onDialog(event); return NpcAPI.EVENT_BUS.post(event); } @@ -304,9 +298,7 @@ public class EventHooks { NpcEvent.DialogClosedEvent event = new NpcEvent.DialogClosedEvent(npc.wrappedNPC, player, dialogId, optionId, dialog); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.DIALOG_CLOSE, event); npc.script.callScript(EnumScriptType.DIALOG_CLOSE, event, "player", event.getPlayer(), "dialog", event.getDialogId(), "option", event.getOptionId(), "dialogObj", event.getDialog()); - if (npc.janinoHandler != null) - npc.janinoHandler.onDialogClosed(event); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCInteract(EntityNPCInterface npc, EntityPlayer player) { @@ -316,9 +308,7 @@ public class EventHooks { NpcEvent.InteractEvent event = new NpcEvent.InteractEvent(npc.wrappedNPC, player); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.INTERACT, event); boolean result = npc.script.callScript(EnumScriptType.INTERACT, event, "player", player); - if (npc.janinoHandler != null) - npc.janinoHandler.onInteract(event); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); return result; } @@ -329,9 +319,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ATTACK_MELEE, event); npc.script.callScript(EnumScriptType.ATTACK_MELEE, event, "target", event.target); npc.script.callScript(EnumScriptType.ATTACK, event, "target", event.target); - if (npc.janinoHandler != null) - npc.janinoHandler.onAttack(event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCMeleeSwing(EntityNPCInterface npc, NpcEvent.SwingEvent event) { @@ -340,9 +328,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ATTACK_SWING, event); npc.script.callScript(EnumScriptType.ATTACK_SWING, event); - if (npc.janinoHandler != null) - npc.janinoHandler.onMeleeSwing(event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCRangedAttack(EntityNPCInterface npc, NpcEvent.RangedLaunchedEvent event) { @@ -352,9 +338,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.RANGED_LAUNCHED, event); npc.script.callScript(EnumScriptType.RANGED_LAUNCHED, event, "target", event.target); npc.script.callScript(EnumScriptType.ATTACK, event, "target", event.target); - if (npc.janinoHandler != null) - npc.janinoHandler.onRangedAttack(event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static void onNPCKilledEntity(EntityNPCInterface npc, EntityLivingBase entity) { @@ -364,9 +348,7 @@ public class EventHooks { NpcEvent.KilledEntityEvent event = new NpcEvent.KilledEntityEvent(npc.wrappedNPC, entity); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.KILLS, event); npc.script.callScript(EnumScriptType.KILLS, event, "target", entity); - if (npc.janinoHandler != null) - npc.janinoHandler.onKillEntity(event); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCTarget(EntityNPCInterface npc, NpcEvent.TargetEvent event) { @@ -375,9 +357,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.TARGET, event); npc.script.callScript(EnumScriptType.TARGET, event); - if (npc.janinoHandler != null) - npc.janinoHandler.onTarget(event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCTargetLost(EntityNPCInterface npc, EntityLivingBase prevtarget, EntityLivingBase newTarget) { @@ -387,9 +367,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.TARGET_LOST, event); npc.script.callScript(EnumScriptType.TARGET_LOST, event); - if (npc.janinoHandler != null) - npc.janinoHandler.onTargetLost(event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static void onNPCCollide(EntityNPCInterface npc, Entity entity) { @@ -399,9 +377,7 @@ public class EventHooks { NpcEvent.CollideEvent event = new NpcEvent.CollideEvent(npc.wrappedNPC, entity); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.COLLIDE, event); npc.script.callScript(EnumScriptType.COLLIDE, event, "entity", entity); - if (npc.janinoHandler != null) - npc.janinoHandler.onCollide(event); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCDamaged(EntityNPCInterface npc, NpcEvent.DamagedEvent event) { @@ -410,9 +386,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.DAMAGED, event); npc.script.callScript(EnumScriptType.DAMAGED, event); - if (npc.janinoHandler != null) - npc.janinoHandler.onDamaged(event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCKilled(EntityNPCInterface npc, NpcEvent.DiedEvent event) { @@ -421,9 +395,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.KILLED, event); npc.script.callScript(EnumScriptType.KILLED, event); - if (npc.janinoHandler != null) - npc.janinoHandler.onNPCKilled(event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static void onNPCTimer(EntityNPCInterface npc, int id) { @@ -433,9 +405,7 @@ public class EventHooks { NpcEvent.TimerEvent event = new NpcEvent.TimerEvent(npc.wrappedNPC, id); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.TIMER, event); npc.script.callScript(EnumScriptType.TIMER, event); - if (npc.janinoHandler != null) - npc.janinoHandler.onTimer(event); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); } public static void onProjectileTick(EntityProjectile projectile) { @@ -453,9 +423,7 @@ public class EventHooks { EntityNPCInterface npc = (EntityNPCInterface) thrower; npc.script.callScript(EnumScriptType.PROJECTILE_TICK, event); - if (npc.janinoHandler != null) - npc.janinoHandler.onProjectileTick(event); - } + } } NpcAPI.EVENT_BUS.post(event); @@ -475,9 +443,7 @@ public class EventHooks { EntityNPCInterface npc = (EntityNPCInterface) thrower; npc.script.callScript(EnumScriptType.PROJECTILE_IMPACT, event); - if (npc.janinoHandler != null) - npc.janinoHandler.onProjectileImpact(event); - } + } } NpcAPI.EVENT_BUS.post(event); diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 185f2966f..96310a01e 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -4,8 +4,6 @@ import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; -import io.github.somehussar.janinoloader.api.script.IScriptBodyBuilder; -import io.github.somehussar.janinoloader.api.script.IScriptClassBody; import io.netty.buffer.ByteBuf; import kamkeel.npcs.addon.DBCAddon; import kamkeel.npcs.addon.client.DBCClient; @@ -162,12 +160,8 @@ import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.entity.ScriptNpc; import noppes.npcs.scripted.event.NpcEvent; import noppes.npcs.util.GameProfileAlt; -import org.codehaus.commons.compiler.Sandbox; -import somehussar.janino.wrapper.AbstractNPCJavaHandler; -import somehussar.janino.wrapper.NPCWrapper; import java.io.IOException; -import java.security.Permissions; import java.util.ArrayList; import java.util.ConcurrentModificationException; import java.util.HashMap; @@ -197,7 +191,6 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti public float baseHeight = 1.8f; public float scaleX, scaleY, scaleZ; - public NPCWrapper janinoHandler; private boolean wasKilled = false; public RoleInterface roleInterface; public JobInterface jobInterface; @@ -257,20 +250,6 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti if (!this.isRemote() && this.wrappedNPC == null) { this.wrappedNPC = new ScriptNpc<>(this); } - if (!this.isRemote()) { - IScriptClassBody javaCompiler = IScriptBodyBuilder.getBuilder(AbstractNPCJavaHandler.class, CustomNpcs.getDynamicCompiler()) - .setDefaultImports("noppes.npcs.api.event.INpcEvent").build(); - javaCompiler.setScript("" + - "" + - "void onInteract(INpcEvent.InteractEvent event) {" + - " event.getNpc().say(\"Lol this is from a script!\");" + - "}" + - ""); - - Permissions permissions = new Permissions(); - Sandbox sandbox = new Sandbox(permissions); - this.janinoHandler = new NPCWrapper(sandbox, javaCompiler); - } } catch (Exception e) { e.printStackTrace(); } diff --git a/src/main/java/somehussar/janino/EntityUnloadListener.java b/src/main/java/somehussar/janino/EntityUnloadListener.java index 8e7d917bb..592b0750c 100644 --- a/src/main/java/somehussar/janino/EntityUnloadListener.java +++ b/src/main/java/somehussar/janino/EntityUnloadListener.java @@ -45,9 +45,7 @@ public class EntityUnloadListener implements IWorldAccess { public void onEntityDestroy(Entity entity) { if (entity instanceof EntityNPCInterface) { EntityNPCInterface npc = (EntityNPCInterface) entity; - if (npc.janinoHandler != null) - npc.janinoHandler.prepareToUnload(); - npc.janinoHandler = null; + // TODO: REMOVE SCRIPTED OBJECT AND CLASSLOADER FOR IT } } diff --git a/src/main/java/somehussar/janino/wrapper/AbstractNPCJavaHandler.java b/src/main/java/somehussar/janino/wrapper/AbstractNPCJavaHandler.java deleted file mode 100644 index 058219d48..000000000 --- a/src/main/java/somehussar/janino/wrapper/AbstractNPCJavaHandler.java +++ /dev/null @@ -1,62 +0,0 @@ -package somehussar.janino.wrapper; - -import io.github.somehussar.janinoloader.annotations.PermissableScriptHandler; -import noppes.npcs.api.event.INpcEvent; -import noppes.npcs.scripted.event.ProjectileEvent; - -public abstract class AbstractNPCJavaHandler { - - public void prepareToUnload() { - } - - public void onInitialize(INpcEvent.InitEvent event) { - } - - public void onUpdate(INpcEvent.UpdateEvent event) { - } - - public void onDialog(INpcEvent.DialogEvent event) { - } - - public void onProjectileTick(ProjectileEvent.UpdateEvent event) { - } - - public void onDialogClosed(INpcEvent.DialogClosedEvent event) { - } - - public void onAttack(INpcEvent.MeleeAttackEvent event) { - } - - public void onRangedAttack(INpcEvent.RangedLaunchedEvent event) { - } - - public void onMeleeSwing(INpcEvent.SwingEvent event) { - } - - public void onKillEntity(INpcEvent.KilledEntityEvent event) { - } - - public void onTarget(INpcEvent.TargetEvent event) { - } - - public void onTargetLost(INpcEvent.TargetLostEvent event) { - } - - public void onCollide(INpcEvent.CollideEvent event) { - } - - public void onDamaged(INpcEvent.DamagedEvent event) { - } - - public void onNPCKilled(INpcEvent.DiedEvent event) { - } - - public void onTimer(INpcEvent.TimerEvent event) { - } - - public void onProjectileImpact(ProjectileEvent.ImpactEvent event) { - } - - public void onInteract(INpcEvent.InteractEvent event) { - } -} diff --git a/src/main/java/somehussar/janino/wrapper/NPCWrapper.java b/src/main/java/somehussar/janino/wrapper/NPCWrapper.java deleted file mode 100644 index 6158e2279..000000000 --- a/src/main/java/somehussar/janino/wrapper/NPCWrapper.java +++ /dev/null @@ -1,88 +0,0 @@ -package somehussar.janino.wrapper; - -import io.github.somehussar.janinoloader.api.script.IScriptClassBody; -import noppes.npcs.scripted.event.NpcEvent; -import org.codehaus.commons.compiler.Sandbox; - -public final class NPCWrapper { - - private final Sandbox sandbox; - public final IScriptClassBody scriptBody; - - public NPCWrapper(Sandbox sandbox, IScriptClassBody scriptBody) { - this.sandbox = sandbox; - this.scriptBody = scriptBody; - } - - public void prepareToUnload() { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().prepareToUnload(); return null; }); - } - - public void onInitialize(noppes.npcs.api.event.INpcEvent.InitEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onInitialize(event); return null; }); - } - - public void onUpdate(noppes.npcs.scripted.event.NpcEvent.UpdateEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onUpdate(event); return null; }); - } - - public void onDialog(noppes.npcs.scripted.event.NpcEvent.DialogEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onDialog(event); return null; }); - } - - public void onProjectileTick(noppes.npcs.scripted.event.ProjectileEvent.UpdateEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onProjectileTick(event); return null; }); - } - - public void onDialogClosed(noppes.npcs.scripted.event.NpcEvent.DialogClosedEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onDialogClosed(event); return null; }); - } - - public void onAttack(noppes.npcs.scripted.event.NpcEvent.MeleeAttackEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onAttack(event); return null; }); - } - - public void onRangedAttack(noppes.npcs.scripted.event.NpcEvent.RangedLaunchedEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onRangedAttack(event); return null; }); - } - - public void onMeleeSwing(noppes.npcs.scripted.event.NpcEvent.SwingEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onMeleeSwing(event); return null; }); - } - - public void onKillEntity(noppes.npcs.scripted.event.NpcEvent.KilledEntityEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onKillEntity(event); return null; }); - } - - public void onTarget(noppes.npcs.scripted.event.NpcEvent.TargetEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onTarget(event); return null; }); - } - - public void onTargetLost(noppes.npcs.scripted.event.NpcEvent.TargetLostEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onTargetLost(event); return null; }); - } - - public void onCollide(noppes.npcs.scripted.event.NpcEvent.CollideEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onCollide(event); return null; }); - } - - public void onDamaged(noppes.npcs.scripted.event.NpcEvent.DamagedEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onDamaged(event); return null; }); - } - - public void onNPCKilled(noppes.npcs.scripted.event.NpcEvent.DiedEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onNPCKilled(event); return null; }); - } - - public void onTimer(noppes.npcs.scripted.event.NpcEvent.TimerEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onTimer(event); return null; }); - } - - public void onProjectileImpact(noppes.npcs.scripted.event.ProjectileEvent.ImpactEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onProjectileImpact(event); return null; }); - } - - public void onInteract(NpcEvent.InteractEvent event) { - sandbox.confine((java.security.PrivilegedAction) () -> { scriptBody.get().onInteract(event); return null; }); - } -} From e261f18aa9b98cb39c8009b95fbbeef8330c2592 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 16 Dec 2025 01:05:23 +0200 Subject: [PATCH 014/358] GuiScrollWindow backgroundColor --- src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java b/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java index 3555b30fd..59afbea09 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java @@ -207,6 +207,7 @@ public class GuiScrollWindow extends GuiScreen implements ITextfieldListener, IC return scrolls.get(id); } + public int backgroundColor = 0x66000000; @Override public void drawScreen(int mouseX, int mouseY, float partialTicks) { scrollY = (float) lerp(scrollY, nextScrollY, partialTicks); @@ -220,7 +221,7 @@ public class GuiScrollWindow extends GuiScreen implements ITextfieldListener, IC this.drawDefaultBackground = false; if (drawBackground) - this.drawGradientRect(0, 0, this.width, this.height, 0x66000000, 0x88000000); + this.drawGradientRect(0, 0, this.width, this.height, backgroundColor, backgroundColor); if (maxScrollY > 0) From d59e7f5d33bcc1e1f9c130f951c516e7a9da6212 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 16 Dec 2025 18:33:55 +0200 Subject: [PATCH 015/358] All new Java syntax highlighting (WIP!) --- .../client/gui/util/GuiScriptTextArea.java | 11 +- .../client/gui/util/JavaTextContainer.java | 292 ++++++++++++++++++ .../npcs/client/gui/util/TextContainer.java | 6 +- 3 files changed, 302 insertions(+), 7 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/JavaTextContainer.java diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 9b257f47e..d034a591b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -5,7 +5,7 @@ import net.minecraft.client.gui.GuiScreen; import net.minecraft.util.ChatAllowedCharacters; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; -import noppes.npcs.client.gui.util.TextContainer.LineData; + import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; @@ -13,6 +13,8 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; +import noppes.npcs.client.gui.util.JavaTextContainer.LineData; + import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; @@ -25,7 +27,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { private int cursorCounter; private ITextChangeListener listener; public String text = null; - private TextContainer container = null; + private JavaTextContainer container = null; public boolean active = false; public boolean enabled = true; public boolean visible = true; @@ -115,7 +117,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } - List list = new ArrayList(container.lines); + List list = new ArrayList<>(container.lines); String wordHightLight = null; if (startSelection != endSelection) { @@ -578,7 +580,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { } this.text = text; - this.container = new TextContainer(text); + //this.container = new TextContainer(text); + this.container = new JavaTextContainer(text); this.container.init(this.width, this.height); if (this.enableCodeHighlighting) { this.container.formatCodeText(); diff --git a/src/main/java/noppes/npcs/client/gui/util/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/JavaTextContainer.java new file mode 100644 index 000000000..a36fec46f --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/JavaTextContainer.java @@ -0,0 +1,292 @@ +package noppes.npcs.client.gui.util; + +import noppes.npcs.client.ClientProxy; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class JavaTextContainer extends TextContainer { + public static final char colorChar = '\u00A7'; + + public static final Pattern MODIFIER = Pattern.compile( + "\\b(public|protected|private|static|final|abstract|synchronized|native|default)\\b"); + public static final Pattern KEYWORD = Pattern.compile( + "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof)\\b"); + + public static final Pattern TYPE_DECL = Pattern.compile( + "\\b([A-Z][a-zA-Z0-9_]*)" + // Group 1 → main type + "\\s*(<([^>]+)>)?" + // Group 2 → <…>, Group 3 → inner type + "\\s+[a-zA-Z_][a-zA-Z0-9_]*" // variable name + ); + + public static final Pattern NEW_TYPE = Pattern.compile("\\bnew\\s+([A-Z][a-zA-Z0-9_]*)"); + + public static final Pattern METHOD_DECL = Pattern.compile( + "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type + "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(" // method name + ); + public static final Pattern METHOD_CALL = Pattern.compile("([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\("); + + public static final Pattern STRING = Pattern.compile("([\"'])(?:(?=(\\\\?))\\2.)*?\\1"); + public static final Pattern COMMENT = Pattern.compile("/\\*[\\s\\S]*?(?:\\*/|$)|//.*|#.*"); + public static final Pattern NUMBER = Pattern.compile( + "\\b-?(?:0[xX][\\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?(?:[fFbBdDlLsS])?|NaN|null|Infinity|true|false)\\b"); + + public String text; + public List lines = new ArrayList<>(); + public int lineHeight; + public int totalHeight; + public int visibleLines = 1; + + public JavaTextContainer(String text) { + super(text); + this.text = text.replaceAll("\\r?\\n|\\r", "\n"); + } + + public void init(int width, int height) { + lineHeight = ClientProxy.Font.height(); + if (lineHeight == 0) + lineHeight = 12; + + lines.clear(); + + String[] split = text.split("\n"); + int charIndex = 0; + + for (String rawLine : split) { + StringBuilder currentLine = new StringBuilder(); + int startIndex = charIndex; + + int i = 0; + Matcher m = Pattern.compile(".").matcher(rawLine); // iterate each char + while (m.find()) { + String c = rawLine.substring(m.start(), m.end()); + if (ClientProxy.Font.width(currentLine.toString() + c) > width - 10) { + LineData lineData = new LineData(currentLine.toString(), startIndex, + startIndex + currentLine.length()); + startIndex += currentLine.length(); + lines.add(lineData); + currentLine = new StringBuilder(); + } + currentLine.append(c); + i = m.end(); + } + + if (currentLine.length() > 0) { + LineData lineData = new LineData(currentLine.toString(), startIndex, startIndex + currentLine.length()); + lines.add(lineData); + } + + charIndex += rawLine.length() + 1; // +1 for newline + } + + totalHeight = lines.size() * lineHeight; + visibleLines = Math.max(height / lineHeight, 1); + } + + public void formatCodeText() { + // Step 1: Tokenize the full text + List marks = new ArrayList<>(); + + collectPatternMatches(marks, COMMENT, TokenType.COMMENT); + collectPatternMatches(marks, STRING, TokenType.STRING); + collectPatternMatches(marks, KEYWORD, TokenType.KEYWORD); + collectPatternMatches(marks, MODIFIER, TokenType.MODIFIER); + + collectTypeDeclarations(marks); + collectPatternMatches(marks, NEW_TYPE, TokenType.NEW_TYPE, 1); + + collectMethodDeclarations(marks); + collectPatternMatches(marks, METHOD_CALL, TokenType.METHOD_CALL); + + collectPatternMatches(marks, NUMBER, TokenType.NUMBER); + + marks = resolveConflicts(marks); + + // Step 2: Clear existing tokens + for (LineData line : lines) { + line.tokens.clear(); + } + + // Step 3: Assign tokens to the correct lines + for (LineData line : lines) { + int cursor = line.start; + + for (Mark mark : marks) { + if (mark.end <= line.start || mark.start >= line.end) + continue; + + int tokenStart = Math.max(mark.start, line.start); + int tokenEnd = Math.min(mark.end, line.end); + + // plain text before token + if (cursor < tokenStart) { + line.tokens.add( + new Token(text.substring(cursor, tokenStart), TokenType.DEFAULT, cursor, tokenStart)); + } + + // token text + line.tokens.add(new Token(text.substring(tokenStart, tokenEnd), mark.type, tokenStart, tokenEnd)); + + cursor = tokenEnd; + } + + // trailing plain text + if (cursor < line.end) { + line.tokens.add(new Token(text.substring(cursor, line.end), TokenType.DEFAULT, cursor, line.end)); + } + } + } + + private void collectMethodDeclarations(List marks) { + Matcher m = METHOD_DECL.matcher(text); + while (m.find()) { // method name + marks.add(new Mark(m.start(2), m.end(2), TokenType.METHOD_DECARE)); + // return type + marks.add(new Mark(m.start() + m.group(1).length(), m.start(2) - 1, TokenType.TYPE_DECL)); + } + } + + private void collectTypeDeclarations(List marks) { + Matcher m = TYPE_DECL.matcher(text); + while (m.find()) { // method name + // main type + marks.add(new Mark(m.start(1), m.end(1), TokenType.TYPE_DECL)); + + if (m.group(2) != null) { + int start = m.start(2); + int end = m.end(2); + // Color < and > as white + marks.add(new Mark(start, start + 1, TokenType.DEFAULT)); // < + marks.add(new Mark(end - 1, end, TokenType.DEFAULT)); // > + + // Inner type (String) + if (m.group(3) != null) { + marks.add(new Mark(m.start(3), m.end(3), TokenType.NEW_TYPE)); // or another color + } + } + } + } + + private void collectPatternMatches(List marks, Pattern pattern, TokenType type, int group) { + Matcher m = pattern.matcher(text); + while (m.find()) { + marks.add(new Mark(m.start(group), m.end(group), type)); + } + } + + private void collectPatternMatches(List marks, Pattern pattern, TokenType type) { + collectPatternMatches(marks, pattern, type, 0); + } + + private List resolveConflicts(List marks) { + // Sort by start index first, then by descending priority + marks.sort((a, b) -> { + if (a.start != b.start) + return Integer.compare(a.start, b.start); + return Integer.compare(b.type.priority, a.type.priority); + }); + + List result = new ArrayList<>(); + for (Mark m : marks) { + boolean overlap = false; + for (Mark r : result) { + if (m.start < r.end && m.end > r.start && r.type.priority >= m.type.priority) { + overlap = true; + break; + } + } + if (!overlap) + result.add(m); + } + return result; + } + + public class LineData { + public String text; + public int start, end; + public List tokens = new ArrayList<>(); + + public LineData(String text, int startIndex, int end) { + this.text = text; + this.start = startIndex; + this.end = end; + } + + public void drawString(int x, int y, int color) { + StringBuilder builder = new StringBuilder(); + int lastIndex = 0; + + for (Token t : tokens) { + int tokenStart = text.indexOf(t.text, lastIndex); // find actual index in line + if (tokenStart > lastIndex) { + // append the text before the token (spaces, punctuation) + builder.append(text, lastIndex, tokenStart); + } + builder.append(colorChar).append(t.type.color).append(t.text).append(colorChar).append('f'); + lastIndex = tokenStart + t.text.length(); + } + + // append any remaining text after the last token + if (lastIndex < text.length()) { + builder.append(text.substring(lastIndex)); + } + + ClientProxy.Font.drawString(builder.toString(), x, y, color); + } + } + + public static class Token { + public String text; + public TokenType type; + public int start, end; // start/end relative to the line + + public Token(String text, TokenType type, int start, int end) { + this.text = text; + this.type = type; + this.start = start; + this.end = end; + } + + @Override + public String toString() { + return "Token{'" + text + '\'' + ", " + type + ", (color=" + type.color + ", priority=" + type.priority + ")" + '}'; + } + } + + private static class Mark { + public int start, end; + public TokenType type; + + public Mark(int start, int end, TokenType type) { + this.start = start; + this.end = end; + this.type = type; + } + + } + + public enum TokenType { + COMMENT('7', 130), + STRING('5', 120), + KEYWORD('c', 100), + MODIFIER('6', 90), + NEW_TYPE('d', 80), + TYPE_DECL('3', 70), + METHOD_DECARE('2', 60), + METHOD_CALL('a', 50), + NUMBER('7', 40), + VARIABLE('f', 30), + DEFAULT('f', 0); + + public final char color; + public final int priority; + + TokenType(char color, int priority) { + this.color = color; + this.priority = priority; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/TextContainer.java b/src/main/java/noppes/npcs/client/gui/util/TextContainer.java index dc66332cb..c248b8b67 100644 --- a/src/main/java/noppes/npcs/client/gui/util/TextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/TextContainer.java @@ -8,7 +8,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; public class TextContainer { - private final static char colorChar = '\u00A7'; + protected final static char colorChar = '\u00A7'; //public final Pattern regexString = Pattern.compile("\"(?:[^\"\\\\]|\\\\.)*?(\"|$)|'(?:[^'\\\\]|\\\\.)*?('|$)"); //public final Pattern regexString = Pattern.compile("\"(.*?(? conflicting = new ArrayList(); for (MarkUp m : makeup) { if (start >= m.start && start <= m.end || end >= m.start && end <= m.end || start < m.start && end > m.start) { From d162804c64a8eccbb71ea5e2adeb1ceae8fa7b3f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 09:53:32 +0200 Subject: [PATCH 016/358] Script Editor Keybinds: 1)Ctrl + D for line duplication 2)Ctrl + Slash for comments --- .../client/gui/util/GuiScriptTextArea.java | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index d034a591b..06b5a92e0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -6,6 +6,7 @@ import net.minecraft.util.ChatAllowedCharacters; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.util.script.JavaTextContainer; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; @@ -13,8 +14,8 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; -import noppes.npcs.client.gui.util.JavaTextContainer.LineData; +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; @@ -412,6 +413,43 @@ public class GuiScriptTextArea extends GuiNpcTextField { addText(Character.toString(c)); } + if (i == Keyboard.KEY_SLASH && isCtrlKeyDown()) { + // Use LineData to find the line the cursor is on + for (LineData line : container.lines) { + if (cursorPosition >= line.start && cursorPosition <= line.end) { + int lineStart = line.start; + int lineEnd = line.end; + String lineText = text.substring(lineStart, lineEnd); + int nonWs = 0; + while (nonWs < lineText.length() && Character.isWhitespace(lineText.charAt(nonWs))) + nonWs++; + boolean hasComment = lineText.startsWith("//", nonWs); + String newLineText = hasComment ? lineText.substring(0, nonWs) + lineText.substring(nonWs + 2) : lineText.substring(0, nonWs) + "//" + lineText.substring(nonWs); + setText(text.substring(0, lineStart) + newLineText + text.substring(lineEnd)); + int cursorDelta = hasComment ? -2 : 2; + int newCursor = cursorPosition + cursorDelta; + if (cursorPosition < lineStart + nonWs + (hasComment ? 2 : 0)) + newCursor = cursorPosition; + setCursor(Math.max(lineStart, newCursor), false); + return true; + } + } + } + + if (i == Keyboard.KEY_D && isCtrlKeyDown()) { + // Use LineData to find the line the cursor is on + for (LineData line : container.lines) { + if (cursorPosition >= line.start && cursorPosition <= line.end) { + int lineStart = line.start, lineEnd = line.end; + String lineText = text.substring(lineStart, lineEnd); + String insertText = "\n" + lineText; + setText(text.substring(0, lineEnd) + insertText + text.substring(lineEnd)); + setCursor(lineEnd + 1, false); + return true; + } + } + } + return true; } From 3cbdab3dc45dc5fbb427033fa25dfb315c503ebb Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 10:17:15 +0200 Subject: [PATCH 017/358] Added duplication/commenting for multiple lines --- .../client/gui/util/GuiScriptTextArea.java | 113 ++++++++++++++---- 1 file changed, 87 insertions(+), 26 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 06b5a92e0..4fdf58846 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -414,39 +414,100 @@ public class GuiScriptTextArea extends GuiNpcTextField { } if (i == Keyboard.KEY_SLASH && isCtrlKeyDown()) { - // Use LineData to find the line the cursor is on - for (LineData line : container.lines) { - if (cursorPosition >= line.start && cursorPosition <= line.end) { - int lineStart = line.start; - int lineEnd = line.end; - String lineText = text.substring(lineStart, lineEnd); - int nonWs = 0; - while (nonWs < lineText.length() && Character.isWhitespace(lineText.charAt(nonWs))) - nonWs++; - boolean hasComment = lineText.startsWith("//", nonWs); - String newLineText = hasComment ? lineText.substring(0, nonWs) + lineText.substring(nonWs + 2) : lineText.substring(0, nonWs) + "//" + lineText.substring(nonWs); - setText(text.substring(0, lineStart) + newLineText + text.substring(lineEnd)); - int cursorDelta = hasComment ? -2 : 2; - int newCursor = cursorPosition + cursorDelta; - if (cursorPosition < lineStart + nonWs + (hasComment ? 2 : 0)) - newCursor = cursorPosition; - setCursor(Math.max(lineStart, newCursor), false); - return true; + if (startSelection != endSelection) { + // Handle multi-line selection: toggle each line individually + StringBuilder newText = new StringBuilder(text.length() + 100); // Pre-size for efficiency + int prevEnd = 0; + for (LineData line : container.lines) { + // Append text before this line (including previous \n) + if (line.start > prevEnd) { + newText.append(text, prevEnd, line.start); + } + // Check if line intersects selection + if (line.end > startSelection && line.start < endSelection) { + String lineText = text.substring(line.start, line.end); + int nonWs = 0; + while (nonWs < lineText.length() && Character.isWhitespace(lineText.charAt(nonWs))) nonWs++; + boolean hasContent = nonWs < lineText.length(); + boolean hasComment = hasContent && lineText.startsWith("//", nonWs); + String newLineText = hasComment ? lineText.substring(0, nonWs) + lineText.substring(nonWs + 2) : (hasContent ? lineText.substring(0, nonWs) + "//" + lineText.substring(nonWs) : lineText); + newText.append(newLineText); + } else { + newText.append(text, line.start, line.end); + } + prevEnd = line.end; + } + // Append remaining text + if (prevEnd < text.length()) { + newText.append(text, prevEnd, text.length()); + } + setText(newText.toString()); + // Keep selection + return true; + } else { + // Single line toggle (existing logic) + for (LineData line : container.lines) { + if (cursorPosition >= line.start && cursorPosition <= line.end) { + int lineStart = line.start, lineEnd = line.end; + String lineText = text.substring(lineStart, lineEnd); + int nonWs = 0; + while (nonWs < lineText.length() && Character.isWhitespace(lineText.charAt(nonWs))) nonWs++; + boolean hasComment = lineText.startsWith("//", nonWs); + String newLineText = hasComment ? lineText.substring(0, nonWs) + lineText.substring(nonWs + 2) : lineText.substring(0, nonWs) + "//" + lineText.substring(nonWs); + setText(text.substring(0, lineStart) + newLineText + text.substring(lineEnd)); + int cursorDelta = hasComment ? -2 : 2; + int newCursor = cursorPosition + cursorDelta; + if (cursorPosition < lineStart + nonWs + (hasComment ? 2 : 0)) newCursor = cursorPosition; + setCursor(Math.max(lineStart, newCursor), false); + return true; + } } } } if (i == Keyboard.KEY_D && isCtrlKeyDown()) { - // Use LineData to find the line the cursor is on - for (LineData line : container.lines) { - if (cursorPosition >= line.start && cursorPosition <= line.end) { - int lineStart = line.start, lineEnd = line.end; - String lineText = text.substring(lineStart, lineEnd); - String insertText = "\n" + lineText; - setText(text.substring(0, lineEnd) + insertText + text.substring(lineEnd)); - setCursor(lineEnd + 1, false); + if (startSelection != endSelection) { + // Handle multi-line selection duplication + LineData firstLine = null, lastLine = null; + for (LineData line : container.lines) { + if (line.end > startSelection && line.start < endSelection) { + if (firstLine == null) firstLine = line; + lastLine = line; + } + } + if (firstLine != null && lastLine != null) { + String selectedText = text.substring(firstLine.start, lastLine.end); + String insertText = "\n" + selectedText; + // Save selection before setText + int savedStart = startSelection; + int savedEnd = endSelection; + setText(text.substring(0, lastLine.end) + insertText + text.substring(lastLine.end)); + // Move cursor to start of duplicated block + setCursor(lastLine.end + 1, false); + // Restore selection + startSelection = savedStart; + endSelection = savedEnd; return true; } + } else { + // Single line duplication (existing logic) + for (LineData line : container.lines) { + if (cursorPosition >= line.start && cursorPosition <= line.end) { + int lineStart = line.start, lineEnd = line.end; + String lineText = text.substring(lineStart, lineEnd); + String insertText = "\n" + lineText; + // Save selection before setText + int savedStart = startSelection; + int savedEnd = endSelection; + setText(text.substring(0, lineEnd) + insertText + text.substring(lineEnd)); + // Move cursor to start of duplicated line + setCursor(lineEnd + 1, false); + // Restore selection + startSelection = savedStart; + endSelection = savedEnd; + return true; + } + } } } From 4ed809989d907bc5c9c611266a5229582885a577 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 10:45:45 +0200 Subject: [PATCH 018/358] Fixed Script Editor text cursor not drawing on last line character --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 4fdf58846..4f9456812 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -170,7 +170,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { int yPos = y + (i - scrolledLine) * container.lineHeight + 1; data.drawString(x + 1, yPos, 0xFFe0e0e0); - if (active && isEnabled() && (cursorCounter / 6) % 2 == 0 && cursorPosition >= data.start && cursorPosition < data.end) { + if (active && isEnabled() && (cursorCounter / 6) % 2 == 0 && cursorPosition >= data.start && cursorPosition <= data.end) { int posX = x + ClientProxy.Font.width(line.substring(0, cursorPosition - data.start)); drawRect(posX + 1, yPos, posX + 2, yPos + 1 + container.lineHeight, -3092272); } From 422ebea54f7933684b55093895c4291cd6ee9e76 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 10:46:08 +0200 Subject: [PATCH 019/358] Script Editor: BACKSPACE now deletes lines with empty spaces --- .../client/gui/util/GuiScriptTextArea.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 4f9456812..a30410b76 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -352,6 +352,35 @@ public class GuiScriptTextArea extends GuiNpcTextField { return true; } if (i == Keyboard.KEY_BACK) { + if (startSelection == endSelection && startSelection > 0) { + // Check if we should remove an empty/whitespace-only line + LineData currentLine = null; + for (LineData line : this.container.lines) { + if (startSelection >= line.start && startSelection <= line.end) { + currentLine = line; + break; + } + } + + if (currentLine != null) { + // Check if cursor is at line start or after only whitespace + String beforeCursor = text.substring(currentLine.start, startSelection); + boolean atLineStartOrWhitespace = beforeCursor.trim().isEmpty(); + + // Check if line contains only whitespace + boolean lineOnlyWhitespace = currentLine.text.trim().isEmpty(); + + if (atLineStartOrWhitespace && lineOnlyWhitespace && currentLine.start > 0) { + // Remove the entire line including newline + String before = text.substring(0, currentLine.start - 1); // -1 to include the newline before + String after = text.substring(currentLine.end); + setText(before + after); + endSelection = cursorPosition = startSelection = currentLine.start - 1; + return true; + } + } + } + String s = getSelectionBeforeText(); if (startSelection > 0 && startSelection == endSelection) { s = s.substring(0, s.length() - 1); From 04d3a1339e7183295233a9f0f0d0b64c43900373 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 11:55:58 +0200 Subject: [PATCH 020/358] Local and Global fields syntax highlight --- .../util/{ => script}/JavaTextContainer.java | 115 +++++++++++++-- .../client/gui/util/script/MethodBlock.java | 138 ++++++++++++++++++ 2 files changed, 239 insertions(+), 14 deletions(-) rename src/main/java/noppes/npcs/client/gui/util/{ => script}/JavaTextContainer.java (70%) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java diff --git a/src/main/java/noppes/npcs/client/gui/util/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java similarity index 70% rename from src/main/java/noppes/npcs/client/gui/util/JavaTextContainer.java rename to src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index a36fec46f..cc494dd32 100644 --- a/src/main/java/noppes/npcs/client/gui/util/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -1,6 +1,7 @@ -package noppes.npcs.client.gui.util; +package noppes.npcs.client.gui.util.script; import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.util.TextContainer; import java.util.ArrayList; import java.util.List; @@ -15,20 +16,26 @@ public class JavaTextContainer extends TextContainer { public static final Pattern KEYWORD = Pattern.compile( "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof)\\b"); - public static final Pattern TYPE_DECL = Pattern.compile( - "\\b([A-Z][a-zA-Z0-9_]*)" + // Group 1 → main type - "\\s*(<([^>]+)>)?" + // Group 2 → <…>, Group 3 → inner type + public static final Pattern TYPE_DECL = Pattern.compile("\\b([A-Za-z_][a-zA-Z0-9_]*)" + // Group 1 → main type + "\\s*(<([^>]+)>)?" + // Group 2 → <…>, Group 3 → inner type "\\s+[a-zA-Z_][a-zA-Z0-9_]*" // variable name ); - public static final Pattern NEW_TYPE = Pattern.compile("\\bnew\\s+([A-Z][a-zA-Z0-9_]*)"); + public static final Pattern NEW_TYPE = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)"); - public static final Pattern METHOD_DECL = Pattern.compile( - "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type - "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(" // method name + public static final Pattern METHOD_DECL = Pattern.compile("\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type + "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(" // method name ); public static final Pattern METHOD_CALL = Pattern.compile("([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\("); + // Class-level global fields + public static final Pattern GLOBAL_FIELD_DECL = Pattern.compile( + "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + + // Local fields inside methods (simplified) + public static final Pattern LOCAL_FIELD_DECL = Pattern.compile( + "\\b([A-Z][a-zA-Z0-9_<>\\[\\]]*|[a-z][a-zA-Z0-9_]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + public static final Pattern STRING = Pattern.compile("([\"'])(?:(?=(\\\\?))\\2.)*?\\1"); public static final Pattern COMMENT = Pattern.compile("/\\*[\\s\\S]*?(?:\\*/|$)|//.*|#.*"); public static final Pattern NUMBER = Pattern.compile( @@ -36,6 +43,7 @@ public class JavaTextContainer extends TextContainer { public String text; public List lines = new ArrayList<>(); + public List methodBlocks = new ArrayList<>(); public int lineHeight; public int totalHeight; public int visibleLines = 1; @@ -86,10 +94,81 @@ public class JavaTextContainer extends TextContainer { visibleLines = Math.max(height / lineHeight, 1); } + private List globalFields = new ArrayList<>(); + private List localFields = new ArrayList<>(); + + private void collectFields() { + globalFields.clear(); + localFields.clear(); + methodBlocks.clear(); + + // Extract method blocks first + methodBlocks = MethodBlock.collectMethodBlocks(text); + + // Global fields (excluding those inside methods) + Matcher mGlobal = GLOBAL_FIELD_DECL.matcher(text); + while (mGlobal.find()) { + String varName = mGlobal.group(2); + int varPosition = mGlobal.start(2); + + // Check if this variable is inside a method + boolean isInsideMethod = false; + for (MethodBlock block : methodBlocks) { + if (block.containsPosition(varPosition)) { + isInsideMethod = true; + break; + } + } + + // Only add as global if it's not inside any method + if (!isInsideMethod) { + globalFields.add(varName); + } + } + + // Extract local variables from each method block + for (MethodBlock block : methodBlocks) { + // localVariables are already extracted in MethodBlock constructor + for (String var : block.localVariables) { + if (!globalFields.contains(var) && !localFields.contains(var)) { + localFields.add(var); + } + } + } + } + + // Find which method block contains a given position + private MethodBlock findMethodBlockAtPosition(int position) { + for (MethodBlock block : methodBlocks) { + if (block.containsPosition(position)) { + return block; + } + } + return null; + } + + private void highlightVariableReferences(List marks) { + Pattern identifier = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); + Matcher m = identifier.matcher(text); + while (m.find()) { + String name = m.group(1); + int position = m.start(1); + + // Check if this variable is local to the method it's in + MethodBlock methodBlock = findMethodBlockAtPosition(position); + if (methodBlock != null && methodBlock.localVariables.contains(name)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD)); + } else if (globalFields.contains(name)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); + } + } + } + public void formatCodeText() { // Step 1: Tokenize the full text List marks = new ArrayList<>(); + collectFields(); // Extract global and local fields with method scoping collectPatternMatches(marks, COMMENT, TokenType.COMMENT); collectPatternMatches(marks, STRING, TokenType.STRING); collectPatternMatches(marks, KEYWORD, TokenType.KEYWORD); @@ -103,6 +182,8 @@ public class JavaTextContainer extends TextContainer { collectPatternMatches(marks, NUMBER, TokenType.NUMBER); + highlightVariableReferences(marks); + marks = resolveConflicts(marks); // Step 2: Clear existing tokens @@ -145,13 +226,13 @@ public class JavaTextContainer extends TextContainer { while (m.find()) { // method name marks.add(new Mark(m.start(2), m.end(2), TokenType.METHOD_DECARE)); // return type - marks.add(new Mark(m.start() + m.group(1).length(), m.start(2) - 1, TokenType.TYPE_DECL)); + marks.add(new Mark(m.start(1), m.end(1), TokenType.TYPE_DECL)); } } private void collectTypeDeclarations(List marks) { Matcher m = TYPE_DECL.matcher(text); - while (m.find()) { // method name + while (m.find()) { // main type marks.add(new Mark(m.start(1), m.end(1), TokenType.TYPE_DECL)); @@ -160,11 +241,11 @@ public class JavaTextContainer extends TextContainer { int end = m.end(2); // Color < and > as white marks.add(new Mark(start, start + 1, TokenType.DEFAULT)); // < - marks.add(new Mark(end - 1, end, TokenType.DEFAULT)); // > + marks.add(new Mark(end - 1, end, TokenType.DEFAULT)); // > // Inner type (String) if (m.group(3) != null) { - marks.add(new Mark(m.start(3), m.end(3), TokenType.NEW_TYPE)); // or another color + marks.add(new Mark(m.start(3), m.end(3), TokenType.NEW_TYPE)); } } } @@ -203,7 +284,7 @@ public class JavaTextContainer extends TextContainer { } return result; } - + public class LineData { public String text; public int start, end; @@ -265,7 +346,11 @@ public class JavaTextContainer extends TextContainer { this.end = end; this.type = type; } - + + @Override + public String toString() { + return "Mark{" + type + ", (start=" + start + ", end=" + end + ")" + '}'; + } } public enum TokenType { @@ -279,6 +364,8 @@ public class JavaTextContainer extends TextContainer { METHOD_CALL('a', 50), NUMBER('7', 40), VARIABLE('f', 30), + GLOBAL_FIELD('b', 35), // new + LOCAL_FIELD('e', 25), // new DEFAULT('f', 0); public final char color; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java new file mode 100644 index 000000000..6be400d3f --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java @@ -0,0 +1,138 @@ +package noppes.npcs.client.gui.util.script; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class MethodBlock { + + // pattern matches returnType + methodName + parentheses + public static final Pattern METHOD_PATTERN = Pattern.compile( + "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type + "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(" // method name + ); + + // Pattern for variable declarations (same as in JavaTextContainer) + public static final Pattern LOCAL_VAR_DECL = Pattern.compile( + "\\b([A-Z][a-zA-Z0-9_<>\\[\\]]*|[a-z][a-zA-Z0-9_]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + + public int startOffset; + public int endOffset; + public String text; + public List localVariables = new ArrayList<>(); + + public MethodBlock(int start, int end, String text) { + this.startOffset = start; + this.endOffset = end; + this.text = text; + extractLocalVariables(); + } + + // Extract local variables declared within this method + private void extractLocalVariables() { + localVariables.clear(); + Matcher m = LOCAL_VAR_DECL.matcher(text); + while (m.find()) { + String varName = m.group(2); + if (!localVariables.contains(varName)) { + localVariables.add(varName); + } + } + } + + // Check if a position (in the full text) falls within this method + public boolean containsPosition(int position) { + return position >= startOffset && position < endOffset; + } + + public String toString() { + return text; + } + + public static List collectMethodBlocks(String text) { + List methods = new ArrayList<>(); + + // Matches: returnType methodName(...) { (non-greedy capture until first { ) + Pattern methodPattern = Pattern.compile( + "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type + "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\([^)]*\\)\\s*" + // method name + parameters + "\\{", // opening brace + Pattern.MULTILINE); + + Matcher m = methodPattern.matcher(text); + while (m.find()) { + int start = m.start(); // start of method declaration + int braceStart = text.indexOf("{", m.end() - 1); // first { after declaration + if (braceStart == -1) + continue; + + int braceEnd = findMatchingBrace(text, braceStart); + if (braceEnd == -1) + continue; + + String methodText = text.substring(start, braceEnd + 1); + methods.add(new MethodBlock(start, braceEnd + 1, methodText)); + } + + return methods; + } + + // Find ranges of text to skip (strings and comments) + private static List findExcludedRanges(String text) { + List ranges = new ArrayList<>(); + + // Find all strings using the pattern from JavaTextContainer + Pattern stringPattern = Pattern.compile("([\"'])(?:(?=(\\\\?))\\2.)*?\\1"); + Matcher stringMatcher = stringPattern.matcher(text); + while (stringMatcher.find()) { + ranges.add(new int[] { stringMatcher.start(), stringMatcher.end() }); + } + + // Find all comments + Pattern commentPattern = Pattern.compile("/\\*[\\s\\S]*?(?:\\*/|$)|//.*|#.*"); + Matcher commentMatcher = commentPattern.matcher(text); + while (commentMatcher.find()) { + ranges.add(new int[] { commentMatcher.start(), commentMatcher.end() }); + } + + // Sort ranges by start position + ranges.sort((a, b) -> Integer.compare(a[0], b[0])); + return ranges; + } + + // Check if a position is within an excluded range + private static boolean isInExcludedRange(int pos, List ranges) { + for (int[] range : ranges) { + if (pos >= range[0] && pos < range[1]) { + return true; + } + } + return false; + } + + // Utility to find the matching closing brace, ignoring braces in strings and + // comments + public static int findMatchingBrace(String text, int openBraceIndex) { + List excludedRanges = findExcludedRanges(text); + + int depth = 0; + for (int i = openBraceIndex; i < text.length(); i++) { + // Skip characters inside strings or comments + if (isInExcludedRange(i, excludedRanges)) { + continue; + } + + char c = text.charAt(i); + if (c == '{') { + depth++; + } else if (c == '}') { + depth--; + if (depth == 0) { + return i; + } + } + } + return -1; // no match + } +} From 31ea78499825ddf170439062a308e74980d2a070 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 14:33:17 +0200 Subject: [PATCH 021/358] Fixed text cursor movement bugs --- .../client/gui/util/GuiScriptTextArea.java | 31 +++++----- .../gui/util/script/JavaTextContainer.java | 56 +++++++++---------- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index a30410b76..22de3e54b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -170,8 +170,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { int yPos = y + (i - scrolledLine) * container.lineHeight + 1; data.drawString(x + 1, yPos, 0xFFe0e0e0); - if (active && isEnabled() && (cursorCounter / 6) % 2 == 0 && cursorPosition >= data.start && cursorPosition <= data.end) { - int posX = x + ClientProxy.Font.width(line.substring(0, cursorPosition - data.start)); + if (active && isEnabled() && (cursorCounter / 6) % 2 == 0 && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { + int posX = x + ClientProxy.Font.width( + line.substring(0, Math.min(cursorPosition - data.start, line.length()))); drawRect(posX + 1, yPos, posX + 2, yPos + 1 + container.lineHeight, -3092272); } } @@ -270,6 +271,14 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (!isEnabled()) return false; + LineData currentLine = null; + for (LineData line : this.container.lines) { + if (startSelection >= line.start && startSelection <= line.end) { + currentLine = line; + break; + } + } + String original = text; if (i == Keyboard.KEY_LEFT) { int j = 1; @@ -281,7 +290,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { j = cursorPosition - m.start(); } } - setCursor(cursorPosition - j, GuiScreen.isShiftKeyDown()); + int newPos = Math.max(cursorPosition - j, 0); + setCursor(newPos, GuiScreen.isShiftKeyDown()); return true; } if (i == Keyboard.KEY_RIGHT) { @@ -292,7 +302,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { j = m.start(); } } - setCursor(cursorPosition + j, GuiScreen.isShiftKeyDown()); + int newPos = Math.min(cursorPosition + j, text.length()); //-2 for the \n + setCursor(newPos, GuiScreen.isShiftKeyDown()); return true; } if (i == Keyboard.KEY_UP) { @@ -354,14 +365,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (i == Keyboard.KEY_BACK) { if (startSelection == endSelection && startSelection > 0) { // Check if we should remove an empty/whitespace-only line - LineData currentLine = null; - for (LineData line : this.container.lines) { - if (startSelection >= line.start && startSelection <= line.end) { - currentLine = line; - break; - } - } - if (currentLine != null) { // Check if cursor is at line start or after only whitespace String beforeCursor = text.substring(currentLine.start, startSelection); @@ -373,7 +376,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (atLineStartOrWhitespace && lineOnlyWhitespace && currentLine.start > 0) { // Remove the entire line including newline String before = text.substring(0, currentLine.start - 1); // -1 to include the newline before - String after = text.substring(currentLine.end); + String after = currentLine.end + 1 > text.length() ? "" : text.substring(currentLine.end + 1); setText(before + after); endSelection = cursorPosition = startSelection = currentLine.start - 1; return true; @@ -639,6 +642,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { } private int cursorDown() { + if (cursorPosition == text.length()) + return cursorPosition; for (int i = 0; i < this.container.lines.size(); ++i) { LineData data = (LineData) this.container.lines.get(i); if (this.cursorPosition >= data.start && this.cursorPosition < data.end) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index cc494dd32..30fff9ccf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -57,40 +57,29 @@ public class JavaTextContainer extends TextContainer { lineHeight = ClientProxy.Font.height(); if (lineHeight == 0) lineHeight = 12; - - lines.clear(); - String[] split = text.split("\n"); - int charIndex = 0; - - for (String rawLine : split) { - StringBuilder currentLine = new StringBuilder(); - int startIndex = charIndex; + int totalChars = 0; + for (String l : split) { + StringBuilder line = new StringBuilder(); + Matcher m = regexWord.matcher(l); int i = 0; - Matcher m = Pattern.compile(".").matcher(rawLine); // iterate each char while (m.find()) { - String c = rawLine.substring(m.start(), m.end()); - if (ClientProxy.Font.width(currentLine.toString() + c) > width - 10) { - LineData lineData = new LineData(currentLine.toString(), startIndex, - startIndex + currentLine.length()); - startIndex += currentLine.length(); - lines.add(lineData); - currentLine = new StringBuilder(); + String word = l.substring(i, m.start()); + if (ClientProxy.Font.width(line + word) > width - 10) { + lines.add(new LineData(line.toString(), totalChars, totalChars + line.length())); + totalChars += line.length(); + line = new StringBuilder(); } - currentLine.append(c); - i = m.end(); + line.append(word); + i = m.start(); } + lines.add(new LineData(line.toString(), totalChars, totalChars + line.length() + 1)); - if (currentLine.length() > 0) { - LineData lineData = new LineData(currentLine.toString(), startIndex, startIndex + currentLine.length()); - lines.add(lineData); - } - - charIndex += rawLine.length() + 1; // +1 for newline + totalChars += line.length() + 1; } - - totalHeight = lines.size() * lineHeight; + linesCount = lines.size(); + totalHeight = linesCount * lineHeight; visibleLines = Math.max(height / lineHeight, 1); } @@ -202,21 +191,28 @@ public class JavaTextContainer extends TextContainer { int tokenStart = Math.max(mark.start, line.start); int tokenEnd = Math.min(mark.end, line.end); + tokenStart = Math.max(0, Math.min(tokenStart, text.length())); + tokenEnd = Math.max(0, Math.min(tokenEnd, text.length())); + // plain text before token if (cursor < tokenStart) { + int end = Math.min(tokenStart, text.length()); line.tokens.add( - new Token(text.substring(cursor, tokenStart), TokenType.DEFAULT, cursor, tokenStart)); + new Token(text.substring(cursor, end), TokenType.DEFAULT, cursor, end)); } // token text - line.tokens.add(new Token(text.substring(tokenStart, tokenEnd), mark.type, tokenStart, tokenEnd)); + if (tokenStart < tokenEnd) { + line.tokens.add(new Token(text.substring(tokenStart, Math.min(tokenEnd, text.length())), mark.type, tokenStart, Math.min(tokenEnd, text.length()))); + } cursor = tokenEnd; } // trailing plain text if (cursor < line.end) { - line.tokens.add(new Token(text.substring(cursor, line.end), TokenType.DEFAULT, cursor, line.end)); + int end = Math.min(line.end, text.length()); + line.tokens.add(new Token(text.substring(cursor, end), TokenType.DEFAULT, cursor, end)); } } } @@ -301,7 +297,7 @@ public class JavaTextContainer extends TextContainer { int lastIndex = 0; for (Token t : tokens) { - int tokenStart = text.indexOf(t.text, lastIndex); // find actual index in line + int tokenStart = t.start - this.start; // relative position in line if (tokenStart > lastIndex) { // append the text before the token (spaces, punctuation) builder.append(text, lastIndex, tokenStart); From c062c1340bf3ddfb2034f5b03bb98c0c8e830036 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 15:13:38 +0200 Subject: [PATCH 022/358] Can now click empty lines in Script Editor (accounted for empty trailing lines) --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 8 +++++++- .../npcs/client/gui/util/script/JavaTextContainer.java | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 22de3e54b..b97537cd2 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -249,7 +249,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { lineWidth = w; } - return data.end - 1; + // Place cursor after the last visible character of the line. + // `data.end - 1` previously pointed at the newline for non-last lines + // which made clicks land on the newline rather than after the text. + // Use data.start + chars.length to return the position directly + // after the line's characters, clamped to the total text length. + int posAfterChars = data.start + chars.length; + return Math.min(posAfterChars, text.length()); } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 30fff9ccf..7474657e2 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -57,8 +57,8 @@ public class JavaTextContainer extends TextContainer { lineHeight = ClientProxy.Font.height(); if (lineHeight == 0) lineHeight = 12; - String[] split = text.split("\n"); - + + String[] split = text.split("\\n", -1); // -1 preserves empty lines "" int totalChars = 0; for (String l : split) { StringBuilder line = new StringBuilder(); From afd2a9fb3576e8ee03b7b91b4b94ed813e3cd454 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 15:18:33 +0200 Subject: [PATCH 023/358] Multi line comment (Ctrl + /) now calculates new start/end selection points --- .../client/gui/util/GuiScriptTextArea.java | 182 +++++++++++++----- 1 file changed, 136 insertions(+), 46 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index b97537cd2..54b09944e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -453,54 +453,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (i == Keyboard.KEY_SLASH && isCtrlKeyDown()) { if (startSelection != endSelection) { - // Handle multi-line selection: toggle each line individually - StringBuilder newText = new StringBuilder(text.length() + 100); // Pre-size for efficiency - int prevEnd = 0; - for (LineData line : container.lines) { - // Append text before this line (including previous \n) - if (line.start > prevEnd) { - newText.append(text, prevEnd, line.start); - } - // Check if line intersects selection - if (line.end > startSelection && line.start < endSelection) { - String lineText = text.substring(line.start, line.end); - int nonWs = 0; - while (nonWs < lineText.length() && Character.isWhitespace(lineText.charAt(nonWs))) nonWs++; - boolean hasContent = nonWs < lineText.length(); - boolean hasComment = hasContent && lineText.startsWith("//", nonWs); - String newLineText = hasComment ? lineText.substring(0, nonWs) + lineText.substring(nonWs + 2) : (hasContent ? lineText.substring(0, nonWs) + "//" + lineText.substring(nonWs) : lineText); - newText.append(newLineText); - } else { - newText.append(text, line.start, line.end); - } - prevEnd = line.end; - } - // Append remaining text - if (prevEnd < text.length()) { - newText.append(text, prevEnd, text.length()); - } - setText(newText.toString()); - // Keep selection - return true; + toggleCommentSelection(); } else { - // Single line toggle (existing logic) - for (LineData line : container.lines) { - if (cursorPosition >= line.start && cursorPosition <= line.end) { - int lineStart = line.start, lineEnd = line.end; - String lineText = text.substring(lineStart, lineEnd); - int nonWs = 0; - while (nonWs < lineText.length() && Character.isWhitespace(lineText.charAt(nonWs))) nonWs++; - boolean hasComment = lineText.startsWith("//", nonWs); - String newLineText = hasComment ? lineText.substring(0, nonWs) + lineText.substring(nonWs + 2) : lineText.substring(0, nonWs) + "//" + lineText.substring(nonWs); - setText(text.substring(0, lineStart) + newLineText + text.substring(lineEnd)); - int cursorDelta = hasComment ? -2 : 2; - int newCursor = cursorPosition + cursorDelta; - if (cursorPosition < lineStart + nonWs + (hasComment ? 2 : 0)) newCursor = cursorPosition; - setCursor(Math.max(lineStart, newCursor), false); - return true; - } - } + toggleCommentLineAtCursor(); } + return true; } if (i == Keyboard.KEY_D && isCtrlKeyDown()) { @@ -556,6 +513,110 @@ public class GuiScriptTextArea extends GuiNpcTextField { return Keyboard.isKeyDown(42) || Keyboard.isKeyDown(54); } + private void toggleCommentSelection() { + StringBuilder newText = new StringBuilder(text.length() + 100); + int prevEnd = 0; + + int newStart = -1; //calculates expanded startSelection + int newEnd = -1; //calculates expanded endSelection + int newIndex = 0; + + for (LineData line : container.lines) { + int safeLineStart = Math.max(0, Math.min(line.start, text.length())); + int safeLineEnd = Math.max(0, Math.min(line.end, text.length())); + + if (safeLineStart > prevEnd) { + if (newStart == -1 && startSelection >= prevEnd && startSelection <= safeLineStart) { + newStart = newIndex + (startSelection - prevEnd); + } + if (newEnd == -1 && endSelection >= prevEnd && endSelection <= safeLineStart) { + newEnd = newIndex + (endSelection - prevEnd); + } + newText.append(text, prevEnd, safeLineStart); + newIndex += safeLineStart - prevEnd; + } + + if (safeLineEnd > startSelection && safeLineStart < endSelection) { + String lineText = text.substring(safeLineStart, safeLineEnd); + CommentToggleInfo ti = toggleLine(lineText); + String newLineText = ti.text; + int nonWs = ti.nonWs; + int delta = ti.delta; + + if (newStart == -1 && startSelection >= safeLineStart && startSelection <= safeLineEnd) { + int offsetInOldStart = startSelection - safeLineStart; + int newOffsetStart = offsetInOldStart; + if (offsetInOldStart >= nonWs) { + newOffsetStart = offsetInOldStart + delta; + } + newStart = newIndex + Math.max(0, newOffsetStart); + } + if (newEnd == -1 && endSelection >= safeLineStart && endSelection <= safeLineEnd) { + int offsetInOld = endSelection - safeLineStart; + int newOffset = offsetInOld; + if (offsetInOld >= nonWs) { + newOffset = offsetInOld + delta; + } + newEnd = newIndex + Math.max(0, newOffset); + } + + newText.append(newLineText); + newIndex += newLineText.length(); + } else { + if (newStart == -1 && startSelection >= safeLineStart && startSelection <= safeLineEnd) { + newStart = newIndex + (startSelection - safeLineStart); + } + if (newEnd == -1 && endSelection >= safeLineStart && endSelection <= safeLineEnd) { + newEnd = newIndex + (endSelection - safeLineStart); + } + newText.append(text, safeLineStart, safeLineEnd); + newIndex += safeLineEnd - safeLineStart; + } + prevEnd = safeLineEnd; + } + + if (prevEnd < text.length()) { + if (newStart == -1 && startSelection >= prevEnd && startSelection <= text.length()) { + newStart = newIndex + (startSelection - prevEnd); + } + if (newEnd == -1 && endSelection >= prevEnd && endSelection <= text.length()) { + newEnd = newIndex + (endSelection - prevEnd); + } + newText.append(text, prevEnd, text.length()); + newIndex += text.length() - prevEnd; + } + + if (newStart == -1) newStart = Math.max(0, Math.min(startSelection, newText.length())); + if (newEnd == -1) newEnd = Math.max(0, Math.min(endSelection, newText.length())); + + setText(newText.toString()); + startSelection = newStart; + endSelection = newEnd; + cursorPosition = endSelection; + } + + private void toggleCommentLineAtCursor() { + for (LineData line : container.lines) { + int lineStart = Math.max(0, Math.min(line.start, text.length())); + int lineEnd = Math.max(0, Math.min(line.end, text.length())); + if (cursorPosition >= lineStart && cursorPosition <= lineEnd) { + String lineText = text.substring(lineStart, lineEnd); + CommentToggleInfo ti = toggleLine(lineText); + String newLineText = ti.text; + int nonWs = ti.nonWs; + boolean hasComment = ti.delta < 0; + String before = text.substring(0, lineStart); + String after = lineEnd <= text.length() ? text.substring(lineEnd) : ""; + setText(before + newLineText + after); + int cursorDelta = hasComment ? -2 : 2; + int newCursor = cursorPosition + cursorDelta; + if (cursorPosition < lineStart + nonWs + (hasComment ? 2 : 0)) newCursor = cursorPosition; + setCursor(Math.max(lineStart, newCursor), false); + return; + } + } + } + private boolean isAltKeyDown() { return Keyboard.isKeyDown(56) || Keyboard.isKeyDown(184); } @@ -768,6 +829,35 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.cursorPosition = Math.max(0, Math.min(this.cursorPosition, length)); } + private static class CommentToggleInfo { + public final String text; + public final int delta; // newText.length() - oldText.length() + public final int nonWs; + + public CommentToggleInfo(String text, int delta, int nonWs) { + this.text = text; + this.delta = delta; + this.nonWs = nonWs; + } + } + + private CommentToggleInfo toggleLine(String lineText) { + int nonWs = 0; + while (nonWs < lineText.length() && Character.isWhitespace(lineText.charAt(nonWs))) nonWs++; + boolean hasContent = nonWs < lineText.length(); + boolean hasComment = hasContent && lineText.startsWith("//", nonWs); + String newLineText; + if (hasComment) { + int cut = Math.min(nonWs + 2, lineText.length()); + newLineText = lineText.substring(0, nonWs) + (cut <= lineText.length() ? lineText.substring(cut) : ""); + } else if (hasContent) { + newLineText = lineText.substring(0, nonWs) + "//" + lineText.substring(nonWs); + } else { + newLineText = lineText; + } + return new CommentToggleInfo(newLineText, newLineText.length() - lineText.length(), nonWs); + } + class UndoData { public String text; public int cursorPosition; From 0f29e654aa05dbef3b3461a1c6eb5805c777645a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 15:38:19 +0200 Subject: [PATCH 024/358] Fixed and refined broken line backspace logic --- .../client/gui/util/GuiScriptTextArea.java | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 54b09944e..eb722ff67 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -370,6 +370,16 @@ public class GuiScriptTextArea extends GuiNpcTextField { } if (i == Keyboard.KEY_BACK) { if (startSelection == endSelection && startSelection > 0) { + // If the character before the cursor is a newline, remove that single newline. + // This handles backspacing into empty lines (joining the previous and current lines) + // without relying on LineData start/end calculations. + if (startSelection - 1 >= 0 && startSelection - 1 < text.length() && text.charAt(startSelection - 1) == '\n') { + String before = text.substring(0, startSelection - 1); + String after = startSelection < text.length() ? text.substring(startSelection) : ""; + setText(before + after); + endSelection = cursorPosition = startSelection = startSelection - 1; + return true; + } // Check if we should remove an empty/whitespace-only line if (currentLine != null) { // Check if cursor is at line start or after only whitespace @@ -380,11 +390,21 @@ public class GuiScriptTextArea extends GuiNpcTextField { boolean lineOnlyWhitespace = currentLine.text.trim().isEmpty(); if (atLineStartOrWhitespace && lineOnlyWhitespace && currentLine.start > 0) { - // Remove the entire line including newline - String before = text.substring(0, currentLine.start - 1); // -1 to include the newline before - String after = currentLine.end + 1 > text.length() ? "" : text.substring(currentLine.end + 1); + // Remove the empty/whitespace-only line by deleting the substring + // from the start of this line up to and including the next newline (if any). + int removeStart = currentLine.start; + int removeEnd = text.indexOf('\n', removeStart); + if (removeEnd == -1) { + removeEnd = text.length(); + } else { + removeEnd = removeEnd + 1; // include the newline + } + String before = text.substring(0, removeStart); + String after = removeEnd > text.length() ? "" : text.substring(removeEnd); setText(before + after); - endSelection = cursorPosition = startSelection = currentLine.start - 1; + // Place cursor at end of previous line (just before where the removed newline was) + int newCursor = Math.max(0, removeStart - 1); + endSelection = cursorPosition = startSelection = newCursor; return true; } } From 32e82fc9c94f52c7883a9d4d6c58606b6edb303f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 17:37:42 +0200 Subject: [PATCH 025/358] temp indents --- .../client/gui/util/GuiScriptTextArea.java | 51 +++++ .../gui/util/script/JavaTextContainer.java | 182 +++++++++++++++++- .../client/gui/util/script/MethodBlock.java | 32 ++- 3 files changed, 257 insertions(+), 8 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index eb722ff67..314f3ced6 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -7,6 +7,7 @@ import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.util.script.JavaTextContainer; +import noppes.npcs.client.gui.util.script.MethodBlock; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; @@ -120,6 +121,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { List list = new ArrayList<>(container.lines); + // Get cached brace spans from container + List braceSpans = container.getBraceSpans(); + String wordHightLight = null; if (startSelection != endSelection) { Matcher m = container.regexWord.matcher(text); @@ -168,6 +172,40 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } int yPos = y + (i - scrolledLine) * container.lineHeight + 1; + int visStart = scrolledLine; + int visEnd = Math.min(list.size() - 1, scrolledLine + container.visibleLines - 1); + if (!braceSpans.isEmpty() && i == visStart) { + // compute maximum level in visible spans + int maxLevel = 0; + for (int[] s : braceSpans) { + if (s[2] < visStart || s[1] > visEnd) continue; + maxLevel = Math.max(maxLevel, s[0]); + } + for (int lvl = 1; lvl <= maxLevel; lvl++) { + int gx = x + 6 + (lvl - 1) * 8; + int spanStart = -1; + for (int li = visStart; li <= visEnd + 1; li++) { + boolean has = false; + if (li <= visEnd && li >= 0) { + for (int[] s : braceSpans) { + if (s[0] != lvl) continue; + if (li >= s[1] && li <= s[2]) { has = true; break; } + } + } + if (has) { + if (spanStart == -1) spanStart = li; + } else { + if (spanStart != -1) { + int topY = y + (spanStart - scrolledLine) * container.lineHeight + 3; + int bottomY = y + (li - 1 - scrolledLine) * container.lineHeight + container.lineHeight - 2; + drawRect(gx, topY, gx + 2, bottomY, 0x33FFFFFF); + spanStart = -1; + } + } + } + } + } + data.drawString(x + 1, yPos, 0xFFe0e0e0); if (active && isEnabled() && (cursorCounter / 6) % 2 == 0 && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { @@ -263,6 +301,19 @@ public class GuiScriptTextArea extends GuiNpcTextField { return this.container.text.length(); } + private int getLineIndex(List lines, int pos) { + if (lines == null || lines.isEmpty()) return 0; + int length = text == null ? 0 : text.length(); + for (int i = 0; i < lines.size(); i++) { + LineData d = lines.get(i); + int s = Math.max(0, Math.min(d.start, length)); + int e = Math.max(0, Math.min(d.end, length)); + if (pos >= s && pos <= e) return i; + } + return Math.max(0, lines.size() - 1); + } + + @Override public boolean textboxKeyTyped(char c, int i) { if (!active) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 7474657e2..b0bed6ea9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -17,7 +17,7 @@ public class JavaTextContainer extends TextContainer { "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof)\\b"); public static final Pattern TYPE_DECL = Pattern.compile("\\b([A-Za-z_][a-zA-Z0-9_]*)" + // Group 1 → main type - "\\s*(<([^>]+)>)?" + // Group 2 → <…>, Group 3 → inner type + "\\s*(<([^>]+)>)?" + // Group c2 → <…>, Group 3 → inner type "\\s+[a-zA-Z_][a-zA-Z0-9_]*" // variable name ); @@ -47,12 +47,102 @@ public class JavaTextContainer extends TextContainer { public int lineHeight; public int totalHeight; public int visibleLines = 1; + // Cached brace spans: list of {level, startLine, endLine} + private List braceSpansCache = null; public JavaTextContainer(String text) { super(text); this.text = text.replaceAll("\\r?\\n|\\r", "\n"); } + public synchronized List getBraceSpans() { + if (braceSpansCache != null) + return braceSpansCache; + List spans = new ArrayList<>(); + if (text == null || text.isEmpty() || lines == null || lines.isEmpty()) { + braceSpansCache = spans; + return spans; + } + + // Heuristics limits to avoid runaway spans when braces are unbalanced. + final int MAX_SPAN_LINES = 200; // cap span length to this many lines + final int MAX_STACK_DEPTH = 512; // avoid excessive stack growth + + List excluded = MethodBlock.getExcludedRanges(text); + int exIdx = 0; + java.util.ArrayList openStack = new java.util.ArrayList<>(); + int len = text.length(); + for (int p = 0; p < len; p++) { + while (exIdx < excluded.size() && p >= excluded.get(exIdx)[1]) + exIdx++; + if (exIdx < excluded.size()) { + int[] r = excluded.get(exIdx); + if (p >= r[0] && p < r[1]) { + p = r[1] - 1; + continue; + } + } + char c = text.charAt(p); + if (c == '{') { + if (openStack.size() < MAX_STACK_DEPTH) { + // Look ahead to see if this open has a plausible closing brace within limits. + int openLine = getLineIndex(p); + int lookaheadChars = Math.max(1000, MAX_SPAN_LINES * 120); + int searchEnd = Math.min(len, p + lookaheadChars); + boolean foundClose = false; + int exIdx2 = exIdx; // local index for scanning ahead + for (int q = p + 1; q < searchEnd; q++) { + while (exIdx2 < excluded.size() && q >= excluded.get(exIdx2)[1]) exIdx2++; + if (exIdx2 < excluded.size()) { + int[] r2 = excluded.get(exIdx2); + if (q >= r2[0] && q < r2[1]) { q = r2[1] - 1; continue; } + } + if (text.charAt(q) == '}') { + int closeLineCandidate = getLineIndex(q); + if (closeLineCandidate - openLine <= MAX_SPAN_LINES) { + foundClose = true; + break; + } + } + } + if (foundClose) openStack.add(p); + } + } else if (c == '}') { + if (!openStack.isEmpty()) { + int openPos = openStack.remove(openStack.size() - 1); + int level = openStack.size() + 1; + int openLine = getLineIndex(openPos); + int closeLine = getLineIndex(p); + // Cap spans to avoid drawing across the whole document when syntax is broken + if (closeLine - openLine > MAX_SPAN_LINES) { + closeLine = Math.min(openLine + MAX_SPAN_LINES, lines.size() - 1); + } + // Only add spans where closeLine > openLine (multi-line blocks) + if (closeLine > openLine) { + spans.add(new int[]{level, openLine, closeLine}); + } + } + } + } + + braceSpansCache = spans; + return spans; + } + + private int getLineIndex(int pos) { + if (lines == null || lines.isEmpty()) + return 0; + int length = text == null ? 0 : text.length(); + for (int i = 0; i < lines.size(); i++) { + LineData d = lines.get(i); + int s = Math.max(0, Math.min(d.start, length)); + int e = Math.max(0, Math.min(d.end, length)); + if (pos >= s && pos <= e) + return i; + } + return Math.max(0, lines.size() - 1); + } + public void init(int width, int height) { lineHeight = ClientProxy.Font.height(); if (lineHeight == 0) @@ -81,6 +171,9 @@ public class JavaTextContainer extends TextContainer { linesCount = lines.size(); totalHeight = linesCount * lineHeight; visibleLines = Math.max(height / lineHeight, 1); + + // Invalidate caches that depend on line layout + invalidateCaches(); } private List globalFields = new ArrayList<>(); @@ -153,6 +246,9 @@ public class JavaTextContainer extends TextContainer { } } + public synchronized void invalidateCaches() { + this.braceSpansCache = null; + } public void formatCodeText() { // Step 1: Tokenize the full text List marks = new ArrayList<>(); @@ -175,6 +271,14 @@ public class JavaTextContainer extends TextContainer { marks = resolveConflicts(marks); + // Compute indent guides based on matched braces, ignoring strings/comments + computeIndentGuides(marks); + + // Invalidate cached brace spans; will be recomputed on demand + invalidateCaches(); + + + // Step 2: Clear existing tokens for (LineData line : lines) { line.tokens.clear(); @@ -285,6 +389,7 @@ public class JavaTextContainer extends TextContainer { public String text; public int start, end; public List tokens = new ArrayList<>(); + public List indentCols = new ArrayList<>(); public LineData(String text, int startIndex, int end) { this.text = text; @@ -372,4 +477,79 @@ public class JavaTextContainer extends TextContainer { this.priority = priority; } } + + // Compute indent guide columns per line based on brace matching. + private void computeIndentGuides(List marks) { + // Clear existing guides + for (LineData ld : lines) { + ld.indentCols.clear(); + } + + // Build ignored ranges from STRING and COMMENT tokens + List ignored = new ArrayList<>(); + for (Mark m : marks) { + if (m.type == TokenType.STRING || m.type == TokenType.COMMENT) { + ignored.add(new int[]{m.start, m.end}); + } + } + + java.util.function.Predicate isIgnored = (pos) -> { + for (int[] r : ignored) { + if (pos >= r[0] && pos < r[1]) + return true; + } + return false; + }; + + class OpenBrace { + int line, col; + + OpenBrace(int l, int c) { + line = l; + col = c; + } + } + java.util.Deque stack = new java.util.ArrayDeque<>(); + + final int tabSize = 4; + + for (int li = 0; li < lines.size(); li++) { + LineData ld = lines.get(li); + String s = ld.text; + for (int i = 0; i < s.length(); i++) { + int absPos = ld.start + i; + if (isIgnored.test(absPos)) + continue; + char c = s.charAt(i); + if (c == '{') { + // compute leading indentation (expanded) up to this char + int leading = 0; + for (int k = 0; k < i; k++) { + char ch = s.charAt(k); + if (ch == '\t') + leading += tabSize; + else + leading += 1; + } + stack.push(new OpenBrace(li, leading)); + } else if (c == '}') { + if (!stack.isEmpty()) { + OpenBrace open = stack.pop(); + int startLine = open.line; + int col = open.col; + if (startLine == li) + continue; // same-line block, skip + // add guide column to lines inside the block + int from = Math.max(0, startLine + 1); + int to = Math.min(lines.size() - 1, li); + for (int l = from; l <= to; l++) { + List list = lines.get(l).indentCols; + if (!list.contains(col)) + list.add(col); + } + } + } + } + } + } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java index 6be400d3f..a85b2ce35 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java @@ -78,27 +78,45 @@ public class MethodBlock { return methods; } - // Find ranges of text to skip (strings and comments) - private static List findExcludedRanges(String text) { + // Find ranges of text to skip (strings and comments). Returns merged, sorted ranges. + public static List getExcludedRanges(String text) { List ranges = new ArrayList<>(); - // Find all strings using the pattern from JavaTextContainer + // Find all strings and char literals (simple heuristic) Pattern stringPattern = Pattern.compile("([\"'])(?:(?=(\\\\?))\\2.)*?\\1"); Matcher stringMatcher = stringPattern.matcher(text); while (stringMatcher.find()) { ranges.add(new int[] { stringMatcher.start(), stringMatcher.end() }); } - // Find all comments - Pattern commentPattern = Pattern.compile("/\\*[\\s\\S]*?(?:\\*/|$)|//.*|#.*"); + // Find all comments (block and line comments) + Pattern commentPattern = Pattern.compile("/\\*[\\s\\S]*?(?:\\*/|$)|//.*"); Matcher commentMatcher = commentPattern.matcher(text); while (commentMatcher.find()) { ranges.add(new int[] { commentMatcher.start(), commentMatcher.end() }); } + if (ranges.isEmpty()) + return ranges; + // Sort ranges by start position ranges.sort((a, b) -> Integer.compare(a[0], b[0])); - return ranges; + + // Merge overlapping/adjacent ranges for faster scanning + List merged = new ArrayList<>(); + int[] current = ranges.get(0); + for (int i = 1; i < ranges.size(); i++) { + int[] next = ranges.get(i); + if (next[0] <= current[1]) { + // overlap or adjacent + current[1] = Math.max(current[1], next[1]); + } else { + merged.add(current); + current = next; + } + } + merged.add(current); + return merged; } // Check if a position is within an excluded range @@ -114,7 +132,7 @@ public class MethodBlock { // Utility to find the matching closing brace, ignoring braces in strings and // comments public static int findMatchingBrace(String text, int openBraceIndex) { - List excludedRanges = findExcludedRanges(text); + List excludedRanges = getExcludedRanges(text); int depth = 0; for (int i = openBraceIndex; i < text.length(); i++) { From ae598003a8155e64670481ade01fdd2f22796503 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 18:13:29 +0200 Subject: [PATCH 026/358] Better indents MK.I --- .../client/gui/util/GuiScriptTextArea.java | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index eb722ff67..8bee0ace9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -120,6 +120,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { List list = new ArrayList<>(container.lines); + // Build brace spans: depth, open line, close line (inclusive) + List braceSpans = computeBraceSpans(text, list); + String wordHightLight = null; if (startSelection != endSelection) { Matcher m = container.regexWord.matcher(text); @@ -168,6 +171,40 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } int yPos = y + (i - scrolledLine) * container.lineHeight + 1; + + // Draw indent guides once per visible block based on brace spans + if (i == scrolledLine && !braceSpans.isEmpty()) { + int visStart = scrolledLine; + int visEnd = Math.min(list.size() - 1, scrolledLine + container.visibleLines - 1); + for (int[] span : braceSpans) { + int depth = span[0]; + int openLine = span[1]; + int closeLine = span[2]; + // Skip top-level (depth 1) to avoid class/global fields guides + if (depth <= 1) + continue; + int startLine = openLine + 1; // start under the opening brace + int endLine = closeLine - 1; // stop before the closing brace + if (startLine > endLine) + continue; + if (endLine < visStart || startLine > visEnd) + continue; + + int drawStart = Math.max(startLine, visStart); + int drawEnd = Math.min(endLine, visEnd); + // Compute horizontal position: 4 spaces per indent level, minus a tiny left offset + int spaces = (depth - 1) * 4; + StringBuilder sb = new StringBuilder(); + for (int k = 0; k < spaces; k++) + sb.append(' '); + int px = ClientProxy.Font.width(sb.toString()); + int gx = x + 4 + px - 2; // shift left ~2px for the IntelliJ feel + + int topY = y + (drawStart - scrolledLine) * container.lineHeight; + int bottomY = y + (drawEnd - scrolledLine + 1) * container.lineHeight - 2; + drawRect(gx, topY, gx + 1, bottomY, 0x33FFFFFF); + } + } data.drawString(x + 1, yPos, 0xFFe0e0e0); if (active && isEnabled() && (cursorCounter / 6) % 2 == 0 && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { @@ -227,6 +264,41 @@ public class GuiScriptTextArea extends GuiNpcTextField { return 0; } + // Computes brace spans as {depth, openLineIndex, closeLineIndex}, inclusive of brace lines + private List computeBraceSpans(String fullText, List lines) { + List spans = new ArrayList<>(); + if (fullText == null || fullText.isEmpty() || lines == null || lines.isEmpty()) + return spans; + + int depth = 0; + int lineIdx = 0; + int lineEnd = lines.get(0).end; + java.util.ArrayList openStack = new java.util.ArrayList<>(); // store open line indices + + for (int pos = 0; pos < fullText.length(); pos++) { + // advance line index + while (lineIdx < lines.size() - 1 && pos >= lineEnd) { + lineIdx++; + lineEnd = lines.get(lineIdx).end; + } + + char c = fullText.charAt(pos); + if (c == '{') { + openStack.add(lineIdx); + depth++; + } else if (c == '}') { + if (!openStack.isEmpty()) { + int openLine = openStack.remove(openStack.size() - 1); + int spanDepth = depth; // depth before decrement + int closeLine = lineIdx; + spans.add(new int[]{spanDepth, openLine, closeLine}); + } + depth = Math.max(0, depth - 1); + } + } + return spans; + } + private int getSelectionPos(int xMouse, int yMouse) { xMouse -= this.x + 1; yMouse -= this.y + 1; From fb7bd685d4d873abe485b5443a6c56ab179055c2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 18:35:26 +0200 Subject: [PATCH 027/358] Enter account for indent level --- .../client/gui/util/GuiScriptTextArea.java | 127 +++++++++++++++++- 1 file changed, 120 insertions(+), 7 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 8bee0ace9..4d35addd8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -270,32 +270,105 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (fullText == null || fullText.isEmpty() || lines == null || lines.isEmpty()) return spans; - int depth = 0; int lineIdx = 0; int lineEnd = lines.get(0).end; - java.util.ArrayList openStack = new java.util.ArrayList<>(); // store open line indices + List openStack = new ArrayList<>(); // line indices of unmatched opens + List unmatchedOpenLines = new ArrayList<>(); + + boolean inLineComment = false; + boolean inBlockComment = false; + boolean inString = false; + boolean escape = false; + char stringDelimiter = 0; for (int pos = 0; pos < fullText.length(); pos++) { - // advance line index while (lineIdx < lines.size() - 1 && pos >= lineEnd) { lineIdx++; lineEnd = lines.get(lineIdx).end; } char c = fullText.charAt(pos); + char next = pos + 1 < fullText.length() ? fullText.charAt(pos + 1) : 0; + + if (inString) { + if (escape) { + escape = false; + } else if (c == '\\') { + escape = true; + } else if (c == stringDelimiter) { + inString = false; + } + continue; + } + + if (inBlockComment) { + if (c == '*' && next == '/') { + inBlockComment = false; + pos++; + } + continue; + } + + if (inLineComment) { + if (c == '\n') { + inLineComment = false; + } + continue; + } + + if (c == '/' && next == '/') { + inLineComment = true; + pos++; + continue; + } + if (c == '/' && next == '*') { + inBlockComment = true; + pos++; + continue; + } + if (c == '"' || c == '\'') { + inString = true; + stringDelimiter = c; + escape = false; + continue; + } + if (c == '{') { openStack.add(lineIdx); - depth++; } else if (c == '}') { if (!openStack.isEmpty()) { int openLine = openStack.remove(openStack.size() - 1); - int spanDepth = depth; // depth before decrement + int spanDepth = openStack.size() + 1; int closeLine = lineIdx; spans.add(new int[]{spanDepth, openLine, closeLine}); } - depth = Math.max(0, depth - 1); } } + + if (!openStack.isEmpty()) { + unmatchedOpenLines.addAll(openStack); + unmatchedOpenLines.sort(Integer::compareTo); + + List adjusted = new ArrayList<>(spans.size()); + for (int[] span : spans) { + int openLine = span[1]; + int adjust = 0; + for (int unmatched : unmatchedOpenLines) { + if (unmatched <= openLine) { + adjust++; + } else { + break; + } + } + + int newDepth = span[0] - adjust; + if (newDepth > 0) { + adjusted.add(new int[]{newDepth, openLine, span[2]}); + } + } + spans = adjusted; + } + return spans; } @@ -451,6 +524,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(before + after); endSelection = cursorPosition = startSelection = startSelection - 1; return true; + } // Check if we should remove an empty/whitespace-only line if (currentLine != null) { @@ -537,7 +611,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { addText(" "); } if (i == Keyboard.KEY_RETURN) { - addText(Character.toString('\n') + getIndentCurrentLine()); + addText(Character.toString('\n') + getAutoIndentForEnter()); } if (ChatAllowedCharacters.isAllowedCharacter(c)) { addText(Character.toString(c)); @@ -753,6 +827,45 @@ public class GuiScriptTextArea extends GuiNpcTextField { return data.text.substring(0, i); } + private String getAutoIndentForEnter() { + LineData currentLine = null; + for (LineData line : this.container.lines) { + if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) { + currentLine = line; + break; + } + } + + if (currentLine == null) { + return ""; + } + + String lineText = currentLine.text; + int leading = 0; + while (leading < lineText.length() && (lineText.charAt(leading) == ' ' || lineText.charAt(leading) == '\t')) { + leading++; + } + + String baseIndent = lineText.substring(0, leading); + int relativeCursor = Math.max(0, Math.min(this.cursorPosition - currentLine.start, lineText.length())); + String beforeCursor = lineText.substring(0, relativeCursor); + + int lastNonWs = -1; + for (int idx = beforeCursor.length() - 1; idx >= 0; idx--) { + if (!Character.isWhitespace(beforeCursor.charAt(idx))) { + lastNonWs = idx; + break; + } + } + + boolean opensBlock = lastNonWs >= 0 && beforeCursor.charAt(lastNonWs) == '{'; + if (opensBlock) { + return baseIndent + " "; + } + + return baseIndent; + } + private void setCursor(int i, boolean select) { i = Math.min(Math.max(i, 0), this.text.length()); if (i != this.cursorPosition) { From e6423a6de06377c64e9b10b368825c8a8b6fc299 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 20:06:47 +0200 Subject: [PATCH 028/358] Huge Text Editor Update: 1) Finished Indent levels rendering & logic 2) Added Format Code bind (SHIFT+F) 3) Added auto-completion to "", '', [] and () pairs and auto-generated brace {} scaffolds for if/try/catch/method blocks with proper indent level. 4) Enter/Backspace/Up/Down now account for current indent level 5) Tab/SHIF+TAB now cycle between next/previous indent levels accordingly --- .../client/gui/util/GuiScriptTextArea.java | 404 ++++++++++++++++-- 1 file changed, 373 insertions(+), 31 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 4d35addd8..db70bb46c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -120,7 +120,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { List list = new ArrayList<>(container.lines); - // Build brace spans: depth, open line, close line (inclusive) + // Build brace spans: {origDepth, open line, close line, adjustedDepth} List braceSpans = computeBraceSpans(text, list); String wordHightLight = null; @@ -177,11 +177,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { int visStart = scrolledLine; int visEnd = Math.min(list.size() - 1, scrolledLine + container.visibleLines - 1); for (int[] span : braceSpans) { - int depth = span[0]; + int originalDepth = span[0]; int openLine = span[1]; int closeLine = span[2]; - // Skip top-level (depth 1) to avoid class/global fields guides - if (depth <= 1) + int depth = span.length > 3 ? span[3] : originalDepth; + // Skip top-level (depth 1) using the original depth to avoid hiding nested guides when adjusted + if (originalDepth <= 1) continue; int startLine = openLine + 1; // start under the opening brace int endLine = closeLine - 1; // stop before the closing brace @@ -193,7 +194,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { int drawStart = Math.max(startLine, visStart); int drawEnd = Math.min(endLine, visEnd); // Compute horizontal position: 4 spaces per indent level, minus a tiny left offset - int spaces = (depth - 1) * 4; + int safeDepth = Math.max(1, depth); + int spaces = (safeDepth - 1) * 4; StringBuilder sb = new StringBuilder(); for (int k = 0; k < spaces; k++) sb.append(' '); @@ -264,7 +266,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { return 0; } - // Computes brace spans as {depth, openLineIndex, closeLineIndex}, inclusive of brace lines + // Computes brace spans as {originalDepth, openLineIndex, closeLineIndex, adjustedDepth}, inclusive of brace lines private List computeBraceSpans(String fullText, List lines) { List spans = new ArrayList<>(); if (fullText == null || fullText.isEmpty() || lines == null || lines.isEmpty()) @@ -273,7 +275,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { int lineIdx = 0; int lineEnd = lines.get(0).end; List openStack = new ArrayList<>(); // line indices of unmatched opens - List unmatchedOpenLines = new ArrayList<>(); boolean inLineComment = false; boolean inBlockComment = false; @@ -297,6 +298,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { escape = true; } else if (c == stringDelimiter) { inString = false; + } else if (c == '\n') { + // Treat unterminated strings as ending at EOL to avoid breaking later brace parsing + inString = false; } continue; } @@ -345,28 +349,20 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } + // Normalize depths to reduce impact of unmatched opens without hiding nested guides if (!openStack.isEmpty()) { - unmatchedOpenLines.addAll(openStack); - unmatchedOpenLines.sort(Integer::compareTo); - + int baseline = openStack.size(); List adjusted = new ArrayList<>(spans.size()); for (int[] span : spans) { - int openLine = span[1]; - int adjust = 0; - for (int unmatched : unmatchedOpenLines) { - if (unmatched <= openLine) { - adjust++; - } else { - break; - } - } - - int newDepth = span[0] - adjust; - if (newDepth > 0) { - adjusted.add(new int[]{newDepth, openLine, span[2]}); - } + int adjustedDepth = Math.max(1, span[0] - baseline); + adjusted.add(new int[]{span[0], span[1], span[2], adjustedDepth}); } spans = adjusted; + } else { + for (int idx = 0; idx < spans.size(); idx++) { + int[] span = spans.get(idx); + spans.set(idx, new int[]{span[0], span[1], span[2], span[0]}); + } } return spans; @@ -453,7 +449,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { j = m.start(); } } - int newPos = Math.min(cursorPosition + j, text.length()); //-2 for the \n + int newPos = Math.min(cursorPosition + j, text.length()); setCursor(newPos, GuiScreen.isShiftKeyDown()); return true; } @@ -558,6 +554,50 @@ public class GuiScriptTextArea extends GuiNpcTextField { String s = getSelectionBeforeText(); if (startSelection > 0 && startSelection == endSelection) { + LineData curr = null; + for (LineData line : container.lines) { + if (cursorPosition >= line.start && cursorPosition <= line.end) { + curr = line; + break; + } + } + if (curr != null) { + int col = cursorPosition - curr.start; + int indent = getLineIndent(curr.text); + if (col <= indent && startSelection - 1 >= 0 && text.charAt(startSelection - 1) == '\n') { + // At or before indent: jump to previous line indent by removing newline + LineData prev = null; + int idx = container.lines.indexOf(curr); + if (idx > 0) + prev = container.lines.get(idx - 1); + String before = text.substring(0, startSelection - 1); + String after = startSelection < text.length() ? text.substring(startSelection) : ""; + setText(before + after); + int newCursor = startSelection - 1; + if (prev != null) { + int prevIndent = getLineIndent(prev.text); + newCursor = prev.start + Math.min(prevIndent, Math.max(0, prev.text.length())); + } + endSelection = cursorPosition = startSelection = Math.max(0, newCursor); + return true; + } + } + // If deleting an opening bracket/quote and the immediate next char is the matching closer, + // remove both so the auto-inserted closer doesn't remain orphaned. + if (startSelection > 0 && startSelection < text.length()) { + char prev = text.charAt(startSelection - 1); + char nextc = text.charAt(startSelection); + boolean removedPair = false; + if ((prev == '(' && nextc == ')') || (prev == '[' && nextc == ']') || (prev == '{' && nextc == '}') || (prev == '\'' && nextc == '\'') || (prev == '"' && nextc == '"')) { + String before = text.substring(0, startSelection - 1); + String after = startSelection + 1 < text.length() ? text.substring(startSelection + 1) : ""; + setText(before + after); + startSelection -= 1; + endSelection = cursorPosition = startSelection; + return true; + } + } + s = s.substring(0, s.length() - 1); startSelection--; } @@ -608,12 +648,93 @@ public class GuiScriptTextArea extends GuiNpcTextField { return true; } if (i == Keyboard.KEY_TAB) { - addText(" "); + boolean shift = isShiftKeyDown(); + if (shift) { + handleShiftTab(); + } else { + handleTab(); + } + } + if (i == Keyboard.KEY_F && isCtrlKeyDown()) { + formatText(); + return true; } if (i == Keyboard.KEY_RETURN) { - addText(Character.toString('\n') + getAutoIndentForEnter()); + if (cursorPosition > 0 && cursorPosition <= text.length() && text.charAt(cursorPosition - 1) == '{') { + String indent = getIndentCurrentLine(); + String childIndent = indent + " "; + String before = getSelectionBeforeText(); + String after = getSelectionAfterText(); + + // Find the next non-whitespace character and its indent to decide if this brace is already closed at the same scope. + int nextNonWsIndex = -1; + for (int idx = 0; idx < after.length(); idx++) { + char ch = after.charAt(idx); + if (!Character.isWhitespace(ch)) { + nextNonWsIndex = idx; + break; + } + } + + boolean hasMatchingCloseSameIndent = false; + if (nextNonWsIndex >= 0 && after.charAt(nextNonWsIndex) == '}') { + // Compute indent of that closing brace + int lineStart = after.lastIndexOf('\n', nextNonWsIndex); + int indentLen = 0; + for (int k = lineStart + 1; k < after.length(); k++) { + char ch = after.charAt(k); + if (ch == ' ') indentLen++; else if (ch == '\t') indentLen += 4; else break; + if (k == nextNonWsIndex) break; + } + if (indentLen == indent.length()) { + hasMatchingCloseSameIndent = true; + } + } + + if (hasMatchingCloseSameIndent) { + addText("\n" + childIndent); + } else { + String insert = "\n" + childIndent + "\n" + indent + "}"; + setText(before + insert + after); + int newCursor = before.length() + 1 + childIndent.length(); + startSelection = endSelection = cursorPosition = newCursor; + } + } else { + addText(Character.toString('\n') + getAutoIndentForEnter()); + } } if (ChatAllowedCharacters.isAllowedCharacter(c)) { + String before = getSelectionBeforeText(); + String after = getSelectionAfterText(); + + // If typing a closing char and the next char is already that closer, skip over it + if ((c == ')' || c == ']' || c == '"' || c == '\'' ) && after.length() > 0 && after.charAt(0) == c) { + // move cursor forward over existing closer + startSelection = endSelection = cursorPosition = before.length() + 1; + return true; + } + + if (c == '"') { + setText(before + "\"\"" + after); + startSelection = endSelection = cursorPosition = before.length() + 1; + return true; + } + if (c == '\'') { + setText(before + "''" + after); + startSelection = endSelection = cursorPosition = before.length() + 1; + return true; + } + if (c == '[') { + setText(before + "[]" + after); + startSelection = endSelection = cursorPosition = before.length() + 1; + return true; + } + if (c == '(') { + setText(before + "()" + after); + startSelection = endSelection = cursorPosition = before.length() + 1; + return true; + } + addText(Character.toString(c)); } @@ -866,6 +987,219 @@ public class GuiScriptTextArea extends GuiNpcTextField { return baseIndent; } + private int getLineIndent(String lineText) { + int leading = 0; + while (leading < lineText.length() && (lineText.charAt(leading) == ' ' || lineText.charAt( + leading) == '\t')) { + leading++; + } + return leading; + } + + private int getTabSize() { + return 4; + } + + private String repeatSpace(int count) { + if (count <= 0) + return ""; + char[] arr = new char[count]; + java.util.Arrays.fill(arr, ' '); + return new String(arr); + } + + private void formatText() { + String[] linesArr = text.split("\n", -1); + StringBuilder out = new StringBuilder(text.length() + 32); + + boolean inString = false; + boolean escape = false; + boolean inBlockComment = false; + int depth = 0; + int tab = getTabSize(); + + for (int li = 0; li < linesArr.length; li++) { + String line = linesArr[li]; + String trimmedLeading = line.replaceAll("^[ \t]+", ""); + + int lineIndentDec = 0; + int opens = 0, closes = 0; + boolean startsWithClose = false; + + // First pass to determine startsWithClose and brace counts ignoring strings/comments + for (int idx = 0; idx < line.length(); idx++) { + char c = line.charAt(idx); + char next = idx + 1 < line.length() ? line.charAt(idx + 1) : 0; + + if (inString) { + if (escape) { + escape = false; + } else if (c == '\\') { + escape = true; + } else if (c == '"') { + inString = false; + } + continue; + } + + if (inBlockComment) { + if (c == '*' && next == '/') { + inBlockComment = false; + idx++; + } + continue; + } + + if (c == '/' && next == '/') { + break; // rest of line is comment + } + if (c == '/' && next == '*') { + inBlockComment = true; + idx++; + continue; + } + if (c == '"') { + inString = true; + escape = false; + continue; + } + + if (c == '{') { + opens++; + } else if (c == '}') { + closes++; + if (!startsWithClose) { + String prefix = line.substring(0, idx); + if (prefix.trim().isEmpty()) { + startsWithClose = true; + } + } + } + } + + int indentLevel = depth; + if (startsWithClose) + indentLevel = Math.max(0, indentLevel - 1); + + int targetIndent = indentLevel * tab; + out.append(repeatSpace(targetIndent)).append(trimmedLeading); + if (li < linesArr.length - 1) + out.append('\n'); + + depth = Math.max(0, depth + opens - closes); + } + + setText(out.toString()); + } + + private void handleTab() { + LineData currentLine = null; + for (LineData line : this.container.lines) { + if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) { + currentLine = line; + break; + } + } + if (currentLine == null) { + addText(" "); + return; + } + int tab = getTabSize(); + int indentLen = getLineIndent(currentLine.text); + int textStartPos = currentLine.start + indentLen; + + if (this.cursorPosition <= textStartPos) { + // Cursor before any text: if cursor is exactly at text start, move forward to next tab stop. + // If cursor is inside leading whitespace (before text start), choose nearest tab stop (tie -> forward). + int targetIndent; + if (this.cursorPosition == textStartPos) { + targetIndent = ((indentLen / tab) + 1) * tab; + } else { + int remainder = indentLen % tab; + if (remainder == 0) { + targetIndent = indentLen + tab; // already aligned -> next level + } else { + int down = indentLen - remainder; + int up = indentLen + (tab - remainder); + int distDown = remainder; + int distUp = tab - remainder; + if (distUp < distDown) targetIndent = up; + else if (distUp > distDown) targetIndent = down; + else targetIndent = up; // tie -> forward + } + } + if (targetIndent < 0) targetIndent = 0; + String newIndent = repeatSpace(targetIndent); + String rest = currentLine.text.substring(indentLen); + String before = text.substring(0, currentLine.start); + int contentEnd = Math.min(currentLine.start + currentLine.text.length(), text.length()); + int sepEnd = Math.min(currentLine.end, text.length()); + String sep = contentEnd < sepEnd ? text.substring(contentEnd, sepEnd) : ""; + String after = text.substring(sepEnd); + setText(before + newIndent + rest + sep + after); + int newCursor = currentLine.start + targetIndent; + startSelection = endSelection = cursorPosition = Math.min(newCursor, this.text.length()); + } else { + // Cursor is after start of text: insert spaces at cursor to move following text to next tab stop + int column = this.cursorPosition - currentLine.start; + int targetColumn = ((column / tab) + 1) * tab; + int toInsert = Math.max(0, targetColumn - column); + if (toInsert > 0) { + String spaces = repeatSpace(toInsert); + addText(spaces); + } + } + } + + private void handleShiftTab() { + LineData currentLine = null; + for (LineData line : this.container.lines) { + if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) { + currentLine = line; + break; + } + } + if (currentLine == null) + return; + int tab = getTabSize(); + int indentLen = getLineIndent(currentLine.text); + int textStartPos = currentLine.start + indentLen; + + if (this.cursorPosition <= textStartPos) { + // Cursor before any text: reduce leading indent to previous tab stop + int targetIndent = Math.max(0, ((indentLen - 1) / tab) * tab); + String newIndent = repeatSpace(targetIndent); + String rest = currentLine.text.substring(indentLen); + String before = text.substring(0, currentLine.start); + int contentEnd = Math.min(currentLine.start + currentLine.text.length(), text.length()); + int sepEnd = Math.min(currentLine.end, text.length()); + String sep = contentEnd < sepEnd ? text.substring(contentEnd, sepEnd) : ""; + String after = text.substring(sepEnd); + setText(before + newIndent + rest + sep + after); + int newCursor = currentLine.start + targetIndent; + startSelection = endSelection = cursorPosition = Math.min(newCursor, this.text.length()); + } else { + // Cursor after start of text: remove up to previous tab stop worth of spaces immediately before cursor + int column = this.cursorPosition - currentLine.start; + int mod = column % tab; + int toRemove = mod == 0 ? tab : mod; + int removed = 0; + int pos = this.cursorPosition - 1; + while (pos >= currentLine.start && removed < toRemove && text.charAt(pos) == ' ') { + pos--; + removed++; + } + if (removed > 0) { + int removeStart = pos + 1; + String before = text.substring(0, removeStart); + String after = text.substring(this.cursorPosition); + setText(before + after); + int newCursor = removeStart; + startSelection = endSelection = cursorPosition = Math.min(newCursor, this.text.length()); + } + } + } + private void setCursor(int i, boolean select) { i = Math.min(Math.max(i, 0), this.text.length()); if (i != this.cursorPosition) { @@ -905,8 +1239,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { return 0; } - int var10000 = this.cursorPosition - data.start; - return this.getSelectionPos(this.x + 1 + ClientProxy.Font.width(data.text.substring(0, this.cursorPosition - data.start)), this.y + 1 + (i - 1 - this.scrolledLine) * this.container.lineHeight); + int column = this.cursorPosition - data.start; + LineData target = this.container.lines.get(i - 1); + int targetPos = this.getSelectionPos(this.x + 1 + ClientProxy.Font.width(data.text.substring(0, column)), this.y + 1 + (i - 1 - this.scrolledLine) * this.container.lineHeight); + int targetIndent = getLineIndent(target.text); + int minPos = target.start + Math.min(targetIndent, Math.max(0, target.text.length())); + return Math.max(minPos, targetPos); } } @@ -919,8 +1257,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { for (int i = 0; i < this.container.lines.size(); ++i) { LineData data = (LineData) this.container.lines.get(i); if (this.cursorPosition >= data.start && this.cursorPosition < data.end) { - int var10000 = this.cursorPosition - data.start; - return this.getSelectionPos(this.x + 1 + ClientProxy.Font.width(data.text.substring(0, this.cursorPosition - data.start)), this.y + 1 + (i + 1 - this.scrolledLine) * this.container.lineHeight); + int column = this.cursorPosition - data.start; + LineData target = this.container.lines.get(Math.min(i + 1, this.container.lines.size() - 1)); + int targetPos = this.getSelectionPos(this.x + 1 + ClientProxy.Font.width(data.text.substring(0, column)), this.y + 1 + (i + 1 - this.scrolledLine) * this.container.lineHeight); + int targetIndent = getLineIndent(target.text); + int minPos = target.start + Math.min(targetIndent, Math.max(0, target.text.length())); + return Math.max(minPos, targetPos); } } From caab5da3bc48ab6f6fed470b0f84bf1a679ae743 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 20:20:31 +0200 Subject: [PATCH 029/358] Having cursor after braces also highlights them --- .../client/gui/util/GuiScriptTextArea.java | 59 +++++++++++++------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index db70bb46c..e97e24c31 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -95,26 +95,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { scrolledLine = Math.min(Math.max((int) (1f * diff * (yMouse - y) / height), 0), diff); } int startBracket = 0, endBracket = 0; - if (startSelection >= 0 && startSelection < text.length() && + if (startSelection >= 0 && text != null && text.length() > 0 && (endSelection - startSelection == 1 || startSelection == endSelection)) { - char c = text.charAt(startSelection); - int found = 0; - if (c == '{') { - found = findClosingBracket(text.substring(startSelection), '{', '}'); - } else if (c == '[') { - found = findClosingBracket(text.substring(startSelection), '[', ']'); - } else if (c == '(') { - found = findClosingBracket(text.substring(startSelection), '(', ')'); - } else if (c == '}') { - found = findOpeningBracket(text.substring(0, startSelection + 1), '{', '}'); - } else if (c == ']') { - found = findOpeningBracket(text.substring(0, startSelection + 1), '[', ']'); - } else if (c == ')') { - found = findOpeningBracket(text.substring(0, startSelection + 1), '(', ')'); - } - if (found != 0) { - startBracket = startSelection; - endBracket = startSelection + found; + int[] span = findBracketSpanAt(startSelection); + if (span != null) { + startBracket = span[0]; + endBracket = span[1]; } } @@ -368,7 +354,41 @@ public class GuiScriptTextArea extends GuiNpcTextField { return spans; } + // Find a bracket span for a given cursor position. Returns {start, end} or null. + private int[] findBracketSpanAt(int pos) { + if (text == null || text.isEmpty()) return null; + // First try at pos (caret before the char) + if (pos >= 0 && pos < text.length()) { + char c = text.charAt(pos); + int found = 0; + if (c == '{') found = findClosingBracket(text.substring(pos), '{', '}'); + else if (c == '[') found = findClosingBracket(text.substring(pos), '[', ']'); + else if (c == '(') found = findClosingBracket(text.substring(pos), '(', ')'); + else if (c == '}') found = findOpeningBracket(text.substring(0, pos + 1), '{', '}'); + else if (c == ']') found = findOpeningBracket(text.substring(0, pos + 1), '[', ']'); + else if (c == ')') found = findOpeningBracket(text.substring(0, pos + 1), '(', ')'); + if (found != 0) return new int[]{pos, pos + found}; + } + + // Then try immediately before pos (caret right after a char) + if (pos > 0 && pos - 1 < text.length()) { + int probe = pos - 1; + char c2 = text.charAt(probe); + int found2 = 0; + if (c2 == '{') found2 = findClosingBracket(text.substring(probe), '{', '}'); + else if (c2 == '[') found2 = findClosingBracket(text.substring(probe), '[', ']'); + else if (c2 == '(') found2 = findClosingBracket(text.substring(probe), '(', ')'); + else if (c2 == '}') found2 = findOpeningBracket(text.substring(0, probe + 1), '{', '}'); + else if (c2 == ']') found2 = findOpeningBracket(text.substring(0, probe + 1), '[', ']'); + else if (c2 == ')') found2 = findOpeningBracket(text.substring(0, probe + 1), '(', ')'); + if (found2 != 0) return new int[]{probe, probe + found2}; + } + + return null; + } private int getSelectionPos(int xMouse, int yMouse) { + + xMouse -= this.x + 1; yMouse -= this.y + 1; ArrayList list = new ArrayList(this.container.lines); @@ -441,6 +461,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setCursor(newPos, GuiScreen.isShiftKeyDown()); return true; } + if (i == Keyboard.KEY_RIGHT) { int j = 1; if (isCtrlKeyDown()) { From cddf2c2d60ffac336269fbe57f51f49b55a1639d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 20:59:54 +0200 Subject: [PATCH 030/358] Improved {} scaffold creation logic + tab indent bug fixes --- .../client/gui/util/GuiScriptTextArea.java | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index e97e24c31..a88171f94 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -440,7 +440,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { LineData currentLine = null; for (LineData line : this.container.lines) { - if (startSelection >= line.start && startSelection <= line.end) { + if (startSelection >= line.start && startSelection < line.end) { currentLine = line; break; } @@ -577,7 +577,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (startSelection > 0 && startSelection == endSelection) { LineData curr = null; for (LineData line : container.lines) { - if (cursorPosition >= line.start && cursorPosition <= line.end) { + if (cursorPosition >= line.start && cursorPosition < line.end) { curr = line; break; } @@ -687,29 +687,40 @@ public class GuiScriptTextArea extends GuiNpcTextField { String before = getSelectionBeforeText(); String after = getSelectionAfterText(); - // Find the next non-whitespace character and its indent to decide if this brace is already closed at the same scope. - int nextNonWsIndex = -1; - for (int idx = 0; idx < after.length(); idx++) { - char ch = after.charAt(idx); - if (!Character.isWhitespace(ch)) { - nextNonWsIndex = idx; - break; - } - } - + // Determine whether this opening brace already has a matching closing + // brace at the same scope (and indent). Prefer using the brace-span + // computation which skips strings/comments and handles nesting. boolean hasMatchingCloseSameIndent = false; - if (nextNonWsIndex >= 0 && after.charAt(nextNonWsIndex) == '}') { - // Compute indent of that closing brace - int lineStart = after.lastIndexOf('\n', nextNonWsIndex); - int indentLen = 0; - for (int k = lineStart + 1; k < after.length(); k++) { - char ch = after.charAt(k); - if (ch == ' ') indentLen++; else if (ch == '\t') indentLen += 4; else break; - if (k == nextNonWsIndex) break; + try { + // Find the line index containing the opening brace (cursorPosition-1) + int openLineIdx = -1; + int bracePos = cursorPosition - 1; + for (int li = 0; li < this.container.lines.size(); li++) { + LineData ld = this.container.lines.get(li); + if (bracePos >= ld.start && bracePos < ld.end) { + openLineIdx = li; + break; + } } - if (indentLen == indent.length()) { - hasMatchingCloseSameIndent = true; + + if (openLineIdx >= 0) { + List spans = computeBraceSpans(text, this.container.lines); + for (int[] span : spans) { + int spanOpen = span[1]; + int spanClose = span[2]; + if (spanOpen == openLineIdx) { + // Found a matching close; check its indent equals current indent + int closeIndent = getLineIndent(this.container.lines.get(spanClose).text); + if (closeIndent == indent.length()) { + hasMatchingCloseSameIndent = true; + } + break; + } + } } + } catch (Exception ex) { + // Fallback: if anything goes wrong, conservatively behave as before + hasMatchingCloseSameIndent = false; } if (hasMatchingCloseSameIndent) { @@ -795,7 +806,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } else { // Single line duplication (existing logic) for (LineData line : container.lines) { - if (cursorPosition >= line.start && cursorPosition <= line.end) { + if (cursorPosition >= line.start && cursorPosition < line.end) { int lineStart = line.start, lineEnd = line.end; String lineText = text.substring(lineStart, lineEnd); String insertText = "\n" + lineText; @@ -972,7 +983,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { private String getAutoIndentForEnter() { LineData currentLine = null; for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) { + if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { currentLine = line; break; } @@ -1116,7 +1127,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void handleTab() { LineData currentLine = null; for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) { + if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { currentLine = line; break; } @@ -1175,7 +1186,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void handleShiftTab() { LineData currentLine = null; for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) { + if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { currentLine = line; break; } From a9a2ce368c4b4ba32a6550901a20a62d3440db54 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 21:59:35 +0200 Subject: [PATCH 031/358] brain hurt probably temp gonna reroll this back --- .../client/gui/util/GuiScriptTextArea.java | 137 ++++++++++-------- 1 file changed, 79 insertions(+), 58 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index a88171f94..a37444c0a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -532,49 +532,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { } if (i == Keyboard.KEY_BACK) { if (startSelection == endSelection && startSelection > 0) { - // If the character before the cursor is a newline, remove that single newline. - // This handles backspacing into empty lines (joining the previous and current lines) - // without relying on LineData start/end calculations. - if (startSelection - 1 >= 0 && startSelection - 1 < text.length() && text.charAt(startSelection - 1) == '\n') { - String before = text.substring(0, startSelection - 1); - String after = startSelection < text.length() ? text.substring(startSelection) : ""; - setText(before + after); - endSelection = cursorPosition = startSelection = startSelection - 1; - return true; - - } - // Check if we should remove an empty/whitespace-only line - if (currentLine != null) { - // Check if cursor is at line start or after only whitespace - String beforeCursor = text.substring(currentLine.start, startSelection); - boolean atLineStartOrWhitespace = beforeCursor.trim().isEmpty(); - - // Check if line contains only whitespace - boolean lineOnlyWhitespace = currentLine.text.trim().isEmpty(); - - if (atLineStartOrWhitespace && lineOnlyWhitespace && currentLine.start > 0) { - // Remove the empty/whitespace-only line by deleting the substring - // from the start of this line up to and including the next newline (if any). - int removeStart = currentLine.start; - int removeEnd = text.indexOf('\n', removeStart); - if (removeEnd == -1) { - removeEnd = text.length(); - } else { - removeEnd = removeEnd + 1; // include the newline - } - String before = text.substring(0, removeStart); - String after = removeEnd > text.length() ? "" : text.substring(removeEnd); - setText(before + after); - // Place cursor at end of previous line (just before where the removed newline was) - int newCursor = Math.max(0, removeStart - 1); - endSelection = cursorPosition = startSelection = newCursor; - return true; - } - } - } - - String s = getSelectionBeforeText(); - if (startSelection > 0 && startSelection == endSelection) { LineData curr = null; for (LineData line : container.lines) { if (cursorPosition >= line.start && cursorPosition < line.end) { @@ -585,24 +542,56 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (curr != null) { int col = cursorPosition - curr.start; int indent = getLineIndent(curr.text); - if (col <= indent && startSelection - 1 >= 0 && text.charAt(startSelection - 1) == '\n') { - // At or before indent: jump to previous line indent by removing newline - LineData prev = null; - int idx = container.lines.indexOf(curr); - if (idx > 0) - prev = container.lines.get(idx - 1); - String before = text.substring(0, startSelection - 1); - String after = startSelection < text.length() ? text.substring(startSelection) : ""; - setText(before + after); - int newCursor = startSelection - 1; - if (prev != null) { - int prevIndent = getLineIndent(prev.text); - newCursor = prev.start + Math.min(prevIndent, Math.max(0, prev.text.length())); + int expectedIndent = computeExpectedIndent(curr); + // If caret is at or before the indent, merge with previous line and place + // caret at that line's indent. + if (col <= expectedIndent) { + int prevNewlineIdx = text.lastIndexOf('\n', curr.start - 1); + if (prevNewlineIdx >= 0) { + // Determine previous line bounds/text + int prevLineStart = text.lastIndexOf('\n', Math.max(0, prevNewlineIdx - 1)); + if (prevLineStart == -1) + prevLineStart = 0; + else + prevLineStart += 1; + + String prevLineText = text.substring(prevLineStart, prevNewlineIdx); + int prevIndent = getLineIndent(prevLineText); + + // Current line text without leading whitespace + int currLineEnd = text.indexOf('\n', curr.start); + if (currLineEnd == -1) + currLineEnd = text.length(); + String currLineText = text.substring(curr.start, currLineEnd); + String currTrimmed = currLineText.replaceFirst("^[ \\t]+", ""); + + // Build merged text: drop the newline and the current line's leading indent. + String before = text.substring(0, prevNewlineIdx); + boolean needSpace = !prevLineText.isEmpty() && !currTrimmed.isEmpty() && !Character.isWhitespace( + prevLineText.charAt(prevLineText.length() - 1)); + String after = text.substring(currLineEnd); + setText(before + (needSpace ? " " : "") + currTrimmed + after); + + int newCursor = prevLineStart + Math.min(prevIndent, Math.max(0, prevLineText.length())); + endSelection = cursorPosition = startSelection = prevLineStart + prevLineText.length();// Math.max(0, newCursor); + return true; } - endSelection = cursorPosition = startSelection = Math.max(0, newCursor); - return true; + } else { //BREAKS SINGLE CHARACTER BACK +// // Delete backwards until we reach the expected indent for this line +// int targetPos = Math.max(0, curr.start + expectedIndent); +// if (targetPos < cursorPosition) { +// String before = text.substring(0, targetPos); +// String after = text.substring(cursorPosition); +// setText(before + after); +// startSelection = endSelection = cursorPosition = Math.min(targetPos, text.length()); +// } +// return true; } } + } + + String s = getSelectionBeforeText(); + if (startSelection > 0 && startSelection == endSelection) { // If deleting an opening bracket/quote and the immediate next char is the matching closer, // remove both so the auto-inserted closer doesn't remain orphaned. if (startSelection > 0 && startSelection < text.length()) { @@ -1028,6 +1017,38 @@ public class GuiScriptTextArea extends GuiNpcTextField { return leading; } + // Estimate the logical indent for a line based on surrounding lines and braces, + // rather than its current raw whitespace (which may be over-indented). + private int computeExpectedIndent(LineData line) { + int tab = getTabSize(); + int idx = container.lines.indexOf(line); + if (idx < 0) + return getLineIndent(line.text); + + // Find previous non-empty line + int prevIdx = idx - 1; + while (prevIdx >= 0 && container.lines.get(prevIdx).text.trim().isEmpty()) { + prevIdx--; + } + + int baseIndent = 0; + if (prevIdx >= 0) { + LineData prev = container.lines.get(prevIdx); + String prevTrim = prev.text.trim(); + baseIndent = getLineIndent(prev.text); + if (!prevTrim.isEmpty() && prevTrim.endsWith("{")) { + baseIndent += tab; + } + } + + String currTrim = line.text.trim(); + if (!currTrim.isEmpty() && currTrim.startsWith("}")) { + baseIndent = Math.max(0, baseIndent - tab); + } + + return Math.max(0, baseIndent); + } + private int getTabSize() { return 4; } From 7a20c8f3c861bcc184e021de4dfb674757a735bc Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 23:51:28 +0200 Subject: [PATCH 032/358] temp fix( backspace indents semi working) --- .../client/gui/util/GuiScriptTextArea.java | 92 +++++++++++++++---- 1 file changed, 75 insertions(+), 17 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index db70bb46c..44210d5ba 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -556,30 +556,45 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (startSelection > 0 && startSelection == endSelection) { LineData curr = null; for (LineData line : container.lines) { - if (cursorPosition >= line.start && cursorPosition <= line.end) { + if (cursorPosition >= line.start && cursorPosition < line.end) { curr = line; break; } } if (curr != null) { int col = cursorPosition - curr.start; - int indent = getLineIndent(curr.text); - if (col <= indent && startSelection - 1 >= 0 && text.charAt(startSelection - 1) == '\n') { - // At or before indent: jump to previous line indent by removing newline - LineData prev = null; - int idx = container.lines.indexOf(curr); - if (idx > 0) - prev = container.lines.get(idx - 1); - String before = text.substring(0, startSelection - 1); - String after = startSelection < text.length() ? text.substring(startSelection) : ""; - setText(before + after); - int newCursor = startSelection - 1; - if (prev != null) { - int prevIndent = getLineIndent(prev.text); - newCursor = prev.start + Math.min(prevIndent, Math.max(0, prev.text.length())); + int expectedIndent = getExpectedIndent(curr); + // If cursor is at or before the expected indent position, merge with previous line + if (col <= expectedIndent && curr.start > 0) { + // Find the newline before this line (at curr.start - 1) + int newlinePos = curr.start - 1; + if (newlinePos >= 0 && newlinePos < text.length() && text.charAt(newlinePos) == '\n') { + // Get previous line for cursor positioning + LineData prev = null; + int idx = container.lines.indexOf(curr); + if (idx > 0) + prev = container.lines.get(idx - 1); + + // Get current line text and strip leading whitespace + int actualIndent = getLineIndent(curr.text); + String currLineContent = curr.text.substring(actualIndent); + + // Build new text: everything before newline + stripped current line + everything after current line + String before = text.substring(0, newlinePos); + int currLineEnd = Math.min(curr.end, text.length()); + String after = currLineEnd < text.length() ? text.substring(currLineEnd) : ""; + + setText(before + currLineContent + after); + + // Position cursor at previous line's indent + int newCursor = newlinePos; + if (prev != null) { + int prevIndent = getLineIndent(prev.text); + newCursor = prev.start + Math.min(prevIndent, Math.max(0, prev.text.length())); + } + endSelection = cursorPosition = startSelection = Math.max(0, newCursor); + return true; } - endSelection = cursorPosition = startSelection = Math.max(0, newCursor); - return true; } } // If deleting an opening bracket/quote and the immediate next char is the matching closer, @@ -996,6 +1011,49 @@ public class GuiScriptTextArea extends GuiNpcTextField { return leading; } + // Calculate the expected/proper indent for the current line based on surrounding context + private int getExpectedIndent(LineData currentLine) { + if (currentLine == null) { + return 0; + } + + int idx = container.lines.indexOf(currentLine); + if (idx <= 0) { + return 0; // First line has no expected indent + } + + // Find the previous non-empty line + LineData prevLine = null; + for (int i = idx - 1; i >= 0; i--) { + LineData line = container.lines.get(i); + if (line.text.trim().length() > 0) { + prevLine = line; + break; + } + } + + if (prevLine == null) { + return 0; + } + + int prevIndent = getLineIndent(prevLine.text); + String prevTrimmed = prevLine.text.trim(); + String currTrimmed = currentLine.text.trim(); + + // If current line starts with }, expected indent is one level back + if (currTrimmed.startsWith("}")) { + return Math.max(0, prevIndent - getTabSize()); + } + + // If previous line ends with {, expected indent is one level forward + if (prevTrimmed.endsWith("{")) { + return prevIndent + getTabSize(); + } + + // Otherwise, expected indent matches previous line + return prevIndent; + } + private int getTabSize() { return 4; } From 2d0e16bdd8ef6cd331727b96abe486a4a43e7359 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 17 Dec 2025 23:54:36 +0200 Subject: [PATCH 033/358] backspace cursor position and enter indents fixed --- .../client/gui/util/GuiScriptTextArea.java | 37 ++++++++++++------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 44210d5ba..bdaffc39d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -569,29 +569,37 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Find the newline before this line (at curr.start - 1) int newlinePos = curr.start - 1; if (newlinePos >= 0 && newlinePos < text.length() && text.charAt(newlinePos) == '\n') { - // Get previous line for cursor positioning - LineData prev = null; + // Get previous line info int idx = container.lines.indexOf(curr); - if (idx > 0) - prev = container.lines.get(idx - 1); + LineData prev = idx > 0 ? container.lines.get(idx - 1) : null; // Get current line text and strip leading whitespace int actualIndent = getLineIndent(curr.text); String currLineContent = curr.text.substring(actualIndent); - // Build new text: everything before newline + stripped current line + everything after current line + // Determine if we need a space between previous line and current line content String before = text.substring(0, newlinePos); + boolean needSpace = false; + if (prev != null && prev.text.trim().length() > 0 && currLineContent.length() > 0) { + char lastChar = before.charAt(before.length() - 1); + char firstChar = currLineContent.charAt(0); + // Add space unless previous ends with whitespace/bracket or current starts with bracket/semicolon/comma + if (!Character.isWhitespace(lastChar) && lastChar != '{' && lastChar != '(' && lastChar != '[' && + firstChar != '}' && firstChar != ')' && firstChar != ']' && firstChar != ';' && firstChar != ',') { + needSpace = true; + } + } + + // Build new text int currLineEnd = Math.min(curr.end, text.length()); String after = currLineEnd < text.length() ? text.substring(currLineEnd) : ""; + String spacer = needSpace ? " " : ""; - setText(before + currLineContent + after); + setText(before + spacer + currLineContent + after); - // Position cursor at previous line's indent - int newCursor = newlinePos; - if (prev != null) { - int prevIndent = getLineIndent(prev.text); - newCursor = prev.start + Math.min(prevIndent, Math.max(0, prev.text.length())); - } + // Position cursor at the merge point (where the newline was) + // After removing newline and indent, cursor should be at previous line's end + int newCursor = newlinePos + spacer.length(); endSelection = cursorPosition = startSelection = Math.max(0, newCursor); return true; } @@ -986,6 +994,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { int relativeCursor = Math.max(0, Math.min(this.cursorPosition - currentLine.start, lineText.length())); String beforeCursor = lineText.substring(0, relativeCursor); + // Find last non-whitespace before cursor int lastNonWs = -1; for (int idx = beforeCursor.length() - 1; idx >= 0; idx--) { if (!Character.isWhitespace(beforeCursor.charAt(idx))) { @@ -994,7 +1003,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } - boolean opensBlock = lastNonWs >= 0 && beforeCursor.charAt(lastNonWs) == '{'; + // Only add extra indent if cursor is actually after the opening brace (not just in trailing whitespace) + // Check that the last non-whitespace is at or after the line's indent position + boolean opensBlock = lastNonWs >= 0 && lastNonWs >= leading && beforeCursor.charAt(lastNonWs) == '{'; if (opensBlock) { return baseIndent + " "; } From 91a7a8bf195cdf0301bbcaddc7e459d5d108eda9 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 00:35:38 +0200 Subject: [PATCH 034/358] FIXED BACKSPACE INDENT ONCE AND FOR ALL (W opus 4.5) --- .../client/gui/util/GuiScriptTextArea.java | 190 ++++++++---------- 1 file changed, 89 insertions(+), 101 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index bdaffc39d..39d7d01fa 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -510,120 +510,108 @@ public class GuiScriptTextArea extends GuiNpcTextField { return true; } if (i == Keyboard.KEY_BACK) { - if (startSelection == endSelection && startSelection > 0) { - // If the character before the cursor is a newline, remove that single newline. - // This handles backspacing into empty lines (joining the previous and current lines) - // without relying on LineData start/end calculations. - if (startSelection - 1 >= 0 && startSelection - 1 < text.length() && text.charAt(startSelection - 1) == '\n') { - String before = text.substring(0, startSelection - 1); - String after = startSelection < text.length() ? text.substring(startSelection) : ""; - setText(before + after); - endSelection = cursorPosition = startSelection = startSelection - 1; - return true; - + // Handle selection deletion first + if (startSelection != endSelection) { + String s = getSelectionBeforeText(); + setText(s + getSelectionAfterText()); + endSelection = cursorPosition = startSelection; + return true; + } + + // Nothing to delete if at start + if (startSelection <= 0) { + return true; + } + + // Find current line for indent-aware backspace + LineData curr = null; + for (LineData line : container.lines) { + if (cursorPosition >= line.start && cursorPosition <= line.end) { + curr = line; + break; } - // Check if we should remove an empty/whitespace-only line - if (currentLine != null) { - // Check if cursor is at line start or after only whitespace - String beforeCursor = text.substring(currentLine.start, startSelection); - boolean atLineStartOrWhitespace = beforeCursor.trim().isEmpty(); - - // Check if line contains only whitespace - boolean lineOnlyWhitespace = currentLine.text.trim().isEmpty(); - - if (atLineStartOrWhitespace && lineOnlyWhitespace && currentLine.start > 0) { - // Remove the empty/whitespace-only line by deleting the substring - // from the start of this line up to and including the next newline (if any). - int removeStart = currentLine.start; - int removeEnd = text.indexOf('\n', removeStart); + } + + // Indent-aware backspace: if cursor is at or before the EXPECTED indent, merge with previous line + // If cursor is after expected indent (even in extra whitespace), do normal backspace + if (curr != null && curr.start > 0) { + int col = cursorPosition - curr.start; + int actualIndent = getLineIndent(curr.text); + int expectedIndent = getExpectedIndent(curr); + + // Only trigger smart merge if cursor is at or before the expected indent position + if (col <= expectedIndent) { + boolean lineHasContent = curr.text.trim().length() > 0; + int newlinePos = curr.start - 1; // Position of newline before this line + + if (!lineHasContent) { + // Empty/whitespace-only line: remove the entire line including its trailing newline + int removeEnd = text.indexOf('\n', curr.start); if (removeEnd == -1) { removeEnd = text.length(); } else { - removeEnd = removeEnd + 1; // include the newline + removeEnd = removeEnd + 1; // Include the newline } - String before = text.substring(0, removeStart); - String after = removeEnd > text.length() ? "" : text.substring(removeEnd); + String before = text.substring(0, curr.start); + String after = removeEnd <= text.length() ? text.substring(removeEnd) : ""; setText(before + after); - // Place cursor at end of previous line (just before where the removed newline was) - int newCursor = Math.max(0, removeStart - 1); + // Cursor goes to end of previous line (at the newline position, which is now end of prev line content) + int newCursor = Math.max(0, curr.start - 1); + endSelection = cursorPosition = startSelection = newCursor; + return true; + } else { + // Line has content: merge with previous line + // Remove newline + indent, keep content + int contentStart = curr.start + actualIndent; + + String before = text.substring(0, newlinePos); + String content = contentStart <= text.length() ? text.substring(contentStart) : ""; + + // Determine if we need spacing between merged content + String spacer = ""; + if (before.length() > 0 && content.length() > 0) { + char lastChar = before.charAt(before.length() - 1); + char firstChar = content.charAt(0); + // Add space unless previous ends with whitespace/open bracket or current starts with close bracket/punctuation + if (!Character.isWhitespace(lastChar) && + lastChar != '{' && lastChar != '(' && lastChar != '[' && + firstChar != '}' && firstChar != ')' && firstChar != ']' && + firstChar != ';' && firstChar != ',' && firstChar != '.' && + firstChar != '\n') { + spacer = " "; + } + } + + setText(before + spacer + content); + int newCursor = before.length() + spacer.length(); endSelection = cursorPosition = startSelection = newCursor; return true; } } } - - String s = getSelectionBeforeText(); - if (startSelection > 0 && startSelection == endSelection) { - LineData curr = null; - for (LineData line : container.lines) { - if (cursorPosition >= line.start && cursorPosition < line.end) { - curr = line; - break; - } + + // Auto-pair deletion: if deleting an opening bracket/quote and next char is the matching closer + if (startSelection > 0 && startSelection < text.length()) { + char prev = text.charAt(startSelection - 1); + char nextc = text.charAt(startSelection); + if ((prev == '(' && nextc == ')') || + (prev == '[' && nextc == ']') || + (prev == '{' && nextc == '}') || + (prev == '\'' && nextc == '\'') || + (prev == '"' && nextc == '"')) { + String before = text.substring(0, startSelection - 1); + String after = startSelection + 1 < text.length() ? text.substring(startSelection + 1) : ""; + setText(before + after); + startSelection -= 1; + endSelection = cursorPosition = startSelection; + return true; } - if (curr != null) { - int col = cursorPosition - curr.start; - int expectedIndent = getExpectedIndent(curr); - // If cursor is at or before the expected indent position, merge with previous line - if (col <= expectedIndent && curr.start > 0) { - // Find the newline before this line (at curr.start - 1) - int newlinePos = curr.start - 1; - if (newlinePos >= 0 && newlinePos < text.length() && text.charAt(newlinePos) == '\n') { - // Get previous line info - int idx = container.lines.indexOf(curr); - LineData prev = idx > 0 ? container.lines.get(idx - 1) : null; - - // Get current line text and strip leading whitespace - int actualIndent = getLineIndent(curr.text); - String currLineContent = curr.text.substring(actualIndent); - - // Determine if we need a space between previous line and current line content - String before = text.substring(0, newlinePos); - boolean needSpace = false; - if (prev != null && prev.text.trim().length() > 0 && currLineContent.length() > 0) { - char lastChar = before.charAt(before.length() - 1); - char firstChar = currLineContent.charAt(0); - // Add space unless previous ends with whitespace/bracket or current starts with bracket/semicolon/comma - if (!Character.isWhitespace(lastChar) && lastChar != '{' && lastChar != '(' && lastChar != '[' && - firstChar != '}' && firstChar != ')' && firstChar != ']' && firstChar != ';' && firstChar != ',') { - needSpace = true; - } - } - - // Build new text - int currLineEnd = Math.min(curr.end, text.length()); - String after = currLineEnd < text.length() ? text.substring(currLineEnd) : ""; - String spacer = needSpace ? " " : ""; - - setText(before + spacer + currLineContent + after); - - // Position cursor at the merge point (where the newline was) - // After removing newline and indent, cursor should be at previous line's end - int newCursor = newlinePos + spacer.length(); - endSelection = cursorPosition = startSelection = Math.max(0, newCursor); - return true; - } - } - } - // If deleting an opening bracket/quote and the immediate next char is the matching closer, - // remove both so the auto-inserted closer doesn't remain orphaned. - if (startSelection > 0 && startSelection < text.length()) { - char prev = text.charAt(startSelection - 1); - char nextc = text.charAt(startSelection); - boolean removedPair = false; - if ((prev == '(' && nextc == ')') || (prev == '[' && nextc == ']') || (prev == '{' && nextc == '}') || (prev == '\'' && nextc == '\'') || (prev == '"' && nextc == '"')) { - String before = text.substring(0, startSelection - 1); - String after = startSelection + 1 < text.length() ? text.substring(startSelection + 1) : ""; - setText(before + after); - startSelection -= 1; - endSelection = cursorPosition = startSelection; - return true; - } - } - - s = s.substring(0, s.length() - 1); - startSelection--; } + + // Normal backspace: delete one character + String s = getSelectionBeforeText(); + s = s.substring(0, s.length() - 1); + startSelection--; setText(s + getSelectionAfterText()); endSelection = cursorPosition = startSelection; return true; From 7a661df47dbdc2639f187b2be8b7de06a58cff8a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 00:40:45 +0200 Subject: [PATCH 035/358] CTRL + F formatting now calculates new cursor position correctly --- .../client/gui/util/GuiScriptTextArea.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 39d7d01fa..4b51791cf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1066,7 +1066,25 @@ public class GuiScriptTextArea extends GuiNpcTextField { } private void formatText() { + // Find which line the cursor is on and calculate position within that line (after stripping indent) + int cursorLine = -1; + int cursorColInContent = 0; // position in line after removing leading whitespace + int lineStartPos = 0; String[] linesArr = text.split("\n", -1); + + for (int li = 0; li < linesArr.length; li++) { + String line = linesArr[li]; + int lineEndPos = lineStartPos + line.length(); + + if (cursorPosition >= lineStartPos && cursorPosition <= lineEndPos) { + cursorLine = li; + int leadingSpaces = line.length() - line.replaceAll("^[ \t]+", "").length(); + cursorColInContent = Math.max(0, cursorPosition - lineStartPos - leadingSpaces); + break; + } + lineStartPos = lineEndPos + 1; // +1 for newline + } + StringBuilder out = new StringBuilder(text.length() + 32); boolean inString = false; @@ -1074,6 +1092,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { boolean inBlockComment = false; int depth = 0; int tab = getTabSize(); + + int newCursorPos = 0; for (int li = 0; li < linesArr.length; li++) { String line = linesArr[li]; @@ -1139,6 +1159,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { indentLevel = Math.max(0, indentLevel - 1); int targetIndent = indentLevel * tab; + + // Track cursor position in the formatted output + if (li == cursorLine) { + newCursorPos = out.length() + targetIndent + Math.min(cursorColInContent, trimmedLeading.length()); + } + out.append(repeatSpace(targetIndent)).append(trimmedLeading); if (li < linesArr.length - 1) out.append('\n'); @@ -1147,6 +1173,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { } setText(out.toString()); + + // Restore cursor position + if (cursorLine >= 0) { + newCursorPos = Math.max(0, Math.min(newCursorPos, this.text.length())); + startSelection = endSelection = cursorPosition = newCursorPos; + } } private void handleTab() { From e41ab8ad914de0692182059776ef7117d99665a9 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 00:50:27 +0200 Subject: [PATCH 036/358] Fixed first TAB at the very start of line missing first indent level by 1 character --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 4b51791cf..22a5f7528 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1184,7 +1184,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void handleTab() { LineData currentLine = null; for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) { + if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { currentLine = line; break; } @@ -1243,7 +1243,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void handleShiftTab() { LineData currentLine = null; for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) { + if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { currentLine = line; break; } From 10d4681963af634edd965a88febeb3172040e32a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 00:51:40 +0200 Subject: [PATCH 037/358] Braces{} scaffold creation is no longer done if the scope's } already exists --- .../client/gui/util/GuiScriptTextArea.java | 54 +++++++++++-------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 22a5f7528..0b706089d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -677,30 +677,42 @@ public class GuiScriptTextArea extends GuiNpcTextField { String before = getSelectionBeforeText(); String after = getSelectionAfterText(); - // Find the next non-whitespace character and its indent to decide if this brace is already closed at the same scope. - int nextNonWsIndex = -1; - for (int idx = 0; idx < after.length(); idx++) { - char ch = after.charAt(idx); - if (!Character.isWhitespace(ch)) { - nextNonWsIndex = idx; - break; + // Determine whether this opening brace already has a matching closing + // brace at the same scope (and indent). Prefer using the brace-span + // computation which skips strings/comments and handles nesting. + boolean hasMatchingCloseSameIndent = false; + try { + // Find the line index containing the opening brace (cursorPosition-1) + int openLineIdx = -1; + int bracePos = cursorPosition - 1; + for (int li = 0; li < this.container.lines.size(); li++) { + LineData ld = this.container.lines.get(li); + if (bracePos >= ld.start && bracePos < ld.end) { + openLineIdx = li; + break; + } } + + if (openLineIdx >= 0) { + List spans = computeBraceSpans(text, this.container.lines); + for (int[] span : spans) { + int spanOpen = span[1]; + int spanClose = span[2]; + if (spanOpen == openLineIdx) { + // Found a matching close; check its indent equals current indent + int closeIndent = getLineIndent(this.container.lines.get(spanClose).text); + if (closeIndent == indent.length()) { + hasMatchingCloseSameIndent = true; + } + break; + } + } + } + } catch (Exception ex) { + // Fallback: if anything goes wrong, conservatively behave as before + hasMatchingCloseSameIndent = false; } - boolean hasMatchingCloseSameIndent = false; - if (nextNonWsIndex >= 0 && after.charAt(nextNonWsIndex) == '}') { - // Compute indent of that closing brace - int lineStart = after.lastIndexOf('\n', nextNonWsIndex); - int indentLen = 0; - for (int k = lineStart + 1; k < after.length(); k++) { - char ch = after.charAt(k); - if (ch == ' ') indentLen++; else if (ch == '\t') indentLen += 4; else break; - if (k == nextNonWsIndex) break; - } - if (indentLen == indent.length()) { - hasMatchingCloseSameIndent = true; - } - } if (hasMatchingCloseSameIndent) { addText("\n" + childIndent); From dceb600e526e771d4aa07475e488af51f22b9ec6 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 00:52:50 +0200 Subject: [PATCH 038/358] Having cursor AFTER the braces { also highlight's the scope's remaining } --- .../client/gui/util/GuiScriptTextArea.java | 57 ++++++++++++------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 0b706089d..a61610b4d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -95,26 +95,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { scrolledLine = Math.min(Math.max((int) (1f * diff * (yMouse - y) / height), 0), diff); } int startBracket = 0, endBracket = 0; - if (startSelection >= 0 && startSelection < text.length() && - (endSelection - startSelection == 1 || startSelection == endSelection)) { - char c = text.charAt(startSelection); - int found = 0; - if (c == '{') { - found = findClosingBracket(text.substring(startSelection), '{', '}'); - } else if (c == '[') { - found = findClosingBracket(text.substring(startSelection), '[', ']'); - } else if (c == '(') { - found = findClosingBracket(text.substring(startSelection), '(', ')'); - } else if (c == '}') { - found = findOpeningBracket(text.substring(0, startSelection + 1), '{', '}'); - } else if (c == ']') { - found = findOpeningBracket(text.substring(0, startSelection + 1), '[', ']'); - } else if (c == ')') { - found = findOpeningBracket(text.substring(0, startSelection + 1), '(', ')'); - } - if (found != 0) { - startBracket = startSelection; - endBracket = startSelection + found; + if (startSelection >= 0 && text != null && text.length() > 0 && + (endSelection - startSelection == 1 || startSelection == endSelection)) { + int[] span = findBracketSpanAt(startSelection); + if (span != null) { + startBracket = span[0]; + endBracket = span[1]; } } @@ -227,7 +213,38 @@ public class GuiScriptTextArea extends GuiNpcTextField { drawRect(posX, posY, posX + 5, posY + sbSize, 0xFFe0e0e0); } } + // Find a bracket span for a given cursor position. Returns {start, end} or null. + private int[] findBracketSpanAt(int pos) { + if (text == null || text.isEmpty()) return null; + // First try at pos (caret before the char) + if (pos >= 0 && pos < text.length()) { + char c = text.charAt(pos); + int found = 0; + if (c == '{') found = findClosingBracket(text.substring(pos), '{', '}'); + else if (c == '[') found = findClosingBracket(text.substring(pos), '[', ']'); + else if (c == '(') found = findClosingBracket(text.substring(pos), '(', ')'); + else if (c == '}') found = findOpeningBracket(text.substring(0, pos + 1), '{', '}'); + else if (c == ']') found = findOpeningBracket(text.substring(0, pos + 1), '[', ']'); + else if (c == ')') found = findOpeningBracket(text.substring(0, pos + 1), '(', ')'); + if (found != 0) return new int[]{pos, pos + found}; + } + // Then try immediately before pos (caret right after a char) + if (pos > 0 && pos - 1 < text.length()) { + int probe = pos - 1; + char c2 = text.charAt(probe); + int found2 = 0; + if (c2 == '{') found2 = findClosingBracket(text.substring(probe), '{', '}'); + else if (c2 == '[') found2 = findClosingBracket(text.substring(probe), '[', ']'); + else if (c2 == '(') found2 = findClosingBracket(text.substring(probe), '(', ')'); + else if (c2 == '}') found2 = findOpeningBracket(text.substring(0, probe + 1), '{', '}'); + else if (c2 == ']') found2 = findOpeningBracket(text.substring(0, probe + 1), '[', ']'); + else if (c2 == ')') found2 = findOpeningBracket(text.substring(0, probe + 1), '(', ')'); + if (found2 != 0) return new int[]{probe, probe + found2}; + } + + return null; + } private int findClosingBracket(String str, char s, char e) { int found = 0; char[] chars = str.toCharArray(); From 8d4ec579a7888d19dd11ebed755199436b83f014 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 01:00:27 +0200 Subject: [PATCH 039/358] Improved currentLine get logic --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index a61610b4d..df5dd1e39 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -437,7 +437,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { LineData currentLine = null; for (LineData line : this.container.lines) { - if (startSelection >= line.start && startSelection <= line.end) { + if (startSelection >= line.start && startSelection < line.end) { currentLine = line; break; } @@ -543,7 +543,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Find current line for indent-aware backspace LineData curr = null; for (LineData line : container.lines) { - if (cursorPosition >= line.start && cursorPosition <= line.end) { + if (cursorPosition >= line.start && cursorPosition < line.end) { curr = line; break; } @@ -982,7 +982,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { int i; for (i = 0; i < data.text.length() && data.text.charAt(i) == 32; ++i) { - ; } return data.text.substring(0, i); @@ -991,7 +990,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { private String getAutoIndentForEnter() { LineData currentLine = null; for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) { + if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { currentLine = line; break; } From 0220b804e83c22317ced5dcc92514fb1b3cb8f8b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 01:09:06 +0200 Subject: [PATCH 040/358] Improved Line duplication logic (removed extra whitespace line inbetween and set cursor position correctly) --- .../client/gui/util/GuiScriptTextArea.java | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index df5dd1e39..a84c62284 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -799,16 +799,16 @@ public class GuiScriptTextArea extends GuiNpcTextField { } if (firstLine != null && lastLine != null) { String selectedText = text.substring(firstLine.start, lastLine.end); - String insertText = "\n" + selectedText; + // Insert the selected block immediately after the last line without adding an extra newline + String insertText = selectedText; // Save selection before setText int savedStart = startSelection; int savedEnd = endSelection; - setText(text.substring(0, lastLine.end) + insertText + text.substring(lastLine.end)); - // Move cursor to start of duplicated block - setCursor(lastLine.end + 1, false); - // Restore selection + int insertAt = lastLine.end; + setText(text.substring(0, insertAt) + insertText + text.substring(insertAt)); + // Restore cursor and selection startSelection = savedStart; - endSelection = savedEnd; + endSelection = this.cursorPosition = savedEnd; return true; } } else { @@ -817,16 +817,19 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (cursorPosition >= line.start && cursorPosition <= line.end) { int lineStart = line.start, lineEnd = line.end; String lineText = text.substring(lineStart, lineEnd); - String insertText = "\n" + lineText; - // Save selection before setText - int savedStart = startSelection; - int savedEnd = endSelection; - setText(text.substring(0, lineEnd) + insertText + text.substring(lineEnd)); - // Move cursor to start of duplicated line - setCursor(lineEnd + 1, false); - // Restore selection - startSelection = savedStart; - endSelection = savedEnd; + // Avoid inserting an extra blank line: if lineText already ends with a newline, + // reuse it; otherwise prepend a newline so the duplicate appears immediately after. + String insertText; + if (lineText.endsWith("\n")) { + insertText = lineText; + } else { + insertText = "\n" + lineText; + } + int insertionPoint = lineEnd; + setText(text.substring(0, insertionPoint) + insertText + text.substring(insertionPoint)); + // Place cursor at end of duplicated line (just before any trailing newline) + int newCursor = insertionPoint + insertText.length() - (insertText.endsWith("\n") ? 1 : 0); + startSelection = endSelection = cursorPosition = Math.max(0, Math.min(newCursor, this.text.length())); return true; } } From 5da2c75c8f0cfb1af49a278e11b11eb3d997cc30 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 01:14:16 +0200 Subject: [PATCH 041/358] Added double click line selection --- .../npcs/client/gui/util/GuiScriptTextArea.java | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index a84c62284..eeb161858 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1409,12 +1409,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.clickScrolling = true; } else if (time - this.lastClicked < 500L) { this.doubleClicked = true; - Matcher m = this.container.regexWord.matcher(this.text); - - while (m.find()) { - if (this.cursorPosition > m.start() && this.cursorPosition < m.end()) { - this.startSelection = m.start(); - this.endSelection = m.end(); + // On double-click select the entire line under the cursor + for (LineData line : container.lines) { + if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) { + this.startSelection = line.start; + this.endSelection = line.end; break; } } From ca6409e3e562c715cffb5f2ac6a7724a50b0a1eb Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 01:21:45 +0200 Subject: [PATCH 042/358] Improved highlight selection rendering (changed color to more brighter and lines no longer vertically intersect --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index eeb161858..1e7f8fec5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -16,6 +16,7 @@ import java.util.List; import java.util.regex.Matcher; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import org.lwjgl.opengl.GL11; import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; @@ -153,7 +154,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { int s = ClientProxy.Font.width(line.substring(0, Math.max(startSelection - data.start, 0))); int e = ClientProxy.Font.width(line.substring(0, Math.min(endSelection - data.start, w))) + 1; int posY = y + 1 + (i - scrolledLine) * container.lineHeight; - drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x990000ff); + drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight, 0x992172ff); } } int yPos = y + (i - scrolledLine) * container.lineHeight + 1; From 678c32e2da8688c6155edece39f8d9c23816e75d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 01:22:20 +0200 Subject: [PATCH 043/358] Decreased double click capture time window for more realistic capture --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 1e7f8fec5..8b6d5c993 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1408,7 +1408,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (this.clicked && this.container.linesCount * this.container.lineHeight > this.height && xMouse > this.x + this.width - 8) { this.clicked = false; this.clickScrolling = true; - } else if (time - this.lastClicked < 500L) { + } else if (time - this.lastClicked < 300L) { this.doubleClicked = true; // On double-click select the entire line under the cursor for (LineData line : container.lines) { From 02be805c144c74794b3622697f7f3c2c89406b33 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 02:06:14 +0200 Subject: [PATCH 044/358] Another braces scaffold generation bug fixed --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 8b6d5c993..3cbafa413 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -695,6 +695,16 @@ public class GuiScriptTextArea extends GuiNpcTextField { String before = getSelectionBeforeText(); String after = getSelectionAfterText(); + // If there is non-empty code on the same line after the brace (before the next newline), + // treat it as code that should be moved into the newly created inner line. In that case + // just insert the child indent and return — do not insert the closing brace before that code. + int firstNewline = after.indexOf('\n'); + String leadingSegment = firstNewline == -1 ? after : after.substring(0, firstNewline); + if (leadingSegment.trim().length() > 0) { + addText("\n" + childIndent); + return true; + } + // Determine whether this opening brace already has a matching closing // brace at the same scope (and indent). Prefer using the brace-span // computation which skips strings/comments and handles nesting. From 2f6e8b940878fd5cd5111e1887564e7ded1a87a1 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 09:40:48 +0200 Subject: [PATCH 045/358] Drawn indents for the very first brace level --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 3cbafa413..0fede64ca 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -169,8 +169,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { int closeLine = span[2]; int depth = span.length > 3 ? span[3] : originalDepth; // Skip top-level (depth 1) using the original depth to avoid hiding nested guides when adjusted - if (originalDepth <= 1) - continue; + //if (originalDepth <= 1) + //continue; int startLine = openLine + 1; // start under the opening brace int endLine = closeLine - 1; // stop before the closing brace if (startLine > endLine) From 395ed1dc4aba920d7470d653b8f77b0890793f0d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 09:53:02 +0200 Subject: [PATCH 046/358] Highlighted current line the text cursor is on --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 0fede64ca..06aa43371 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -149,6 +149,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } + // Highlight the current line (light gray) under any selection + if (active && isEnabled() && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { + int lineY = y + 1 + (i - scrolledLine) * container.lineHeight; + drawRect(x + 1, lineY, x + width - 1, lineY + container.lineHeight, 0x22e0e0e0); + } if (startSelection != endSelection && endSelection > data.start && startSelection <= data.end) { if (startSelection < data.end) { int s = ClientProxy.Font.width(line.substring(0, Math.max(startSelection - data.start, 0))); From 7bb5c541e3e9560c2eb34ba657ec4b58a94bf30e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 10:00:02 +0200 Subject: [PATCH 047/358] Restored original double-click word select logic. Triple clicking now selects whole line --- .../client/gui/util/GuiScriptTextArea.java | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 06aa43371..dd8f0426e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -35,7 +35,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { public boolean visible = true; public boolean clicked = false; public boolean doubleClicked = false; + public boolean tripleClicked = false; public boolean clickScrolling = false; + private int clickCount = 0; private int startSelection; private int endSelection; private int cursorPosition; @@ -80,14 +82,16 @@ public class GuiScriptTextArea extends GuiNpcTextField { clicked = Mouse.isButtonDown(0); int i = getSelectionPos(xMouse, yMouse); if (i != cursorPosition) { - if (doubleClicked) { + if (doubleClicked || tripleClicked) { startSelection = endSelection = cursorPosition; doubleClicked = false; + tripleClicked = false; } setCursor(i, true); } - } else if (doubleClicked) { + } else if (doubleClicked || tripleClicked) { doubleClicked = false; + tripleClicked = false; } if (clickScrolling) { @@ -1419,19 +1423,40 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.startSelection = this.endSelection = this.cursorPosition = this.getSelectionPos(xMouse, yMouse); this.clicked = mouseButton == 0; this.doubleClicked = false; + this.tripleClicked = false; long time = System.currentTimeMillis(); if (this.clicked && this.container.linesCount * this.container.lineHeight > this.height && xMouse > this.x + this.width - 8) { this.clicked = false; this.clickScrolling = true; - } else if (time - this.lastClicked < 300L) { - this.doubleClicked = true; - // On double-click select the entire line under the cursor - for (LineData line : container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) { - this.startSelection = line.start; - this.endSelection = line.end; - break; + } else { + if (time - this.lastClicked < 300L) { + this.clickCount++; + } else { + this.clickCount = 1; + } + + if (this.clickCount == 2) { + // Double-click: select word under cursor (restore prior behavior) + this.doubleClicked = true; + Matcher m = this.container.regexWord.matcher(this.text); + while (m.find()) { + if (this.cursorPosition > m.start() && this.cursorPosition < m.end()) { + this.startSelection = m.start(); + this.endSelection = m.end(); + break; + } } + } else if (this.clickCount >= 3) { + // Triple-click: select entire line + this.tripleClicked = true; + for (LineData line : container.lines) { + if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) { + this.startSelection = line.start; + this.endSelection = line.end; + break; + } + } + this.clickCount = 0; } } From 3a0deb8c6d2f9dd686a0184ccfaf5b1100953807 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 10:10:42 +0200 Subject: [PATCH 048/358] Centered word/sentence/selection highlights on their line more fitted --- .../npcs/client/gui/util/GuiScriptTextArea.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index dd8f0426e..ac1360253 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -131,13 +131,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (startBracket >= data.start && startBracket < data.end) { int s = ClientProxy.Font.width(line.substring(0, startBracket - data.start)); int e = ClientProxy.Font.width(line.substring(0, startBracket - data.start + 1)) + 1; - int posY = y + 1 + (i - scrolledLine) * container.lineHeight; + int posY = y + 0 + (i - scrolledLine) * container.lineHeight; drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x9900cc00); } if (endBracket >= data.start && endBracket < data.end) { int s = ClientProxy.Font.width(line.substring(0, endBracket - data.start)); int e = ClientProxy.Font.width(line.substring(0, endBracket - data.start + 1)) + 1; - int posY = y + 1 + (i - scrolledLine) * container.lineHeight; + int posY = y + 0 + (i - scrolledLine) * container.lineHeight; drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x9900cc00); } } @@ -148,21 +148,21 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (line.substring(m.start(), m.end()).equals(wordHightLight)) { int s = ClientProxy.Font.width(line.substring(0, m.start())); int e = ClientProxy.Font.width(line.substring(0, m.end())) + 1; - int posY = y + 1 + (i - scrolledLine) * container.lineHeight; - drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x99004c00); + int posY = y + 0 + (i - scrolledLine) * container.lineHeight; + drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight, 0x99004c00); } } } // Highlight the current line (light gray) under any selection if (active && isEnabled() && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { - int lineY = y + 1 + (i - scrolledLine) * container.lineHeight; - drawRect(x + 1, lineY, x + width - 1, lineY + container.lineHeight, 0x22e0e0e0); + int lineY = y + 0 + (i - scrolledLine) * container.lineHeight; + drawRect(x + 0, lineY, x + width - 1, lineY + container.lineHeight, 0x22e0e0e0); } if (startSelection != endSelection && endSelection > data.start && startSelection <= data.end) { if (startSelection < data.end) { int s = ClientProxy.Font.width(line.substring(0, Math.max(startSelection - data.start, 0))); int e = ClientProxy.Font.width(line.substring(0, Math.min(endSelection - data.start, w))) + 1; - int posY = y + 1 + (i - scrolledLine) * container.lineHeight; + int posY = y + 0 + (i - scrolledLine) * container.lineHeight; drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight, 0x992172ff); } } From 4ee2bf432debd4c8bc73dbe6b0375324906738cb Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 10:25:10 +0200 Subject: [PATCH 049/358] Highlighted indents of the scope being viewed with green --- .../client/gui/util/GuiScriptTextArea.java | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index ac1360253..d4bc38236 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -114,6 +114,31 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Build brace spans: {origDepth, open line, close line, adjustedDepth} List braceSpans = computeBraceSpans(text, list); + // Determine which exact brace span (openLine/closeLine) should be highlighted based on bracket under caret + int highlightedOpenLine = -1; + int highlightedCloseLine = -1; + if (startBracket != endBracket && startBracket >= 0) { + int bracketLineIdx = -1; + for (int li = 0; li < list.size(); li++) { + LineData ld = list.get(li); + if (startBracket >= ld.start && startBracket < ld.end) { + bracketLineIdx = li; + break; + } + } + if (bracketLineIdx >= 0) { + for (int[] span : braceSpans) { + int openLine = span[1]; + int closeLine = span[2]; + if (bracketLineIdx >= openLine && bracketLineIdx <= closeLine) { + highlightedOpenLine = openLine; + highlightedCloseLine = closeLine; + break; + } + } + } + } + String wordHightLight = null; if (startSelection != endSelection) { Matcher m = container.regexWord.matcher(text); @@ -200,7 +225,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { int topY = y + (drawStart - scrolledLine) * container.lineHeight; int bottomY = y + (drawEnd - scrolledLine + 1) * container.lineHeight - 2; - drawRect(gx, topY, gx + 1, bottomY, 0x33FFFFFF); + int guideColor = (openLine == highlightedOpenLine && closeLine == highlightedCloseLine) ? 0x9933cc00 : 0x33FFFFFF; + drawRect(gx, topY, gx + 1, bottomY, guideColor); } } data.drawString(x + 1, yPos, 0xFFe0e0e0); From 10d6d0c40831e857e10f250869b149efef16c78b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 10:27:17 +0200 Subject: [PATCH 050/358] Fixed highlighted braces drawing outside of text editor view port --- .../client/gui/util/GuiScriptTextArea.java | 33 +++++++++++-------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index d4bc38236..56e8f8bb9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -152,21 +152,24 @@ public class GuiScriptTextArea extends GuiNpcTextField { LineData data = list.get(i); String line = data.text; int w = line.length(); - if (startBracket != endBracket) { - if (startBracket >= data.start && startBracket < data.end) { - int s = ClientProxy.Font.width(line.substring(0, startBracket - data.start)); - int e = ClientProxy.Font.width(line.substring(0, startBracket - data.start + 1)) + 1; - int posY = y + 0 + (i - scrolledLine) * container.lineHeight; - drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x9900cc00); - } - if (endBracket >= data.start && endBracket < data.end) { - int s = ClientProxy.Font.width(line.substring(0, endBracket - data.start)); - int e = ClientProxy.Font.width(line.substring(0, endBracket - data.start + 1)) + 1; - int posY = y + 0 + (i - scrolledLine) * container.lineHeight; - drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x9900cc00); - } - } + if (i >= scrolledLine && i < scrolledLine + container.visibleLines) { + //Highlight braces the cursor position is on + if (startBracket != endBracket) { + if (startBracket >= data.start && startBracket < data.end) { + int s = ClientProxy.Font.width(line.substring(0, startBracket - data.start)); + int e = ClientProxy.Font.width(line.substring(0, startBracket - data.start + 1)) + 1; + int posY = y + 0 + (i - scrolledLine) * container.lineHeight; + drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x9900cc00); + } + if (endBracket >= data.start && endBracket < data.end) { + int s = ClientProxy.Font.width(line.substring(0, endBracket - data.start)); + int e = ClientProxy.Font.width(line.substring(0, endBracket - data.start + 1)) + 1; + int posY = y + 0 + (i - scrolledLine) * container.lineHeight; + drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x9900cc00); + } + } + //Highlight words if (wordHightLight != null) { Matcher m = container.regexWord.matcher(line); while (m.find()) { @@ -183,6 +186,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { int lineY = y + 0 + (i - scrolledLine) * container.lineHeight; drawRect(x + 0, lineY, x + width - 1, lineY + container.lineHeight, 0x22e0e0e0); } + // Highlight selection if (startSelection != endSelection && endSelection > data.start && startSelection <= data.end) { if (startSelection < data.end) { int s = ClientProxy.Font.width(line.substring(0, Math.max(startSelection - data.start, 0))); @@ -231,6 +235,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } data.drawString(x + 1, yPos, 0xFFe0e0e0); + // Draw cursor if (active && isEnabled() && (cursorCounter / 6) % 2 == 0 && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { int posX = x + ClientProxy.Font.width( line.substring(0, Math.min(cursorPosition - data.start, line.length()))); From 1ab26a9e84ec4c00bcad9c7961bc0e64e097f85b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 10:32:25 +0200 Subject: [PATCH 051/358] Refined text cursor rendering --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 56e8f8bb9..07c18c860 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -236,10 +236,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { data.drawString(x + 1, yPos, 0xFFe0e0e0); // Draw cursor - if (active && isEnabled() && (cursorCounter / 6) % 2 == 0 && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { + if (active && isEnabled() && (cursorCounter / 10) % 2 == 0 && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { int posX = x + ClientProxy.Font.width( line.substring(0, Math.min(cursorPosition - data.start, line.length()))); - drawRect(posX + 1, yPos, posX + 2, yPos + 1 + container.lineHeight, -3092272); + drawRect(posX + 1, yPos, posX + 2, yPos - 2 + container.lineHeight, 0xffffffff); } } } From 19e5213f3c27825b968bfd2e84c3acfe5f3f90bf Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 10:48:34 +0200 Subject: [PATCH 052/358] Fixed double click word highlight not accounting for numbers/single letters --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 7 ++----- .../java/noppes/npcs/client/gui/util/TextContainer.java | 3 ++- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 07c18c860..39659e4a7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -5,8 +5,8 @@ import net.minecraft.client.gui.GuiScreen; import net.minecraft.util.ChatAllowedCharacters; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; - import noppes.npcs.client.gui.util.script.JavaTextContainer; +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; @@ -15,9 +15,6 @@ import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; -import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; -import org.lwjgl.opengl.GL11; - import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; public class GuiScriptTextArea extends GuiNpcTextField { @@ -1471,7 +1468,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.doubleClicked = true; Matcher m = this.container.regexWord.matcher(this.text); while (m.find()) { - if (this.cursorPosition > m.start() && this.cursorPosition < m.end()) { + if (this.cursorPosition >= m.start() && this.cursorPosition <= m.end()) { this.startSelection = m.start(); this.endSelection = m.end(); break; diff --git a/src/main/java/noppes/npcs/client/gui/util/TextContainer.java b/src/main/java/noppes/npcs/client/gui/util/TextContainer.java index c248b8b67..2ea02cfd6 100644 --- a/src/main/java/noppes/npcs/client/gui/util/TextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/TextContainer.java @@ -15,7 +15,8 @@ public class TextContainer { public final Pattern regexString = Pattern.compile("([\"'])(?:(?=(\\\\?))\\2.)*?\\1", Pattern.MULTILINE); public final Pattern regexFunction = Pattern.compile("\\b(if|else|switch|with|for|while|in|var|const|let|throw|then|function|continue|break|foreach|return|try|catch|finally|do|this|typeof|instanceof|new)(?=[^\\w])"); public final Pattern regexType = Pattern.compile("\\b(Array|Date|eval|hasOwnProperty|Infinity|isFinite|isNaN|isPrototypeOf|Math|NaN|Number|Object|prototype|String|toString|undefined|valueOf|parseInt|parseFloat|print)(?=[^\\w])"); - public final Pattern regexWord = Pattern.compile("[\\p{L}-]+|\\n|$"); + // Include letters, numbers and underscore so single chars like "x" and numeric tokens match + public final Pattern regexWord = Pattern.compile("[\\p{L}\\p{N}_-]+|\\n|$"); public final Pattern regexNumber = Pattern.compile("\\b-?(?:0[xX][\\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?(?:[fFbBdDlLsS])?|NaN|null|Infinity|unidentified|true|false)\\b"); public final Pattern regexComment = Pattern.compile("\\/\\*[\\s\\S]*?(?:\\*\\/|$)|\\/\\/.*|#.*"); From 2e72eb2126ac6974f987f3ada3b604e902b06593 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 11:12:59 +0200 Subject: [PATCH 053/358] Current scope braces highlighting now accounts for whitespaces between brace and cursor --- .../client/gui/util/GuiScriptTextArea.java | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 39659e4a7..bc673e624 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -267,18 +267,30 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (found != 0) return new int[]{pos, pos + found}; } - // Then try immediately before pos (caret right after a char) - if (pos > 0 && pos - 1 < text.length()) { - int probe = pos - 1; - char c2 = text.charAt(probe); - int found2 = 0; - if (c2 == '{') found2 = findClosingBracket(text.substring(probe), '{', '}'); - else if (c2 == '[') found2 = findClosingBracket(text.substring(probe), '[', ']'); - else if (c2 == '(') found2 = findClosingBracket(text.substring(probe), '(', ')'); - else if (c2 == '}') found2 = findOpeningBracket(text.substring(0, probe + 1), '{', '}'); - else if (c2 == ']') found2 = findOpeningBracket(text.substring(0, probe + 1), '[', ']'); - else if (c2 == ')') found2 = findOpeningBracket(text.substring(0, probe + 1), '(', ')'); - if (found2 != 0) return new int[]{probe, probe + found2}; + // Scan backwards from just before the caret, skipping spaces/tabs on the same line, + // to find a nearby bracket that should be treated as "immediately before" the caret. + if (pos > 0) { + int scan = pos - 1; + // Move left over spaces/tabs but do not cross a newline + while (scan >= 0) { + char sc = text.charAt(scan); + if (sc == ' ' || sc == '\t') {scan--;continue;} + if (sc == '\n') {scan = -1; // stop, do not cross line boundary + break; + } + + int found2 = 0; + if (sc == '{') found2 = findClosingBracket(text.substring(scan), '{', '}'); + else if (sc == '[') found2 = findClosingBracket(text.substring(scan), '[', ']'); + else if (sc == '(') found2 = findClosingBracket(text.substring(scan), '(', ')'); + else if (sc == '}') found2 = findOpeningBracket(text.substring(0, scan + 1), '{', '}'); + else if (sc == ']') found2 = findOpeningBracket(text.substring(0, scan + 1), '[', ']'); + else if (sc == ')') found2 = findOpeningBracket(text.substring(0, scan + 1), '(', ')'); + + if (found2 != 0) return new int[]{scan, scan + found2}; + // Not a bracket; stop scanning further + break; + } } return null; From 7952661a7e83df4d99482beb1d07bb14526f74b3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 11:23:54 +0200 Subject: [PATCH 054/358] Cursor animation now stays solid (no blinking) for 500ms after typing/clicking --- .../npcs/client/gui/util/GuiScriptTextArea.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index bc673e624..85363ed93 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -24,6 +24,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { public int width; public int height; private int cursorCounter; + private long lastInputTime = 0L; private ITextChangeListener listener; public String text = null; private JavaTextContainer container = null; @@ -232,8 +233,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { } data.drawString(x + 1, yPos, 0xFFe0e0e0); - // Draw cursor - if (active && isEnabled() && (cursorCounter / 10) % 2 == 0 && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { + // Draw cursor: pause blinking while user is active recently + boolean recentInput = System.currentTimeMillis() - this.lastInputTime < 500; + if (active && isEnabled() && (recentInput || (cursorCounter / 10) % 2 == 0) && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { int posX = x + ClientProxy.Font.width( line.substring(0, Math.min(cursorPosition - data.start, line.length()))); drawRect(posX + 1, yPos, posX + 2, yPos - 2 + container.lineHeight, 0xffffffff); @@ -1403,12 +1405,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { } clampSelectionBounds(); + // Moving the cursor is user activity — pause blinking briefly + this.lastInputTime = System.currentTimeMillis(); } } private void addText(String s) { this.setText(this.getSelectionBeforeText() + s + this.getSelectionAfterText()); this.endSelection = this.startSelection = this.cursorPosition = this.startSelection + s.length(); + this.lastInputTime = System.currentTimeMillis(); } private int cursorUp() { @@ -1461,6 +1466,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.active = xMouse >= this.x && xMouse < this.x + this.width && yMouse >= this.y && yMouse < this.y + this.height; if (this.active) { this.startSelection = this.endSelection = this.cursorPosition = this.getSelectionPos(xMouse, yMouse); + this.lastInputTime = System.currentTimeMillis(); this.clicked = mouseButton == 0; this.doubleClicked = false; this.tripleClicked = false; @@ -1540,6 +1546,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { clampSelectionBounds(); + // Consider text changes user activity to pause caret blinking briefly + this.lastInputTime = System.currentTimeMillis(); + } } From 24c96cf8e1df38f299f2eb82d94e299455f68683 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 11:25:46 +0200 Subject: [PATCH 055/358] Increased cursor size --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 85363ed93..13cac95b3 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -238,7 +238,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (active && isEnabled() && (recentInput || (cursorCounter / 10) % 2 == 0) && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { int posX = x + ClientProxy.Font.width( line.substring(0, Math.min(cursorPosition - data.start, line.length()))); - drawRect(posX + 1, yPos, posX + 2, yPos - 2 + container.lineHeight, 0xffffffff); + drawRect(posX + 1, yPos -1, posX + 2, yPos - 1 + container.lineHeight, 0xffffffff); } } } From 741ce3558dbcf74cbc18af7ad14a3b144182e049 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 11:27:00 +0200 Subject: [PATCH 056/358] Adjust brace highlight size --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 13cac95b3..64da78536 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -158,13 +158,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { int s = ClientProxy.Font.width(line.substring(0, startBracket - data.start)); int e = ClientProxy.Font.width(line.substring(0, startBracket - data.start + 1)) + 1; int posY = y + 0 + (i - scrolledLine) * container.lineHeight; - drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x9900cc00); + drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 0, 0x9900cc00); } if (endBracket >= data.start && endBracket < data.end) { int s = ClientProxy.Font.width(line.substring(0, endBracket - data.start)); int e = ClientProxy.Font.width(line.substring(0, endBracket - data.start + 1)) + 1; int posY = y + 0 + (i - scrolledLine) * container.lineHeight; - drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 1, 0x9900cc00); + drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 0, 0x9900cc00); } } //Highlight words From 1e4630c61953a128d202491318b74cb88ff7b729 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 11:51:50 +0200 Subject: [PATCH 057/358] Fixed last line possible visible line in text editor viewport not being rendered + scissored the viewport --- .../client/gui/util/GuiScriptTextArea.java | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 64da78536..722b4cb6c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.util; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.ScaledResolution; import net.minecraft.util.ChatAllowedCharacters; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; @@ -9,6 +10,7 @@ import noppes.npcs.client.gui.util.script.JavaTextContainer; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; import java.util.ArrayList; import java.util.Iterator; @@ -64,15 +66,28 @@ public class GuiScriptTextArea extends GuiNpcTextField { return; clampSelectionBounds(); drawRect(x - 1, y - 1, x + width + 1, y + height + 1, 0xffa0a0a0); - drawRect(x, y, x + width, y + height, 0xff000000); + drawRect(x, y, x + width, y + height, 0xff000000); //THIS IS THE VIEWPORT + // Enable scissor test to clip drawing to the viewport rectangle + GL11.glEnable(GL11.GL_SCISSOR_TEST); + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int scaleFactor = sr.getScaleFactor(); + int scissorX = this.x * scaleFactor; + int scissorY = (sr.getScaledHeight() - (this.y + this.height)) * scaleFactor; + int scissorW = this.width * scaleFactor; + int scissorH = this.height * scaleFactor; + GL11.glScissor(scissorX, scissorY, scissorW, scissorH); + container.visibleLines = height / container.lineHeight; if (listener instanceof GuiNPCInterface) { int k2 = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); if (k2 != 0) { - this.scrolledLine -= k2 / 40; - this.scrolledLine = Math.max(Math.min(this.scrolledLine, this.container.linesCount - this.height / this.container.lineHeight), 0); + //this.scrolledLine -= k2 / 80; + this.scrolledLine -= (int) Math.copySign(1, k2); + this.scrolledLine = Math.max(Math.min(this.scrolledLine, + this.container.linesCount - this.height / this.container.lineHeight), 0); } } @@ -150,8 +165,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { LineData data = list.get(i); String line = data.text; int w = line.length(); - - if (i >= scrolledLine && i < scrolledLine + container.visibleLines) { + + if (i >= scrolledLine && i <= scrolledLine + container.visibleLines) { //Highlight braces the cursor position is on if (startBracket != endBracket) { if (startBracket >= data.start && startBracket < data.end) { @@ -252,6 +267,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { drawRect(posX, posY, posX + 5, posY + sbSize, 0xFFe0e0e0); } + GL11.glDisable(GL11.GL_SCISSOR_TEST); } // Find a bracket span for a given cursor position. Returns {start, end} or null. private int[] findBracketSpanAt(int pos) { From b583608d5a83d977976b7bae70ed8bb493230a41 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 12:05:57 +0200 Subject: [PATCH 058/358] Smoothened scrolling experience in text editor --- .../client/gui/util/GuiScriptTextArea.java | 65 +++++++++++++------ 1 file changed, 44 insertions(+), 21 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 722b4cb6c..6cda57974 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -42,6 +42,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { private int endSelection; private int cursorPosition; private int scrolledLine = 0; + // Smooth scrolling state: fractional line position and target position + private double scrollPos = 0.0; + private double targetScroll = 0.0; private boolean enableCodeHighlighting = false; public List undoList = new ArrayList(); public List redoList = new ArrayList(); @@ -81,13 +84,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { container.visibleLines = height / container.lineHeight; + int maxScroll = Math.max(0, this.container.linesCount - container.visibleLines); if (listener instanceof GuiNPCInterface) { int k2 = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); if (k2 != 0) { - //this.scrolledLine -= k2 / 80; - this.scrolledLine -= (int) Math.copySign(1, k2); - this.scrolledLine = Math.max(Math.min(this.scrolledLine, - this.container.linesCount - this.height / this.container.lineHeight), 0); + // update target scroll smoothly instead of jumping + double sign = Math.copySign(1.0, k2); + targetScroll -= sign * 3.0; // one line per wheel tick + if (targetScroll < 0) targetScroll = 0; + if (targetScroll > maxScroll) targetScroll = maxScroll; } } @@ -110,8 +115,21 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (clickScrolling) { clickScrolling = Mouse.isButtonDown(0); int diff = container.linesCount - container.visibleLines; - scrolledLine = Math.min(Math.max((int) (1f * diff * (yMouse - y) / height), 0), diff); + double target = Math.min(Math.max(1f * diff * (yMouse - y) / height, 0f), (float) diff); + targetScroll = target; } + + // Animate scrollPos towards targetScroll + if (scrollPos < 0) scrollPos = scrolledLine; // initialize + double d = targetScroll - scrollPos; + if (Math.abs(d) > 0.001) { + scrollPos += d * 0.25; // easing factor + } else { + scrollPos = targetScroll; + } + // Update integer scrolledLine for compatibility + scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); + double fracOffset = scrollPos - scrolledLine; int startBracket = 0, endBracket = 0; if (startSelection >= 0 && text != null && text.length() > 0 && (endSelection - startSelection == 1 || startSelection == endSelection)) { @@ -161,24 +179,26 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } - for (int i = 0; i < list.size(); i++) { + // Expand render range by one line above/below so partially-visible lines are drawn + int renderStart = Math.max(0, (int) Math.floor(scrollPos) - 1); + int renderEnd = Math.min(list.size() - 1, (int) Math.ceil(scrollPos + container.visibleLines) + 1); + for (int i = renderStart; i <= renderEnd; i++) { LineData data = list.get(i); String line = data.text; int w = line.length(); - - if (i >= scrolledLine && i <= scrolledLine + container.visibleLines) { + // compute Y using fractional scrollPos so animation is smooth + int posY = y + (int) Math.round((i - scrollPos) * container.lineHeight); + if (i >= renderStart && i <= renderEnd) { //Highlight braces the cursor position is on if (startBracket != endBracket) { if (startBracket >= data.start && startBracket < data.end) { int s = ClientProxy.Font.width(line.substring(0, startBracket - data.start)); int e = ClientProxy.Font.width(line.substring(0, startBracket - data.start + 1)) + 1; - int posY = y + 0 + (i - scrolledLine) * container.lineHeight; drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 0, 0x9900cc00); } if (endBracket >= data.start && endBracket < data.end) { int s = ClientProxy.Font.width(line.substring(0, endBracket - data.start)); int e = ClientProxy.Font.width(line.substring(0, endBracket - data.start + 1)) + 1; - int posY = y + 0 + (i - scrolledLine) * container.lineHeight; drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 0, 0x9900cc00); } } @@ -189,31 +209,28 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (line.substring(m.start(), m.end()).equals(wordHightLight)) { int s = ClientProxy.Font.width(line.substring(0, m.start())); int e = ClientProxy.Font.width(line.substring(0, m.end())) + 1; - int posY = y + 0 + (i - scrolledLine) * container.lineHeight; drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight, 0x99004c00); } } } // Highlight the current line (light gray) under any selection if (active && isEnabled() && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { - int lineY = y + 0 + (i - scrolledLine) * container.lineHeight; - drawRect(x + 0, lineY, x + width - 1, lineY + container.lineHeight, 0x22e0e0e0); + drawRect(x + 0, posY, x + width - 1, posY + container.lineHeight, 0x22e0e0e0); } // Highlight selection if (startSelection != endSelection && endSelection > data.start && startSelection <= data.end) { if (startSelection < data.end) { int s = ClientProxy.Font.width(line.substring(0, Math.max(startSelection - data.start, 0))); int e = ClientProxy.Font.width(line.substring(0, Math.min(endSelection - data.start, w))) + 1; - int posY = y + 0 + (i - scrolledLine) * container.lineHeight; drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight, 0x992172ff); } } - int yPos = y + (i - scrolledLine) * container.lineHeight + 1; + int yPos = posY + 1; // Draw indent guides once per visible block based on brace spans - if (i == scrolledLine && !braceSpans.isEmpty()) { - int visStart = scrolledLine; - int visEnd = Math.min(list.size() - 1, scrolledLine + container.visibleLines - 1); + if (i == Math.max(0, (int) Math.floor(scrollPos)) && !braceSpans.isEmpty()) { + int visStart = Math.max(0, (int) Math.floor(scrollPos)); + int visEnd = Math.min(list.size() - 1, visStart + container.visibleLines - 1); for (int[] span : braceSpans) { int originalDepth = span[0]; int openLine = span[1]; @@ -263,7 +280,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { int sbSize = Math.max((int) (1f * container.visibleLines / container.linesCount * height), 2); int posX = x + width - 6; - int posY = (int) (y + 1f * scrolledLine / container.linesCount * (height - 4)) + 1; + double linesCount = Math.max(1, (double) container.linesCount); + int posY = (int) (y + 1f * scrollPos / linesCount * (height - 4)) + 1; drawRect(posX, posY, posX + 5, posY + sbSize, 0xFFe0e0e0); } @@ -1556,9 +1574,14 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.container.formatCodeText(); } - if (this.scrolledLine > this.container.linesCount - this.container.visibleLines) { - this.scrolledLine = Math.max(0, this.container.linesCount - this.container.visibleLines); + // Ensure scrolledLine stays in bounds and snap smooth scroll to avoid perceived lag + int maxScroll = Math.max(0, this.container.linesCount - this.container.visibleLines); + if (this.scrolledLine > maxScroll) { + this.scrolledLine = maxScroll; } + // Immediately update fractional scroll targets so new content appears without delay + this.targetScroll = this.scrolledLine; + this.scrollPos = this.scrolledLine; clampSelectionBounds(); From d774688bb612f6a56ec4636a20fdd45cf281ae1f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 12:28:17 +0200 Subject: [PATCH 059/358] Scrolling animation easing --- .../client/gui/util/GuiScriptTextArea.java | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 6cda57974..a19002c0c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -45,6 +45,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Smooth scrolling state: fractional line position and target position private double scrollPos = 0.0; private double targetScroll = 0.0; + // velocity used for spring-based easing + private double scrollVelocity = 0.0; + // last timestamp used for scroll integration (ms) + private long lastScrollTime = 0L; private boolean enableCodeHighlighting = false; public List undoList = new ArrayList(); public List redoList = new ArrayList(); @@ -89,8 +93,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { int k2 = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); if (k2 != 0) { // update target scroll smoothly instead of jumping - double sign = Math.copySign(1.0, k2); - targetScroll -= sign * 3.0; // one line per wheel tick + double sign = Math.copySign(1, k2); + targetScroll -= sign * 1.0; // one line per wheel tick if (targetScroll < 0) targetScroll = 0; if (targetScroll > maxScroll) targetScroll = maxScroll; } @@ -119,13 +123,32 @@ public class GuiScriptTextArea extends GuiNpcTextField { targetScroll = target; } - // Animate scrollPos towards targetScroll - if (scrollPos < 0) scrollPos = scrolledLine; // initialize - double d = targetScroll - scrollPos; - if (Math.abs(d) > 0.001) { - scrollPos += d * 0.25; // easing factor - } else { + // Animate scrollPos towards targetScroll using exponential smoothing + long nowMs = System.currentTimeMillis(); + if (scrollPos < 0) { + scrollPos = scrolledLine; // initialize + lastScrollTime = nowMs; + } + double dt = Math.min(0.05, (nowMs - lastScrollTime) / 1000.0); // clamp dt for stability + lastScrollTime = nowMs; + + double dist = targetScroll - scrollPos; + // If close enough, snap to target to avoid long slow tail + if (Math.abs(dist) < 0.01) { scrollPos = targetScroll; + scrollVelocity = 0.0; + } else { + // Time constant controlling speed (seconds). Smaller -> faster snap. + final double tau = 0.025; // ~55ms time constant feels snappy + double alpha = 1.0 - Math.exp(-dt / Math.max(1e-6, tau)); + double prev = scrollPos; + scrollPos += dist * alpha; + scrollVelocity = (scrollPos - prev) / (dt > 0 ? dt : 1e-6); + // Clamp overshoot + if ((dist > 0 && scrollPos > targetScroll) || (dist < 0 && scrollPos < targetScroll)) { + scrollPos = targetScroll; + scrollVelocity = 0.0; + } } // Update integer scrolledLine for compatibility scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); @@ -257,8 +280,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { int px = ClientProxy.Font.width(sb.toString()); int gx = x + 4 + px - 2; // shift left ~2px for the IntelliJ feel - int topY = y + (drawStart - scrolledLine) * container.lineHeight; - int bottomY = y + (drawEnd - scrolledLine + 1) * container.lineHeight - 2; + int topY = y + (int) Math.round((drawStart - scrollPos) * container.lineHeight); + int bottomY = y + (int) Math.round((drawEnd - scrollPos + 1) * container.lineHeight) - 2; int guideColor = (openLine == highlightedOpenLine && closeLine == highlightedCloseLine) ? 0x9933cc00 : 0x33FFFFFF; drawRect(gx, topY, gx + 1, bottomY, guideColor); } From 58a559e28b47ffee5da85daaf96831377454f3a7 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 13:13:53 +0200 Subject: [PATCH 060/358] Further scroll smoothening + improved scroll bar mouse click/drag functionality --- .../client/gui/util/GuiScriptTextArea.java | 112 +++++++++++++----- 1 file changed, 80 insertions(+), 32 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index a19002c0c..a36339262 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -49,6 +49,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { private double scrollVelocity = 0.0; // last timestamp used for scroll integration (ms) private long lastScrollTime = 0L; + // scrollbar drag offset in pixels (mouseY - thumbTop) when dragging begins + private int scrollbarDragOffset = 0; private boolean enableCodeHighlighting = false; public List undoList = new ArrayList(); public List redoList = new ArrayList(); @@ -94,33 +96,48 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (k2 != 0) { // update target scroll smoothly instead of jumping double sign = Math.copySign(1, k2); - targetScroll -= sign * 1.0; // one line per wheel tick + targetScroll -= sign * 2.0; // one line per wheel tick if (targetScroll < 0) targetScroll = 0; if (targetScroll > maxScroll) targetScroll = maxScroll; } } - if (clicked) { - clicked = Mouse.isButtonDown(0); - int i = getSelectionPos(xMouse, yMouse); - if (i != cursorPosition) { - if (doubleClicked || tripleClicked) { - startSelection = endSelection = cursorPosition; - doubleClicked = false; - tripleClicked = false; - } - setCursor(i, true); - } - } else if (doubleClicked || tripleClicked) { - doubleClicked = false; - tripleClicked = false; - } - if (clickScrolling) { + // keep dragging while mouse button is down clickScrolling = Mouse.isButtonDown(0); - int diff = container.linesCount - container.visibleLines; - double target = Math.min(Math.max(1f * diff * (yMouse - y) / height, 0f), (float) diff); - targetScroll = target; + int diff = Math.max(0, container.linesCount - container.visibleLines); + if (diff > 0) { + int sbSize = Math.max((int) (1f * container.visibleLines / container.linesCount * height), 2); + int trackTop = y + 1; + int trackHeight = Math.max(1, height - 4); + int thumbRange = Math.max(1, trackHeight - sbSize); + + // current thumb top for hit testing + double linesCountD = Math.max(1, (double) container.linesCount); + int thumbTop = (int) (y + 1f * scrollPos / linesCountD * (height - 4)) + 1; + + // If user clicked on the track (outside the thumb), jump to proportional position + if (yMouse < thumbTop || yMouse > thumbTop + sbSize) { + double centerRatio = (double) (yMouse - trackTop) / (double) trackHeight; + centerRatio = Math.max(0.0, Math.min(1.0, centerRatio)); + targetScroll = Math.max(0, Math.min(diff, centerRatio * diff)); + // prepare drag offset for subsequent drags (center grab) + scrollbarDragOffset = sbSize / 2; + } else { + // Dragging the thumb itself: preserve initial grab offset + int desiredTop = yMouse - scrollbarDragOffset; + desiredTop = Math.max(trackTop, Math.min(trackTop + thumbRange, desiredTop)); + // Speed up dragging a bit for snappier feel + final double dragSpeed = 1.5; + double ratio = (double) (desiredTop - trackTop) * dragSpeed / (double) thumbRange; + ratio = Math.max(0.0, Math.min(1.0, ratio)); + targetScroll = Math.max(0, Math.min(diff, ratio * diff)); + } + } + if (!clickScrolling) { + // reset drag offset when released + scrollbarDragOffset = 0; + } } // Animate scrollPos towards targetScroll using exponential smoothing @@ -139,7 +156,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { scrollVelocity = 0.0; } else { // Time constant controlling speed (seconds). Smaller -> faster snap. - final double tau = 0.025; // ~55ms time constant feels snappy + final double tau = 0.1; // ~55ms time constant feels snappy double alpha = 1.0 - Math.exp(-dt / Math.max(1e-6, tau)); double prev = scrollPos; scrollPos += dist * alpha; @@ -152,7 +169,25 @@ public class GuiScriptTextArea extends GuiNpcTextField { } // Update integer scrolledLine for compatibility scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); - double fracOffset = scrollPos - scrolledLine; + + + if (clicked) { + clicked = Mouse.isButtonDown(0); + int i = getSelectionPos(xMouse, yMouse); + if (i != cursorPosition) { + if (doubleClicked || tripleClicked) { + startSelection = endSelection = cursorPosition; + doubleClicked = false; + tripleClicked = false; + } + setCursor(i, true); + } + } else if (doubleClicked || tripleClicked) { + doubleClicked = false; + tripleClicked = false; + } + + //Calculate braces next to cursor to highlight int startBracket = 0, endBracket = 0; if (startSelection >= 0 && text != null && text.length() > 0 && (endSelection - startSelection == 1 || startSelection == endSelection)) { @@ -203,14 +238,21 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } // Expand render range by one line above/below so partially-visible lines are drawn - int renderStart = Math.max(0, (int) Math.floor(scrollPos) - 1); - int renderEnd = Math.min(list.size() - 1, (int) Math.ceil(scrollPos + container.visibleLines) + 1); + int renderStart = Math.max(0, scrolledLine - 1); + int renderEnd = Math.min(list.size() - 1, scrolledLine + container.visibleLines + 1); + + // Apply fractional GL translate for sub-pixel smooth scrolling + double fracOffset = scrollPos - scrolledLine; + float fracPixels = (float) (fracOffset * container.lineHeight); + GL11.glPushMatrix(); + GL11.glTranslatef(0.0f, -fracPixels, 0.0f); + for (int i = renderStart; i <= renderEnd; i++) { LineData data = list.get(i); String line = data.text; int w = line.length(); - // compute Y using fractional scrollPos so animation is smooth - int posY = y + (int) Math.round((i - scrollPos) * container.lineHeight); + // Use integer Y relative to scrolledLine; fractional offset applied via GL translate + int posY = y + (i - scrolledLine) * container.lineHeight; if (i >= renderStart && i <= renderEnd) { //Highlight braces the cursor position is on if (startBracket != endBracket) { @@ -251,8 +293,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { int yPos = posY + 1; // Draw indent guides once per visible block based on brace spans - if (i == Math.max(0, (int) Math.floor(scrollPos)) && !braceSpans.isEmpty()) { - int visStart = Math.max(0, (int) Math.floor(scrollPos)); + if (i == Math.max(0, scrolledLine) && !braceSpans.isEmpty()) { + int visStart = Math.max(0, scrolledLine); int visEnd = Math.min(list.size() - 1, visStart + container.visibleLines - 1); for (int[] span : braceSpans) { int originalDepth = span[0]; @@ -280,8 +322,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { int px = ClientProxy.Font.width(sb.toString()); int gx = x + 4 + px - 2; // shift left ~2px for the IntelliJ feel - int topY = y + (int) Math.round((drawStart - scrollPos) * container.lineHeight); - int bottomY = y + (int) Math.round((drawEnd - scrollPos + 1) * container.lineHeight) - 2; + int topY = y + (drawStart - scrolledLine) * container.lineHeight; + int bottomY = y + (drawEnd - scrolledLine + 1) * container.lineHeight - 2; int guideColor = (openLine == highlightedOpenLine && closeLine == highlightedCloseLine) ? 0x9933cc00 : 0x33FFFFFF; drawRect(gx, topY, gx + 1, bottomY, guideColor); } @@ -297,16 +339,17 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } + GL11.glPopMatrix(); if (hasVerticalScrollbar()) { Minecraft.getMinecraft().renderEngine.bindTexture(GuiCustomScroll.resource); - int sbSize = Math.max((int) (1f * container.visibleLines / container.linesCount * height), 2); + int sbSize = Math.max((int) (1f * (container.visibleLines) / container.linesCount * height), 2); int posX = x + width - 6; double linesCount = Math.max(1, (double) container.linesCount); int posY = (int) (y + 1f * scrollPos / linesCount * (height - 4)) + 1; - drawRect(posX, posY, posX + 5, posY + sbSize, 0xFFe0e0e0); + drawRect(posX, posY, posX + 5, posY + sbSize + 2, 0xFFe0e0e0); } GL11.glDisable(GL11.GL_SCISSOR_TEST); } @@ -497,6 +540,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { private int getSelectionPos(int xMouse, int yMouse) { xMouse -= this.x + 1; yMouse -= this.y + 1; + // Adjust yMouse to account for fractional GL translation (negative offset applied in rendering) + double fracOffset = scrollPos - scrolledLine; + double fracPixels = fracOffset * container.lineHeight; + yMouse = (int) Math.round(yMouse + fracPixels); + ArrayList list = new ArrayList(this.container.lines); for (int i = 0; i < list.size(); ++i) { From be9bf8759a6046914ea25228d09bd88341df0609 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 13:39:14 +0200 Subject: [PATCH 061/358] Fixed lowest line-length segment of indent being cut off --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index a36339262..80942c4e9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -295,7 +295,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Draw indent guides once per visible block based on brace spans if (i == Math.max(0, scrolledLine) && !braceSpans.isEmpty()) { int visStart = Math.max(0, scrolledLine); - int visEnd = Math.min(list.size() - 1, visStart + container.visibleLines - 1); + int visEnd = Math.min(list.size() - 1, visStart + container.visibleLines - 0); for (int[] span : braceSpans) { int originalDepth = span[0]; int openLine = span[1]; @@ -323,7 +323,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { int gx = x + 4 + px - 2; // shift left ~2px for the IntelliJ feel int topY = y + (drawStart - scrolledLine) * container.lineHeight; - int bottomY = y + (drawEnd - scrolledLine + 1) * container.lineHeight - 2; + int bottomY = y + (endLine - scrolledLine + 1) * container.lineHeight; int guideColor = (openLine == highlightedOpenLine && closeLine == highlightedCloseLine) ? 0x9933cc00 : 0x33FFFFFF; drawRect(gx, topY, gx + 1, bottomY, guideColor); } From 4fa6770b64a1831351dc9d26533756fdccfca8ca Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 13:39:32 +0200 Subject: [PATCH 062/358] Improved scroll bar mouse click logic --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 80942c4e9..b32fed03f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -127,9 +127,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Dragging the thumb itself: preserve initial grab offset int desiredTop = yMouse - scrollbarDragOffset; desiredTop = Math.max(trackTop, Math.min(trackTop + thumbRange, desiredTop)); - // Speed up dragging a bit for snappier feel - final double dragSpeed = 1.5; - double ratio = (double) (desiredTop - trackTop) * dragSpeed / (double) thumbRange; + double ratio = (double) (desiredTop - trackTop) / (double) thumbRange; ratio = Math.max(0.0, Math.min(1.0, ratio)); targetScroll = Math.max(0, Math.min(diff, ratio * diff)); } @@ -1579,6 +1577,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (this.clicked && this.container.linesCount * this.container.lineHeight > this.height && xMouse > this.x + this.width - 8) { this.clicked = false; this.clickScrolling = true; + double linesCountD = Math.max(1, (double) this.container.linesCount); + int thumbTop = (int) (this.y + 1f * this.scrollPos / linesCountD * (this.height - 4)) + 1; + this.scrollbarDragOffset = yMouse - thumbTop; } else { if (time - this.lastClicked < 300L) { this.clickCount++; From 01cbcbd0260c27d06010513b153034c575ae8cb9 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 14:05:39 +0200 Subject: [PATCH 063/358] ADDED LINE NUMBER GUTTER TO TEXT EDITOR --- .../client/gui/util/GuiScriptTextArea.java | 70 +++++++++++++++---- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index b32fed03f..c3150d41e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -56,6 +56,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { public List redoList = new ArrayList(); public boolean undoing = false; private long lastClicked = 0L; + // Line number gutter width (left margin for line numbers) + private static int LINE_NUMBER_GUTTER_WIDTH = 25; // private static TrueTypeFont font = new TrueTypeFont(new Font("Arial Unicode MS", Font.PLAIN, ConfigClient.FontSize), 1); public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) { @@ -74,17 +76,23 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (!visible) return; clampSelectionBounds(); + // Draw outer border around entire area drawRect(x - 1, y - 1, x + width + 1, y + height + 1, 0xffa0a0a0); - drawRect(x, y, x + width, y + height, 0xff000000); //THIS IS THE VIEWPORT + // Draw line number gutter background + drawRect(x, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff000000); + // Draw text viewport background (starts after gutter) + drawRect(x + LINE_NUMBER_GUTTER_WIDTH, y, x + width, y + height, 0xff000000); + // Draw separator line between gutter and text area + drawRect(x + LINE_NUMBER_GUTTER_WIDTH, y, x + LINE_NUMBER_GUTTER_WIDTH + 1, y + height, 0xff3c3f41); - // Enable scissor test to clip drawing to the viewport rectangle + // Enable scissor test to clip drawing to the TEXT viewport rectangle (excludes gutter) GL11.glEnable(GL11.GL_SCISSOR_TEST); Minecraft mc = Minecraft.getMinecraft(); ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); int scaleFactor = sr.getScaleFactor(); - int scissorX = this.x * scaleFactor; + int scissorX = (this.x ) * scaleFactor; int scissorY = (sr.getScaledHeight() - (this.y + this.height)) * scaleFactor; - int scissorW = this.width * scaleFactor; + int scissorW = (this.width ) * scaleFactor; int scissorH = this.height * scaleFactor; GL11.glScissor(scissorX, scissorY, scissorW, scissorH); @@ -245,6 +253,38 @@ public class GuiScriptTextArea extends GuiNpcTextField { GL11.glPushMatrix(); GL11.glTranslatef(0.0f, -fracPixels, 0.0f); + // Render line numbers in the gutter (outside scissor region, so pop first) + GL11.glPopMatrix(); + // GL11.glDisable(GL11.GL_SCISSOR_TEST); + GL11.glPushMatrix(); + GL11.glTranslatef(0.0f, -fracPixels, 0.0f); + for (int i = renderStart; i <= renderEnd; i++) { + int posY = y + (i - scrolledLine) * container.lineHeight; + String lineNum = String.valueOf(i + 1); + int lineNumWidth = ClientProxy.Font.width(lineNum); + int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5; // right-align with 5px padding + int lineNumY = posY + 1; + // Highlight current line number + int lineNumColor = 0xFF606366; + if (active && isEnabled()) { + for (int li = 0; li < list.size(); li++) { + LineData ld = list.get(li); + if (cursorPosition >= ld.start && cursorPosition < ld.end || (li == list.size() - 1 && cursorPosition == text.length())) { + if (li == i) { + lineNumColor = 0xFFa9b7c6; + break; + } + } + } + } + ClientProxy.Font.drawString(lineNum, lineNumX, lineNumY, lineNumColor); + } + GL11.glPopMatrix(); + // Re-enable scissor for text area and push matrix again + GL11.glEnable(GL11.GL_SCISSOR_TEST); + GL11.glPushMatrix(); + GL11.glTranslatef(0.0f, -fracPixels, 0.0f); + for (int i = renderStart; i <= renderEnd; i++) { LineData data = list.get(i); String line = data.text; @@ -257,12 +297,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (startBracket >= data.start && startBracket < data.end) { int s = ClientProxy.Font.width(line.substring(0, startBracket - data.start)); int e = ClientProxy.Font.width(line.substring(0, startBracket - data.start + 1)) + 1; - drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 0, 0x9900cc00); + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight + 0, 0x9900cc00); } if (endBracket >= data.start && endBracket < data.end) { int s = ClientProxy.Font.width(line.substring(0, endBracket - data.start)); int e = ClientProxy.Font.width(line.substring(0, endBracket - data.start + 1)) + 1; - drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight + 0, 0x9900cc00); + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight + 0, 0x9900cc00); } } //Highlight words @@ -272,20 +312,20 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (line.substring(m.start(), m.end()).equals(wordHightLight)) { int s = ClientProxy.Font.width(line.substring(0, m.start())); int e = ClientProxy.Font.width(line.substring(0, m.end())) + 1; - drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight, 0x99004c00); + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0x99004c00); } } } // Highlight the current line (light gray) under any selection if (active && isEnabled() && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { - drawRect(x + 0, posY, x + width - 1, posY + container.lineHeight, 0x22e0e0e0); + drawRect(x , posY, x + width - 1, posY + container.lineHeight, 0x22e0e0e0); } // Highlight selection if (startSelection != endSelection && endSelection > data.start && startSelection <= data.end) { if (startSelection < data.end) { int s = ClientProxy.Font.width(line.substring(0, Math.max(startSelection - data.start, 0))); int e = ClientProxy.Font.width(line.substring(0, Math.min(endSelection - data.start, w))) + 1; - drawRect(x + 1 + s, posY, x + 1 + e, posY + container.lineHeight, 0x992172ff); + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0x992172ff); } } int yPos = posY + 1; @@ -318,7 +358,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { for (int k = 0; k < spaces; k++) sb.append(' '); int px = ClientProxy.Font.width(sb.toString()); - int gx = x + 4 + px - 2; // shift left ~2px for the IntelliJ feel + int gx = x + LINE_NUMBER_GUTTER_WIDTH + 4 + px - 2; // shift left ~2px for the IntelliJ feel int topY = y + (drawStart - scrolledLine) * container.lineHeight; int bottomY = y + (endLine - scrolledLine + 1) * container.lineHeight; @@ -326,12 +366,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { drawRect(gx, topY, gx + 1, bottomY, guideColor); } } - data.drawString(x + 1, yPos, 0xFFe0e0e0); + data.drawString(x + LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); // Draw cursor: pause blinking while user is active recently boolean recentInput = System.currentTimeMillis() - this.lastInputTime < 500; if (active && isEnabled() && (recentInput || (cursorCounter / 10) % 2 == 0) && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { - int posX = x + ClientProxy.Font.width( + int posX = x + LINE_NUMBER_GUTTER_WIDTH + ClientProxy.Font.width( line.substring(0, Math.min(cursorPosition - data.start, line.length()))); drawRect(posX + 1, yPos -1, posX + 2, yPos - 1 + container.lineHeight, 0xffffffff); } @@ -536,7 +576,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } private int getSelectionPos(int xMouse, int yMouse) { - xMouse -= this.x + 1; + xMouse -= (this.x + LINE_NUMBER_GUTTER_WIDTH + 1); yMouse -= this.y + 1; // Adjust yMouse to account for fractional GL translation (negative offset applied in rendering) double fracOffset = scrollPos - scrolledLine; @@ -1529,7 +1569,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { int column = this.cursorPosition - data.start; LineData target = this.container.lines.get(i - 1); - int targetPos = this.getSelectionPos(this.x + 1 + ClientProxy.Font.width(data.text.substring(0, column)), this.y + 1 + (i - 1 - this.scrolledLine) * this.container.lineHeight); + int targetPos = this.getSelectionPos(this.x + LINE_NUMBER_GUTTER_WIDTH + 1 + ClientProxy.Font.width(data.text.substring(0, column)), this.y + 1 + (i - 1 - this.scrolledLine) * this.container.lineHeight); int targetIndent = getLineIndent(target.text); int minPos = target.start + Math.min(targetIndent, Math.max(0, target.text.length())); return Math.max(minPos, targetPos); @@ -1547,7 +1587,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (this.cursorPosition >= data.start && this.cursorPosition < data.end) { int column = this.cursorPosition - data.start; LineData target = this.container.lines.get(Math.min(i + 1, this.container.lines.size() - 1)); - int targetPos = this.getSelectionPos(this.x + 1 + ClientProxy.Font.width(data.text.substring(0, column)), this.y + 1 + (i + 1 - this.scrolledLine) * this.container.lineHeight); + int targetPos = this.getSelectionPos(this.x + LINE_NUMBER_GUTTER_WIDTH + 1 + ClientProxy.Font.width(data.text.substring(0, column)), this.y + 1 + (i + 1 - this.scrolledLine) * this.container.lineHeight); int targetIndent = getLineIndent(target.text); int minPos = target.start + Math.min(targetIndent, Math.max(0, target.text.length())); return Math.max(minPos, targetPos); From cec05c33cfed43490b1f74ca4fb8a84547fd6bdc Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 14:07:18 +0200 Subject: [PATCH 064/358] No longer drawing first indent depth as new line gutter's indent replaces that --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index c3150d41e..8bfcd7b13 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -340,8 +340,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { int closeLine = span[2]; int depth = span.length > 3 ? span[3] : originalDepth; // Skip top-level (depth 1) using the original depth to avoid hiding nested guides when adjusted - //if (originalDepth <= 1) - //continue; + if (originalDepth <= 1) + continue; int startLine = openLine + 1; // start under the opening brace int endLine = closeLine - 1; // stop before the closing brace if (startLine > endLine) From 793a2753fe3ad6bf456976f89fecca3442790f09 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 14:44:09 +0200 Subject: [PATCH 065/358] When Enter is pressed to insert new line, compensated by scrolling +1 to keep cursor at same position --- .../client/gui/util/GuiScriptTextArea.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 8bfcd7b13..19b1e92d7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -285,6 +285,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { GL11.glPushMatrix(); GL11.glTranslatef(0.0f, -fracPixels, 0.0f); + renderStart = Math.max(0, scrolledLine - 1); for (int i = renderStart; i <= renderEnd; i++) { LineData data = list.get(i); String line = data.text; @@ -616,6 +617,57 @@ public class GuiScriptTextArea extends GuiNpcTextField { return this.container.text.length(); } + // Ensure the cursor's line is visible by updating the smooth scroll target + // Mimics IntelliJ IDEA behavior: when pressing Enter, maintain the cursor's relative Y position + // in the viewport by scrolling forward to compensate for the newly added line. + private void ensureCursorVisible() { + if (container == null || container.lines == null || container.lines.isEmpty()) return; + + // Find the cursor's current line index + int lineIdx = 0; + for (int i = 0; i < container.lines.size(); i++) { + LineData ld = container.lines.get(i); + if (cursorPosition >= ld.start && cursorPosition <= ld.end) { + lineIdx = i; + break; + } + } + + int visible = Math.max(1, container.visibleLines); + int maxScroll = Math.max(0, container.linesCount - visible); + + // Calculate the cursor line's relative position in the current viewport (0 = top, visible-1 = bottom) + double relativePos = lineIdx - scrollPos; + + // If cursor is already visible in the viewport + if (relativePos >= 0 && relativePos < visible) { + // When Enter is pressed, a new line is inserted and cursor advances by 1. + // To maintain the same visual Y position, scroll forward by 1 line. + // This keeps the cursor at the same relative position while content scrolls up. + double newScroll = scrollPos + 1.0; + newScroll = Math.max(0, Math.min(newScroll, maxScroll)); + + targetScroll = newScroll; + scrollPos = newScroll; + scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); + scrollVelocity = 0.0; + } else if (relativePos < 0) { + // Cursor moved above the viewport; scroll up to show it near the top + double newScroll = Math.max(0, lineIdx - 2); // keep 2 lines above for context + targetScroll = newScroll; + scrollPos = newScroll; + scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); + scrollVelocity = 0.0; + } else { + // Cursor is below viewport; scroll down to show it + double newScroll = Math.max(0, Math.min(lineIdx - visible + 3, maxScroll)); + targetScroll = newScroll; + scrollPos = newScroll; + scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); + scrollVelocity = 0.0; + } + } + @Override public boolean textboxKeyTyped(char c, int i) { if (!active) @@ -896,6 +948,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { String leadingSegment = firstNewline == -1 ? after : after.substring(0, firstNewline); if (leadingSegment.trim().length() > 0) { addText("\n" + childIndent); + ensureCursorVisible(); return true; } @@ -938,14 +991,17 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (hasMatchingCloseSameIndent) { addText("\n" + childIndent); + ensureCursorVisible(); } else { String insert = "\n" + childIndent + "\n" + indent + "}"; setText(before + insert + after); int newCursor = before.length() + 1 + childIndent.length(); startSelection = endSelection = cursorPosition = newCursor; + ensureCursorVisible(); } } else { addText(Character.toString('\n') + getAutoIndentForEnter()); + ensureCursorVisible(); } } if (ChatAllowedCharacters.isAllowedCharacter(c)) { From 9afe569677cab6a51b5b18ced7bfad355231c2b9 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 14:48:14 +0200 Subject: [PATCH 066/358] Shifted LINE GUTTER to the left by 1 px --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 19b1e92d7..0ca82a4f1 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -83,7 +83,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Draw text viewport background (starts after gutter) drawRect(x + LINE_NUMBER_GUTTER_WIDTH, y, x + width, y + height, 0xff000000); // Draw separator line between gutter and text area - drawRect(x + LINE_NUMBER_GUTTER_WIDTH, y, x + LINE_NUMBER_GUTTER_WIDTH + 1, y + height, 0xff3c3f41); + drawRect(x + LINE_NUMBER_GUTTER_WIDTH-1, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff3c3f41); // Enable scissor test to clip drawing to the TEXT viewport rectangle (excludes gutter) GL11.glEnable(GL11.GL_SCISSOR_TEST); From 492bec21c3206838c57dc91b16d9fc2b204b6ba5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 15:57:48 +0200 Subject: [PATCH 067/358] Dynamically calculated gutter width based on line number digits --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 0ca82a4f1..92a58b042 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -76,6 +76,14 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (!visible) return; clampSelectionBounds(); + + // Dynamically calculate gutter width based on line count digits + if (container != null && container.linesCount > 0) { + int maxLineNum = container.linesCount; + String maxLineStr = String.valueOf(maxLineNum); + int digitWidth = ClientProxy.Font.width(maxLineStr); + LINE_NUMBER_GUTTER_WIDTH = digitWidth + 10; // 10px total padding (5px left + 5px right) + } // Draw outer border around entire area drawRect(x - 1, y - 1, x + width + 1, y + height + 1, 0xffa0a0a0); // Draw line number gutter background From 52580bd1c1d285effc1102e03c3cb2f22c445cdf Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 17:51:49 +0200 Subject: [PATCH 068/358] TEMP CHECK POINT (Up/Down buttons auto scroll working) --- .../client/gui/util/GuiScriptTextArea.java | 196 ++++++++++++------ .../gui/util/script/JavaTextContainer.java | 11 +- 2 files changed, 135 insertions(+), 72 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 92a58b042..a5817cbc9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -56,8 +56,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { public List redoList = new ArrayList(); public boolean undoing = false; private long lastClicked = 0L; - // Line number gutter width (left margin for line numbers) - private static int LINE_NUMBER_GUTTER_WIDTH = 25; + // Line number gutter width (left margin for line numbers) - dynamically calculated + private static int LINE_NUMBER_GUTTER_WIDTH = 25; // private static TrueTypeFont font = new TrueTypeFont(new Font("Arial Unicode MS", Font.PLAIN, ConfigClient.FontSize), 1); public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) { @@ -181,6 +181,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { scrollVelocity = 0.0; } } + // Ensure scrollPos stays in valid bounds + if (scrollPos < 0) scrollPos = 0; + if (scrollPos > maxScroll) scrollPos = maxScroll; + // Update integer scrolledLine for compatibility scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); @@ -266,8 +270,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { // GL11.glDisable(GL11.GL_SCISSOR_TEST); GL11.glPushMatrix(); GL11.glTranslatef(0.0f, -fracPixels, 0.0f); + + int stringYOffset = 2; + container.lineHeight=13; + + // Render LINE GUTTER numbers for (int i = renderStart; i <= renderEnd; i++) { - int posY = y + (i - scrolledLine) * container.lineHeight; + int posY = y + (i - scrolledLine) * container.lineHeight + stringYOffset; String lineNum = String.valueOf(i + 1); int lineNumWidth = ClientProxy.Font.width(lineNum); int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5; // right-align with 5px padding @@ -279,7 +288,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { LineData ld = list.get(li); if (cursorPosition >= ld.start && cursorPosition < ld.end || (li == list.size() - 1 && cursorPosition == text.length())) { if (li == i) { - lineNumColor = 0xFFa9b7c6; + lineNumColor = 0xFFb9c7d6; break; } } @@ -337,7 +346,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0x992172ff); } } - int yPos = posY + 1; // Draw indent guides once per visible block based on brace spans if (i == Math.max(0, scrolledLine) && !braceSpans.isEmpty()) { @@ -375,6 +383,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { drawRect(gx, topY, gx + 1, bottomY, guideColor); } } + int yPos = posY + stringYOffset; data.drawString(x + LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); // Draw cursor: pause blinking while user is active recently @@ -625,55 +634,58 @@ public class GuiScriptTextArea extends GuiNpcTextField { return this.container.text.length(); } - // Ensure the cursor's line is visible by updating the smooth scroll target - // Mimics IntelliJ IDEA behavior: when pressing Enter, maintain the cursor's relative Y position - // in the viewport by scrolling forward to compensate for the newly added line. - private void ensureCursorVisible() { - if (container == null || container.lines == null || container.lines.isEmpty()) return; - - // Find the cursor's current line index - int lineIdx = 0; + // Find which line the cursor is on (0-indexed) + private int getCursorLineIndex() { + if (container == null || container.lines == null || container.lines.isEmpty()) return 0; for (int i = 0; i < container.lines.size(); i++) { LineData ld = container.lines.get(i); - if (cursorPosition >= ld.start && cursorPosition <= ld.end) { - lineIdx = i; - break; + // For last line, use <= since end doesn't include +1 for newline + // For other lines, use < since end includes the newline position + boolean isLastLine = (i == container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (cursorPosition >= ld.start && cursorPosition <= ld.end) + : (cursorPosition >= ld.start && cursorPosition < ld.end); + if (isOnLine) { + return i; } } + return container.lines.size() - 1; + } + + // Check if cursor is on a specific line, handling the last line specially + // (last line doesn't have trailing newline, so use <= for end boundary) + private boolean isCursorOnLine(int lineIndex, LineData line) { + boolean isLastLine = (lineIndex == container.lines.size() - 1); + return isLastLine + ? (cursorPosition >= line.start && cursorPosition <= line.end) + : (cursorPosition >= line.start && cursorPosition < line.end); + } + + // Scroll viewport to keep cursor visible (minimal adjustment, like IntelliJ) + // Only scrolls if cursor is outside the visible area + private void scrollToCursor() { + if (container == null || container.lines == null || container.lines.isEmpty()) return; + int lineIdx = getCursorLineIndex(); int visible = Math.max(1, container.visibleLines); int maxScroll = Math.max(0, container.linesCount - visible); - // Calculate the cursor line's relative position in the current viewport (0 = top, visible-1 = bottom) - double relativePos = lineIdx - scrollPos; + int firstVisible = scrolledLine; + int lastFullyVisible = scrolledLine + visible - 1; - // If cursor is already visible in the viewport - if (relativePos >= 0 && relativePos < visible) { - // When Enter is pressed, a new line is inserted and cursor advances by 1. - // To maintain the same visual Y position, scroll forward by 1 line. - // This keeps the cursor at the same relative position while content scrolls up. - double newScroll = scrollPos + 1.0; - newScroll = Math.max(0, Math.min(newScroll, maxScroll)); - - targetScroll = newScroll; - scrollPos = newScroll; - scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); - scrollVelocity = 0.0; - } else if (relativePos < 0) { - // Cursor moved above the viewport; scroll up to show it near the top - double newScroll = Math.max(0, lineIdx - 2); // keep 2 lines above for context - targetScroll = newScroll; - scrollPos = newScroll; - scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); - scrollVelocity = 0.0; - } else { - // Cursor is below viewport; scroll down to show it - double newScroll = Math.max(0, Math.min(lineIdx - visible + 3, maxScroll)); - targetScroll = newScroll; - scrollPos = newScroll; - scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); - scrollVelocity = 0.0; + if (lineIdx < firstVisible) { + // Cursor is above viewport - scroll up just enough to show it + targetScroll = lineIdx; + } else if (lineIdx > lastFullyVisible) { + // Cursor is below viewport - scroll down just enough to show it + targetScroll = Math.min(lineIdx - visible + 1, maxScroll); } + // If cursor is visible, don't change scroll + } + + // Legacy method name for compatibility + private void ensureCursorVisible() { + scrollToCursor(); } @Override @@ -727,10 +739,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { } if (i == Keyboard.KEY_UP) { setCursor(cursorUp(), GuiScreen.isShiftKeyDown()); + scrollToCursor(); return true; } if (i == Keyboard.KEY_DOWN) { setCursor(cursorDown(), GuiScreen.isShiftKeyDown()); + scrollToCursor(); return true; } if (i == Keyboard.KEY_DELETE) { @@ -797,8 +811,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Find current line for indent-aware backspace LineData curr = null; - for (LineData line : container.lines) { - if (cursorPosition >= line.start && cursorPosition < line.end) { + for (int idx = 0; idx < container.lines.size(); idx++) { + LineData line = container.lines.get(idx); + boolean isLastLine = (idx == container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (cursorPosition >= line.start && cursorPosition <= line.end) + : (cursorPosition >= line.start && cursorPosition < line.end); + if (isOnLine) { curr = line; break; } @@ -956,7 +975,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { String leadingSegment = firstNewline == -1 ? after : after.substring(0, firstNewline); if (leadingSegment.trim().length() > 0) { addText("\n" + childIndent); - ensureCursorVisible(); + scrollToCursor(); return true; } @@ -999,17 +1018,17 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (hasMatchingCloseSameIndent) { addText("\n" + childIndent); - ensureCursorVisible(); + scrollToCursor(); } else { String insert = "\n" + childIndent + "\n" + indent + "}"; setText(before + insert + after); int newCursor = before.length() + 1 + childIndent.length(); startSelection = endSelection = cursorPosition = newCursor; - ensureCursorVisible(); + scrollToCursor(); } } else { addText(Character.toString('\n') + getAutoIndentForEnter()); - ensureCursorVisible(); + scrollToCursor(); } } if (ChatAllowedCharacters.isAllowedCharacter(c)) { @@ -1261,8 +1280,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { private String getAutoIndentForEnter() { LineData currentLine = null; - for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { + for (int idx = 0; idx < this.container.lines.size(); idx++) { + LineData line = this.container.lines.get(idx); + boolean isLastLine = (idx == this.container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (this.cursorPosition >= line.start && this.cursorPosition <= line.end) + : (this.cursorPosition >= line.start && this.cursorPosition < line.end); + if (isOnLine) { currentLine = line; break; } @@ -1483,8 +1507,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void handleTab() { LineData currentLine = null; - for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { + for (int i = 0; i < this.container.lines.size(); i++) { + LineData line = this.container.lines.get(i); + boolean isLastLine = (i == this.container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (this.cursorPosition >= line.start && this.cursorPosition <= line.end) + : (this.cursorPosition >= line.start && this.cursorPosition < line.end); + if (isOnLine) { currentLine = line; break; } @@ -1542,8 +1571,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void handleShiftTab() { LineData currentLine = null; - for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { + for (int i = 0; i < this.container.lines.size(); i++) { + LineData line = this.container.lines.get(i); + boolean isLastLine = (i == this.container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (this.cursorPosition >= line.start && this.cursorPosition <= line.end) + : (this.cursorPosition >= line.start && this.cursorPosition < line.end); + if (isOnLine) { currentLine = line; break; } @@ -1626,16 +1660,24 @@ public class GuiScriptTextArea extends GuiNpcTextField { private int cursorUp() { for (int i = 0; i < this.container.lines.size(); ++i) { LineData data = (LineData) this.container.lines.get(i); - if (this.cursorPosition >= data.start && this.cursorPosition < data.end) { + // For last line, use <= since end doesn't include +1 for newline + // For other lines, use < since end includes the newline position + boolean isLastLine = (i == this.container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (this.cursorPosition >= data.start && this.cursorPosition <= data.end) + : (this.cursorPosition >= data.start && this.cursorPosition < data.end); + if (isOnLine) { if (i == 0) { return 0; } - int column = this.cursorPosition - data.start; + int column = Math.min(this.cursorPosition - data.start, data.text.length()); LineData target = this.container.lines.get(i - 1); - int targetPos = this.getSelectionPos(this.x + LINE_NUMBER_GUTTER_WIDTH + 1 + ClientProxy.Font.width(data.text.substring(0, column)), this.y + 1 + (i - 1 - this.scrolledLine) * this.container.lineHeight); + // Calculate target position on the line above, maintaining column if possible + int targetColumn = Math.min(column, target.text.length()); int targetIndent = getLineIndent(target.text); - int minPos = target.start + Math.min(targetIndent, Math.max(0, target.text.length())); + int minPos = target.start + Math.min(targetIndent, target.text.length()); + int targetPos = target.start + targetColumn; return Math.max(minPos, targetPos); } } @@ -1648,12 +1690,25 @@ public class GuiScriptTextArea extends GuiNpcTextField { return cursorPosition; for (int i = 0; i < this.container.lines.size(); ++i) { LineData data = (LineData) this.container.lines.get(i); - if (this.cursorPosition >= data.start && this.cursorPosition < data.end) { - int column = this.cursorPosition - data.start; - LineData target = this.container.lines.get(Math.min(i + 1, this.container.lines.size() - 1)); - int targetPos = this.getSelectionPos(this.x + LINE_NUMBER_GUTTER_WIDTH + 1 + ClientProxy.Font.width(data.text.substring(0, column)), this.y + 1 + (i + 1 - this.scrolledLine) * this.container.lineHeight); + // For last line, use <= since end doesn't include +1 for newline + // For other lines, use < since end includes the newline position + boolean isLastLine = (i == this.container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (this.cursorPosition >= data.start && this.cursorPosition <= data.end) + : (this.cursorPosition >= data.start && this.cursorPosition < data.end); + if (isOnLine) { + // If already on last line, stay at current position + if (i >= this.container.lines.size() - 1) { + return cursorPosition; + } + + int column = Math.min(this.cursorPosition - data.start, data.text.length()); + LineData target = this.container.lines.get(i + 1); + // Calculate target position on the line below, maintaining column if possible + int targetColumn = Math.min(column, target.text.length()); int targetIndent = getLineIndent(target.text); - int minPos = target.start + Math.min(targetIndent, Math.max(0, target.text.length())); + int minPos = target.start + Math.min(targetIndent, target.text.length()); + int targetPos = target.start + targetColumn; return Math.max(minPos, targetPos); } } @@ -1750,14 +1805,17 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.container.formatCodeText(); } - // Ensure scrolledLine stays in bounds and snap smooth scroll to avoid perceived lag + // Ensure scroll state stays in bounds after text change int maxScroll = Math.max(0, this.container.linesCount - this.container.visibleLines); if (this.scrolledLine > maxScroll) { this.scrolledLine = maxScroll; } - // Immediately update fractional scroll targets so new content appears without delay - this.targetScroll = this.scrolledLine; - this.scrollPos = this.scrolledLine; + if (this.targetScroll > maxScroll) { + this.targetScroll = maxScroll; + } + if (this.scrollPos > maxScroll) { + this.scrollPos = maxScroll; + } clampSelectionBounds(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index b0bed6ea9..37bfe9d45 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -150,7 +150,8 @@ public class JavaTextContainer extends TextContainer { String[] split = text.split("\\n", -1); // -1 preserves empty lines "" int totalChars = 0; - for (String l : split) { + for (int lineIndex = 0; lineIndex < split.length; lineIndex++) { + String l = split[lineIndex]; StringBuilder line = new StringBuilder(); Matcher m = regexWord.matcher(l); int i = 0; @@ -164,9 +165,13 @@ public class JavaTextContainer extends TextContainer { line.append(word); i = m.start(); } - lines.add(new LineData(line.toString(), totalChars, totalChars + line.length() + 1)); + // Only add +1 for the newline if this is NOT the last line + // The last line has no newline after it + boolean isLastLine = (lineIndex == split.length - 1); + int endOffset = isLastLine ? 0 : 1; + lines.add(new LineData(line.toString(), totalChars, totalChars + line.length() + endOffset)); - totalChars += line.length() + 1; + totalChars += line.length() + endOffset; } linesCount = lines.size(); totalHeight = linesCount * lineHeight; From 8a5e1fb0620d0bb4a9230b46407f6aa3c40ae6ba Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 19:07:05 +0200 Subject: [PATCH 069/358] Enter/Backspace/Tab/Up/Down now auto scroll to the cursor's position !!! --- .../client/gui/util/GuiScriptTextArea.java | 198 ++++++++++++------ .../gui/util/script/JavaTextContainer.java | 19 +- 2 files changed, 143 insertions(+), 74 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 92a58b042..857df428e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -56,8 +56,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { public List redoList = new ArrayList(); public boolean undoing = false; private long lastClicked = 0L; - // Line number gutter width (left margin for line numbers) - private static int LINE_NUMBER_GUTTER_WIDTH = 25; + // Line number gutter width (left margin for line numbers) - dynamically calculated + private static int LINE_NUMBER_GUTTER_WIDTH = 25; // private static TrueTypeFont font = new TrueTypeFont(new Font("Arial Unicode MS", Font.PLAIN, ConfigClient.FontSize), 1); public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) { @@ -104,7 +104,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { int scissorH = this.height * scaleFactor; GL11.glScissor(scissorX, scissorY, scissorW, scissorH); - container.visibleLines = height / container.lineHeight; + container.visibleLines = height / container.lineHeight-1; int maxScroll = Math.max(0, this.container.linesCount - container.visibleLines); if (listener instanceof GuiNPCInterface) { @@ -181,6 +181,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { scrollVelocity = 0.0; } } + // Ensure scrollPos stays in valid bounds + if (scrollPos < 0) scrollPos = 0; + if (scrollPos > maxScroll) scrollPos = maxScroll; + // Update integer scrolledLine for compatibility scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); @@ -596,7 +600,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { for (int i = 0; i < list.size(); ++i) { LineData data = (LineData) list.get(i); - if (i >= this.scrolledLine && i < this.scrolledLine + this.container.visibleLines) { + if (i >= this.scrolledLine && i <= this.scrolledLine + this.container.visibleLines) { int yPos = (i - this.scrolledLine) * this.container.lineHeight; if (yMouse >= yPos && yMouse < yPos + this.container.lineHeight) { int lineWidth = 0; @@ -625,55 +629,58 @@ public class GuiScriptTextArea extends GuiNpcTextField { return this.container.text.length(); } - // Ensure the cursor's line is visible by updating the smooth scroll target - // Mimics IntelliJ IDEA behavior: when pressing Enter, maintain the cursor's relative Y position - // in the viewport by scrolling forward to compensate for the newly added line. - private void ensureCursorVisible() { - if (container == null || container.lines == null || container.lines.isEmpty()) return; - - // Find the cursor's current line index - int lineIdx = 0; + // Find which line the cursor is on (0-indexed) + private int getCursorLineIndex() { + if (container == null || container.lines == null || container.lines.isEmpty()) return 0; for (int i = 0; i < container.lines.size(); i++) { LineData ld = container.lines.get(i); - if (cursorPosition >= ld.start && cursorPosition <= ld.end) { - lineIdx = i; - break; + // For last line, use <= since end doesn't include +1 for newline + // For other lines, use < since end includes the newline position + boolean isLastLine = (i == container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (cursorPosition >= ld.start && cursorPosition <= ld.end) + : (cursorPosition >= ld.start && cursorPosition < ld.end); + if (isOnLine) { + return i; } } + return container.lines.size() - 1; + } + + // Check if cursor is on a specific line, handling the last line specially + // (last line doesn't have trailing newline, so use <= for end boundary) + private boolean isCursorOnLine(int lineIndex, LineData line) { + boolean isLastLine = (lineIndex == container.lines.size() - 1); + return isLastLine + ? (cursorPosition >= line.start && cursorPosition <= line.end) + : (cursorPosition >= line.start && cursorPosition < line.end); + } + + // Scroll viewport to keep cursor visible (minimal adjustment, like IntelliJ) + // Only scrolls if cursor is outside the visible area + private void scrollToCursor() { + if (container == null || container.lines == null || container.lines.isEmpty()) return; + int lineIdx = getCursorLineIndex(); int visible = Math.max(1, container.visibleLines); int maxScroll = Math.max(0, container.linesCount - visible); - // Calculate the cursor line's relative position in the current viewport (0 = top, visible-1 = bottom) - double relativePos = lineIdx - scrollPos; + int firstVisible = scrolledLine; + int lastFullyVisible = scrolledLine + visible; - // If cursor is already visible in the viewport - if (relativePos >= 0 && relativePos < visible) { - // When Enter is pressed, a new line is inserted and cursor advances by 1. - // To maintain the same visual Y position, scroll forward by 1 line. - // This keeps the cursor at the same relative position while content scrolls up. - double newScroll = scrollPos + 1.0; - newScroll = Math.max(0, Math.min(newScroll, maxScroll)); - - targetScroll = newScroll; - scrollPos = newScroll; - scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); - scrollVelocity = 0.0; - } else if (relativePos < 0) { - // Cursor moved above the viewport; scroll up to show it near the top - double newScroll = Math.max(0, lineIdx - 2); // keep 2 lines above for context - targetScroll = newScroll; - scrollPos = newScroll; - scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); - scrollVelocity = 0.0; - } else { - // Cursor is below viewport; scroll down to show it - double newScroll = Math.max(0, Math.min(lineIdx - visible + 3, maxScroll)); - targetScroll = newScroll; - scrollPos = newScroll; - scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); - scrollVelocity = 0.0; + if (lineIdx < firstVisible) { + // Cursor is above viewport - scroll up just enough to show it + targetScroll = lineIdx; + } else if (lineIdx > lastFullyVisible) { + // Cursor is below viewport - scroll down just enough to show it + targetScroll = Math.min(lineIdx - visible, maxScroll); } + // If cursor is visible, don't change scroll + } + + // Legacy method name for compatibility + private void ensureCursorVisible() { + scrollToCursor(); } @Override @@ -727,10 +734,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { } if (i == Keyboard.KEY_UP) { setCursor(cursorUp(), GuiScreen.isShiftKeyDown()); + scrollToCursor(); return true; } if (i == Keyboard.KEY_DOWN) { setCursor(cursorDown(), GuiScreen.isShiftKeyDown()); + scrollToCursor(); return true; } if (i == Keyboard.KEY_DELETE) { @@ -787,6 +796,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { String s = getSelectionBeforeText(); setText(s + getSelectionAfterText()); endSelection = cursorPosition = startSelection; + scrollToCursor(); return true; } @@ -797,8 +807,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Find current line for indent-aware backspace LineData curr = null; - for (LineData line : container.lines) { - if (cursorPosition >= line.start && cursorPosition < line.end) { + for (int idx = 0; idx < container.lines.size(); idx++) { + LineData line = container.lines.get(idx); + boolean isLastLine = (idx == container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (cursorPosition >= line.start && cursorPosition <= line.end) + : (cursorPosition >= line.start && cursorPosition < line.end); + if (isOnLine) { curr = line; break; } @@ -830,6 +845,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Cursor goes to end of previous line (at the newline position, which is now end of prev line content) int newCursor = Math.max(0, curr.start - 1); endSelection = cursorPosition = startSelection = newCursor; + scrollToCursor(); return true; } else { // Line has content: merge with previous line @@ -857,6 +873,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(before + spacer + content); int newCursor = before.length() + spacer.length(); endSelection = cursorPosition = startSelection = newCursor; + scrollToCursor(); return true; } } @@ -876,6 +893,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(before + after); startSelection -= 1; endSelection = cursorPosition = startSelection; + scrollToCursor(); return true; } } @@ -886,6 +904,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { startSelection--; setText(s + getSelectionAfterText()); endSelection = cursorPosition = startSelection; + scrollToCursor(); return true; } if (this.isKeyComboCtrlX(i)) { @@ -894,7 +913,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { String s = getSelectionBeforeText(); setText(s + getSelectionAfterText()); endSelection = startSelection = cursorPosition = s.length(); - + scrollToCursor(); } return true; } @@ -906,6 +925,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } if (this.isKeyComboCtrlV(i)) { addText(NoppesStringUtils.getClipboardContents()); + scrollToCursor(); return true; } if (i == Keyboard.KEY_Z && isCtrlKeyDown()) { @@ -917,6 +937,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(data.text); endSelection = startSelection = cursorPosition = data.cursorPosition; undoing = false; + scrollToCursor(); return true; } if (i == Keyboard.KEY_Y && isCtrlKeyDown()) { @@ -928,6 +949,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(data.text); endSelection = startSelection = cursorPosition = data.cursorPosition; undoing = false; + scrollToCursor(); return true; } if (i == Keyboard.KEY_TAB) { @@ -937,6 +959,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { } else { handleTab(); } + scrollToCursor(); + return true; } if (i == Keyboard.KEY_F && isCtrlKeyDown()) { formatText(); @@ -956,7 +980,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { String leadingSegment = firstNewline == -1 ? after : after.substring(0, firstNewline); if (leadingSegment.trim().length() > 0) { addText("\n" + childIndent); - ensureCursorVisible(); + scrollToCursor(); return true; } @@ -999,17 +1023,17 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (hasMatchingCloseSameIndent) { addText("\n" + childIndent); - ensureCursorVisible(); + scrollToCursor(); } else { String insert = "\n" + childIndent + "\n" + indent + "}"; setText(before + insert + after); int newCursor = before.length() + 1 + childIndent.length(); startSelection = endSelection = cursorPosition = newCursor; - ensureCursorVisible(); + scrollToCursor(); } } else { addText(Character.toString('\n') + getAutoIndentForEnter()); - ensureCursorVisible(); + scrollToCursor(); } } if (ChatAllowedCharacters.isAllowedCharacter(c)) { @@ -1020,31 +1044,37 @@ public class GuiScriptTextArea extends GuiNpcTextField { if ((c == ')' || c == ']' || c == '"' || c == '\'' ) && after.length() > 0 && after.charAt(0) == c) { // move cursor forward over existing closer startSelection = endSelection = cursorPosition = before.length() + 1; + scrollToCursor(); return true; } if (c == '"') { setText(before + "\"\"" + after); startSelection = endSelection = cursorPosition = before.length() + 1; + scrollToCursor(); return true; } if (c == '\'') { setText(before + "''" + after); startSelection = endSelection = cursorPosition = before.length() + 1; + scrollToCursor(); return true; } if (c == '[') { setText(before + "[]" + after); startSelection = endSelection = cursorPosition = before.length() + 1; + scrollToCursor(); return true; } if (c == '(') { setText(before + "()" + after); startSelection = endSelection = cursorPosition = before.length() + 1; + scrollToCursor(); return true; } addText(Character.toString(c)); + scrollToCursor(); } if (i == Keyboard.KEY_SLASH && isCtrlKeyDown()) { @@ -1483,8 +1513,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void handleTab() { LineData currentLine = null; - for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { + for (int i = 0; i < this.container.lines.size(); i++) { + LineData line = this.container.lines.get(i); + boolean isLastLine = (i == this.container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (this.cursorPosition >= line.start && this.cursorPosition <= line.end) + : (this.cursorPosition >= line.start && this.cursorPosition < line.end); + if (isOnLine) { currentLine = line; break; } @@ -1542,8 +1577,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void handleShiftTab() { LineData currentLine = null; - for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { + for (int i = 0; i < this.container.lines.size(); i++) { + LineData line = this.container.lines.get(i); + boolean isLastLine = (i == this.container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (this.cursorPosition >= line.start && this.cursorPosition <= line.end) + : (this.cursorPosition >= line.start && this.cursorPosition < line.end); + if (isOnLine) { currentLine = line; break; } @@ -1626,16 +1666,24 @@ public class GuiScriptTextArea extends GuiNpcTextField { private int cursorUp() { for (int i = 0; i < this.container.lines.size(); ++i) { LineData data = (LineData) this.container.lines.get(i); - if (this.cursorPosition >= data.start && this.cursorPosition < data.end) { + // For last line, use <= since end doesn't include +1 for newline + // For other lines, use < since end includes the newline position + boolean isLastLine = (i == this.container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (this.cursorPosition >= data.start && this.cursorPosition <= data.end) + : (this.cursorPosition >= data.start && this.cursorPosition < data.end); + if (isOnLine) { if (i == 0) { return 0; } - int column = this.cursorPosition - data.start; + int column = Math.min(this.cursorPosition - data.start, data.text.length()); LineData target = this.container.lines.get(i - 1); - int targetPos = this.getSelectionPos(this.x + LINE_NUMBER_GUTTER_WIDTH + 1 + ClientProxy.Font.width(data.text.substring(0, column)), this.y + 1 + (i - 1 - this.scrolledLine) * this.container.lineHeight); + // Calculate target position on the line above, maintaining column if possible + int targetColumn = Math.min(column, target.text.length()); int targetIndent = getLineIndent(target.text); - int minPos = target.start + Math.min(targetIndent, Math.max(0, target.text.length())); + int minPos = target.start + Math.min(targetIndent, target.text.length()); + int targetPos = target.start + targetColumn; return Math.max(minPos, targetPos); } } @@ -1648,12 +1696,25 @@ public class GuiScriptTextArea extends GuiNpcTextField { return cursorPosition; for (int i = 0; i < this.container.lines.size(); ++i) { LineData data = (LineData) this.container.lines.get(i); - if (this.cursorPosition >= data.start && this.cursorPosition < data.end) { - int column = this.cursorPosition - data.start; - LineData target = this.container.lines.get(Math.min(i + 1, this.container.lines.size() - 1)); - int targetPos = this.getSelectionPos(this.x + LINE_NUMBER_GUTTER_WIDTH + 1 + ClientProxy.Font.width(data.text.substring(0, column)), this.y + 1 + (i + 1 - this.scrolledLine) * this.container.lineHeight); + // For last line, use <= since end doesn't include +1 for newline + // For other lines, use < since end includes the newline position + boolean isLastLine = (i == this.container.lines.size() - 1); + boolean isOnLine = isLastLine + ? (this.cursorPosition >= data.start && this.cursorPosition <= data.end) + : (this.cursorPosition >= data.start && this.cursorPosition < data.end); + if (isOnLine) { + // If already on last line, stay at current position + if (i >= this.container.lines.size() - 1) { + return cursorPosition; + } + + int column = Math.min(this.cursorPosition - data.start, data.text.length()); + LineData target = this.container.lines.get(i + 1); + // Calculate target position on the line below, maintaining column if possible + int targetColumn = Math.min(column, target.text.length()); int targetIndent = getLineIndent(target.text); - int minPos = target.start + Math.min(targetIndent, Math.max(0, target.text.length())); + int minPos = target.start + Math.min(targetIndent, target.text.length()); + int targetPos = target.start + targetColumn; return Math.max(minPos, targetPos); } } @@ -1750,14 +1811,17 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.container.formatCodeText(); } - // Ensure scrolledLine stays in bounds and snap smooth scroll to avoid perceived lag + // Ensure scroll state stays in bounds after text change int maxScroll = Math.max(0, this.container.linesCount - this.container.visibleLines); if (this.scrolledLine > maxScroll) { this.scrolledLine = maxScroll; } - // Immediately update fractional scroll targets so new content appears without delay - this.targetScroll = this.scrolledLine; - this.scrollPos = this.scrolledLine; + if (this.targetScroll > maxScroll) { + this.targetScroll = maxScroll; + } + if (this.scrollPos > maxScroll) { + this.scrollPos = maxScroll; + } clampSelectionBounds(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index b0bed6ea9..b225766f9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -144,13 +144,14 @@ public class JavaTextContainer extends TextContainer { } public void init(int width, int height) { - lineHeight = ClientProxy.Font.height(); - if (lineHeight == 0) - lineHeight = 12; + // lineHeight = ClientProxy.Font.height(); + // if (lineHeight == 0) + lineHeight = 13; String[] split = text.split("\\n", -1); // -1 preserves empty lines "" int totalChars = 0; - for (String l : split) { + for (int lineIndex = 0; lineIndex < split.length; lineIndex++) { + String l = split[lineIndex]; StringBuilder line = new StringBuilder(); Matcher m = regexWord.matcher(l); int i = 0; @@ -164,13 +165,17 @@ public class JavaTextContainer extends TextContainer { line.append(word); i = m.start(); } - lines.add(new LineData(line.toString(), totalChars, totalChars + line.length() + 1)); + // Only add +1 for the newline if this is NOT the last line + // The last line has no newline after it + boolean isLastLine = (lineIndex == split.length - 1); + int endOffset = isLastLine ? 0 : 1; + lines.add(new LineData(line.toString(), totalChars, totalChars + line.length() + endOffset)); - totalChars += line.length() + 1; + totalChars += line.length() + endOffset; } linesCount = lines.size(); totalHeight = linesCount * lineHeight; - visibleLines = Math.max(height / lineHeight, 1); + visibleLines = Math.max(height / lineHeight-1, 1); // Invalidate caches that depend on line layout invalidateCaches(); From e987f596ef44bb28d9d375cd3d8dbab595b15ab7 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 19:08:32 +0200 Subject: [PATCH 070/358] Centered text in new line height + brightened current selected line number color --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 857df428e..47bcf1447 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -270,8 +270,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { // GL11.glDisable(GL11.GL_SCISSOR_TEST); GL11.glPushMatrix(); GL11.glTranslatef(0.0f, -fracPixels, 0.0f); + + int stringYOffset = 2; + + // Render LINE GUTTER numbers for (int i = renderStart; i <= renderEnd; i++) { - int posY = y + (i - scrolledLine) * container.lineHeight; + int posY = y + (i - scrolledLine) * container.lineHeight + stringYOffset; String lineNum = String.valueOf(i + 1); int lineNumWidth = ClientProxy.Font.width(lineNum); int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5; // right-align with 5px padding @@ -283,7 +287,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { LineData ld = list.get(li); if (cursorPosition >= ld.start && cursorPosition < ld.end || (li == list.size() - 1 && cursorPosition == text.length())) { if (li == i) { - lineNumColor = 0xFFa9b7c6; + lineNumColor = 0xFFb9c7d6; break; } } @@ -341,7 +345,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0x992172ff); } } - int yPos = posY + 1; // Draw indent guides once per visible block based on brace spans if (i == Math.max(0, scrolledLine) && !braceSpans.isEmpty()) { @@ -379,6 +382,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { drawRect(gx, topY, gx + 1, bottomY, guideColor); } } + int yPos = posY + stringYOffset; data.drawString(x + LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); // Draw cursor: pause blinking while user is active recently From 6373839870bf39a47cfe04bb1be5c85604006dcf Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 21:10:25 +0200 Subject: [PATCH 071/358] CRAZY GuiScriptTextArea REFACTOR (W OPUS 4.5) --- .../client/gui/util/GuiScriptTextArea.java | 544 +++--------------- 1 file changed, 94 insertions(+), 450 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 47bcf1447..4ecd25847 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -6,30 +6,38 @@ import net.minecraft.client.gui.ScaledResolution; import net.minecraft.util.ChatAllowedCharacters; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; -import noppes.npcs.client.gui.util.script.JavaTextContainer; +import noppes.npcs.client.gui.util.script.*; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import java.util.regex.Matcher; import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; +/** + * Script text editor component with syntax highlighting, bracket matching, + * smooth scrolling, and IDE-like features. + * + * Helper classes used: + * - BracketMatcher: bracket matching and brace span computation + * - IndentHelper: indentation utilities and text formatting + * - CommentHandler: line comment toggling + * - CursorNavigation: cursor movement logic + */ public class GuiScriptTextArea extends GuiNpcTextField { + + // ==================== DIMENSIONS & POSITION ==================== public int id; public int x; public int y; public int width; public int height; - private int cursorCounter; - private long lastInputTime = 0L; - private ITextChangeListener listener; - public String text = null; - private JavaTextContainer container = null; + + // ==================== STATE FLAGS ==================== public boolean active = false; public boolean enabled = true; public boolean visible = true; @@ -38,27 +46,43 @@ public class GuiScriptTextArea extends GuiNpcTextField { public boolean tripleClicked = false; public boolean clickScrolling = false; private int clickCount = 0; + private long lastClicked = 0L; + + // ==================== TEXT & CONTAINER ==================== + public String text = null; + private JavaTextContainer container = null; + private boolean enableCodeHighlighting = false; + + // ==================== SELECTION STATE ==================== private int startSelection; private int endSelection; private int cursorPosition; + + // ==================== SCROLL STATE ==================== private int scrolledLine = 0; - // Smooth scrolling state: fractional line position and target position private double scrollPos = 0.0; private double targetScroll = 0.0; - // velocity used for spring-based easing private double scrollVelocity = 0.0; - // last timestamp used for scroll integration (ms) private long lastScrollTime = 0L; - // scrollbar drag offset in pixels (mouseY - thumbTop) when dragging begins private int scrollbarDragOffset = 0; - private boolean enableCodeHighlighting = false; - public List undoList = new ArrayList(); - public List redoList = new ArrayList(); - public boolean undoing = false; - private long lastClicked = 0L; - // Line number gutter width (left margin for line numbers) - dynamically calculated + + // Scroll animation constants + private static final double SCROLL_TAU = 0.1; // Time constant (~55ms) + private static final double SCROLL_SNAP = 0.01; // Snap threshold + private static final double SCROLL_MAX_DT = 0.05; // Max delta time + + // ==================== UI COMPONENTS ==================== + private int cursorCounter; + private long lastInputTime = 0L; + private ITextChangeListener listener; private static int LINE_NUMBER_GUTTER_WIDTH = 25; - // private static TrueTypeFont font = new TrueTypeFont(new Font("Arial Unicode MS", Font.PLAIN, ConfigClient.FontSize), 1); + + // ==================== UNDO/REDO ==================== + public List undoList = new ArrayList<>(); + public List redoList = new ArrayList<>(); + public boolean undoing = false; + + // ==================== CONSTRUCTOR ==================== public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) { super(id, guiScreen, x, y, width, height, null); @@ -71,6 +95,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.setText(text); this.undoing = false; } + + // ==================== RENDERING ==================== public void drawTextBox(int xMouse, int yMouse) { if (!visible) @@ -408,190 +434,27 @@ public class GuiScriptTextArea extends GuiNpcTextField { } GL11.glDisable(GL11.GL_SCISSOR_TEST); } - // Find a bracket span for a given cursor position. Returns {start, end} or null. + // ==================== BRACKET MATCHING ==================== + // Uses BracketMatcher helper for bracket span detection + private int[] findBracketSpanAt(int pos) { - if (text == null || text.isEmpty()) return null; - // First try at pos (caret before the char) - if (pos >= 0 && pos < text.length()) { - char c = text.charAt(pos); - int found = 0; - if (c == '{') found = findClosingBracket(text.substring(pos), '{', '}'); - else if (c == '[') found = findClosingBracket(text.substring(pos), '[', ']'); - else if (c == '(') found = findClosingBracket(text.substring(pos), '(', ')'); - else if (c == '}') found = findOpeningBracket(text.substring(0, pos + 1), '{', '}'); - else if (c == ']') found = findOpeningBracket(text.substring(0, pos + 1), '[', ']'); - else if (c == ')') found = findOpeningBracket(text.substring(0, pos + 1), '(', ')'); - if (found != 0) return new int[]{pos, pos + found}; - } - - // Scan backwards from just before the caret, skipping spaces/tabs on the same line, - // to find a nearby bracket that should be treated as "immediately before" the caret. - if (pos > 0) { - int scan = pos - 1; - // Move left over spaces/tabs but do not cross a newline - while (scan >= 0) { - char sc = text.charAt(scan); - if (sc == ' ' || sc == '\t') {scan--;continue;} - if (sc == '\n') {scan = -1; // stop, do not cross line boundary - break; - } - - int found2 = 0; - if (sc == '{') found2 = findClosingBracket(text.substring(scan), '{', '}'); - else if (sc == '[') found2 = findClosingBracket(text.substring(scan), '[', ']'); - else if (sc == '(') found2 = findClosingBracket(text.substring(scan), '(', ')'); - else if (sc == '}') found2 = findOpeningBracket(text.substring(0, scan + 1), '{', '}'); - else if (sc == ']') found2 = findOpeningBracket(text.substring(0, scan + 1), '[', ']'); - else if (sc == ')') found2 = findOpeningBracket(text.substring(0, scan + 1), '(', ')'); - - if (found2 != 0) return new int[]{scan, scan + found2}; - // Not a bracket; stop scanning further - break; - } - } - - return null; + return BracketMatcher.findBracketSpanAt(text, pos); } + private int findClosingBracket(String str, char s, char e) { - int found = 0; - char[] chars = str.toCharArray(); - - for (int i = 0; i < chars.length; ++i) { - char c = chars[i]; - if (c == s) { - ++found; - } else if (c == e) { - --found; - if (found == 0) { - return i; - } - } - } - - return 0; + return BracketMatcher.findClosingBracket(str, s, e); } private int findOpeningBracket(String str, char s, char e) { - int found = 0; - char[] chars = str.toCharArray(); - - for (int i = chars.length - 1; i >= 0; --i) { - char c = chars[i]; - if (c == e) { - ++found; - } else if (c == s) { - --found; - if (found == 0) { - return i - chars.length + 1; - } - } - } - - return 0; + return BracketMatcher.findOpeningBracket(str, s, e); } - // Computes brace spans as {originalDepth, openLineIndex, closeLineIndex, adjustedDepth}, inclusive of brace lines private List computeBraceSpans(String fullText, List lines) { - List spans = new ArrayList<>(); - if (fullText == null || fullText.isEmpty() || lines == null || lines.isEmpty()) - return spans; - - int lineIdx = 0; - int lineEnd = lines.get(0).end; - List openStack = new ArrayList<>(); // line indices of unmatched opens - - boolean inLineComment = false; - boolean inBlockComment = false; - boolean inString = false; - boolean escape = false; - char stringDelimiter = 0; - - for (int pos = 0; pos < fullText.length(); pos++) { - while (lineIdx < lines.size() - 1 && pos >= lineEnd) { - lineIdx++; - lineEnd = lines.get(lineIdx).end; - } - - char c = fullText.charAt(pos); - char next = pos + 1 < fullText.length() ? fullText.charAt(pos + 1) : 0; - - if (inString) { - if (escape) { - escape = false; - } else if (c == '\\') { - escape = true; - } else if (c == stringDelimiter) { - inString = false; - } else if (c == '\n') { - // Treat unterminated strings as ending at EOL to avoid breaking later brace parsing - inString = false; - } - continue; - } - - if (inBlockComment) { - if (c == '*' && next == '/') { - inBlockComment = false; - pos++; - } - continue; - } - - if (inLineComment) { - if (c == '\n') { - inLineComment = false; - } - continue; - } - - if (c == '/' && next == '/') { - inLineComment = true; - pos++; - continue; - } - if (c == '/' && next == '*') { - inBlockComment = true; - pos++; - continue; - } - if (c == '"' || c == '\'') { - inString = true; - stringDelimiter = c; - escape = false; - continue; - } - - if (c == '{') { - openStack.add(lineIdx); - } else if (c == '}') { - if (!openStack.isEmpty()) { - int openLine = openStack.remove(openStack.size() - 1); - int spanDepth = openStack.size() + 1; - int closeLine = lineIdx; - spans.add(new int[]{spanDepth, openLine, closeLine}); - } - } - } - - // Normalize depths to reduce impact of unmatched opens without hiding nested guides - if (!openStack.isEmpty()) { - int baseline = openStack.size(); - List adjusted = new ArrayList<>(spans.size()); - for (int[] span : spans) { - int adjustedDepth = Math.max(1, span[0] - baseline); - adjusted.add(new int[]{span[0], span[1], span[2], adjustedDepth}); - } - spans = adjusted; - } else { - for (int idx = 0; idx < spans.size(); idx++) { - int[] span = spans.get(idx); - spans.set(idx, new int[]{span[0], span[1], span[2], span[0]}); - } - } - - return spans; + return BracketMatcher.computeBraceSpans(fullText, lines); } + // ==================== SELECTION & CURSOR POSITION ==================== + private int getSelectionPos(int xMouse, int yMouse) { xMouse -= (this.x + LINE_NUMBER_GUTTER_WIDTH + 1); yMouse -= this.y + 1; @@ -686,6 +549,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void ensureCursorVisible() { scrollToCursor(); } + + // ==================== KEYBOARD INPUT HANDLING ==================== @Override public boolean textboxKeyTyped(char c, int i) { @@ -1146,109 +1011,26 @@ public class GuiScriptTextArea extends GuiNpcTextField { return Keyboard.isKeyDown(42) || Keyboard.isKeyDown(54); } + // ==================== COMMENT TOGGLING ==================== + // Uses CommentHandler helper for comment operations + private void toggleCommentSelection() { - StringBuilder newText = new StringBuilder(text.length() + 100); - int prevEnd = 0; - - int newStart = -1; //calculates expanded startSelection - int newEnd = -1; //calculates expanded endSelection - int newIndex = 0; - - for (LineData line : container.lines) { - int safeLineStart = Math.max(0, Math.min(line.start, text.length())); - int safeLineEnd = Math.max(0, Math.min(line.end, text.length())); - - if (safeLineStart > prevEnd) { - if (newStart == -1 && startSelection >= prevEnd && startSelection <= safeLineStart) { - newStart = newIndex + (startSelection - prevEnd); - } - if (newEnd == -1 && endSelection >= prevEnd && endSelection <= safeLineStart) { - newEnd = newIndex + (endSelection - prevEnd); - } - newText.append(text, prevEnd, safeLineStart); - newIndex += safeLineStart - prevEnd; - } - - if (safeLineEnd > startSelection && safeLineStart < endSelection) { - String lineText = text.substring(safeLineStart, safeLineEnd); - CommentToggleInfo ti = toggleLine(lineText); - String newLineText = ti.text; - int nonWs = ti.nonWs; - int delta = ti.delta; - - if (newStart == -1 && startSelection >= safeLineStart && startSelection <= safeLineEnd) { - int offsetInOldStart = startSelection - safeLineStart; - int newOffsetStart = offsetInOldStart; - if (offsetInOldStart >= nonWs) { - newOffsetStart = offsetInOldStart + delta; - } - newStart = newIndex + Math.max(0, newOffsetStart); - } - if (newEnd == -1 && endSelection >= safeLineStart && endSelection <= safeLineEnd) { - int offsetInOld = endSelection - safeLineStart; - int newOffset = offsetInOld; - if (offsetInOld >= nonWs) { - newOffset = offsetInOld + delta; - } - newEnd = newIndex + Math.max(0, newOffset); - } - - newText.append(newLineText); - newIndex += newLineText.length(); - } else { - if (newStart == -1 && startSelection >= safeLineStart && startSelection <= safeLineEnd) { - newStart = newIndex + (startSelection - safeLineStart); - } - if (newEnd == -1 && endSelection >= safeLineStart && endSelection <= safeLineEnd) { - newEnd = newIndex + (endSelection - safeLineStart); - } - newText.append(text, safeLineStart, safeLineEnd); - newIndex += safeLineEnd - safeLineStart; - } - prevEnd = safeLineEnd; - } - - if (prevEnd < text.length()) { - if (newStart == -1 && startSelection >= prevEnd && startSelection <= text.length()) { - newStart = newIndex + (startSelection - prevEnd); - } - if (newEnd == -1 && endSelection >= prevEnd && endSelection <= text.length()) { - newEnd = newIndex + (endSelection - prevEnd); - } - newText.append(text, prevEnd, text.length()); - newIndex += text.length() - prevEnd; - } - - if (newStart == -1) newStart = Math.max(0, Math.min(startSelection, newText.length())); - if (newEnd == -1) newEnd = Math.max(0, Math.min(endSelection, newText.length())); - - setText(newText.toString()); - startSelection = newStart; - endSelection = newEnd; + CommentHandler.SelectionToggleResult result = CommentHandler.toggleCommentSelection( + text, container.lines, startSelection, endSelection); + setText(result.newText); + startSelection = result.newStartSelection; + endSelection = result.newEndSelection; cursorPosition = endSelection; } private void toggleCommentLineAtCursor() { - for (LineData line : container.lines) { - int lineStart = Math.max(0, Math.min(line.start, text.length())); - int lineEnd = Math.max(0, Math.min(line.end, text.length())); - if (cursorPosition >= lineStart && cursorPosition <= lineEnd) { - String lineText = text.substring(lineStart, lineEnd); - CommentToggleInfo ti = toggleLine(lineText); - String newLineText = ti.text; - int nonWs = ti.nonWs; - boolean hasComment = ti.delta < 0; - String before = text.substring(0, lineStart); - String after = lineEnd <= text.length() ? text.substring(lineEnd) : ""; - setText(before + newLineText + after); - int cursorDelta = hasComment ? -2 : 2; - int newCursor = cursorPosition + cursorDelta; - if (cursorPosition < lineStart + nonWs + (hasComment ? 2 : 0)) newCursor = cursorPosition; - setCursor(Math.max(lineStart, newCursor), false); - return; - } - } + CommentHandler.SingleLineToggleResult result = CommentHandler.toggleCommentAtCursor( + text, container.lines, cursorPosition); + setText(result.newText); + setCursor(result.newCursorPosition, false); } + + // ==================== KEYBOARD MODIFIERS ==================== private boolean isAltKeyDown() { return Keyboard.isKeyDown(56) || Keyboard.isKeyDown(184); @@ -1275,22 +1057,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { } private String getIndentCurrentLine() { - Iterator var1 = this.container.lines.iterator(); - - LineData data; - do { - if (!var1.hasNext()) { - return ""; + for (LineData data : this.container.lines) { + if (this.cursorPosition > data.start && this.cursorPosition <= data.end) { + int i; + for (i = 0; i < data.text.length() && data.text.charAt(i) == ' '; ++i) { + } + return data.text.substring(0, i); } - - data = (LineData) var1.next(); - } while (this.cursorPosition <= data.start || this.cursorPosition > data.end); - - int i; - for (i = 0; i < data.text.length() && data.text.charAt(i) == 32; ++i) { } - - return data.text.substring(0, i); + return ""; } private String getAutoIndentForEnter() { @@ -1387,133 +1162,23 @@ public class GuiScriptTextArea extends GuiNpcTextField { return prevIndent; } + // ==================== TEXT FORMATTING ==================== + private int getTabSize() { - return 4; + return IndentHelper.TAB_SIZE; } private String repeatSpace(int count) { - if (count <= 0) - return ""; - char[] arr = new char[count]; - java.util.Arrays.fill(arr, ' '); - return new String(arr); + return IndentHelper.spaces(count); } private void formatText() { - // Find which line the cursor is on and calculate position within that line (after stripping indent) - int cursorLine = -1; - int cursorColInContent = 0; // position in line after removing leading whitespace - int lineStartPos = 0; - String[] linesArr = text.split("\n", -1); - - for (int li = 0; li < linesArr.length; li++) { - String line = linesArr[li]; - int lineEndPos = lineStartPos + line.length(); - - if (cursorPosition >= lineStartPos && cursorPosition <= lineEndPos) { - cursorLine = li; - int leadingSpaces = line.length() - line.replaceAll("^[ \t]+", "").length(); - cursorColInContent = Math.max(0, cursorPosition - lineStartPos - leadingSpaces); - break; - } - lineStartPos = lineEndPos + 1; // +1 for newline - } - - StringBuilder out = new StringBuilder(text.length() + 32); - - boolean inString = false; - boolean escape = false; - boolean inBlockComment = false; - int depth = 0; - int tab = getTabSize(); - - int newCursorPos = 0; - - for (int li = 0; li < linesArr.length; li++) { - String line = linesArr[li]; - String trimmedLeading = line.replaceAll("^[ \t]+", ""); - - int lineIndentDec = 0; - int opens = 0, closes = 0; - boolean startsWithClose = false; - - // First pass to determine startsWithClose and brace counts ignoring strings/comments - for (int idx = 0; idx < line.length(); idx++) { - char c = line.charAt(idx); - char next = idx + 1 < line.length() ? line.charAt(idx + 1) : 0; - - if (inString) { - if (escape) { - escape = false; - } else if (c == '\\') { - escape = true; - } else if (c == '"') { - inString = false; - } - continue; - } - - if (inBlockComment) { - if (c == '*' && next == '/') { - inBlockComment = false; - idx++; - } - continue; - } - - if (c == '/' && next == '/') { - break; // rest of line is comment - } - if (c == '/' && next == '*') { - inBlockComment = true; - idx++; - continue; - } - if (c == '"') { - inString = true; - escape = false; - continue; - } - - if (c == '{') { - opens++; - } else if (c == '}') { - closes++; - if (!startsWithClose) { - String prefix = line.substring(0, idx); - if (prefix.trim().isEmpty()) { - startsWithClose = true; - } - } - } - } - - int indentLevel = depth; - if (startsWithClose) - indentLevel = Math.max(0, indentLevel - 1); - - int targetIndent = indentLevel * tab; - - // Track cursor position in the formatted output - if (li == cursorLine) { - newCursorPos = out.length() + targetIndent + Math.min(cursorColInContent, trimmedLeading.length()); - } - - out.append(repeatSpace(targetIndent)).append(trimmedLeading); - if (li < linesArr.length - 1) - out.append('\n'); - - depth = Math.max(0, depth + opens - closes); - } - - setText(out.toString()); - - // Restore cursor position - if (cursorLine >= 0) { - newCursorPos = Math.max(0, Math.min(newCursorPos, this.text.length())); - startSelection = endSelection = cursorPosition = newCursorPos; - } + IndentHelper.FormatResult result = IndentHelper.formatText(text, cursorPosition); + setText(result.text); + startSelection = endSelection = cursorPosition = Math.max(0, Math.min(result.cursorPosition, this.text.length())); } + + // ==================== TAB HANDLING ==================== private void handleTab() { LineData currentLine = null; @@ -1632,6 +1297,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } + + // ==================== CURSOR MANAGEMENT ==================== private void setCursor(int i, boolean select) { i = Math.min(Math.max(i, 0), this.text.length()); @@ -1733,6 +1400,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { public String getSelectionAfterText() { return this.text.substring(this.endSelection); } + + // ==================== MOUSE HANDLING ==================== public void mouseClicked(int xMouse, int yMouse, int mouseButton) { this.active = xMouse >= this.x && xMouse < this.x + this.width && yMouse >= this.y && yMouse < this.y + this.height; @@ -1789,6 +1458,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { public void updateCursorCounter() { ++this.cursorCounter; } + + // ==================== TEXT MANAGEMENT ==================== public void setText(String text) { if (text == null) { @@ -1867,35 +1538,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.endSelection = Math.max(0, Math.min(this.endSelection, length)); this.cursorPosition = Math.max(0, Math.min(this.cursorPosition, length)); } - - private static class CommentToggleInfo { - public final String text; - public final int delta; // newText.length() - oldText.length() - public final int nonWs; - - public CommentToggleInfo(String text, int delta, int nonWs) { - this.text = text; - this.delta = delta; - this.nonWs = nonWs; - } - } - - private CommentToggleInfo toggleLine(String lineText) { - int nonWs = 0; - while (nonWs < lineText.length() && Character.isWhitespace(lineText.charAt(nonWs))) nonWs++; - boolean hasContent = nonWs < lineText.length(); - boolean hasComment = hasContent && lineText.startsWith("//", nonWs); - String newLineText; - if (hasComment) { - int cut = Math.min(nonWs + 2, lineText.length()); - newLineText = lineText.substring(0, nonWs) + (cut <= lineText.length() ? lineText.substring(cut) : ""); - } else if (hasContent) { - newLineText = lineText.substring(0, nonWs) + "//" + lineText.substring(nonWs); - } else { - newLineText = lineText; - } - return new CommentToggleInfo(newLineText, newLineText.length() - lineText.length(), nonWs); - } + + // ==================== INNER CLASSES ==================== class UndoData { public String text; From 79edd08ff490cf509cb22f325ae37fd9b2402fde Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 22:11:42 +0200 Subject: [PATCH 072/358] CRAZY GuiScriptTextArea MODULATION (W OPUS 4.5) --- .../client/gui/util/GuiScriptTextArea.java | 663 +++++------------- .../gui/util/script/BracketMatcher.java | 252 +++++++ .../gui/util/script/CommentHandler.java | 217 ++++++ .../gui/util/script/CursorNavigation.java | 217 ++++++ .../client/gui/util/script/IndentHelper.java | 246 +++++++ .../client/gui/util/script/ScrollState.java | 153 ++++ .../gui/util/script/SelectionState.java | 192 +++++ .../client/gui/util/script/UndoManager.java | 119 ++++ 8 files changed, 1571 insertions(+), 488 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/CommentHandler.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/CursorNavigation.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/UndoManager.java diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 4ecd25847..f9b1c74ca 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -23,6 +23,8 @@ import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; * smooth scrolling, and IDE-like features. * * Helper classes used: + * - ScrollState: smooth scroll animation and state management + * - SelectionState: cursor position and text selection management * - BracketMatcher: bracket matching and brace span computation * - IndentHelper: indentation utilities and text formatting * - CommentHandler: line comment toggling @@ -44,7 +46,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { public boolean clicked = false; public boolean doubleClicked = false; public boolean tripleClicked = false; - public boolean clickScrolling = false; private int clickCount = 0; private long lastClicked = 0L; @@ -52,28 +53,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { public String text = null; private JavaTextContainer container = null; private boolean enableCodeHighlighting = false; - - // ==================== SELECTION STATE ==================== - private int startSelection; - private int endSelection; - private int cursorPosition; - - // ==================== SCROLL STATE ==================== - private int scrolledLine = 0; - private double scrollPos = 0.0; - private double targetScroll = 0.0; - private double scrollVelocity = 0.0; - private long lastScrollTime = 0L; - private int scrollbarDragOffset = 0; - - // Scroll animation constants - private static final double SCROLL_TAU = 0.1; // Time constant (~55ms) - private static final double SCROLL_SNAP = 0.01; // Snap threshold - private static final double SCROLL_MAX_DT = 0.05; // Max delta time + + // ==================== HELPER CLASS INSTANCES ==================== + private final ScrollState scroll = new ScrollState(); + private final SelectionState selection = new SelectionState(); // ==================== UI COMPONENTS ==================== private int cursorCounter; - private long lastInputTime = 0L; private ITextChangeListener listener; private static int LINE_NUMBER_GUTTER_WIDTH = 25; @@ -133,94 +119,56 @@ public class GuiScriptTextArea extends GuiNpcTextField { container.visibleLines = height / container.lineHeight-1; int maxScroll = Math.max(0, this.container.linesCount - container.visibleLines); + + // Handle mouse wheel scroll if (listener instanceof GuiNPCInterface) { - int k2 = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); - if (k2 != 0) { - // update target scroll smoothly instead of jumping - double sign = Math.copySign(1, k2); - targetScroll -= sign * 2.0; // one line per wheel tick - if (targetScroll < 0) targetScroll = 0; - if (targetScroll > maxScroll) targetScroll = maxScroll; + int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); + if (wheelDelta != 0) { + scroll.applyWheelScroll(wheelDelta, maxScroll); } } - if (clickScrolling) { - // keep dragging while mouse button is down - clickScrolling = Mouse.isButtonDown(0); + // Handle scrollbar dragging + if (scroll.isClickScrolling()) { + scroll.setClickScrolling(Mouse.isButtonDown(0)); int diff = Math.max(0, container.linesCount - container.visibleLines); if (diff > 0) { int sbSize = Math.max((int) (1f * container.visibleLines / container.linesCount * height), 2); int trackTop = y + 1; int trackHeight = Math.max(1, height - 4); int thumbRange = Math.max(1, trackHeight - sbSize); - - // current thumb top for hit testing double linesCountD = Math.max(1, (double) container.linesCount); - int thumbTop = (int) (y + 1f * scrollPos / linesCountD * (height - 4)) + 1; + int thumbTop = (int) (y + 1f * scroll.getScrollPos() / linesCountD * (height - 4)) + 1; - // If user clicked on the track (outside the thumb), jump to proportional position if (yMouse < thumbTop || yMouse > thumbTop + sbSize) { double centerRatio = (double) (yMouse - trackTop) / (double) trackHeight; centerRatio = Math.max(0.0, Math.min(1.0, centerRatio)); - targetScroll = Math.max(0, Math.min(diff, centerRatio * diff)); - // prepare drag offset for subsequent drags (center grab) - scrollbarDragOffset = sbSize / 2; + scroll.setTargetScroll(centerRatio * diff, maxScroll); + scroll.setScrollbarDragOffset(sbSize / 2); } else { - // Dragging the thumb itself: preserve initial grab offset - int desiredTop = yMouse - scrollbarDragOffset; + int desiredTop = yMouse - scroll.getScrollbarDragOffset(); desiredTop = Math.max(trackTop, Math.min(trackTop + thumbRange, desiredTop)); double ratio = (double) (desiredTop - trackTop) / (double) thumbRange; ratio = Math.max(0.0, Math.min(1.0, ratio)); - targetScroll = Math.max(0, Math.min(diff, ratio * diff)); + scroll.setTargetScroll(ratio * diff, maxScroll); } } - if (!clickScrolling) { - // reset drag offset when released - scrollbarDragOffset = 0; + if (!scroll.isClickScrolling()) { + scroll.setScrollbarDragOffset(0); } } - // Animate scrollPos towards targetScroll using exponential smoothing - long nowMs = System.currentTimeMillis(); - if (scrollPos < 0) { - scrollPos = scrolledLine; // initialize - lastScrollTime = nowMs; - } - double dt = Math.min(0.05, (nowMs - lastScrollTime) / 1000.0); // clamp dt for stability - lastScrollTime = nowMs; - - double dist = targetScroll - scrollPos; - // If close enough, snap to target to avoid long slow tail - if (Math.abs(dist) < 0.01) { - scrollPos = targetScroll; - scrollVelocity = 0.0; - } else { - // Time constant controlling speed (seconds). Smaller -> faster snap. - final double tau = 0.1; // ~55ms time constant feels snappy - double alpha = 1.0 - Math.exp(-dt / Math.max(1e-6, tau)); - double prev = scrollPos; - scrollPos += dist * alpha; - scrollVelocity = (scrollPos - prev) / (dt > 0 ? dt : 1e-6); - // Clamp overshoot - if ((dist > 0 && scrollPos > targetScroll) || (dist < 0 && scrollPos < targetScroll)) { - scrollPos = targetScroll; - scrollVelocity = 0.0; - } - } - // Ensure scrollPos stays in valid bounds - if (scrollPos < 0) scrollPos = 0; - if (scrollPos > maxScroll) scrollPos = maxScroll; - - // Update integer scrolledLine for compatibility - scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); - + // Update scroll animation + scroll.initializeIfNeeded(scroll.getScrolledLine()); + scroll.update(maxScroll); + // Handle click-dragging for selection if (clicked) { clicked = Mouse.isButtonDown(0); int i = getSelectionPos(xMouse, yMouse); - if (i != cursorPosition) { + if (i != selection.getCursorPosition()) { if (doubleClicked || tripleClicked) { - startSelection = endSelection = cursorPosition; + selection.reset(selection.getCursorPosition()); doubleClicked = false; tripleClicked = false; } @@ -231,11 +179,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { tripleClicked = false; } - //Calculate braces next to cursor to highlight + // Calculate braces next to cursor to highlight int startBracket = 0, endBracket = 0; - if (startSelection >= 0 && text != null && text.length() > 0 && - (endSelection - startSelection == 1 || startSelection == endSelection)) { - int[] span = findBracketSpanAt(startSelection); + if (selection.getStartSelection() >= 0 && text != null && text.length() > 0 && + (selection.getEndSelection() - selection.getStartSelection() == 1 || !selection.hasSelection())) { + int[] span = BracketMatcher.findBracketSpanAt(text,selection.getStartSelection()); if (span != null) { startBracket = span[0]; endBracket = span[1]; @@ -245,7 +193,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { List list = new ArrayList<>(container.lines); // Build brace spans: {origDepth, open line, close line, adjustedDepth} - List braceSpans = computeBraceSpans(text, list); + List braceSpans = BracketMatcher.computeBraceSpans(text, list); // Determine which exact brace span (openLine/closeLine) should be highlighted based on bracket under caret int highlightedOpenLine = -1; @@ -273,20 +221,20 @@ public class GuiScriptTextArea extends GuiNpcTextField { } String wordHightLight = null; - if (startSelection != endSelection) { + if (selection.hasSelection()) { Matcher m = container.regexWord.matcher(text); while (m.find()) { - if (m.start() == startSelection && m.end() == endSelection) { - wordHightLight = text.substring(startSelection, endSelection); + if (m.start() == selection.getStartSelection() && m.end() == selection.getEndSelection()) { + wordHightLight = text.substring(selection.getStartSelection(), selection.getEndSelection()); } } } // Expand render range by one line above/below so partially-visible lines are drawn - int renderStart = Math.max(0, scrolledLine - 1); - int renderEnd = Math.min(list.size() - 1, scrolledLine + container.visibleLines + 1); + int renderStart = Math.max(0, scroll.getScrolledLine() - 1); + int renderEnd = Math.min(list.size() - 1, scroll.getScrolledLine() + container.visibleLines + 1); // Apply fractional GL translate for sub-pixel smooth scrolling - double fracOffset = scrollPos - scrolledLine; + double fracOffset = scroll.getFractionalOffset(); float fracPixels = (float) (fracOffset * container.lineHeight); GL11.glPushMatrix(); GL11.glTranslatef(0.0f, -fracPixels, 0.0f); @@ -301,7 +249,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Render LINE GUTTER numbers for (int i = renderStart; i <= renderEnd; i++) { - int posY = y + (i - scrolledLine) * container.lineHeight + stringYOffset; + int posY = y + (i - scroll.getScrolledLine()) * container.lineHeight + stringYOffset; String lineNum = String.valueOf(i + 1); int lineNumWidth = ClientProxy.Font.width(lineNum); int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5; // right-align with 5px padding @@ -311,7 +259,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (active && isEnabled()) { for (int li = 0; li < list.size(); li++) { LineData ld = list.get(li); - if (cursorPosition >= ld.start && cursorPosition < ld.end || (li == list.size() - 1 && cursorPosition == text.length())) { + if (selection.getCursorPosition() >= ld.start && selection.getCursorPosition() < ld.end || (li == list.size() - 1 && selection.getCursorPosition() == text.length())) { if (li == i) { lineNumColor = 0xFFb9c7d6; break; @@ -327,13 +275,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { GL11.glPushMatrix(); GL11.glTranslatef(0.0f, -fracPixels, 0.0f); - renderStart = Math.max(0, scrolledLine - 1); + renderStart = Math.max(0, scroll.getScrolledLine() - 1); for (int i = renderStart; i <= renderEnd; i++) { LineData data = list.get(i); String line = data.text; int w = line.length(); // Use integer Y relative to scrolledLine; fractional offset applied via GL translate - int posY = y + (i - scrolledLine) * container.lineHeight; + int posY = y + (i - scroll.getScrolledLine()) * container.lineHeight; if (i >= renderStart && i <= renderEnd) { //Highlight braces the cursor position is on if (startBracket != endBracket) { @@ -360,21 +308,23 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } // Highlight the current line (light gray) under any selection - if (active && isEnabled() && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { + if (active && isEnabled() && (selection.getCursorPosition() >= data.start && selection.getCursorPosition() < data.end || (i == list.size() - 1 && selection.getCursorPosition() == text.length()))) { drawRect(x , posY, x + width - 1, posY + container.lineHeight, 0x22e0e0e0); } // Highlight selection - if (startSelection != endSelection && endSelection > data.start && startSelection <= data.end) { - if (startSelection < data.end) { - int s = ClientProxy.Font.width(line.substring(0, Math.max(startSelection - data.start, 0))); - int e = ClientProxy.Font.width(line.substring(0, Math.min(endSelection - data.start, w))) + 1; + if (selection.hasSelection() && selection.getEndSelection() > data.start && selection.getStartSelection() <= data.end) { + if (selection.getStartSelection() < data.end) { + int s = ClientProxy.Font.width( + line.substring(0, Math.max(selection.getStartSelection() - data.start, 0))); + int e = ClientProxy.Font.width( + line.substring(0, Math.min(selection.getEndSelection() - data.start, w))) + 1; drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0x992172ff); } } // Draw indent guides once per visible block based on brace spans - if (i == Math.max(0, scrolledLine) && !braceSpans.isEmpty()) { - int visStart = Math.max(0, scrolledLine); + if (i == Math.max(0, scroll.getScrolledLine()) && !braceSpans.isEmpty()) { + int visStart = Math.max(0, scroll.getScrolledLine()); int visEnd = Math.min(list.size() - 1, visStart + container.visibleLines - 0); for (int[] span : braceSpans) { int originalDepth = span[0]; @@ -402,8 +352,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { int px = ClientProxy.Font.width(sb.toString()); int gx = x + LINE_NUMBER_GUTTER_WIDTH + 4 + px - 2; // shift left ~2px for the IntelliJ feel - int topY = y + (drawStart - scrolledLine) * container.lineHeight; - int bottomY = y + (endLine - scrolledLine + 1) * container.lineHeight; + int topY = y + (drawStart - scroll.getScrolledLine()) * container.lineHeight; + int bottomY = y + (endLine - scroll.getScrolledLine() + 1) * container.lineHeight; int guideColor = (openLine == highlightedOpenLine && closeLine == highlightedCloseLine) ? 0x9933cc00 : 0x33FFFFFF; drawRect(gx, topY, gx + 1, bottomY, guideColor); } @@ -412,10 +362,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { data.drawString(x + LINE_NUMBER_GUTTER_WIDTH + 1, yPos, 0xFFe0e0e0); // Draw cursor: pause blinking while user is active recently - boolean recentInput = System.currentTimeMillis() - this.lastInputTime < 500; - if (active && isEnabled() && (recentInput || (cursorCounter / 10) % 2 == 0) && (cursorPosition >= data.start && cursorPosition < data.end || (i == list.size() - 1 && cursorPosition == text.length()))) { + boolean recentInput = selection.hadRecentInput(); + if (active && isEnabled() && (recentInput || (cursorCounter / 10) % 2 == 0) && (selection.getCursorPosition() >= data.start && selection.getCursorPosition() < data.end || (i == list.size() - 1 && selection.getCursorPosition() == text.length()))) { int posX = x + LINE_NUMBER_GUTTER_WIDTH + ClientProxy.Font.width( - line.substring(0, Math.min(cursorPosition - data.start, line.length()))); + line.substring(0, Math.min(selection.getCursorPosition() - data.start, line.length()))); drawRect(posX + 1, yPos -1, posX + 2, yPos - 1 + container.lineHeight, 0xffffffff); } } @@ -428,30 +378,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { int posX = x + width - 6; double linesCount = Math.max(1, (double) container.linesCount); - int posY = (int) (y + 1f * scrollPos / linesCount * (height - 4)) + 1; + int posY = (int) (y + 1f * scroll.getScrollPos() / linesCount * (height - 4)) + 1; drawRect(posX, posY, posX + 5, posY + sbSize + 2, 0xFFe0e0e0); } GL11.glDisable(GL11.GL_SCISSOR_TEST); } - // ==================== BRACKET MATCHING ==================== - // Uses BracketMatcher helper for bracket span detection - - private int[] findBracketSpanAt(int pos) { - return BracketMatcher.findBracketSpanAt(text, pos); - } - - private int findClosingBracket(String str, char s, char e) { - return BracketMatcher.findClosingBracket(str, s, e); - } - - private int findOpeningBracket(String str, char s, char e) { - return BracketMatcher.findOpeningBracket(str, s, e); - } - - private List computeBraceSpans(String fullText, List lines) { - return BracketMatcher.computeBraceSpans(fullText, lines); - } // ==================== SELECTION & CURSOR POSITION ==================== @@ -459,16 +391,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { xMouse -= (this.x + LINE_NUMBER_GUTTER_WIDTH + 1); yMouse -= this.y + 1; // Adjust yMouse to account for fractional GL translation (negative offset applied in rendering) - double fracOffset = scrollPos - scrolledLine; - double fracPixels = fracOffset * container.lineHeight; + double fracPixels = scroll.getFractionalOffset() * container.lineHeight; yMouse = (int) Math.round(yMouse + fracPixels); ArrayList list = new ArrayList(this.container.lines); for (int i = 0; i < list.size(); ++i) { LineData data = (LineData) list.get(i); - if (i >= this.scrolledLine && i <= this.scrolledLine + this.container.visibleLines) { - int yPos = (i - this.scrolledLine) * this.container.lineHeight; + if (i >= scroll.getScrolledLine() && i <= scroll.getScrolledLine() + this.container.visibleLines) { + int yPos = (i - scroll.getScrolledLine()) * this.container.lineHeight; if (yMouse >= yPos && yMouse < yPos + this.container.lineHeight) { int lineWidth = 0; char[] chars = data.text.toCharArray(); @@ -498,30 +429,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Find which line the cursor is on (0-indexed) private int getCursorLineIndex() { - if (container == null || container.lines == null || container.lines.isEmpty()) return 0; - for (int i = 0; i < container.lines.size(); i++) { - LineData ld = container.lines.get(i); - // For last line, use <= since end doesn't include +1 for newline - // For other lines, use < since end includes the newline position - boolean isLastLine = (i == container.lines.size() - 1); - boolean isOnLine = isLastLine - ? (cursorPosition >= ld.start && cursorPosition <= ld.end) - : (cursorPosition >= ld.start && cursorPosition < ld.end); - if (isOnLine) { - return i; - } - } - return container.lines.size() - 1; + return selection.getCursorLineIndex(container.lines, text != null ? text.length() : 0); } - // Check if cursor is on a specific line, handling the last line specially - // (last line doesn't have trailing newline, so use <= for end boundary) - private boolean isCursorOnLine(int lineIndex, LineData line) { - boolean isLastLine = (lineIndex == container.lines.size() - 1); - return isLastLine - ? (cursorPosition >= line.start && cursorPosition <= line.end) - : (cursorPosition >= line.start && cursorPosition < line.end); - } // Scroll viewport to keep cursor visible (minimal adjustment, like IntelliJ) // Only scrolls if cursor is outside the visible area @@ -531,23 +441,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { int lineIdx = getCursorLineIndex(); int visible = Math.max(1, container.visibleLines); int maxScroll = Math.max(0, container.linesCount - visible); - - int firstVisible = scrolledLine; - int lastFullyVisible = scrolledLine + visible; - - if (lineIdx < firstVisible) { - // Cursor is above viewport - scroll up just enough to show it - targetScroll = lineIdx; - } else if (lineIdx > lastFullyVisible) { - // Cursor is below viewport - scroll down just enough to show it - targetScroll = Math.min(lineIdx - visible, maxScroll); - } - // If cursor is visible, don't change scroll - } - - // Legacy method name for compatibility - private void ensureCursorVisible() { - scrollToCursor(); + + scroll.scrollToLine(lineIdx, visible, maxScroll); } // ==================== KEYBOARD INPUT HANDLING ==================== @@ -558,8 +453,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { return false; if (this.isKeyComboCtrlA(i)) { - startSelection = cursorPosition = 0; - endSelection = text.length(); + selection.selectAll(text.length()); return true; } @@ -568,7 +462,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { LineData currentLine = null; for (LineData line : this.container.lines) { - if (startSelection >= line.start && startSelection < line.end) { + if (selection.getStartSelection() >= line.start && selection.getStartSelection() < line.end) { currentLine = line; break; } @@ -578,26 +472,26 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (i == Keyboard.KEY_LEFT) { int j = 1; if (isCtrlKeyDown()) { - Matcher m = container.regexWord.matcher(text.substring(0, cursorPosition)); + Matcher m = container.regexWord.matcher(text.substring(0, selection.getCursorPosition())); while (m.find()) { if (m.start() == m.end()) continue; - j = cursorPosition - m.start(); + j = selection.getCursorPosition() - m.start(); } } - int newPos = Math.max(cursorPosition - j, 0); + int newPos = Math.max(selection.getCursorPosition() - j, 0); setCursor(newPos, GuiScreen.isShiftKeyDown()); return true; } if (i == Keyboard.KEY_RIGHT) { int j = 1; if (isCtrlKeyDown()) { - Matcher m = container.regexWord.matcher(text.substring(cursorPosition)); + Matcher m = container.regexWord.matcher(text.substring(selection.getCursorPosition())); if (m.find() && m.start() > 0 || m.find()) { j = m.start(); } } - int newPos = Math.min(cursorPosition + j, text.length()); + int newPos = Math.min(selection.getCursorPosition() + j, text.length()); setCursor(newPos, GuiScreen.isShiftKeyDown()); return true; } @@ -613,21 +507,21 @@ public class GuiScriptTextArea extends GuiNpcTextField { } if (i == Keyboard.KEY_DELETE) { String s = getSelectionAfterText(); - if (!s.isEmpty() && startSelection == endSelection) + if (!s.isEmpty() && !selection.hasSelection()) s = s.substring(1); setText(getSelectionBeforeText() + s); - endSelection = cursorPosition = startSelection; + selection.reset(selection.getStartSelection()); return true; } if (isKeyComboCtrlBackspace(i)) { String s = getSelectionBeforeText(); - if (startSelection > 0 && startSelection == endSelection) { - int nearestCondition = cursorPosition; + if (selection.getStartSelection() > 0 && !selection.hasSelection()) { + int nearestCondition = selection.getCursorPosition(); int g; - boolean cursorInWhitespace = Character.isWhitespace(s.charAt(cursorPosition - 1)); + boolean cursorInWhitespace = Character.isWhitespace(s.charAt(selection.getCursorPosition() - 1)); if (cursorInWhitespace) { // Find the nearest word if we are starting in whitespace - for (g = cursorPosition - 1; g >= 0; g--) { + for (g = selection.getCursorPosition() - 1; g >= 0; g--) { char currentChar = s.charAt(g); if (!Character.isWhitespace(currentChar)) { nearestCondition = g; @@ -639,7 +533,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } else { // Find the nearest blank space or new line - for (g = cursorPosition - 1; g >= 0; g--) { + for (g = selection.getCursorPosition() - 1; g >= 0; g--) { char currentChar = s.charAt(g); if (Character.isWhitespace(currentChar) || currentChar == '\n') { nearestCondition = g; @@ -653,47 +547,37 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Remove all text to the left up to the nearest boundary s = s.substring(0, nearestCondition); - startSelection -= (cursorPosition - nearestCondition); + selection.setStartSelection( + selection.getStartSelection() - (selection.getCursorPosition() - nearestCondition)); } setText(s + getSelectionAfterText()); - endSelection = cursorPosition = startSelection; + selection.reset(selection.getStartSelection()); return true; } if (i == Keyboard.KEY_BACK) { // Handle selection deletion first - if (startSelection != endSelection) { + if (selection.hasSelection()) { String s = getSelectionBeforeText(); setText(s + getSelectionAfterText()); - endSelection = cursorPosition = startSelection; + selection.reset(selection.getStartSelection()); scrollToCursor(); return true; } // Nothing to delete if at start - if (startSelection <= 0) { + if (selection.getStartSelection() <= 0) { return true; } // Find current line for indent-aware backspace - LineData curr = null; - for (int idx = 0; idx < container.lines.size(); idx++) { - LineData line = container.lines.get(idx); - boolean isLastLine = (idx == container.lines.size() - 1); - boolean isOnLine = isLastLine - ? (cursorPosition >= line.start && cursorPosition <= line.end) - : (cursorPosition >= line.start && cursorPosition < line.end); - if (isOnLine) { - curr = line; - break; - } - } + LineData curr = selection.findCurrentLine(container.lines); // Indent-aware backspace: if cursor is at or before the EXPECTED indent, merge with previous line // If cursor is after expected indent (even in extra whitespace), do normal backspace if (curr != null && curr.start > 0) { - int col = cursorPosition - curr.start; - int actualIndent = getLineIndent(curr.text); - int expectedIndent = getExpectedIndent(curr); + int col = selection.getCursorPosition() - curr.start; + int actualIndent = IndentHelper.getLineIndent(curr.text); + int expectedIndent = IndentHelper.getExpectedIndent(curr, container.lines); // Only trigger smart merge if cursor is at or before the expected indent position if (col <= expectedIndent) { @@ -713,7 +597,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(before + after); // Cursor goes to end of previous line (at the newline position, which is now end of prev line content) int newCursor = Math.max(0, curr.start - 1); - endSelection = cursorPosition = startSelection = newCursor; + selection.reset(newCursor); scrollToCursor(); return true; } else { @@ -741,7 +625,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(before + spacer + content); int newCursor = before.length() + spacer.length(); - endSelection = cursorPosition = startSelection = newCursor; + selection.reset(newCursor); scrollToCursor(); return true; } @@ -749,19 +633,20 @@ public class GuiScriptTextArea extends GuiNpcTextField { } // Auto-pair deletion: if deleting an opening bracket/quote and next char is the matching closer - if (startSelection > 0 && startSelection < text.length()) { - char prev = text.charAt(startSelection - 1); - char nextc = text.charAt(startSelection); + if (selection.getStartSelection() > 0 && selection.getStartSelection() < text.length()) { + char prev = text.charAt(selection.getStartSelection() - 1); + char nextc = text.charAt(selection.getStartSelection()); if ((prev == '(' && nextc == ')') || (prev == '[' && nextc == ']') || (prev == '{' && nextc == '}') || (prev == '\'' && nextc == '\'') || (prev == '"' && nextc == '"')) { - String before = text.substring(0, startSelection - 1); - String after = startSelection + 1 < text.length() ? text.substring(startSelection + 1) : ""; + String before = text.substring(0, selection.getStartSelection() - 1); + String after = selection.getStartSelection() + 1 < text.length() ? text.substring( + selection.getStartSelection() + 1) : ""; setText(before + after); - startSelection -= 1; - endSelection = cursorPosition = startSelection; + selection.setStartSelection(selection.getStartSelection() - 1); + selection.reset(selection.getStartSelection()); scrollToCursor(); return true; } @@ -770,25 +655,25 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Normal backspace: delete one character String s = getSelectionBeforeText(); s = s.substring(0, s.length() - 1); - startSelection--; + selection.setStartSelection(selection.getStartSelection() - 1); setText(s + getSelectionAfterText()); - endSelection = cursorPosition = startSelection; + selection.reset(selection.getStartSelection()); scrollToCursor(); return true; } if (this.isKeyComboCtrlX(i)) { - if (startSelection != endSelection) { - NoppesStringUtils.setClipboardContents(text.substring(startSelection, endSelection)); + if (selection.hasSelection()) { + NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); String s = getSelectionBeforeText(); setText(s + getSelectionAfterText()); - endSelection = startSelection = cursorPosition = s.length(); + selection.reset(s.length()); scrollToCursor(); } return true; } if (this.isKeyComboCtrlC(i)) { - if (startSelection != endSelection) { - NoppesStringUtils.setClipboardContents(text.substring(startSelection, endSelection)); + if (selection.hasSelection()) { + NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); } return true; } @@ -801,10 +686,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (undoList.isEmpty()) return false; undoing = true; - redoList.add(new UndoData(this.text, this.cursorPosition)); + redoList.add(new UndoData(this.text, selection.getCursorPosition())); UndoData data = undoList.remove(undoList.size() - 1); setText(data.text); - endSelection = startSelection = cursorPosition = data.cursorPosition; + selection.reset(data.cursorPosition); undoing = false; scrollToCursor(); return true; @@ -813,10 +698,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (redoList.isEmpty()) return false; undoing = true; - undoList.add(new UndoData(this.text, this.cursorPosition)); + undoList.add(new UndoData(this.text, selection.getCursorPosition())); UndoData data = redoList.remove(redoList.size() - 1); setText(data.text); - endSelection = startSelection = cursorPosition = data.cursorPosition; + selection.reset(data.cursorPosition); undoing = false; scrollToCursor(); return true; @@ -836,7 +721,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { return true; } if (i == Keyboard.KEY_RETURN) { - if (cursorPosition > 0 && cursorPosition <= text.length() && text.charAt(cursorPosition - 1) == '{') { + if (selection.getCursorPosition() > 0 && selection.getCursorPosition() <= text.length() && text.charAt( + selection.getCursorPosition() - 1) == '{') { String indent = getIndentCurrentLine(); String childIndent = indent + " "; String before = getSelectionBeforeText(); @@ -860,7 +746,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { try { // Find the line index containing the opening brace (cursorPosition-1) int openLineIdx = -1; - int bracePos = cursorPosition - 1; + int bracePos = selection.getCursorPosition() - 1; for (int li = 0; li < this.container.lines.size(); li++) { LineData ld = this.container.lines.get(li); if (bracePos >= ld.start && bracePos < ld.end) { @@ -870,13 +756,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { } if (openLineIdx >= 0) { - List spans = computeBraceSpans(text, this.container.lines); + List spans = BracketMatcher.computeBraceSpans(text, this.container.lines); for (int[] span : spans) { int spanOpen = span[1]; int spanClose = span[2]; if (spanOpen == openLineIdx) { // Found a matching close; check its indent equals current indent - int closeIndent = getLineIndent(this.container.lines.get(spanClose).text); + int closeIndent = IndentHelper.getLineIndent(this.container.lines.get(spanClose).text); if (closeIndent == indent.length()) { hasMatchingCloseSameIndent = true; } @@ -897,7 +783,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { String insert = "\n" + childIndent + "\n" + indent + "}"; setText(before + insert + after); int newCursor = before.length() + 1 + childIndent.length(); - startSelection = endSelection = cursorPosition = newCursor; + selection.reset(newCursor); scrollToCursor(); } } else { @@ -912,32 +798,32 @@ public class GuiScriptTextArea extends GuiNpcTextField { // If typing a closing char and the next char is already that closer, skip over it if ((c == ')' || c == ']' || c == '"' || c == '\'' ) && after.length() > 0 && after.charAt(0) == c) { // move cursor forward over existing closer - startSelection = endSelection = cursorPosition = before.length() + 1; + selection.reset(before.length() + 1); scrollToCursor(); return true; } if (c == '"') { setText(before + "\"\"" + after); - startSelection = endSelection = cursorPosition = before.length() + 1; + selection.reset(before.length() + 1); scrollToCursor(); return true; } if (c == '\'') { setText(before + "''" + after); - startSelection = endSelection = cursorPosition = before.length() + 1; + selection.reset(before.length() + 1); scrollToCursor(); return true; } if (c == '[') { setText(before + "[]" + after); - startSelection = endSelection = cursorPosition = before.length() + 1; + selection.reset(before.length() + 1); scrollToCursor(); return true; } if (c == '(') { setText(before + "()" + after); - startSelection = endSelection = cursorPosition = before.length() + 1; + selection.reset(before.length() + 1); scrollToCursor(); return true; } @@ -947,7 +833,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } if (i == Keyboard.KEY_SLASH && isCtrlKeyDown()) { - if (startSelection != endSelection) { + if (selection.hasSelection()) { toggleCommentSelection(); } else { toggleCommentLineAtCursor(); @@ -956,11 +842,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { } if (i == Keyboard.KEY_D && isCtrlKeyDown()) { - if (startSelection != endSelection) { + if (selection.hasSelection()) { // Handle multi-line selection duplication LineData firstLine = null, lastLine = null; for (LineData line : container.lines) { - if (line.end > startSelection && line.start < endSelection) { + if (line.end > selection.getStartSelection() && line.start < selection.getEndSelection()) { if (firstLine == null) firstLine = line; lastLine = line; } @@ -970,19 +856,20 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Insert the selected block immediately after the last line without adding an extra newline String insertText = selectedText; // Save selection before setText - int savedStart = startSelection; - int savedEnd = endSelection; + int savedStart = selection.getStartSelection(); + int savedEnd = selection.getEndSelection(); int insertAt = lastLine.end; setText(text.substring(0, insertAt) + insertText + text.substring(insertAt)); // Restore cursor and selection - startSelection = savedStart; - endSelection = this.cursorPosition = savedEnd; + selection.setStartSelection(savedStart); + selection.setEndSelection(savedEnd); + selection.setCursorPositionDirect(savedEnd); return true; } } else { // Single line duplication (existing logic) for (LineData line : container.lines) { - if (cursorPosition >= line.start && cursorPosition <= line.end) { + if (selection.getCursorPosition() >= line.start && selection.getCursorPosition() <= line.end) { int lineStart = line.start, lineEnd = line.end; String lineText = text.substring(lineStart, lineEnd); // Avoid inserting an extra blank line: if lineText already ends with a newline, @@ -997,7 +884,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(text.substring(0, insertionPoint) + insertText + text.substring(insertionPoint)); // Place cursor at end of duplicated line (just before any trailing newline) int newCursor = insertionPoint + insertText.length() - (insertText.endsWith("\n") ? 1 : 0); - startSelection = endSelection = cursorPosition = Math.max(0, Math.min(newCursor, this.text.length())); + selection.reset(Math.max(0, Math.min(newCursor, this.text.length()))); return true; } } @@ -1016,16 +903,16 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void toggleCommentSelection() { CommentHandler.SelectionToggleResult result = CommentHandler.toggleCommentSelection( - text, container.lines, startSelection, endSelection); + text, container.lines, selection.getStartSelection(), selection.getEndSelection()); setText(result.newText); - startSelection = result.newStartSelection; - endSelection = result.newEndSelection; - cursorPosition = endSelection; + selection.setStartSelection(result.newStartSelection); + selection.setEndSelection(result.newEndSelection); + selection.setCursorPositionDirect(result.newEndSelection); } private void toggleCommentLineAtCursor() { CommentHandler.SingleLineToggleResult result = CommentHandler.toggleCommentAtCursor( - text, container.lines, cursorPosition); + text, container.lines, selection.getCursorPosition()); setText(result.newText); setCursor(result.newCursorPosition, false); } @@ -1058,7 +945,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { private String getIndentCurrentLine() { for (LineData data : this.container.lines) { - if (this.cursorPosition > data.start && this.cursorPosition <= data.end) { + if (selection.getCursorPosition() > data.start && selection.getCursorPosition() <= data.end) { int i; for (i = 0; i < data.text.length() && data.text.charAt(i) == ' '; ++i) { } @@ -1069,99 +956,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { } private String getAutoIndentForEnter() { - LineData currentLine = null; - for (LineData line : this.container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition < line.end) { - currentLine = line; - break; - } - } + LineData currentLine = selection.findCurrentLine(container.lines); if (currentLine == null) { return ""; } - - String lineText = currentLine.text; - int leading = 0; - while (leading < lineText.length() && (lineText.charAt(leading) == ' ' || lineText.charAt(leading) == '\t')) { - leading++; - } - - String baseIndent = lineText.substring(0, leading); - int relativeCursor = Math.max(0, Math.min(this.cursorPosition - currentLine.start, lineText.length())); - String beforeCursor = lineText.substring(0, relativeCursor); - - // Find last non-whitespace before cursor - int lastNonWs = -1; - for (int idx = beforeCursor.length() - 1; idx >= 0; idx--) { - if (!Character.isWhitespace(beforeCursor.charAt(idx))) { - lastNonWs = idx; - break; - } - } - - // Only add extra indent if cursor is actually after the opening brace (not just in trailing whitespace) - // Check that the last non-whitespace is at or after the line's indent position - boolean opensBlock = lastNonWs >= 0 && lastNonWs >= leading && beforeCursor.charAt(lastNonWs) == '{'; - if (opensBlock) { - return baseIndent + " "; - } - - return baseIndent; + return IndentHelper.getAutoIndentForEnter(currentLine.text, selection.getCursorPosition() - currentLine.start); } - - private int getLineIndent(String lineText) { - int leading = 0; - while (leading < lineText.length() && (lineText.charAt(leading) == ' ' || lineText.charAt( - leading) == '\t')) { - leading++; - } - return leading; - } - - // Calculate the expected/proper indent for the current line based on surrounding context - private int getExpectedIndent(LineData currentLine) { - if (currentLine == null) { - return 0; - } - - int idx = container.lines.indexOf(currentLine); - if (idx <= 0) { - return 0; // First line has no expected indent - } - - // Find the previous non-empty line - LineData prevLine = null; - for (int i = idx - 1; i >= 0; i--) { - LineData line = container.lines.get(i); - if (line.text.trim().length() > 0) { - prevLine = line; - break; - } - } - - if (prevLine == null) { - return 0; - } - - int prevIndent = getLineIndent(prevLine.text); - String prevTrimmed = prevLine.text.trim(); - String currTrimmed = currentLine.text.trim(); - - // If current line starts with }, expected indent is one level back - if (currTrimmed.startsWith("}")) { - return Math.max(0, prevIndent - getTabSize()); - } - - // If previous line ends with {, expected indent is one level forward - if (prevTrimmed.endsWith("{")) { - return prevIndent + getTabSize(); - } - - // Otherwise, expected indent matches previous line - return prevIndent; - } - // ==================== TEXT FORMATTING ==================== private int getTabSize() { @@ -1173,39 +974,28 @@ public class GuiScriptTextArea extends GuiNpcTextField { } private void formatText() { - IndentHelper.FormatResult result = IndentHelper.formatText(text, cursorPosition); + IndentHelper.FormatResult result = IndentHelper.formatText(text, selection.getCursorPosition()); setText(result.text); - startSelection = endSelection = cursorPosition = Math.max(0, Math.min(result.cursorPosition, this.text.length())); + selection.reset(Math.max(0, Math.min(result.cursorPosition, this.text.length()))); } // ==================== TAB HANDLING ==================== private void handleTab() { - LineData currentLine = null; - for (int i = 0; i < this.container.lines.size(); i++) { - LineData line = this.container.lines.get(i); - boolean isLastLine = (i == this.container.lines.size() - 1); - boolean isOnLine = isLastLine - ? (this.cursorPosition >= line.start && this.cursorPosition <= line.end) - : (this.cursorPosition >= line.start && this.cursorPosition < line.end); - if (isOnLine) { - currentLine = line; - break; - } - } + LineData currentLine = selection.findCurrentLine(container.lines); if (currentLine == null) { addText(" "); return; } int tab = getTabSize(); - int indentLen = getLineIndent(currentLine.text); + int indentLen = IndentHelper.getLineIndent(currentLine.text); int textStartPos = currentLine.start + indentLen; - if (this.cursorPosition <= textStartPos) { + if (selection.getCursorPosition() <= textStartPos) { // Cursor before any text: if cursor is exactly at text start, move forward to next tab stop. // If cursor is inside leading whitespace (before text start), choose nearest tab stop (tie -> forward). int targetIndent; - if (this.cursorPosition == textStartPos) { + if (selection.getCursorPosition() == textStartPos) { targetIndent = ((indentLen / tab) + 1) * tab; } else { int remainder = indentLen % tab; @@ -1231,10 +1021,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { String after = text.substring(sepEnd); setText(before + newIndent + rest + sep + after); int newCursor = currentLine.start + targetIndent; - startSelection = endSelection = cursorPosition = Math.min(newCursor, this.text.length()); + selection.reset(Math.min(newCursor, this.text.length())); } else { // Cursor is after start of text: insert spaces at cursor to move following text to next tab stop - int column = this.cursorPosition - currentLine.start; + int column = selection.getCursorPosition() - currentLine.start; int targetColumn = ((column / tab) + 1) * tab; int toInsert = Math.max(0, targetColumn - column); if (toInsert > 0) { @@ -1245,25 +1035,14 @@ public class GuiScriptTextArea extends GuiNpcTextField { } private void handleShiftTab() { - LineData currentLine = null; - for (int i = 0; i < this.container.lines.size(); i++) { - LineData line = this.container.lines.get(i); - boolean isLastLine = (i == this.container.lines.size() - 1); - boolean isOnLine = isLastLine - ? (this.cursorPosition >= line.start && this.cursorPosition <= line.end) - : (this.cursorPosition >= line.start && this.cursorPosition < line.end); - if (isOnLine) { - currentLine = line; - break; - } - } + LineData currentLine = selection.findCurrentLine(container.lines); if (currentLine == null) return; int tab = getTabSize(); - int indentLen = getLineIndent(currentLine.text); + int indentLen = IndentHelper.getLineIndent(currentLine.text); int textStartPos = currentLine.start + indentLen; - if (this.cursorPosition <= textStartPos) { + if (selection.getCursorPosition() <= textStartPos) { // Cursor before any text: reduce leading indent to previous tab stop int targetIndent = Math.max(0, ((indentLen - 1) / tab) * tab); String newIndent = repeatSpace(targetIndent); @@ -1275,14 +1054,14 @@ public class GuiScriptTextArea extends GuiNpcTextField { String after = text.substring(sepEnd); setText(before + newIndent + rest + sep + after); int newCursor = currentLine.start + targetIndent; - startSelection = endSelection = cursorPosition = Math.min(newCursor, this.text.length()); + selection.reset(Math.min(newCursor, this.text.length())); } else { // Cursor after start of text: remove up to previous tab stop worth of spaces immediately before cursor - int column = this.cursorPosition - currentLine.start; + int column = selection.getCursorPosition() - currentLine.start; int mod = column % tab; int toRemove = mod == 0 ? tab : mod; int removed = 0; - int pos = this.cursorPosition - 1; + int pos = selection.getCursorPosition() - 1; while (pos >= currentLine.start && removed < toRemove && text.charAt(pos) == ' ') { pos--; removed++; @@ -1290,10 +1069,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (removed > 0) { int removeStart = pos + 1; String before = text.substring(0, removeStart); - String after = text.substring(this.cursorPosition); + String after = text.substring(selection.getCursorPosition()); setText(before + after); int newCursor = removeStart; - startSelection = endSelection = cursorPosition = Math.min(newCursor, this.text.length()); + selection.reset(Math.min(newCursor, this.text.length())); } } } @@ -1301,104 +1080,29 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== CURSOR MANAGEMENT ==================== private void setCursor(int i, boolean select) { - i = Math.min(Math.max(i, 0), this.text.length()); - if (i != this.cursorPosition) { - if (!select) { - this.endSelection = this.startSelection = this.cursorPosition = i; - } else { - int diff = this.cursorPosition - i; - if (this.cursorPosition == this.startSelection) { - this.startSelection -= diff; - } else if (this.cursorPosition == this.endSelection) { - this.endSelection -= diff; - } - - if (this.startSelection > this.endSelection) { - int j = this.endSelection; - this.endSelection = this.startSelection; - this.startSelection = j; - } - - this.cursorPosition = i; - } - - clampSelectionBounds(); - // Moving the cursor is user activity — pause blinking briefly - this.lastInputTime = System.currentTimeMillis(); - } + selection.setCursor(i, text != null ? text.length() : 0, select); } private void addText(String s) { + int insertPos = selection.getStartSelection(); this.setText(this.getSelectionBeforeText() + s + this.getSelectionAfterText()); - this.endSelection = this.startSelection = this.cursorPosition = this.startSelection + s.length(); - this.lastInputTime = System.currentTimeMillis(); + selection.afterTextInsert(insertPos + s.length()); } private int cursorUp() { - for (int i = 0; i < this.container.lines.size(); ++i) { - LineData data = (LineData) this.container.lines.get(i); - // For last line, use <= since end doesn't include +1 for newline - // For other lines, use < since end includes the newline position - boolean isLastLine = (i == this.container.lines.size() - 1); - boolean isOnLine = isLastLine - ? (this.cursorPosition >= data.start && this.cursorPosition <= data.end) - : (this.cursorPosition >= data.start && this.cursorPosition < data.end); - if (isOnLine) { - if (i == 0) { - return 0; - } - - int column = Math.min(this.cursorPosition - data.start, data.text.length()); - LineData target = this.container.lines.get(i - 1); - // Calculate target position on the line above, maintaining column if possible - int targetColumn = Math.min(column, target.text.length()); - int targetIndent = getLineIndent(target.text); - int minPos = target.start + Math.min(targetIndent, target.text.length()); - int targetPos = target.start + targetColumn; - return Math.max(minPos, targetPos); - } - } - - return 0; + return CursorNavigation.cursorUp(selection.getCursorPosition(), container.lines, text); } private int cursorDown() { - if (cursorPosition == text.length()) - return cursorPosition; - for (int i = 0; i < this.container.lines.size(); ++i) { - LineData data = (LineData) this.container.lines.get(i); - // For last line, use <= since end doesn't include +1 for newline - // For other lines, use < since end includes the newline position - boolean isLastLine = (i == this.container.lines.size() - 1); - boolean isOnLine = isLastLine - ? (this.cursorPosition >= data.start && this.cursorPosition <= data.end) - : (this.cursorPosition >= data.start && this.cursorPosition < data.end); - if (isOnLine) { - // If already on last line, stay at current position - if (i >= this.container.lines.size() - 1) { - return cursorPosition; - } - - int column = Math.min(this.cursorPosition - data.start, data.text.length()); - LineData target = this.container.lines.get(i + 1); - // Calculate target position on the line below, maintaining column if possible - int targetColumn = Math.min(column, target.text.length()); - int targetIndent = getLineIndent(target.text); - int minPos = target.start + Math.min(targetIndent, target.text.length()); - int targetPos = target.start + targetColumn; - return Math.max(minPos, targetPos); - } - } - - return this.text.length(); + return CursorNavigation.cursorDown(selection.getCursorPosition(), container.lines, text); } public String getSelectionBeforeText() { - return this.startSelection == 0 ? "" : this.text.substring(0, this.startSelection); + return selection.getTextBefore(text); } public String getSelectionAfterText() { - return this.text.substring(this.endSelection); + return selection.getTextAfter(text); } // ==================== MOUSE HANDLING ==================== @@ -1406,18 +1110,19 @@ public class GuiScriptTextArea extends GuiNpcTextField { public void mouseClicked(int xMouse, int yMouse, int mouseButton) { this.active = xMouse >= this.x && xMouse < this.x + this.width && yMouse >= this.y && yMouse < this.y + this.height; if (this.active) { - this.startSelection = this.endSelection = this.cursorPosition = this.getSelectionPos(xMouse, yMouse); - this.lastInputTime = System.currentTimeMillis(); + int clickPos = this.getSelectionPos(xMouse, yMouse); + selection.reset(clickPos); + selection.markActivity(); this.clicked = mouseButton == 0; this.doubleClicked = false; this.tripleClicked = false; long time = System.currentTimeMillis(); if (this.clicked && this.container.linesCount * this.container.lineHeight > this.height && xMouse > this.x + this.width - 8) { this.clicked = false; - this.clickScrolling = true; + scroll.setClickScrolling(true); double linesCountD = Math.max(1, (double) this.container.linesCount); - int thumbTop = (int) (this.y + 1f * this.scrollPos / linesCountD * (this.height - 4)) + 1; - this.scrollbarDragOffset = yMouse - thumbTop; + int thumbTop = (int) (this.y + 1f * scroll.getScrollPos() / linesCountD * (this.height - 4)) + 1; + scroll.setScrollbarDragOffset(yMouse - thumbTop); } else { if (time - this.lastClicked < 300L) { this.clickCount++; @@ -1430,9 +1135,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.doubleClicked = true; Matcher m = this.container.regexWord.matcher(this.text); while (m.find()) { - if (this.cursorPosition >= m.start() && this.cursorPosition <= m.end()) { - this.startSelection = m.start(); - this.endSelection = m.end(); + if (selection.getCursorPosition() >= m.start() && selection.getCursorPosition() <= m.end()) { + selection.setSelection(m.start(), m.end()); break; } } @@ -1440,9 +1144,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Triple-click: select entire line this.tripleClicked = true; for (LineData line : container.lines) { - if (this.cursorPosition >= line.start && this.cursorPosition <= line.end) { - this.startSelection = line.start; - this.endSelection = line.end; + if (selection.getCursorPosition() >= line.start && selection.getCursorPosition() <= line.end) { + selection.setSelection(line.start, line.end); break; } } @@ -1474,7 +1177,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } if (!this.undoing) { - this.undoList.add(new GuiScriptTextArea.UndoData(this.text, this.cursorPosition)); + this.undoList.add(new GuiScriptTextArea.UndoData(this.text, selection.getCursorPosition())); this.redoList.clear(); } @@ -1488,20 +1191,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Ensure scroll state stays in bounds after text change int maxScroll = Math.max(0, this.container.linesCount - this.container.visibleLines); - if (this.scrolledLine > maxScroll) { - this.scrolledLine = maxScroll; - } - if (this.targetScroll > maxScroll) { - this.targetScroll = maxScroll; - } - if (this.scrollPos > maxScroll) { - this.scrollPos = maxScroll; - } + scroll.clampToBounds(maxScroll); - clampSelectionBounds(); + selection.clamp(this.text.length()); // Consider text changes user activity to pause caret blinking briefly - this.lastInputTime = System.currentTimeMillis(); + selection.markActivity(); } } @@ -1528,15 +1223,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } private void clampSelectionBounds() { - if (this.text == null || this.text.isEmpty()) { - this.startSelection = this.endSelection = this.cursorPosition = 0; - return; - } - - int length = this.text.length(); - this.startSelection = Math.max(0, Math.min(this.startSelection, length)); - this.endSelection = Math.max(0, Math.min(this.endSelection, length)); - this.cursorPosition = Math.max(0, Math.min(this.cursorPosition, length)); + selection.clamp(text != null ? text.length() : 0); } // ==================== INNER CLASSES ==================== diff --git a/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java b/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java new file mode 100644 index 000000000..a4c0e7a30 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java @@ -0,0 +1,252 @@ +package noppes.npcs.client.gui.util.script; + +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import java.util.ArrayList; +import java.util.List; + +/** + * Handles bracket matching, brace span computation, and indent guide generation. + */ +public class BracketMatcher { + + /** + * Find matching bracket at cursor position. + * @return int[2] with {startPos, endPos} or null if no bracket found + */ + public static int[] findBracketSpanAt(String text, int pos) { + if (text == null || text.isEmpty()) return null; + + // Try at current position + if (pos >= 0 && pos < text.length()) { + char c = text.charAt(pos); + int found = 0; + if (c == '{') found = findClosingBracket(text.substring(pos), '{', '}'); + else if (c == '[') found = findClosingBracket(text.substring(pos), '[', ']'); + else if (c == '(') found = findClosingBracket(text.substring(pos), '(', ')'); + else if (c == '}') found = findOpeningBracket(text.substring(0, pos + 1), '{', '}'); + else if (c == ']') found = findOpeningBracket(text.substring(0, pos + 1), '[', ']'); + else if (c == ')') found = findOpeningBracket(text.substring(0, pos + 1), '(', ')'); + if (found != 0) return new int[]{pos, pos + found}; + } + + // Scan backwards, skipping whitespace on same line + if (pos > 0) { + int scan = pos - 1; + while (scan >= 0) { + char sc = text.charAt(scan); + if (sc == ' ' || sc == '\t') { scan--; continue; } + if (sc == '\n') break; // Don't cross line boundary + + int found2 = 0; + if (sc == '{') found2 = findClosingBracket(text.substring(scan), '{', '}'); + else if (sc == '[') found2 = findClosingBracket(text.substring(scan), '[', ']'); + else if (sc == '(') found2 = findClosingBracket(text.substring(scan), '(', ')'); + else if (sc == '}') found2 = findOpeningBracket(text.substring(0, scan + 1), '{', '}'); + else if (sc == ']') found2 = findOpeningBracket(text.substring(0, scan + 1), '[', ']'); + else if (sc == ')') found2 = findOpeningBracket(text.substring(0, scan + 1), '(', ')'); + + if (found2 != 0) return new int[]{scan, scan + found2}; + break; // Not a bracket; stop + } + } + return null; + } + + /** + * Find closing bracket, returns offset from start or 0 if not found + */ + public static int findClosingBracket(String str, char open, char close) { + int depth = 0; + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (c == open) depth++; + else if (c == close) { + depth--; + if (depth == 0) return i; + } + } + return 0; + } + + /** + * Find opening bracket, returns negative offset from end or 0 if not found + */ + public static int findOpeningBracket(String str, char open, char close) { + int depth = 0; + for (int i = str.length() - 1; i >= 0; i--) { + char c = str.charAt(i); + if (c == close) depth++; + else if (c == open) { + depth--; + if (depth == 0) return i - str.length() + 1; + } + } + return 0; + } + + /** + * Compute brace spans for indent guides. + * Returns list of int[4]: {originalDepth, openLineIdx, closeLineIdx, adjustedDepth} + */ + public static List computeBraceSpans(String text, List lines) { + List spans = new ArrayList<>(); + if (text == null || text.isEmpty() || lines == null || lines.isEmpty()) + return spans; + + int lineIdx = 0; + int lineEnd = lines.get(0).end; + List openStack = new ArrayList<>(); + + boolean inLineComment = false; + boolean inBlockComment = false; + boolean inString = false; + boolean escape = false; + char stringDelimiter = 0; + + for (int pos = 0; pos < text.length(); pos++) { + // Advance line index + while (lineIdx < lines.size() - 1 && pos >= lineEnd) { + lineIdx++; + lineEnd = lines.get(lineIdx).end; + } + + char c = text.charAt(pos); + char next = pos + 1 < text.length() ? text.charAt(pos + 1) : 0; + + // String handling + if (inString) { + if (escape) { escape = false; } + else if (c == '\\') { escape = true; } + else if (c == stringDelimiter) { inString = false; } + else if (c == '\n') { inString = false; } // Unterminated string + continue; + } + + // Block comment handling + if (inBlockComment) { + if (c == '*' && next == '/') { inBlockComment = false; pos++; } + continue; + } + + // Line comment handling + if (inLineComment) { + if (c == '\n') { inLineComment = false; } + continue; + } + + // Start of comments + if (c == '/' && next == '/') { inLineComment = true; pos++; continue; } + if (c == '/' && next == '*') { inBlockComment = true; pos++; continue; } + + // Start of string + if (c == '"' || c == '\'') { + inString = true; + stringDelimiter = c; + escape = false; + continue; + } + + // Brace matching + if (c == '{') { + openStack.add(lineIdx); + } else if (c == '}') { + if (!openStack.isEmpty()) { + int openLine = openStack.remove(openStack.size() - 1); + int spanDepth = openStack.size() + 1; + spans.add(new int[]{spanDepth, openLine, lineIdx}); + } + } + } + + // Normalize depths for unmatched braces + if (!openStack.isEmpty()) { + int baseline = openStack.size(); + List adjusted = new ArrayList<>(spans.size()); + for (int[] span : spans) { + int adjustedDepth = Math.max(1, span[0] - baseline); + adjusted.add(new int[]{span[0], span[1], span[2], adjustedDepth}); + } + return adjusted; + } else { + for (int i = 0; i < spans.size(); i++) { + int[] span = spans.get(i); + spans.set(i, new int[]{span[0], span[1], span[2], span[0]}); + } + } + return spans; + } + + /** + * Find which brace span should be highlighted based on bracket at position + * @return int[2] {openLine, closeLine} or null + */ + public static int[] findHighlightedSpan(int bracketPos, List lines, List spans) { + if (bracketPos < 0 || lines == null || spans == null) return null; + + // Find line containing bracket + int bracketLineIdx = -1; + for (int i = 0; i < lines.size(); i++) { + LineData ld = lines.get(i); + if (bracketPos >= ld.start && bracketPos < ld.end) { + bracketLineIdx = i; + break; + } + } + + if (bracketLineIdx >= 0) { + for (int[] span : spans) { + int openLine = span[1]; + int closeLine = span[2]; + if (bracketLineIdx >= openLine && bracketLineIdx <= closeLine) { + return new int[]{openLine, closeLine}; + } + } + } + return null; + } + + /** + * Check if a brace at given position has a matching close with same indent + */ + public static boolean hasMatchingCloseWithSameIndent(String text, int bracePos, List lines, int currentIndent) { + if (text == null || lines == null || bracePos < 0) return false; + + // Find line containing the brace + int openLineIdx = -1; + for (int i = 0; i < lines.size(); i++) { + LineData ld = lines.get(i); + if (bracePos >= ld.start && bracePos < ld.end) { + openLineIdx = i; + break; + } + } + + if (openLineIdx < 0) return false; + + List spans = computeBraceSpans(text, lines); + for (int[] span : spans) { + if (span[1] == openLineIdx) { + int closeLineIdx = span[2]; + if (closeLineIdx < lines.size()) { + String closeLine = lines.get(closeLineIdx).text; + int closeIndent = getLineIndent(closeLine); + return closeIndent == currentIndent; + } + } + } + return false; + } + + /** + * Get leading whitespace count of a line + */ + private static int getLineIndent(String line) { + int indent = 0; + for (int i = 0; i < line.length(); i++) { + char c = line.charAt(i); + if (c == ' ' || c == '\t') indent++; + else break; + } + return indent; + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/gui/util/script/CommentHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/CommentHandler.java new file mode 100644 index 000000000..b5c0f2205 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/CommentHandler.java @@ -0,0 +1,217 @@ +package noppes.npcs.client.gui.util.script; + +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import java.util.List; + +/** + * Handles line comment toggling for the script editor. + */ +public class CommentHandler { + + /** + * Result of comment toggle operation + */ + public static class ToggleResult { + public final String text; + public final int delta; // Change in length (negative if comment removed) + public final int nonWsIndex; // Index of first non-whitespace character + + public ToggleResult(String text, int delta, int nonWsIndex) { + this.text = text; + this.delta = delta; + this.nonWsIndex = nonWsIndex; + } + + public boolean wasCommented() { + return delta < 0; + } + } + + /** + * Toggle line comment on a single line + */ + public static ToggleResult toggleLine(String lineText) { + // Find first non-whitespace + int nonWs = 0; + while (nonWs < lineText.length() && Character.isWhitespace(lineText.charAt(nonWs))) { + nonWs++; + } + + boolean hasContent = nonWs < lineText.length(); + boolean hasComment = hasContent && lineText.startsWith("//", nonWs); + String newText; + + if (hasComment) { + // Remove comment + int cut = Math.min(nonWs + 2, lineText.length()); + newText = lineText.substring(0, nonWs) + lineText.substring(cut); + } else if (hasContent) { + // Add comment + newText = lineText.substring(0, nonWs) + "//" + lineText.substring(nonWs); + } else { + // Empty/whitespace line - no change + newText = lineText; + } + + return new ToggleResult(newText, newText.length() - lineText.length(), nonWs); + } + + /** + * Result of toggling comments on a selection + */ + public static class SelectionToggleResult { + public final String newText; + public final int newStartSelection; + public final int newEndSelection; + + public SelectionToggleResult(String text, int start, int end) { + this.newText = text; + this.newStartSelection = start; + this.newEndSelection = end; + } + } + + /** + * Toggle comments on all lines in a selection + */ + public static SelectionToggleResult toggleCommentSelection( + String text, + List lines, + int startSelection, + int endSelection) { + + StringBuilder newText = new StringBuilder(text.length() + 100); + int prevEnd = 0; + int newStart = -1; + int newEnd = -1; + int newIndex = 0; + + for (LineData line : lines) { + int safeLineStart = Math.max(0, Math.min(line.start, text.length())); + int safeLineEnd = Math.max(0, Math.min(line.end, text.length())); + + // Handle gap between lines (shouldn't happen normally) + if (safeLineStart > prevEnd) { + if (newStart == -1 && startSelection >= prevEnd && startSelection <= safeLineStart) { + newStart = newIndex + (startSelection - prevEnd); + } + if (newEnd == -1 && endSelection >= prevEnd && endSelection <= safeLineStart) { + newEnd = newIndex + (endSelection - prevEnd); + } + newText.append(text, prevEnd, safeLineStart); + newIndex += safeLineStart - prevEnd; + } + + // Check if this line is in selection + if (safeLineEnd > startSelection && safeLineStart < endSelection) { + String lineText = text.substring(safeLineStart, safeLineEnd); + ToggleResult tr = toggleLine(lineText); + String newLineText = tr.text; + int nonWs = tr.nonWsIndex; + int delta = tr.delta; + + // Update start selection position + if (newStart == -1 && startSelection >= safeLineStart && startSelection <= safeLineEnd) { + int offsetInOld = startSelection - safeLineStart; + int newOffset = offsetInOld; + if (offsetInOld >= nonWs) { + newOffset = offsetInOld + delta; + } + newStart = newIndex + Math.max(0, newOffset); + } + + // Update end selection position + if (newEnd == -1 && endSelection >= safeLineStart && endSelection <= safeLineEnd) { + int offsetInOld = endSelection - safeLineStart; + int newOffset = offsetInOld; + if (offsetInOld >= nonWs) { + newOffset = offsetInOld + delta; + } + newEnd = newIndex + Math.max(0, newOffset); + } + + newText.append(newLineText); + newIndex += newLineText.length(); + } else { + // Line not in selection - keep unchanged + if (newStart == -1 && startSelection >= safeLineStart && startSelection <= safeLineEnd) { + newStart = newIndex + (startSelection - safeLineStart); + } + if (newEnd == -1 && endSelection >= safeLineStart && endSelection <= safeLineEnd) { + newEnd = newIndex + (endSelection - safeLineStart); + } + newText.append(text, safeLineStart, safeLineEnd); + newIndex += safeLineEnd - safeLineStart; + } + prevEnd = safeLineEnd; + } + + // Handle remaining text after last line + if (prevEnd < text.length()) { + if (newStart == -1 && startSelection >= prevEnd && startSelection <= text.length()) { + newStart = newIndex + (startSelection - prevEnd); + } + if (newEnd == -1 && endSelection >= prevEnd && endSelection <= text.length()) { + newEnd = newIndex + (endSelection - prevEnd); + } + newText.append(text, prevEnd, text.length()); + newIndex += text.length() - prevEnd; + } + + // Fallback for positions not found + if (newStart == -1) newStart = Math.max(0, Math.min(startSelection, newText.length())); + if (newEnd == -1) newEnd = Math.max(0, Math.min(endSelection, newText.length())); + + return new SelectionToggleResult(newText.toString(), newStart, newEnd); + } + + /** + * Toggle comment on a single line at cursor position + */ + public static SingleLineToggleResult toggleCommentAtCursor( + String text, + List lines, + int cursorPosition) { + + for (LineData line : lines) { + int lineStart = Math.max(0, Math.min(line.start, text.length())); + int lineEnd = Math.max(0, Math.min(line.end, text.length())); + + if (cursorPosition >= lineStart && cursorPosition <= lineEnd) { + String lineText = text.substring(lineStart, lineEnd); + ToggleResult tr = toggleLine(lineText); + boolean hadComment = tr.delta < 0; + + String before = text.substring(0, lineStart); + String after = lineEnd <= text.length() ? text.substring(lineEnd) : ""; + String newText = before + tr.text + after; + + // Calculate new cursor position + int cursorDelta = hadComment ? -2 : 2; + int newCursor = cursorPosition + cursorDelta; + if (cursorPosition < lineStart + tr.nonWsIndex + (hadComment ? 2 : 0)) { + newCursor = cursorPosition; + } + newCursor = Math.max(lineStart, newCursor); + + return new SingleLineToggleResult(newText, newCursor); + } + } + + // No line found at cursor - return unchanged + return new SingleLineToggleResult(text, cursorPosition); + } + + /** + * Result of toggling comment on single line + */ + public static class SingleLineToggleResult { + public final String newText; + public final int newCursorPosition; + + public SingleLineToggleResult(String text, int cursor) { + this.newText = text; + this.newCursorPosition = cursor; + } + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/gui/util/script/CursorNavigation.java b/src/main/java/noppes/npcs/client/gui/util/script/CursorNavigation.java new file mode 100644 index 000000000..2a39d5bbb --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/CursorNavigation.java @@ -0,0 +1,217 @@ +package noppes.npcs.client.gui.util.script; + +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Handles cursor navigation logic (up, down, left, right, word movement). + */ +public class CursorNavigation { + + /** + * Move cursor up one line, maintaining column position + */ + public static int cursorUp(int cursorPosition, List lines, String text) { + if (lines == null || lines.isEmpty()) return 0; + + for (int i = 0; i < lines.size(); i++) { + LineData data = lines.get(i); + boolean isLastLine = (i == lines.size() - 1); + boolean isOnLine = isLastLine + ? (cursorPosition >= data.start && cursorPosition <= data.end) + : (cursorPosition >= data.start && cursorPosition < data.end); + + if (isOnLine) { + if (i == 0) { + return 0; // Already on first line + } + + int column = Math.min(cursorPosition - data.start, data.text.length()); + LineData target = lines.get(i - 1); + int targetColumn = Math.min(column, target.text.length()); + int targetIndent = getLineIndent(target.text); + int minPos = target.start + Math.min(targetIndent, target.text.length()); + int targetPos = target.start + targetColumn; + return Math.max(minPos, targetPos); + } + } + return 0; + } + + /** + * Move cursor down one line, maintaining column position + */ + public static int cursorDown(int cursorPosition, List lines, String text) { + if (lines == null || lines.isEmpty()) return text != null ? text.length() : 0; + if (text != null && cursorPosition == text.length()) return cursorPosition; + + for (int i = 0; i < lines.size(); i++) { + LineData data = lines.get(i); + boolean isLastLine = (i == lines.size() - 1); + boolean isOnLine = isLastLine + ? (cursorPosition >= data.start && cursorPosition <= data.end) + : (cursorPosition >= data.start && cursorPosition < data.end); + + if (isOnLine) { + if (i >= lines.size() - 1) { + return cursorPosition; // Already on last line + } + + int column = Math.min(cursorPosition - data.start, data.text.length()); + LineData target = lines.get(i + 1); + int targetColumn = Math.min(column, target.text.length()); + int targetIndent = getLineIndent(target.text); + int minPos = target.start + Math.min(targetIndent, target.text.length()); + int targetPos = target.start + targetColumn; + return Math.max(minPos, targetPos); + } + } + return text != null ? text.length() : 0; + } + + /** + * Move cursor left, optionally by word + */ + public static int cursorLeft(int cursorPosition, String text, Pattern wordPattern, boolean byWord) { + if (text == null || cursorPosition <= 0) return 0; + + int moveAmount = 1; + if (byWord && wordPattern != null) { + Matcher m = wordPattern.matcher(text.substring(0, cursorPosition)); + while (m.find()) { + if (m.start() != m.end()) { + moveAmount = cursorPosition - m.start(); + } + } + } + return Math.max(cursorPosition - moveAmount, 0); + } + + /** + * Move cursor right, optionally by word + */ + public static int cursorRight(int cursorPosition, String text, Pattern wordPattern, boolean byWord) { + if (text == null) return 0; + int textLen = text.length(); + if (cursorPosition >= textLen) return textLen; + + int moveAmount = 1; + if (byWord && wordPattern != null) { + Matcher m = wordPattern.matcher(text.substring(cursorPosition)); + if (m.find() && m.start() > 0) { + moveAmount = m.start(); + } else if (m.find()) { + moveAmount = m.start(); + } + } + return Math.min(cursorPosition + moveAmount, textLen); + } + + /** + * Get cursor position from mouse click coordinates + */ + public static int getPositionFromMouse( + int mouseX, int mouseY, + int areaX, int areaY, + int gutterWidth, + int lineHeight, + int scrolledLine, + double scrollFracOffset, + List lines, + String text, + FontWidthProvider fontWidth) { + + int xMouse = mouseX - (areaX + gutterWidth + 1); + int yMouse = mouseY - areaY - 1; + + // Account for fractional scroll offset + double fracPixels = scrollFracOffset * lineHeight; + yMouse = (int) Math.round(yMouse + fracPixels); + + if (lines == null || lines.isEmpty()) { + return text != null ? text.length() : 0; + } + + int visibleLines = Math.min(lines.size(), scrolledLine + 50); // Reasonable limit + + for (int i = 0; i < lines.size(); i++) { + LineData data = lines.get(i); + + if (i >= scrolledLine && i <= visibleLines) { + int yPos = (i - scrolledLine) * lineHeight; + + if (yMouse >= yPos && yMouse < yPos + lineHeight) { + // Found the line, now find character position + int lineWidth = 0; + char[] chars = data.text.toCharArray(); + + for (int j = 1; j <= chars.length; j++) { + int w = fontWidth.getWidth(data.text.substring(0, j)); + if (xMouse < lineWidth + (w - lineWidth) / 2) { + return data.start + j - 1; + } + lineWidth = w; + } + + // Click past end of line - place cursor at end of text (not on newline) + int posAfterChars = data.start + chars.length; + return Math.min(posAfterChars, text.length()); + } + } + } + + return text != null ? text.length() : 0; + } + + /** + * Interface for font width measurement + */ + public interface FontWidthProvider { + int getWidth(String text); + } + + /** + * Get leading whitespace count of a line + */ + private static int getLineIndent(String lineText) { + int indent = 0; + for (int i = 0; i < lineText.length(); i++) { + char c = lineText.charAt(i); + if (c == ' ' || c == '\t') indent++; + else break; + } + return indent; + } + + /** + * Find line index containing given position + */ + public static int findLineIndex(int position, List lines) { + if (lines == null || lines.isEmpty()) return 0; + + for (int i = 0; i < lines.size(); i++) { + LineData line = lines.get(i); + boolean isLastLine = (i == lines.size() - 1); + boolean isOnLine = isLastLine + ? (position >= line.start && position <= line.end) + : (position >= line.start && position < line.end); + if (isOnLine) { + return i; + } + } + return lines.size() - 1; + } + + /** + * Find line data containing given position + */ + public static LineData findLineAt(int position, List lines) { + int idx = findLineIndex(position, lines); + if (idx >= 0 && idx < lines.size()) { + return lines.get(idx); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java b/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java new file mode 100644 index 000000000..8a7acaa2a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java @@ -0,0 +1,246 @@ +package noppes.npcs.client.gui.util.script; + +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import java.util.Arrays; +import java.util.List; + +/** + * Helper class for indentation-related operations. + */ +public class IndentHelper { + + public static final int TAB_SIZE = 4; + + /** + * Get leading whitespace count of a line + */ + public static int getLineIndent(String lineText) { + int leading = 0; + for (int i = 0; i < lineText.length(); i++) { + char c = lineText.charAt(i); + if (c == ' ' || c == '\t') leading++; + else break; + } + return leading; + } + + /** + * Extract leading whitespace as string + */ + public static String getIndentString(String lineText) { + int indent = getLineIndent(lineText); + return lineText.substring(0, indent); + } + + /** + * Create a string of n spaces + */ + public static String spaces(int n) { + if (n <= 0) return ""; + char[] arr = new char[n]; + Arrays.fill(arr, ' '); + return new String(arr); + } + + /** + * Calculate expected indent for a line based on context + */ + public static int getExpectedIndent(LineData currentLine, List allLines) { + if (currentLine == null || allLines == null) return 0; + + int idx = allLines.indexOf(currentLine); + if (idx <= 0) return 0; + + // Find previous non-empty line + LineData prevLine = null; + for (int i = idx - 1; i >= 0; i--) { + LineData line = allLines.get(i); + if (line.text.trim().length() > 0) { + prevLine = line; + break; + } + } + + if (prevLine == null) return 0; + + int prevIndent = getLineIndent(prevLine.text); + String prevTrimmed = prevLine.text.trim(); + String currTrimmed = currentLine.text.trim(); + + // If current line starts with }, expected is one level back + if (currTrimmed.startsWith("}")) { + return Math.max(0, prevIndent - TAB_SIZE); + } + + // If previous line ends with {, expected is one level forward + if (prevTrimmed.endsWith("{")) { + return prevIndent + TAB_SIZE; + } + + return prevIndent; + } + + /** + * Calculate auto-indent for Enter key + */ + public static String getAutoIndentForEnter(String lineText, int cursorInLine) { + int leading = 0; + while (leading < lineText.length() && + (lineText.charAt(leading) == ' ' || lineText.charAt(leading) == '\t')) { + leading++; + } + + String baseIndent = lineText.substring(0, leading); + int relativeCursor = Math.max(0, Math.min(cursorInLine, lineText.length())); + String beforeCursor = lineText.substring(0, relativeCursor); + + // Find last non-whitespace before cursor + int lastNonWs = -1; + for (int i = beforeCursor.length() - 1; i >= 0; i--) { + if (!Character.isWhitespace(beforeCursor.charAt(i))) { + lastNonWs = i; + break; + } + } + + // Add extra indent if last char is { + // Only add extra indent if cursor is actually after the opening brace (not just in trailing whitespace) + // Check that the last non-whitespace is at or after the line's indent position + boolean opensBlock = lastNonWs >= 0 && lastNonWs >= leading && beforeCursor.charAt(lastNonWs) == '{'; + if (opensBlock) { + return baseIndent + spaces(TAB_SIZE); + } + + return baseIndent; + } + + /** + * Calculate target indent for Tab key + */ + public static int calculateTabTargetIndent(int currentIndent, int cursorColumn, boolean atTextStart) { + if (atTextStart) { + // At text start, move to next tab stop + return ((currentIndent / TAB_SIZE) + 1) * TAB_SIZE; + } else { + // Inside indent, snap to nearest tab stop + int remainder = currentIndent % TAB_SIZE; + if (remainder == 0) { + return currentIndent + TAB_SIZE; + } else { + int down = currentIndent - remainder; + int up = currentIndent + (TAB_SIZE - remainder); + int distDown = remainder; + int distUp = TAB_SIZE - remainder; + if (distUp <= distDown) return up; + else return down; + } + } + } + + /** + * Calculate target indent for Shift+Tab + */ + public static int calculateShiftTabTargetIndent(int currentIndent) { + return Math.max(0, ((currentIndent - 1) / TAB_SIZE) * TAB_SIZE); + } + + /** + * Format entire text with proper indentation + */ + public static FormatResult formatText(String text, int cursorPosition) { + String[] linesArr = text.split("\n", -1); + StringBuilder out = new StringBuilder(text.length() + 32); + + // Track cursor position in formatted output + int cursorLine = -1; + int cursorColInContent = 0; + int lineStartPos = 0; + + // Find cursor's line and column + for (int li = 0; li < linesArr.length; li++) { + String line = linesArr[li]; + int lineEndPos = lineStartPos + line.length(); + + if (cursorPosition >= lineStartPos && cursorPosition <= lineEndPos) { + cursorLine = li; + int leadingSpaces = line.length() - line.replaceAll("^[ \\t]+", "").length(); + cursorColInContent = Math.max(0, cursorPosition - lineStartPos - leadingSpaces); + break; + } + lineStartPos = lineEndPos + 1; + } + + boolean inString = false; + boolean escape = false; + boolean inBlockComment = false; + int depth = 0; + int newCursorPos = 0; + + for (int li = 0; li < linesArr.length; li++) { + String line = linesArr[li]; + String trimmedLeading = line.replaceAll("^[ \\t]+", ""); + + int opens = 0, closes = 0; + boolean startsWithClose = false; + + // Parse line for braces + for (int idx = 0; idx < line.length(); idx++) { + char c = line.charAt(idx); + char next = idx + 1 < line.length() ? line.charAt(idx + 1) : 0; + + if (inString) { + if (escape) { escape = false; } + else if (c == '\\') { escape = true; } + else if (c == '"') { inString = false; } + continue; + } + + if (inBlockComment) { + if (c == '*' && next == '/') { inBlockComment = false; idx++; } + continue; + } + + if (c == '/' && next == '/') break; + if (c == '/' && next == '*') { inBlockComment = true; idx++; continue; } + if (c == '"') { inString = true; escape = false; continue; } + + if (c == '{') { + opens++; + } else if (c == '}') { + closes++; + if (!startsWithClose && line.substring(0, idx).trim().isEmpty()) { + startsWithClose = true; + } + } + } + + int indentLevel = depth; + if (startsWithClose) indentLevel = Math.max(0, indentLevel - 1); + int targetIndent = indentLevel * TAB_SIZE; + + if (li == cursorLine) { + newCursorPos = out.length() + targetIndent + Math.min(cursorColInContent, trimmedLeading.length()); + } + + out.append(spaces(targetIndent)).append(trimmedLeading); + if (li < linesArr.length - 1) out.append('\n'); + + depth = Math.max(0, depth + opens - closes); + } + + return new FormatResult(out.toString(), newCursorPos); + } + + /** + * Result of text formatting + */ + public static class FormatResult { + public final String text; + public final int cursorPosition; + + public FormatResult(String text, int cursorPosition) { + this.text = text; + this.cursorPosition = cursorPosition; + } + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java b/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java new file mode 100644 index 000000000..cbee81e4e --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java @@ -0,0 +1,153 @@ +package noppes.npcs.client.gui.util.script; + +/** + * Manages smooth scrolling state and animation for the script text area. + * Uses exponential smoothing for fluid scroll animations. + */ +public class ScrollState { + // Current fractional scroll position (in lines) + private double scrollPos = -1.0; + // Target scroll position to animate towards + private double targetScroll = 0.0; + // Current scroll velocity for momentum calculations + private double scrollVelocity = 0.0; + // Last timestamp for delta time calculation + private long lastScrollTime = 0L; + // Integer line index for compatibility with rendering + private int scrolledLine = 0; + // Scrollbar drag offset when dragging thumb + private int scrollbarDragOffset = 0; + // Whether currently dragging the scrollbar + private boolean clickScrolling = false; + + // Animation parameters + private static final double TAU = 0.1; // Time constant (~55ms feels snappy) + private static final double SNAP_THRESHOLD = 0.01; // Snap to target when this close + private static final double MAX_DT = 0.05; // Max delta time for stability + + /** + * Reset scroll state to initial values + */ + public void reset() { + scrollPos = 0.0; + targetScroll = 0.0; + scrollVelocity = 0.0; + scrolledLine = 0; + lastScrollTime = System.currentTimeMillis(); + } + + /** + * Initialize scroll position if not already initialized + */ + public void initializeIfNeeded(int currentLine) { + if (scrollPos < 0) { + scrollPos = currentLine; + lastScrollTime = System.currentTimeMillis(); + } + } + + /** + * Update scroll animation. Call once per frame. + * @param maxScroll Maximum valid scroll position + */ + public void update(int maxScroll) { + long nowMs = System.currentTimeMillis(); + double dt = Math.min(MAX_DT, (nowMs - lastScrollTime) / 1000.0); + lastScrollTime = nowMs; + + double dist = targetScroll - scrollPos; + + // Snap to target if close enough + if (Math.abs(dist) < SNAP_THRESHOLD) { + scrollPos = targetScroll; + scrollVelocity = 0.0; + } else { + // Exponential smoothing + double alpha = 1.0 - Math.exp(-dt / Math.max(1e-6, TAU)); + double prev = scrollPos; + scrollPos += dist * alpha; + scrollVelocity = (scrollPos - prev) / (dt > 0 ? dt : 1e-6); + + // Clamp overshoot + if ((dist > 0 && scrollPos > targetScroll) || (dist < 0 && scrollPos < targetScroll)) { + scrollPos = targetScroll; + scrollVelocity = 0.0; + } + } + + // Keep in bounds + clampToBounds(maxScroll); + + // Update integer line for rendering + scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); + } + + /** + * Clamp scroll position to valid bounds + */ + public void clampToBounds(int maxScroll) { + if (scrollPos < 0) scrollPos = 0; + if (scrollPos > maxScroll) scrollPos = maxScroll; + if (targetScroll < 0) targetScroll = 0; + if (targetScroll > maxScroll) targetScroll = maxScroll; + scrolledLine = Math.max(0, Math.min(scrolledLine, maxScroll)); + } + + /** + * Apply mouse wheel scroll + * @param wheelDelta Positive = scroll up, negative = scroll down + * @param maxScroll Maximum valid scroll position + */ + public void applyWheelScroll(int wheelDelta, int maxScroll) { + double sign = Math.copySign(1, wheelDelta); + targetScroll -= sign * 2.0; // 2 lines per wheel tick + clampToBounds(maxScroll); + } + + /** + * Set target scroll directly (for scrollbar dragging) + */ + public void setTargetScroll(double target, int maxScroll) { + targetScroll = Math.max(0, Math.min(target, maxScroll)); + } + + /** + * Scroll to make a specific line visible + * @param lineIdx Line index to make visible + * @param visibleLines Number of visible lines in viewport + * @param maxScroll Maximum scroll position + */ + public void scrollToLine(int lineIdx, int visibleLines, int maxScroll) { + int firstVisible = scrolledLine; + int lastFullyVisible = scrolledLine + visibleLines; + + if (lineIdx < firstVisible) { + // Line is above viewport - scroll up + targetScroll = lineIdx; + } else if (lineIdx > lastFullyVisible) { + // Line is below viewport - scroll down + targetScroll = Math.min(lineIdx - visibleLines, maxScroll); + } + // If line is visible, don't change scroll + } + + // Getters + public double getScrollPos() { return scrollPos; } + public double getTargetScroll() { return targetScroll; } + public int getScrolledLine() { return scrolledLine; } + public double getScrollVelocity() { return scrollVelocity; } + public boolean isClickScrolling() { return clickScrolling; } + public int getScrollbarDragOffset() { return scrollbarDragOffset; } + + /** + * Get fractional offset for sub-pixel rendering + */ + public double getFractionalOffset() { + return scrollPos - scrolledLine; + } + + // Setters for scrollbar interaction + public void setClickScrolling(boolean scrolling) { this.clickScrolling = scrolling; } + public void setScrollbarDragOffset(int offset) { this.scrollbarDragOffset = offset; } + public void setScrolledLine(int line) { this.scrolledLine = line; } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java b/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java new file mode 100644 index 000000000..2374ad261 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java @@ -0,0 +1,192 @@ +package noppes.npcs.client.gui.util.script; + +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import java.util.List; + +/** + * Manages cursor position, text selection, and related operations. + */ +public class SelectionState { + private int startSelection = 0; + private int endSelection = 0; + private int cursorPosition = 0; + private long lastInputTime = 0L; + + /** + * Reset selection to a single point + */ + public void reset(int position) { + startSelection = endSelection = cursorPosition = position; + } + + /** + * Clamp all selection bounds to valid text range + */ + public void clamp(int textLength) { + if (textLength <= 0) { + startSelection = endSelection = cursorPosition = 0; + return; + } + startSelection = Math.max(0, Math.min(startSelection, textLength)); + endSelection = Math.max(0, Math.min(endSelection, textLength)); + cursorPosition = Math.max(0, Math.min(cursorPosition, textLength)); + } + + /** + * Set cursor position, optionally extending selection + */ + public void setCursor(int position, int textLength, boolean extendSelection) { + position = Math.max(0, Math.min(position, textLength)); + + if (position != cursorPosition) { + if (!extendSelection) { + endSelection = startSelection = cursorPosition = position; + } else { + int diff = cursorPosition - position; + if (cursorPosition == startSelection) { + startSelection -= diff; + } else if (cursorPosition == endSelection) { + endSelection -= diff; + } + + // Ensure start <= end + if (startSelection > endSelection) { + int temp = endSelection; + endSelection = startSelection; + startSelection = temp; + } + cursorPosition = position; + } + clamp(textLength); + markActivity(); + } + } + + /** + * Select a range of text + */ + public void setSelection(int start, int end) { + startSelection = Math.min(start, end); + endSelection = Math.max(start, end); + cursorPosition = endSelection; + } + + /** + * Select all text + */ + public void selectAll(int textLength) { + startSelection = cursorPosition = 0; + endSelection = textLength; + } + + /** + * Check if there's an active selection (not just cursor) + */ + public boolean hasSelection() { + return startSelection != endSelection; + } + + /** + * Get selected text + */ + public String getSelectedText(String text) { + if (!hasSelection() || text == null) return ""; + int start = Math.max(0, Math.min(startSelection, text.length())); + int end = Math.max(0, Math.min(endSelection, text.length())); + return text.substring(start, end); + } + + /** + * Get text before selection/cursor + */ + public String getTextBefore(String text) { + if (text == null || startSelection <= 0) return ""; + return text.substring(0, Math.min(startSelection, text.length())); + } + + /** + * Get text after selection/cursor + */ + public String getTextAfter(String text) { + if (text == null) return ""; + int pos = Math.min(endSelection, text.length()); + return text.substring(pos); + } + + /** + * Find which line the cursor is on (0-indexed) + */ + public int getCursorLineIndex(List lines, int textLength) { + if (lines == null || lines.isEmpty()) return 0; + + for (int i = 0; i < lines.size(); i++) { + LineData ld = lines.get(i); + boolean isLastLine = (i == lines.size() - 1); + boolean isOnLine = isLastLine + ? (cursorPosition >= ld.start && cursorPosition <= ld.end) + : (cursorPosition >= ld.start && cursorPosition < ld.end); + if (isOnLine) { + return i; + } + } + return lines.size() - 1; + } + + /** + * Check if cursor is on a specific line, handling the last line specially + * (last line doesn't have trailing newline, so use <= for end boundary) + */ + public boolean isCursorOnLine(int lineIndex, LineData line, int totalLines) { + boolean isLastLine = (lineIndex == totalLines - 1); + return isLastLine + ? (cursorPosition >= line.start && cursorPosition <= line.end) + : (cursorPosition >= line.start && cursorPosition < line.end); + } + + /** + * Find line containing the cursor + */ + public LineData findCurrentLine(List lines) { + if (lines == null) return null; + for (int i = 0; i < lines.size(); i++) { + LineData line = lines.get(i); + if (isCursorOnLine(i, line, lines.size())) { + return line; + } + } + return null; + } + + /** + * Mark user activity (for cursor blink timing) + */ + public void markActivity() { + lastInputTime = System.currentTimeMillis(); + } + + /** + * Check if there was recent input (for cursor blink pause) + */ + public boolean hadRecentInput() { + return System.currentTimeMillis() - lastInputTime < 500; + } + + // Getters + public int getStartSelection() { return startSelection; } + public int getEndSelection() { return endSelection; } + public int getCursorPosition() { return cursorPosition; } + public long getLastInputTime() { return lastInputTime; } + + // Direct setters (use with caution, prefer higher-level methods) + public void setStartSelection(int pos) { this.startSelection = pos; } + public void setEndSelection(int pos) { this.endSelection = pos; } + public void setCursorPositionDirect(int pos) { this.cursorPosition = pos; } + + /** + * After text modification, update selection to new position + */ + public void afterTextInsert(int newPosition) { + endSelection = startSelection = cursorPosition = newPosition; + markActivity(); + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/gui/util/script/UndoManager.java b/src/main/java/noppes/npcs/client/gui/util/script/UndoManager.java new file mode 100644 index 000000000..d7cad5295 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/UndoManager.java @@ -0,0 +1,119 @@ +package noppes.npcs.client.gui.util.script; + +import java.util.ArrayList; +import java.util.List; + +/** + * Manages undo/redo stacks for the text editor. + */ +public class UndoManager { + + /** + * Stores a snapshot of text state for undo/redo + */ + public static class UndoState { + public final String text; + public final int cursorPosition; + + public UndoState(String text, int cursorPosition) { + this.text = text; + this.cursorPosition = cursorPosition; + } + } + + private final List undoStack = new ArrayList<>(); + private final List redoStack = new ArrayList<>(); + private boolean isUndoing = false; + + // Optional: limit stack size to prevent memory issues + private static final int MAX_STACK_SIZE = 100; + + /** + * Record current state before making a change + */ + public void recordState(String text, int cursorPosition) { + if (isUndoing) return; + + undoStack.add(new UndoState(text, cursorPosition)); + redoStack.clear(); + + // Trim stack if too large + while (undoStack.size() > MAX_STACK_SIZE) { + undoStack.remove(0); + } + } + + /** + * Undo to previous state + * @param currentText Current text before undo + * @param currentCursor Current cursor position + * @return Previous state, or null if nothing to undo + */ + public UndoState undo(String currentText, int currentCursor) { + if (undoStack.isEmpty()) return null; + + isUndoing = true; + redoStack.add(new UndoState(currentText, currentCursor)); + UndoState state = undoStack.remove(undoStack.size() - 1); + isUndoing = false; + + return state; + } + + /** + * Redo to next state + * @param currentText Current text before redo + * @param currentCursor Current cursor position + * @return Next state, or null if nothing to redo + */ + public UndoState redo(String currentText, int currentCursor) { + if (redoStack.isEmpty()) return null; + + isUndoing = true; + undoStack.add(new UndoState(currentText, currentCursor)); + UndoState state = redoStack.remove(redoStack.size() - 1); + isUndoing = false; + + return state; + } + + /** + * Check if undo is available + */ + public boolean canUndo() { + return !undoStack.isEmpty(); + } + + /** + * Check if redo is available + */ + public boolean canRedo() { + return !redoStack.isEmpty(); + } + + /** + * Clear all history + */ + public void clear() { + undoStack.clear(); + redoStack.clear(); + } + + /** + * Check if currently performing an undo/redo operation + */ + public boolean isUndoing() { + return isUndoing; + } + + /** + * Set undoing flag (for external control during setText) + */ + public void setUndoing(boolean undoing) { + this.isUndoing = undoing; + } + + // For backward compatibility with existing code + public List getUndoList() { return undoStack; } + public List getRedoList() { return redoStack; } +} \ No newline at end of file From d420d3228ec73fb0ee197902fdb1737aa7601ed0 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 22:37:40 +0200 Subject: [PATCH 073/358] Post-refactor clean up --- .../client/gui/util/GuiScriptTextArea.java | 86 +++++-------------- .../client/gui/util/script/ScrollState.java | 43 ++++++++++ 2 files changed, 64 insertions(+), 65 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index f9b1c74ca..75be82857 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -83,7 +83,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { } // ==================== RENDERING ==================== - public void drawTextBox(int xMouse, int yMouse) { if (!visible) return; @@ -107,14 +106,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Enable scissor test to clip drawing to the TEXT viewport rectangle (excludes gutter) GL11.glEnable(GL11.GL_SCISSOR_TEST); - Minecraft mc = Minecraft.getMinecraft(); - ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); - int scaleFactor = sr.getScaleFactor(); - int scissorX = (this.x ) * scaleFactor; - int scissorY = (sr.getScaledHeight() - (this.y + this.height)) * scaleFactor; - int scissorW = (this.width ) * scaleFactor; - int scissorH = this.height * scaleFactor; - GL11.glScissor(scissorX, scissorY, scissorW, scissorH); + scissorViewport(); container.visibleLines = height / container.lineHeight-1; @@ -123,41 +115,14 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Handle mouse wheel scroll if (listener instanceof GuiNPCInterface) { int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); - if (wheelDelta != 0) { + if (wheelDelta != 0) scroll.applyWheelScroll(wheelDelta, maxScroll); - } - } - - // Handle scrollbar dragging - if (scroll.isClickScrolling()) { - scroll.setClickScrolling(Mouse.isButtonDown(0)); - int diff = Math.max(0, container.linesCount - container.visibleLines); - if (diff > 0) { - int sbSize = Math.max((int) (1f * container.visibleLines / container.linesCount * height), 2); - int trackTop = y + 1; - int trackHeight = Math.max(1, height - 4); - int thumbRange = Math.max(1, trackHeight - sbSize); - double linesCountD = Math.max(1, (double) container.linesCount); - int thumbTop = (int) (y + 1f * scroll.getScrollPos() / linesCountD * (height - 4)) + 1; - - if (yMouse < thumbTop || yMouse > thumbTop + sbSize) { - double centerRatio = (double) (yMouse - trackTop) / (double) trackHeight; - centerRatio = Math.max(0.0, Math.min(1.0, centerRatio)); - scroll.setTargetScroll(centerRatio * diff, maxScroll); - scroll.setScrollbarDragOffset(sbSize / 2); - } else { - int desiredTop = yMouse - scroll.getScrollbarDragOffset(); - desiredTop = Math.max(trackTop, Math.min(trackTop + thumbRange, desiredTop)); - double ratio = (double) (desiredTop - trackTop) / (double) thumbRange; - ratio = Math.max(0.0, Math.min(1.0, ratio)); - scroll.setTargetScroll(ratio * diff, maxScroll); - } - } - if (!scroll.isClickScrolling()) { - scroll.setScrollbarDragOffset(0); - } } + // Handle scrollbar dragging (delegated to ScrollState) + if (scroll.isClickScrolling()) + scroll.handleClickScrolling(yMouse, x, y, height, container.visibleLines, container.linesCount, maxScroll); + // Update scroll animation scroll.initializeIfNeeded(scroll.getScrolledLine()); scroll.update(maxScroll); @@ -234,19 +199,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { int renderEnd = Math.min(list.size() - 1, scroll.getScrolledLine() + container.visibleLines + 1); // Apply fractional GL translate for sub-pixel smooth scrolling + int stringYOffset = 2; double fracOffset = scroll.getFractionalOffset(); float fracPixels = (float) (fracOffset * container.lineHeight); GL11.glPushMatrix(); GL11.glTranslatef(0.0f, -fracPixels, 0.0f); - - // Render line numbers in the gutter (outside scissor region, so pop first) - GL11.glPopMatrix(); - // GL11.glDisable(GL11.GL_SCISSOR_TEST); - GL11.glPushMatrix(); - GL11.glTranslatef(0.0f, -fracPixels, 0.0f); - int stringYOffset = 2; - // Render LINE GUTTER numbers for (int i = renderStart; i <= renderEnd; i++) { int posY = y + (i - scroll.getScrolledLine()) * container.lineHeight + stringYOffset; @@ -269,13 +227,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { } ClientProxy.Font.drawString(lineNum, lineNumX, lineNumY, lineNumColor); } - GL11.glPopMatrix(); - // Re-enable scissor for text area and push matrix again - GL11.glEnable(GL11.GL_SCISSOR_TEST); - GL11.glPushMatrix(); - GL11.glTranslatef(0.0f, -fracPixels, 0.0f); - renderStart = Math.max(0, scroll.getScrolledLine() - 1); + // Render Viewport for (int i = renderStart; i <= renderEnd; i++) { LineData data = list.get(i); String line = data.text; @@ -371,7 +324,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } GL11.glPopMatrix(); + GL11.glDisable(GL11.GL_SCISSOR_TEST); + if (hasVerticalScrollbar()) { Minecraft.getMinecraft().renderEngine.bindTexture(GuiCustomScroll.resource); int sbSize = Math.max((int) (1f * (container.visibleLines) / container.linesCount * height), 2); @@ -382,11 +337,21 @@ public class GuiScriptTextArea extends GuiNpcTextField { drawRect(posX, posY, posX + 5, posY + sbSize + 2, 0xFFe0e0e0); } - GL11.glDisable(GL11.GL_SCISSOR_TEST); } + private void scissorViewport() { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int scaleFactor = sr.getScaleFactor(); + int scissorX = (this.x) * scaleFactor; + int scissorY = (sr.getScaledHeight() - (this.y + this.height)) * scaleFactor; + int scissorW = (this.width) * scaleFactor; + int scissorH = this.height * scaleFactor; + GL11.glScissor(scissorX, scissorY, scissorW, scissorH); + } // ==================== SELECTION & CURSOR POSITION ==================== + // Get cursor position from mouse coordinates private int getSelectionPos(int xMouse, int yMouse) { xMouse -= (this.x + LINE_NUMBER_GUTTER_WIDTH + 1); yMouse -= this.y + 1; @@ -460,15 +425,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (!isEnabled()) return false; - LineData currentLine = null; - for (LineData line : this.container.lines) { - if (selection.getStartSelection() >= line.start && selection.getStartSelection() < line.end) { - currentLine = line; - break; - } - } - - String original = text; if (i == Keyboard.KEY_LEFT) { int j = 1; if (isCtrlKeyDown()) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java b/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java index cbee81e4e..da9ac067f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java @@ -1,5 +1,7 @@ package noppes.npcs.client.gui.util.script; +import org.lwjgl.input.Mouse; + /** * Manages smooth scrolling state and animation for the script text area. * Uses exponential smoothing for fluid scroll animations. @@ -150,4 +152,45 @@ public class ScrollState { public void setClickScrolling(boolean scrolling) { this.clickScrolling = scrolling; } public void setScrollbarDragOffset(int offset) { this.scrollbarDragOffset = offset; } public void setScrolledLine(int line) { this.scrolledLine = line; } + + /** + * Handle scrollbar dragging interaction. This encapsulates the logic that was + * previously in the GUI class for handling thumb clicks/drags. + * @param yMouse current mouse Y coordinate + * @param areaX GUI area X (unused but kept for parity) + * @param areaY GUI area Y (top of text area) + * @param areaHeight height of the scroll track area + * @param visibleLines number of visible lines in viewport + * @param linesCount total number of lines + * @param maxScroll maximum allowed scroll value + */ + public void handleClickScrolling(int yMouse, int areaX, int areaY, int areaHeight, int visibleLines, int linesCount, int maxScroll) { + // Keep dragging while mouse button is down + setClickScrolling(Mouse.isButtonDown(0)); + int diff = Math.max(0, linesCount - visibleLines); + if (diff > 0) { + int sbSize = Math.max((int) (1f * visibleLines / Math.max(1, linesCount) * areaHeight), 2); + int trackTop = areaY + 1; + int trackHeight = Math.max(1, areaHeight - 4); + int thumbRange = Math.max(1, trackHeight - sbSize); + double linesCountD = Math.max(1, (double) linesCount); + int thumbTop = (int) (areaY + 1f * getScrollPos() / linesCountD * (areaHeight - 4)) + 1; + + if (yMouse < thumbTop || yMouse > thumbTop + sbSize) { + double centerRatio = (double) (yMouse - trackTop) / (double) trackHeight; + centerRatio = Math.max(0.0, Math.min(1.0, centerRatio)); + setTargetScroll(centerRatio * diff, maxScroll); + setScrollbarDragOffset(sbSize / 2); + } else { + int desiredTop = yMouse - getScrollbarDragOffset(); + desiredTop = Math.max(trackTop, Math.min(trackTop + thumbRange, desiredTop)); + double ratio = (double) (desiredTop - trackTop) / (double) thumbRange; + ratio = Math.max(0.0, Math.min(1.0, ratio)); + setTargetScroll(ratio * diff, maxScroll); + } + } + if (!isClickScrolling()) { + setScrollbarDragOffset(0); + } + } } \ No newline at end of file From 7ccd4a57b71ce22488c599dacb50c63f811ebf90 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 22:52:32 +0200 Subject: [PATCH 074/358] More refactoring, code clean up and documentation --- .../client/gui/util/GuiScriptTextArea.java | 308 ++++++++++++------ .../gui/util/script/SelectionState.java | 2 +- 2 files changed, 204 insertions(+), 106 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 75be82857..ce7ca6a9c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -412,6 +412,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== KEYBOARD INPUT HANDLING ==================== + /** + * Handles keyboard input for the text area, delegating to specialized handlers + * for different types of input: navigation, deletion, shortcuts, and character input. + */ @Override public boolean textboxKeyTyped(char c, int i) { if (!active) @@ -425,25 +429,51 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (!isEnabled()) return false; + if (handleNavigationKeys(i)) return true; + if (handleDeletionKeys(i)) return true; + if (handleShortcutKeys(i)) return true; + if (handleCharacterInput(c)) return true; + + return true; + } + + /** + * Handles cursor navigation keys (arrows) with support for word-jumping (Ctrl) + * and selection (Shift). Updates cursor position and scrolls viewport if needed. + */ + private boolean handleNavigationKeys(int i) { + // LEFT ARROW: move cursor left; with Ctrl -> jump by word if (i == Keyboard.KEY_LEFT) { - int j = 1; + int j = 1; // default: move one character if (isCtrlKeyDown()) { + // When Ctrl is down, compute distance to previous word boundary. + // We match words in the text slice before the cursor and pick + // the last match start as the new boundary. Matcher m = container.regexWord.matcher(text.substring(0, selection.getCursorPosition())); while (m.find()) { if (m.start() == m.end()) - continue; + continue; // skip empty matches + // j becomes the number of chars to move left to reach word start j = selection.getCursorPosition() - m.start(); } } int newPos = Math.max(selection.getCursorPosition() - j, 0); + // If Shift is held, extend selection; otherwise place caret. setCursor(newPos, GuiScreen.isShiftKeyDown()); return true; } + + // RIGHT ARROW: move cursor right; with Ctrl -> jump to next word start if (i == Keyboard.KEY_RIGHT) { - int j = 1; + int j = 1; // default: move one character if (isCtrlKeyDown()) { + // With Ctrl, search for the next word boundary starting at the cursor. + // We look at the substring after the cursor and use the matcher to find + // the first match; its start offset indicates how many chars to skip. Matcher m = container.regexWord.matcher(text.substring(selection.getCursorPosition())); - if (m.find() && m.start() > 0 || m.find()) { + // m.find() called once is sufficient; the previous code attempted + // a redundant second find in some cases. Keep first positive find. + if (m.find()) { j = m.start(); } } @@ -451,6 +481,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { setCursor(newPos, GuiScreen.isShiftKeyDown()); return true; } + + // UP/DOWN: logical cursor movement across lines while preserving + // column where possible. After moving, ensure the caret remains visible + // by adjusting the scroll if necessary. if (i == Keyboard.KEY_UP) { setCursor(cursorUp(), GuiScreen.isShiftKeyDown()); scrollToCursor(); @@ -461,22 +495,40 @@ public class GuiScriptTextArea extends GuiNpcTextField { scrollToCursor(); return true; } + + return false; // not a navigation key + } + + /** + * Handles deletion keys: Delete, Backspace, and Ctrl+Backspace. + * Includes smart backspace behavior for indentation-aware line merging, + * auto-pair deletion for brackets/quotes, and word-level deletion. + */ + private boolean handleDeletionKeys(int i) { + // DELETE key: remove the character under the cursor if no selection, + // otherwise remove the selected region's tail. if (i == Keyboard.KEY_DELETE) { String s = getSelectionAfterText(); if (!s.isEmpty() && !selection.hasSelection()) + // remove single character after caret when nothing is selected s = s.substring(1); setText(getSelectionBeforeText() + s); + // Keep caret at same start selection selection.reset(selection.getStartSelection()); return true; } + + // CTRL+BACKSPACE: delete to previous word or whitespace boundary. if (isKeyComboCtrlBackspace(i)) { String s = getSelectionBeforeText(); if (selection.getStartSelection() > 0 && !selection.hasSelection()) { int nearestCondition = selection.getCursorPosition(); int g; + // If the char left of caret is whitespace, find the first non-space to the left; + // otherwise find first whitespace/newline to the left (word boundary). boolean cursorInWhitespace = Character.isWhitespace(s.charAt(selection.getCursorPosition() - 1)); if (cursorInWhitespace) { - // Find the nearest word if we are starting in whitespace + // Scan left until non-whitespace (start of previous word) for (g = selection.getCursorPosition() - 1; g >= 0; g--) { char currentChar = s.charAt(g); if (!Character.isWhitespace(currentChar)) { @@ -488,7 +540,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } else { - // Find the nearest blank space or new line + // Scan left until whitespace/newline is found (word boundary) for (g = selection.getCursorPosition() - 1; g >= 0; g--) { char currentChar = s.charAt(g); if (Character.isWhitespace(currentChar) || currentChar == '\n') { @@ -501,8 +553,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } - // Remove all text to the left up to the nearest boundary + // Trim the prefix up to the discovered boundary s = s.substring(0, nearestCondition); + // Adjust selection start to match removed characters selection.setStartSelection( selection.getStartSelection() - (selection.getCursorPosition() - nearestCondition)); } @@ -510,8 +563,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { selection.reset(selection.getStartSelection()); return true; } + + // BACKSPACE: complex handling with a few cases: + // 1) If a selection exists, delete it + // 2) If at start, nothing to do + // 3) Smart indent-aware merge with previous line when caret is at/near expected indent + // 4) Auto-pair deletion (remove both opening and closing chars when deleting an opener) + // 5) Fallback: delete a single char to the left if (i == Keyboard.KEY_BACK) { - // Handle selection deletion first + // 1) selection deletion if (selection.hasSelection()) { String s = getSelectionBeforeText(); setText(s + getSelectionAfterText()); @@ -519,57 +579,52 @@ public class GuiScriptTextArea extends GuiNpcTextField { scrollToCursor(); return true; } - - // Nothing to delete if at start + + // 2) nothing to delete if (selection.getStartSelection() <= 0) { return true; } - - // Find current line for indent-aware backspace + + // 3) indent-aware merge: find current line and compute expected indent LineData curr = selection.findCurrentLine(container.lines); - - // Indent-aware backspace: if cursor is at or before the EXPECTED indent, merge with previous line - // If cursor is after expected indent (even in extra whitespace), do normal backspace if (curr != null && curr.start > 0) { int col = selection.getCursorPosition() - curr.start; int actualIndent = IndentHelper.getLineIndent(curr.text); int expectedIndent = IndentHelper.getExpectedIndent(curr, container.lines); - - // Only trigger smart merge if cursor is at or before the expected indent position + + // Trigger smart merge only when caret is at or before the expected indent. if (col <= expectedIndent) { boolean lineHasContent = curr.text.trim().length() > 0; - int newlinePos = curr.start - 1; // Position of newline before this line - + int newlinePos = curr.start - 1; // index of newline before this line + if (!lineHasContent) { - // Empty/whitespace-only line: remove the entire line including its trailing newline + // Empty or whitespace-only line: remove it including its trailing newline int removeEnd = text.indexOf('\n', curr.start); if (removeEnd == -1) { removeEnd = text.length(); } else { - removeEnd = removeEnd + 1; // Include the newline + removeEnd = removeEnd + 1; // include the newline in removal } String before = text.substring(0, curr.start); String after = removeEnd <= text.length() ? text.substring(removeEnd) : ""; setText(before + after); - // Cursor goes to end of previous line (at the newline position, which is now end of prev line content) + // Place caret at end of previous line int newCursor = Math.max(0, curr.start - 1); selection.reset(newCursor); scrollToCursor(); return true; } else { - // Line has content: merge with previous line - // Remove newline + indent, keep content + // Merge current line content with the previous line preserving spacing int contentStart = curr.start + actualIndent; - String before = text.substring(0, newlinePos); String content = contentStart <= text.length() ? text.substring(contentStart) : ""; - - // Determine if we need spacing between merged content + + // Decide whether a space is needed between concatenated fragments. String spacer = ""; if (before.length() > 0 && content.length() > 0) { char lastChar = before.charAt(before.length() - 1); char firstChar = content.charAt(0); - // Add space unless previous ends with whitespace/open bracket or current starts with close bracket/punctuation + // Avoid adding space when punctuation/brackets are adjacent if (!Character.isWhitespace(lastChar) && lastChar != '{' && lastChar != '(' && lastChar != '[' && firstChar != '}' && firstChar != ')' && firstChar != ']' && @@ -578,7 +633,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { spacer = " "; } } - + setText(before + spacer + content); int newCursor = before.length() + spacer.length(); selection.reset(newCursor); @@ -587,8 +642,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } - - // Auto-pair deletion: if deleting an opening bracket/quote and next char is the matching closer + + // 4) Auto-pair deletion: when deleting an opener and a matching closer follows, + // remove both so the pair is cleaned up in one backspace. if (selection.getStartSelection() > 0 && selection.getStartSelection() < text.length()) { char prev = text.charAt(selection.getStartSelection() - 1); char nextc = text.charAt(selection.getStartSelection()); @@ -608,7 +664,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } - // Normal backspace: delete one character + // 5) Normal single-character backspace String s = getSelectionBeforeText(); s = s.substring(0, s.length() - 1); selection.setStartSelection(selection.getStartSelection() - 1); @@ -617,8 +673,20 @@ public class GuiScriptTextArea extends GuiNpcTextField { scrollToCursor(); return true; } + + return false; + } + + /** + * Handles keyboard shortcuts: clipboard operations (cut/copy/paste), + * undo/redo, tab indentation, code formatting, enter with brace handling, + * comment toggling, and line duplication. + */ + private boolean handleShortcutKeys(int i) { + // CTRL+X: Cut if (this.isKeyComboCtrlX(i)) { if (selection.hasSelection()) { + // Copy selected text into clipboard, then remove the selection NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); String s = getSelectionBeforeText(); setText(s + getSelectionAfterText()); @@ -627,20 +695,26 @@ public class GuiScriptTextArea extends GuiNpcTextField { } return true; } + + // CTRL+C: Copy if (this.isKeyComboCtrlC(i)) { if (selection.hasSelection()) { NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); } return true; } + + // CTRL+V: Paste (insert clipboard contents at caret) if (this.isKeyComboCtrlV(i)) { addText(NoppesStringUtils.getClipboardContents()); scrollToCursor(); return true; } + + // UNDO (Ctrl+Z): restore last entry from undoList and push current state to redoList if (i == Keyboard.KEY_Z && isCtrlKeyDown()) { if (undoList.isEmpty()) - return false; + return false; // nothing to undo undoing = true; redoList.add(new UndoData(this.text, selection.getCursorPosition())); UndoData data = undoList.remove(undoList.size() - 1); @@ -650,6 +724,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { scrollToCursor(); return true; } + + // REDO (Ctrl+Y): opposite of undo if (i == Keyboard.KEY_Y && isCtrlKeyDown()) { if (redoList.isEmpty()) return false; @@ -662,6 +738,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { scrollToCursor(); return true; } + + // TAB: indent or unindent depending on Shift if (i == Keyboard.KEY_TAB) { boolean shift = isShiftKeyDown(); if (shift) { @@ -672,21 +750,25 @@ public class GuiScriptTextArea extends GuiNpcTextField { scrollToCursor(); return true; } + + // CTRL+F: format the text according to IndentHelper rules if (i == Keyboard.KEY_F && isCtrlKeyDown()) { formatText(); return true; } + + // RETURN/ENTER: special handling when preceding char is an opening brace '{' if (i == Keyboard.KEY_RETURN) { if (selection.getCursorPosition() > 0 && selection.getCursorPosition() <= text.length() && text.charAt( selection.getCursorPosition() - 1) == '{') { + // Compute current and child indent strings String indent = getIndentCurrentLine(); String childIndent = indent + " "; String before = getSelectionBeforeText(); String after = getSelectionAfterText(); - // If there is non-empty code on the same line after the brace (before the next newline), - // treat it as code that should be moved into the newly created inner line. In that case - // just insert the child indent and return — do not insert the closing brace before that code. + // If there's code after the brace on the same line, assume it should be + // moved inside the new inner line: insert child indent only. int firstNewline = after.indexOf('\n'); String leadingSegment = firstNewline == -1 ? after : after.substring(0, firstNewline); if (leadingSegment.trim().length() > 0) { @@ -695,12 +777,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { return true; } - // Determine whether this opening brace already has a matching closing - // brace at the same scope (and indent). Prefer using the brace-span - // computation which skips strings/comments and handles nesting. + // Otherwise, determine whether a matching closing brace already exists at same indent. boolean hasMatchingCloseSameIndent = false; try { - // Find the line index containing the opening brace (cursorPosition-1) int openLineIdx = -1; int bracePos = selection.getCursorPosition() - 1; for (int li = 0; li < this.container.lines.size(); li++) { @@ -717,7 +796,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { int spanOpen = span[1]; int spanClose = span[2]; if (spanOpen == openLineIdx) { - // Found a matching close; check its indent equals current indent + // If the matching close has the same indent as current line, + // we only insert the child indent (do not auto-insert a closing brace). int closeIndent = IndentHelper.getLineIndent(this.container.lines.get(spanClose).text); if (closeIndent == indent.length()) { hasMatchingCloseSameIndent = true; @@ -727,15 +807,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } catch (Exception ex) { - // Fallback: if anything goes wrong, conservatively behave as before + // On any error, default to conservative behavior (insert closing brace) hasMatchingCloseSameIndent = false; } - if (hasMatchingCloseSameIndent) { addText("\n" + childIndent); scrollToCursor(); } else { + // Insert child line and a closing brace aligned with current indent String insert = "\n" + childIndent + "\n" + indent + "}"; setText(before + insert + after); int newCursor = before.length() + 1 + childIndent.length(); @@ -743,22 +823,98 @@ public class GuiScriptTextArea extends GuiNpcTextField { scrollToCursor(); } } else { + // Normal enter: insert newline + auto indent for caret position addText(Character.toString('\n') + getAutoIndentForEnter()); scrollToCursor(); } + return true; } + + // CTRL+/ : toggle comment for selection or current line + if (i == Keyboard.KEY_SLASH && isCtrlKeyDown()) { + if (selection.hasSelection()) { + toggleCommentSelection(); + } else { + toggleCommentLineAtCursor(); + } + return true; + } + + // CTRL+D : duplicate selection or current line + if (i == Keyboard.KEY_D && isCtrlKeyDown()) { + if (selection.hasSelection()) { + // Multi-line selection duplication: find first and last covered lines, + // then insert the whole block after the last line without adding extra newline. + LineData firstLine = null, lastLine = null; + for (LineData line : container.lines) { + if (line.end > selection.getStartSelection() && line.start < selection.getEndSelection()) { + if (firstLine == null) firstLine = line; + lastLine = line; + } + } + if (firstLine != null && lastLine != null) { + String selectedText = text.substring(firstLine.start, lastLine.end); + String insertText = selectedText; + int savedStart = selection.getStartSelection(); + int savedEnd = selection.getEndSelection(); + int insertAt = lastLine.end; + setText(text.substring(0, insertAt) + insertText + text.substring(insertAt)); + // Restore prior selection / cursor positions + selection.setStartSelection(savedStart); + selection.setEndSelection(savedEnd); + selection.setCursorPositionDirect(savedEnd); + return true; + } + } else { + // Duplicate current line when nothing is selected + for (LineData line : container.lines) { + if (selection.getCursorPosition() >= line.start && selection.getCursorPosition() <= line.end) { + int lineStart = line.start, lineEnd = line.end; + String lineText = text.substring(lineStart, lineEnd); + // If the line already ends with a newline, reuse it; otherwise + // prefix a newline so duplicate appears after current line. + String insertText; + if (lineText.endsWith("\n")) { + insertText = lineText; + } else { + insertText = "\n" + lineText; + } + int insertionPoint = lineEnd; + setText(text.substring(0, insertionPoint) + insertText + text.substring(insertionPoint)); + int newCursor = insertionPoint + insertText.length() - (insertText.endsWith("\n") ? 1 : 0); + selection.reset(Math.max(0, Math.min(newCursor, this.text.length()))); + return true; + } + } + } + return true; + } + + return false; + } + + /** + * Handles printable character input with auto-pairing for quotes and brackets, + * and smart skipping over existing closing characters. + */ + private boolean handleCharacterInput(char c) { if (ChatAllowedCharacters.isAllowedCharacter(c)) { String before = getSelectionBeforeText(); String after = getSelectionAfterText(); - // If typing a closing char and the next char is already that closer, skip over it + // If the user types a closing character and that same closer is + // already immediately after the caret, move caret past it instead + // of inserting another closer. This prevents duplicate closers + // when the editor auto-inserts pairs. if ((c == ')' || c == ']' || c == '"' || c == '\'' ) && after.length() > 0 && after.charAt(0) == c) { - // move cursor forward over existing closer + // Move caret forward by one (skip over existing closer) selection.reset(before.length() + 1); scrollToCursor(); return true; } + // Auto-pair insertion: when opening a quote/brace/bracket is typed, + // insert a matching closer and place caret between the pair. if (c == '"') { setText(before + "\"\"" + after); selection.reset(before.length() + 1); @@ -784,70 +940,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { return true; } + // Default insertion for printable characters: insert at caret (replacing selection) addText(Character.toString(c)); scrollToCursor(); - } - - if (i == Keyboard.KEY_SLASH && isCtrlKeyDown()) { - if (selection.hasSelection()) { - toggleCommentSelection(); - } else { - toggleCommentLineAtCursor(); - } return true; } - - if (i == Keyboard.KEY_D && isCtrlKeyDown()) { - if (selection.hasSelection()) { - // Handle multi-line selection duplication - LineData firstLine = null, lastLine = null; - for (LineData line : container.lines) { - if (line.end > selection.getStartSelection() && line.start < selection.getEndSelection()) { - if (firstLine == null) firstLine = line; - lastLine = line; - } - } - if (firstLine != null && lastLine != null) { - String selectedText = text.substring(firstLine.start, lastLine.end); - // Insert the selected block immediately after the last line without adding an extra newline - String insertText = selectedText; - // Save selection before setText - int savedStart = selection.getStartSelection(); - int savedEnd = selection.getEndSelection(); - int insertAt = lastLine.end; - setText(text.substring(0, insertAt) + insertText + text.substring(insertAt)); - // Restore cursor and selection - selection.setStartSelection(savedStart); - selection.setEndSelection(savedEnd); - selection.setCursorPositionDirect(savedEnd); - return true; - } - } else { - // Single line duplication (existing logic) - for (LineData line : container.lines) { - if (selection.getCursorPosition() >= line.start && selection.getCursorPosition() <= line.end) { - int lineStart = line.start, lineEnd = line.end; - String lineText = text.substring(lineStart, lineEnd); - // Avoid inserting an extra blank line: if lineText already ends with a newline, - // reuse it; otherwise prepend a newline so the duplicate appears immediately after. - String insertText; - if (lineText.endsWith("\n")) { - insertText = lineText; - } else { - insertText = "\n" + lineText; - } - int insertionPoint = lineEnd; - setText(text.substring(0, insertionPoint) + insertText + text.substring(insertionPoint)); - // Place cursor at end of duplicated line (just before any trailing newline) - int newCursor = insertionPoint + insertText.length() - (insertText.endsWith("\n") ? 1 : 0); - selection.reset(Math.max(0, Math.min(newCursor, this.text.length()))); - return true; - } - } - } - } - - return true; + return false; } private boolean isShiftKeyDown() { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java b/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java index 2374ad261..d03008905 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java @@ -13,7 +13,7 @@ public class SelectionState { private long lastInputTime = 0L; /** - * Reset selection to a single point + * Reset all cursor selections to a single position */ public void reset(int position) { startSelection = endSelection = cursorPosition = position; From 21a7922a957abb78119c479145355b089b62b17c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 18 Dec 2025 23:16:08 +0200 Subject: [PATCH 075/358] mouseClicked refactor + documentation --- .../client/gui/util/GuiScriptTextArea.java | 34 ++++++++----------- .../client/gui/util/script/ScrollState.java | 13 +++++++ .../gui/util/script/SelectionState.java | 31 +++++++++++++++++ 3 files changed, 59 insertions(+), 19 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index ce7ca6a9c..0986b6add 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1162,22 +1162,29 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== MOUSE HANDLING ==================== public void mouseClicked(int xMouse, int yMouse, int mouseButton) { + // Determine whether click occurred inside the text area bounds this.active = xMouse >= this.x && xMouse < this.x + this.width && yMouse >= this.y && yMouse < this.y + this.height; if (this.active) { + // Compute logical click position in text and reset selection/caret int clickPos = this.getSelectionPos(xMouse, yMouse); selection.reset(clickPos); selection.markActivity(); + + // Prepare click state (left button starts most interactions) this.clicked = mouseButton == 0; this.doubleClicked = false; this.tripleClicked = false; long time = System.currentTimeMillis(); + + // Prefer delegating scrollbar-start logic to ScrollState. If the click + // is on the scrollbar area and the scrollbar can be dragged, we start + // click-scrolling mode and cancel the normal text click-drag behavior. if (this.clicked && this.container.linesCount * this.container.lineHeight > this.height && xMouse > this.x + this.width - 8) { + // We consumed the mouse-down as a scrollbar drag start this.clicked = false; - scroll.setClickScrolling(true); - double linesCountD = Math.max(1, (double) this.container.linesCount); - int thumbTop = (int) (this.y + 1f * scroll.getScrollPos() / linesCountD * (this.height - 4)) + 1; - scroll.setScrollbarDragOffset(yMouse - thumbTop); + scroll.startScrollbarDrag(yMouse,this.y,this.height,container.linesCount); } else { + // Handle double/triple click selection counting if (time - this.lastClicked < 300L) { this.clickCount++; } else { @@ -1185,24 +1192,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { } if (this.clickCount == 2) { - // Double-click: select word under cursor (restore prior behavior) + // Double-click: select the word under the caret using the container's word regex this.doubleClicked = true; - Matcher m = this.container.regexWord.matcher(this.text); - while (m.find()) { - if (selection.getCursorPosition() >= m.start() && selection.getCursorPosition() <= m.end()) { - selection.setSelection(m.start(), m.end()); - break; - } - } + selection.selectWordAtCursor(this.text, this.container.regexWord); } else if (this.clickCount >= 3) { - // Triple-click: select entire line + // Triple-click: select the entire logical line that contains the caret this.tripleClicked = true; - for (LineData line : container.lines) { - if (selection.getCursorPosition() >= line.start && selection.getCursorPosition() <= line.end) { - selection.setSelection(line.start, line.end); - break; - } - } + selection.selectLineAtCursor(container.lines); this.clickCount = 0; } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java b/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java index da9ac067f..b092cd7fe 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java @@ -193,4 +193,17 @@ public class ScrollState { setScrollbarDragOffset(0); } } + + /** + * Attempt to start a scrollbar drag if the mouse down occurred on the scrollbar thumb area. + * Returns true if drag was initiated (caller should cancel normal text-click drag behavior). + */ + public void startScrollbarDrag(int yMouse, int areaY, int areaHeight, int linesCount) { + // Thumb width in GuiScriptTextArea is 6px (posX..posX+5), so check clicks near the right edge + // Start click-scrolling mode and set initial drag offset relative to thumb top. + setClickScrolling(true); + double linesCountD = Math.max(1, (double) linesCount); + int thumbTop = (int) (areaY + 1f * getScrollPos() / linesCountD * (areaHeight - 4)) + 1; + setScrollbarDragOffset(yMouse - thumbTop); + } } \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java b/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java index d03008905..03644ad2e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.util.script; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; import java.util.List; +import java.util.regex.Pattern; /** * Manages cursor position, text selection, and related operations. @@ -189,4 +190,34 @@ public class SelectionState { endSelection = startSelection = cursorPosition = newPosition; markActivity(); } + + /** + * Select the word surrounding the current cursor position using the provided regex. + * The regex should match word tokens; the implementation finds the token which + * contains the cursor and selects its start..end. + */ + public void selectWordAtCursor(String text, Pattern wordPattern) { + if (text == null || wordPattern == null) return; + java.util.regex.Matcher m = wordPattern.matcher(text); + while (m.find()) { + if (cursorPosition >= m.start() && cursorPosition <= m.end()) { + setSelection(m.start(), m.end()); + return; + } + } + } + + /** + * Select the entire line containing the cursor. `lines` is the parsed + * line data from `JavaTextContainer` where each LineData has start/end indices. + */ + public void selectLineAtCursor(List lines) { + if (lines == null) return; + for (LineData line : lines) { + if (isCursorOnLine(lines.indexOf(line), line, lines.size())) { + setSelection(line.start, line.end); + return; + } + } + } } \ No newline at end of file From 8edf09f215cc0cf2f407276c055c6cd8eb0b5d06 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 01:08:36 +0200 Subject: [PATCH 076/358] Fixed cursor double/triple click starting selection from after the cursor --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 0986b6add..81effc8b1 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1195,10 +1195,17 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Double-click: select the word under the caret using the container's word regex this.doubleClicked = true; selection.selectWordAtCursor(this.text, this.container.regexWord); + // Prevent subsequent mouse-drag handling in the render loop from + // treating this double-click as a normal click-drag which would + // immediately reset and extend the selection. Clearing `clicked` + // keeps the double-click selection stable. + this.clicked = false; } else if (this.clickCount >= 3) { // Triple-click: select the entire logical line that contains the caret this.tripleClicked = true; selection.selectLineAtCursor(container.lines); + // Same as double-click: clear `clicked` to avoid accidental drag-extension + this.clicked = false; this.clickCount = 0; } } From 0ed150f067735306129cc0d965ec40d6f2fb82e9 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 01:09:16 +0200 Subject: [PATCH 077/358] Fixed double/triple click selections setting cursor incorrectly --- .../java/noppes/npcs/client/gui/util/script/SelectionState.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java b/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java index 03644ad2e..242c16a93 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java @@ -69,7 +69,6 @@ public class SelectionState { public void setSelection(int start, int end) { startSelection = Math.min(start, end); endSelection = Math.max(start, end); - cursorPosition = endSelection; } /** From c171fac4be11b87ede6483caafaeeb1f3a29a205 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 01:17:07 +0200 Subject: [PATCH 078/358] {} scaffold generation now ignores any whitespaces between { and cursor position --- .../client/gui/util/GuiScriptTextArea.java | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 81effc8b1..81e46ccb4 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -759,16 +759,33 @@ public class GuiScriptTextArea extends GuiNpcTextField { // RETURN/ENTER: special handling when preceding char is an opening brace '{' if (i == Keyboard.KEY_RETURN) { - if (selection.getCursorPosition() > 0 && selection.getCursorPosition() <= text.length() && text.charAt( - selection.getCursorPosition() - 1) == '{') { - // Compute current and child indent strings - String indent = getIndentCurrentLine(); + // If there is an opening brace `{` before the caret (ignoring any + // intervening whitespace), treat Enter as creating an indented child + // block. This allows the user to type `{ ` and still + // receive the scaffold. + int cursorPos = selection.getCursorPosition(); + int prevNonWs = cursorPos - 1; + while (prevNonWs >= 0 && prevNonWs < (text != null ? text.length() : 0) && Character.isWhitespace(text.charAt(prevNonWs))) { + prevNonWs--; + } + + if (prevNonWs >= 0 && cursorPos <= (text != null ? text.length() : 0) && text.charAt(prevNonWs) == '{') { + // Compute indent using the line that contains the brace so trailing + // spaces after the brace do not affect the computed base indent. + String indent = ""; + for (LineData ld : this.container.lines) { + if (prevNonWs >= ld.start && prevNonWs < ld.end) { + indent = ld.text.substring(0, IndentHelper.getLineIndent(ld.text)); + break; + } + } + if (indent == null) indent = ""; String childIndent = indent + " "; String before = getSelectionBeforeText(); String after = getSelectionAfterText(); - // If there's code after the brace on the same line, assume it should be - // moved inside the new inner line: insert child indent only. + // If there's non-empty code after the brace on the same line, + // it should be moved into the newly created inner line. int firstNewline = after.indexOf('\n'); String leadingSegment = firstNewline == -1 ? after : after.substring(0, firstNewline); if (leadingSegment.trim().length() > 0) { @@ -777,11 +794,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { return true; } - // Otherwise, determine whether a matching closing brace already exists at same indent. + // Determine whether this opening brace already has a matching closing + // brace at the same scope (and indent). Use the brace position found + // above (prevNonWs) when computing spans. boolean hasMatchingCloseSameIndent = false; try { int openLineIdx = -1; - int bracePos = selection.getCursorPosition() - 1; + int bracePos = prevNonWs; for (int li = 0; li < this.container.lines.size(); li++) { LineData ld = this.container.lines.get(li); if (bracePos >= ld.start && bracePos < ld.end) { @@ -796,8 +815,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { int spanOpen = span[1]; int spanClose = span[2]; if (spanOpen == openLineIdx) { - // If the matching close has the same indent as current line, - // we only insert the child indent (do not auto-insert a closing brace). int closeIndent = IndentHelper.getLineIndent(this.container.lines.get(spanClose).text); if (closeIndent == indent.length()) { hasMatchingCloseSameIndent = true; @@ -807,7 +824,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } catch (Exception ex) { - // On any error, default to conservative behavior (insert closing brace) hasMatchingCloseSameIndent = false; } @@ -815,7 +831,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { addText("\n" + childIndent); scrollToCursor(); } else { - // Insert child line and a closing brace aligned with current indent String insert = "\n" + childIndent + "\n" + indent + "}"; setText(before + insert + after); int newCursor = before.length() + 1 + childIndent.length(); From 295e7cd9cd381cd83cd93dc92628807cb4d8423e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 01:21:00 +0200 Subject: [PATCH 079/358] Deleted entire whitespace-only lines on Backspace (regardless of indent/scope --- .../client/gui/util/GuiScriptTextArea.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 81e46ccb4..8466b3af2 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -585,6 +585,26 @@ public class GuiScriptTextArea extends GuiNpcTextField { return true; } + // If the current line is whitespace-only, delete the whole line + // (including the trailing newline if present). This makes Backspace + // intuitive on blank/indented lines outside any recognized scope. + LineData currCheck = selection.findCurrentLine(container.lines); + if (currCheck != null && currCheck.text.trim().length() == 0) { + int removeEnd = text.indexOf('\n', currCheck.start); + if (removeEnd == -1) { + removeEnd = text.length(); + } else { + removeEnd = removeEnd + 1; // include the newline + } + String before = text.substring(0, currCheck.start); + String after = removeEnd <= text.length() ? text.substring(removeEnd) : ""; + setText(before + after); + int newCursor = Math.max(0, currCheck.start - 1); + selection.reset(newCursor); + scrollToCursor(); + return true; + } + // 3) indent-aware merge: find current line and compute expected indent LineData curr = selection.findCurrentLine(container.lines); if (curr != null && curr.start > 0) { From 9ef4b6ecdf24e6553122e40df8d6afdadb770b73 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 01:56:03 +0200 Subject: [PATCH 080/358] Fixed phantom empty trailing line being renderer but not removable --- .../npcs/client/gui/util/GuiScriptTextArea.java | 2 ++ .../gui/util/script/JavaTextContainer.java | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 8466b3af2..a25882724 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1263,6 +1263,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { text = text.replace("\r", ""); text = text.replace("\t", " "); + // preserve trailing newlines here — JavaTextContainer.init will + // ignore an extra empty final split when rendering lines. if (this.text == null || !this.text.equals(text)) { if (this.listener != null) { this.listener.textUpdate(text); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index b225766f9..8dae4b782 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -52,7 +52,7 @@ public class JavaTextContainer extends TextContainer { public JavaTextContainer(String text) { super(text); - this.text = text.replaceAll("\\r?\\n|\\r", "\n"); + this.text = text == null ? "" : text.replaceAll("\\r?\\n|\\r", "\n"); } public synchronized List getBraceSpans() { @@ -166,8 +166,15 @@ public class JavaTextContainer extends TextContainer { i = m.start(); } // Only add +1 for the newline if this is NOT the last line - // The last line has no newline after it + // The last line has no newline after it. If the source text + // ends with a trailing '\n' split(...) produces a final empty + // element — skip creating a LineData for that trailing empty + // split so there's no phantom clickable empty line. boolean isLastLine = (lineIndex == split.length - 1); + if (isLastLine && l.isEmpty() && text.endsWith("\n") && split.length > 1) { + // skip trailing empty split caused by final newline + continue; + } int endOffset = isLastLine ? 0 : 1; lines.add(new LineData(line.toString(), totalChars, totalChars + line.length() + endOffset)); @@ -175,7 +182,10 @@ public class JavaTextContainer extends TextContainer { } linesCount = lines.size(); totalHeight = linesCount * lineHeight; - visibleLines = Math.max(height / lineHeight-1, 1); + // Number of fully-visible lines that fit in the given height. + // Use floor division and ensure at least 1 line is visible. + // Don't forget -1, fixes enter auto-scrolling properly + visibleLines = Math.max(height / lineHeight - 1, 1); // Invalidate caches that depend on line layout invalidateCaches(); From e79bf294f1c5845057995f768237139da8686e61 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 02:09:18 +0200 Subject: [PATCH 081/358] Can now click on and highlight fractional lines --- .../npcs/client/gui/util/GuiScriptTextArea.java | 13 ++++++++----- .../npcs/client/gui/util/script/ScrollState.java | 8 ++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index a25882724..b384186e8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -355,17 +355,20 @@ public class GuiScriptTextArea extends GuiNpcTextField { private int getSelectionPos(int xMouse, int yMouse) { xMouse -= (this.x + LINE_NUMBER_GUTTER_WIDTH + 1); yMouse -= this.y + 1; - // Adjust yMouse to account for fractional GL translation (negative offset applied in rendering) + // Adjust yMouse to account for fractional GL translation (negative offset applied in rendering). + // Use a double here (no integer rounding) so clicks that land on partially + // visible lines (fractional positions) correctly hit that line. double fracPixels = scroll.getFractionalOffset() * container.lineHeight; - yMouse = (int) Math.round(yMouse + fracPixels); + double yMouseD = yMouse + fracPixels; ArrayList list = new ArrayList(this.container.lines); for (int i = 0; i < list.size(); ++i) { LineData data = (LineData) list.get(i); - if (i >= scroll.getScrolledLine() && i <= scroll.getScrolledLine() + this.container.visibleLines) { - int yPos = (i - scroll.getScrolledLine()) * this.container.lineHeight; - if (yMouse >= yPos && yMouse < yPos + this.container.lineHeight) { + //+1 to account for the fractional line + if (i >= scroll.getScrolledLine() && i <= scroll.getScrolledLine() + this.container.visibleLines +1) { + double yPos = (i - scroll.getScrolledLine()) * this.container.lineHeight; + if (yMouseD >= yPos && yMouseD < yPos + this.container.lineHeight) { int lineWidth = 0; char[] chars = data.text.toCharArray(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java b/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java index b092cd7fe..2649563ff 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java @@ -15,7 +15,7 @@ public class ScrollState { private double scrollVelocity = 0.0; // Last timestamp for delta time calculation private long lastScrollTime = 0L; - // Integer line index for compatibility with rendering + // First visible line private int scrolledLine = 0; // Scrollbar drag offset when dragging thumb private int scrollbarDragOffset = 0; @@ -136,7 +136,11 @@ public class ScrollState { // Getters public double getScrollPos() { return scrollPos; } public double getTargetScroll() { return targetScroll; } - public int getScrolledLine() { return scrolledLine; } + + /** @return First visible line **/ + public int getScrolledLine() { + return scrolledLine; + } public double getScrollVelocity() { return scrollVelocity; } public boolean isClickScrolling() { return clickScrolling; } public int getScrollbarDragOffset() { return scrollbarDragOffset; } From cd4174713ae390c04f1a943cf99f559f7ad0c7bc Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 02:26:05 +0200 Subject: [PATCH 082/358] Fixed random bug of fractional lines sometimes not drawing during scrolling. --- .../npcs/client/gui/util/GuiScriptTextArea.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index b384186e8..38a83c840 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -194,17 +194,25 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } - // Expand render range by one line above/below so partially-visible lines are drawn - int renderStart = Math.max(0, scroll.getScrolledLine() - 1); - int renderEnd = Math.min(list.size() - 1, scroll.getScrolledLine() + container.visibleLines + 1); // Apply fractional GL translate for sub-pixel smooth scrolling - int stringYOffset = 2; double fracOffset = scroll.getFractionalOffset(); float fracPixels = (float) (fracOffset * container.lineHeight); GL11.glPushMatrix(); GL11.glTranslatef(0.0f, -fracPixels, 0.0f); + + // Expand render range by one line above/below so partially-visible lines are drawn + int renderStart = Math.max(0, scroll.getScrolledLine() - 1); + // Compute the last line index to render, including the last partially-visible line if any. + // Adds the fractional scroll offset (fracOffset * lineHeight) to ensure the bottom line is drawn + // when only part of it is visible in the viewport. + int renderEnd = (int) Math.min(list.size() - 1, + scroll.getScrolledLine() + container.visibleLines + fracPixels + 1); + + // Strings start drawing vertically this much into the line. + int stringYOffset = 2; + // Render LINE GUTTER numbers for (int i = renderStart; i <= renderEnd; i++) { int posY = y + (i - scroll.getScrolledLine()) * container.lineHeight + stringYOffset; From ebd005c2df8a43f00cfc65d68ea1d8e21e13a7e5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 12:10:31 +0200 Subject: [PATCH 083/358] Added needed gui utils --- .../noppes/npcs/client/gui/util/GuiUtil.java | 169 ++++++++++++++++++ .../java/noppes/npcs/client/util/Color.java | 114 ++++++++++++ src/main/java/noppes/npcs/util/ValueUtil.java | 21 +++ 3 files changed, 304 insertions(+) create mode 100644 src/main/java/noppes/npcs/client/gui/util/GuiUtil.java create mode 100644 src/main/java/noppes/npcs/client/util/Color.java diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java b/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java new file mode 100644 index 000000000..10c86a40f --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java @@ -0,0 +1,169 @@ +package noppes.npcs.client.gui.util; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.Tessellator; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; + +public class GuiUtil { + + public static void setScissorClip(int x, int y, int width, int height) { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution res = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int scale = res.getScaleFactor(); + + + // Adjust position to Top-Left origin (OpenGL window/screen space uses bottom-left origin) + y = mc.displayHeight - y * scale; + height *= scale; + + // Set clip + GL11.glScissor(x * scale, y - height, width * scale, height); + } + + public static void drawHorizontalLine(double y, double startX, double endX, int color) { + if (endX < startX) { + double i1 = startX; + startX = endX; + endX = i1; + } + + drawRectD(startX, y, endX + 1, y + 1, color); + } + + public static void drawVerticalLine(double x, double startY, double endY, int color) { + if (endY < startY) { + double i1 = startY; + startY = endY; + endY = i1; + } + + drawRectD(x, startY + 1, x + 1, endY, color); + } + + public static void drawRectD(double left, double top, double right, double bottom) { + double j1; + + if (left < right) { + j1 = left; + left = right; + right = j1; + } + + if (top < bottom) { + j1 = top; + top = bottom; + bottom = j1; + } + + Tessellator tessellator = Tessellator.instance; + GL11.glEnable(GL11.GL_BLEND); + GL11.glDisable(GL11.GL_TEXTURE_2D); + OpenGlHelper.glBlendFunc(770, 771, 1, 0); + tessellator.startDrawingQuads(); + tessellator.addVertex(left, bottom, 0.0D); + tessellator.addVertex(right, bottom, 0.0D); + tessellator.addVertex(right, top, 0.0D); + tessellator.addVertex(left, top, 0.0D); + tessellator.draw(); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_BLEND); + } + + + public static void drawRectD(double left, double top, double right, double bottom, int color) { + float f3 = (float) (color >> 24 & 255) / 255.0F; + float f = (float) (color >> 16 & 255) / 255.0F; + float f1 = (float) (color >> 8 & 255) / 255.0F; + float f2 = (float) (color & 255) / 255.0F; + GL11.glColor4f(f, f1, f2, f3); + drawRectD(left, top, right, bottom); + } + + public static void drawGradientRect(int left, int top, int right, int bottom, int startColor, int endColor) { + double zLevel = 0; + + float f = (float) (startColor >> 24 & 255) / 255.0F; + float f1 = (float) (startColor >> 16 & 255) / 255.0F; + float f2 = (float) (startColor >> 8 & 255) / 255.0F; + float f3 = (float) (startColor & 255) / 255.0F; + float f4 = (float) (endColor >> 24 & 255) / 255.0F; + float f5 = (float) (endColor >> 16 & 255) / 255.0F; + float f6 = (float) (endColor >> 8 & 255) / 255.0F; + float f7 = (float) (endColor & 255) / 255.0F; + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glDisable(GL11.GL_ALPHA_TEST); + OpenGlHelper.glBlendFunc(770, 771, 1, 0); + GL11.glShadeModel(GL11.GL_SMOOTH); + Tessellator tessellator = Tessellator.instance; + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_F(f1, f2, f3, f); + tessellator.addVertex(right, top, zLevel); + tessellator.addVertex(left, top, zLevel); + tessellator.setColorRGBA_F(f5, f6, f7, f4); + tessellator.addVertex(left, bottom, zLevel); + tessellator.addVertex(right, bottom, zLevel); + tessellator.draw(); + GL11.glShadeModel(GL11.GL_FLAT); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glEnable(GL11.GL_TEXTURE_2D); + } + + public static void drawTexturedModalRect(double x, double y, double width, double height, int textureX, int textureY) { + double zLevel = 0; + float f = 1 / 256f; + float f1 = 1 / 256f; + Tessellator tessellator = Tessellator.instance; + tessellator.startDrawingQuads(); + tessellator.addVertexWithUV(x + 0, y + height, zLevel, (float) (textureX) * f, (float) (textureY + height) * f1); + tessellator.addVertexWithUV(x + width, y + height, zLevel, (float) (textureX + width) * f, (float) (textureY + height) * f1); + tessellator.addVertexWithUV(x + width, y + 0, zLevel, (float) (textureX + width) * f, (float) (textureY) * f1); + tessellator.addVertexWithUV(x + 0, y + 0, zLevel, (float) (textureX) * f, (float) (textureY) * f1); + tessellator.draw(); + } + + public static void setMouse(int guiX, int guiY) { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution scaledResolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + + int factor = scaledResolution.getScaleFactor(); + + Mouse.setCursorPosition(guiX * factor, mc.displayHeight - (guiY * factor)); + } + + public static void setMouseX(int guiX) { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution scaledResolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + + int factor = scaledResolution.getScaleFactor(); + Mouse.setCursorPosition(guiX * factor, Mouse.getY()); + } + + public static void setMouseY(int guiY) { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution scaledResolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + + int factor = scaledResolution.getScaleFactor(); + Mouse.setCursorPosition(Mouse.getX(), mc.displayHeight - (guiY * factor)); + } + + public static double preciseMouseX() { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution scaledResolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + + double width = scaledResolution.getScaledWidth(); + return Mouse.getX() * width / mc.displayWidth; + } + + public static double preciseMouseY() { + Minecraft mc = Minecraft.getMinecraft(); + ScaledResolution scaledResolution = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + + double height = scaledResolution.getScaledHeight(); + return height - Mouse.getY() * height / mc.displayHeight - 1; + } +} diff --git a/src/main/java/noppes/npcs/client/util/Color.java b/src/main/java/noppes/npcs/client/util/Color.java new file mode 100644 index 000000000..75450aeee --- /dev/null +++ b/src/main/java/noppes/npcs/client/util/Color.java @@ -0,0 +1,114 @@ +package noppes.npcs.client.util; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.nbt.NBTTagCompound; +import org.lwjgl.opengl.GL11; + +public class Color { + public int color; + public float alpha; + + public Color(int color) { + setColor(color, 1); + } + + public Color(int color, float alpha) { + setColor(color, alpha); + } + + public Color(int red, int green, int blue, float alpha) { + setColor(red, green, blue, alpha); + } + + public void setColor(int color, float alpha) { + this.color = color; + this.alpha = alpha; + } + + public void setColor(int red, int green, int blue, float alpha) { + this.color = (red << 16) + (green << 8) + blue; + this.alpha = alpha; + } + + public static Color lerpRGBA(Color color1, Color color2, float fraction) { + fraction = Math.min(fraction, 1f); + int red = (int) (color1.getRed() + ((color2.getRed() - color1.getRed()) * fraction)); + int green = (int) (color1.getGreen() + ((color2.getGreen() - color1.getGreen()) * fraction)); + int blue = (int) (color1.getBlue() + ((color2.getBlue() - color1.getBlue()) * fraction)); + float newAlpha = color1.alpha + ((color2.alpha - color1.alpha) * fraction); + int newColor = (red << 16) + (green << 8) + blue; + return new Color(newColor, newAlpha); + } + + public Color lerpRGBA(Color color2, float fraction) { + return Color.lerpRGBA(this, color2, fraction); + } + + public Color multiply(float multi) { + if (multi == 1) + return this.clone(); + + int r = (int) ((getRed() * multi)); + int g = (int) ((getGreen() * multi)); + int b = (int) ((getBlue() * multi)); + float a = alpha * multi; + return new Color((r << 16) + (g << 8) + b, a); + } + + @SideOnly(Side.CLIENT) + public void glColor() { + GL11.glColor4f(getRedF(), getGreenF(), getBlueF(), alpha); + } + + public NBTTagCompound writeToNBT(NBTTagCompound compound, String name) { + compound.setInteger(name + "Color", color); + compound.setFloat(name + "Alpha", alpha); + + return compound; + } + + public void readFromNBT(NBTTagCompound compound, String name) { + setColor(compound.getInteger(name + "Color"), compound.getFloat(name + "Alpha")); + } + + public int getRed() { + return (color >> 16 & 0xFF); + } + + public float getRedF() { + return (float) (color >> 16 & 0xFF) / 255f; + } + + public int getGreen() { + return (color >> 8 & 0xFF); + } + + public float getGreenF() { + return (float) (color >> 8 & 0xFF) / 255f; + } + + public int getBlue() { + return (color & 0xFF); + } + + public float getBlueF() { + return (float) (color & 0xFF) / 255f; + } + + public static String getColor(int color) { + String str; + for (str = Integer.toHexString(color); str.length() < 6; str = "0" + str) { + } + return str; + } + + public static String getColor(int color, float alpha) { + String str = getColor(color); + return str + " | " + (int) (alpha * 255); + } + + public Color clone() { + return new Color(color, alpha); + } +} diff --git a/src/main/java/noppes/npcs/util/ValueUtil.java b/src/main/java/noppes/npcs/util/ValueUtil.java index 48cd0b1a9..a81fb9fef 100644 --- a/src/main/java/noppes/npcs/util/ValueUtil.java +++ b/src/main/java/noppes/npcs/util/ValueUtil.java @@ -1,6 +1,15 @@ package noppes.npcs.util; public class ValueUtil { + + public static float lerp(float start, float end, float delta) { + return start + delta * (end - start); + } + + public static double lerp(double start, double end, double delta) { + return start + delta * (end - start); + } + public static float clamp(float given, float min, float max) { if (given < min) return min; @@ -48,4 +57,16 @@ public class ValueUtil { return max; return given; } + + public static String format(double value) { + return (value % 1 == 0) ? String.format("%.0f", value) : String.format("%.2f", value); + } + + public static String format(String precision, float value) { + return (value % 1 == 0) ? String.format("%.0f", value) : String.format(precision, value); + } + + public static String format(String precision, double value) { + return (value % 1 == 0) ? String.format("%.0f", value) : String.format(precision, value); + } } From cd00fcf013f20009579c782a6b9ccc84b2603ea5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 12:14:36 +0200 Subject: [PATCH 084/358] IMPORTED KeyPreset FROM EASINGS BRANCH --- .../gui/util/key/OverlayKeyPresetViewer.java | 419 ++++++++++++++++++ .../noppes/npcs/client/key/KeyPreset.java | 250 +++++++++++ .../npcs/client/key/KeyPresetManager.java | 77 ++++ 3 files changed, 746 insertions(+) create mode 100644 src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java create mode 100644 src/main/java/noppes/npcs/client/key/KeyPreset.java create mode 100644 src/main/java/noppes/npcs/client/key/KeyPresetManager.java diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java new file mode 100644 index 000000000..785f2425f --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -0,0 +1,419 @@ +package noppes.npcs.client.gui.util.key; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.gui.util.GuiUtil; +import noppes.npcs.client.key.KeyPreset; +import noppes.npcs.client.key.KeyPresetManager; +import noppes.npcs.client.util.Color; +import noppes.npcs.util.ValueUtil; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; + +import java.util.LinkedList; +import java.util.List; + +public class OverlayKeyPresetViewer { + public static final ResourceLocation TEXTURE = new ResourceLocation("customnpcs:textures/gui/keypreset_highres.png"); + + public int startX, startY, endX, endY, width, height; + public int mouseX, mouseY; + public boolean showOverlay; + + public int elementSpacing = 5, yStartSpacing = 5; + public float scale = 0.75f; + public Scrollable scroll = new Scrollable(); + + public KeyPresetManager manager; + public OverlayButton viewButton = new OverlayButton(); + public LinkedList list = new LinkedList<>(); + + private final FontRenderer font = Minecraft.getMinecraft().fontRenderer; + + public OverlayKeyPresetViewer(KeyPresetManager manager) { + this.manager = manager; + + manager.keys.forEach((key) -> { + PresetElement element = new PresetElement(key); + list.add(element); + }); + } + + public void initGui(int startX, int startY, int endX, int endY) { + this.startX = startX; + this.startY = startY; + this.endX = endX; + this.endY = endY; + this.width = endX - startX; + this.height = endY - startY; + scroll.init(); + } + + public void draw(int mouseX, int mouseY, int wheel) { + this.mouseX = mouseX; + this.mouseY = mouseY; + boolean aboveButton = viewButton.isMouseAbove(mouseX, mouseY); + boolean aboveOverlay = isMouseAbove(mouseX, mouseY); + + if (aboveButton) + showOverlay = true; + + if (showOverlay) { + drawOverlay(wheel); + + if (!aboveOverlay && !aboveButton) + showOverlay = false; + } + + viewButton.drawButton(); + } + + public void drawOverlay(int wheel) { + scroll.update(wheel); + + GL11.glEnable(GL11.GL_SCISSOR_TEST); + GuiUtil.drawGradientRect(startX, startY, endX, endY, 0x88000000, 0x88303030); + if (scroll.maxScroll > 0) + scroll.drawBar(); + + GuiUtil.setScissorClip(startX, startY, scroll.maxScroll > 0 ? width - scroll.barWidth : width, height); + GL11.glPushMatrix(); + GL11.glTranslatef((startX + 2), startY + yStartSpacing - scroll.scrollY, 0); + + for (int i = 0, height = 0; i < list.size(); i++) { + PresetElement element = list.get(i); + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, 1); + GL11.glTranslatef(0, Math.round(height / scale), 0); + element.drawDescription(); + GL11.glPopMatrix(); + + element.drawBox(height); + height += element.getHeight(); + } + + GL11.glPopMatrix(); + GL11.glDisable(GL11.GL_SCISSOR_TEST); + } + + public void keyTyped(char c, int i) { + if (!showOverlay) + return; + + list.forEach((element) -> { + if (element.isEditing) + element.keyTyped(i); + }); + } + + public void mouseClicked(int mouseX, int mouseY, int button) { + if (!showOverlay) + return; + + list.forEach((element) -> { + if (!element.isMouseAboveBox(mouseX, mouseY)) { + if (element.isEditing) + element.cancelEdit(); + } else + element.boxClicked(button); + + if (element.isMouseAboveReset(mouseX, mouseY) && !element.key.isDefault()) { + element.key.defaultState.writeTo(element.key.currentState); + manager.save(); + } + + }); + } + + public boolean isMouseAbove(int mouseX, int mouseY) { + if (scroll.isMouseDragging) + return true; + + return mouseX >= startX && mouseX < endX && mouseY >= startY && mouseY < endY; + } + + public int getWidth() { + return (int) (width / scale); + } + + public class Scrollable { + private float scrollY, targetScrollY, maxScroll; + private int totalHeight, scrollbarHeight, barWidth = 2; + private float heightFactor, scrollFactor; + + private boolean isMouseDragging; + private int startDragY; + + public Scrollable() { + } + + public void init() { + totalHeight = 0; + for (PresetElement element : list) + totalHeight += element.getHeight(); + + totalHeight -= yStartSpacing; + scroll.maxScroll = Math.max(0, totalHeight - height); + + heightFactor = height / totalHeight; + scrollbarHeight = Math.max((int) (height * heightFactor), 20); + scrollFactor = (height - yStartSpacing - scrollbarHeight) / maxScroll; + } + + public void update(int wheel) { + if (wheel != 0 && isMouseAbove(mouseX, mouseY)) + targetScrollY = ValueUtil.clamp(targetScrollY - wheel / 15, 0, maxScroll); + + + if (Mouse.isButtonDown(0)) { + if (!isMouseDragging && isMouseAboveBar(mouseX, mouseY)) { + isMouseDragging = true; + startDragY = (int) (mouseY - scrollY * scrollFactor); + } + } else + isMouseDragging = false; + + if (isMouseDragging) + scrollY = targetScrollY = ValueUtil.clamp((mouseY - startDragY) / scrollFactor, 0, maxScroll); + + if (scrollY != targetScrollY) { + scrollY = ValueUtil.lerp(scrollY, targetScrollY, 0.1f); + + if (Math.abs(scrollY - targetScrollY) < 0.001) // Snap to exact target value + scrollY = targetScrollY; + } + + scrollY = ValueUtil.clamp(scrollY, 0, maxScroll); + } + + public void drawBar() { + GL11.glPushMatrix(); + GL11.glTranslatef(endX - barWidth - 2, startY + yStartSpacing + scrollY * scrollFactor, 0); + GuiUtil.drawRectD(0, 0, barWidth, scrollbarHeight, (isMouseAboveBar(mouseX, mouseY) || isMouseDragging) ? 0xffbababa : 0xff767676); + GL11.glPopMatrix(); + } + + public boolean isMouseAboveBar(int mouseX, int mouseY) { + int barX = endX - barWidth - 2; + int barY = (int) (startY + yStartSpacing + scrollY * scrollFactor); + + return mouseX >= barX && mouseX < barX + barWidth && mouseY >= barY && mouseY < barY + scrollbarHeight; + } + } + + public class PresetElement { + public KeyPreset key; + public boolean isEditing; + public float boxScreenX, boxScreenY; + + public KeyPreset.KeyState newState = new KeyPreset.KeyState(); + + public PresetElement(KeyPreset key) { + this.key = key; + } + + public void drawDescription() { + String name = String.format("> %s", key.name); + font.drawString(name, 0, 0, 0xffffff); + + if (!key.shouldConflict) + font.drawString("(Conflict-free)", font.getStringWidth(name) + 5, 0, 0xface40); + + String description = String.format("- %s", key.description); + font.drawSplitString(description, 9, font.FONT_HEIGHT, getMaxStringWidth() - 9, 0x888888); + } + + public void drawBox(int offsetY) { + //////////////////////////////////////////// + //////////////////////////////////////////// + //Box texture + Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); + GL11.glPushMatrix(); + GL11.glColor4f(1, 1, 1, 1); + float boxScaleX = 1.75f; + float boxWidth = 32 * boxScaleX; + boxScreenX = getMaxStringWidth() * scale + 8; //remove scaling from maxStringWidth + GL11.glScalef(boxScaleX, 1, 0); + + if (isEditing) + new Color(0x4772b3, 1).glColor(); + else if (isMouseAboveBox(mouseX, mouseY)) + new Color(0x656565, 1).glColor(); + else + new Color(0x545454, 1).glColor(); + GuiUtil.drawTexturedModalRect(boxScreenX / boxScaleX, boxScreenY = offsetY - 5, 32, 20, 0, 492); + GL11.glPopMatrix(); + + //Reset box + boolean isDefault = key.isDefault(); + float resetWidth = 10; + float resetScreenX = boxScreenX + boxWidth + 2; + + if (isMouseAboveReset(mouseX, mouseY) && !isDefault) + new Color(0x656565, 1).glColor(); + else + new Color(0x545454, 1).glColor(); + GuiUtil.drawTexturedModalRect(resetScreenX, boxScreenY, 10, 20, 33, 492); + + //////////////////////////////////////////// + //////////////////////////////////////////// + //Key name + String name = getBoxKeyName(); + float nameWidth = font.getStringWidth(name); + float nameScale = scale; + float nameBoxRatio = nameWidth * scale / boxWidth; + + GL11.glPushMatrix(); + if (nameBoxRatio > 0.9) { + nameScale /= nameWidth / boxWidth / 1.2f; + GL11.glTranslatef(0, nameBoxRatio * 0.5f, 0); + } + + float nameX = boxScreenX + boxWidth / 2 - (nameWidth / 2 * nameScale); + float nameY = offsetY + 11.5f - (font.FONT_HEIGHT / 2); + GL11.glScalef(nameScale, nameScale, 1); + GL11.glTranslatef(nameX / nameScale, nameY / nameScale, 1); + font.drawStringWithShadow(name, 0, 0, conflicts() ? 0xff5555 : 0xffffffff); + GL11.glPopMatrix(); + + //Reset letter + char letter = 'X'; + float letterX = resetScreenX + (resetWidth / 2) - (font.getCharWidth(letter) / 2 * scale) + 0.25f; + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, 1); + GL11.glTranslatef(letterX / scale, nameY / scale, 1); + font.drawStringWithShadow(String.valueOf(letter), 0, 0, isDefault ? 0xff8a8a8a : 0xffffffff); + GL11.glPopMatrix(); + + //////////////////////////////////////////// + //////////////////////////////////////////// + } + + public void boxClicked(int button) { + if (isEditing) + setKey(-100 + button); + else if (button == 0) + isEditing = true; + } + + public void keyTyped(int typedKey) { + if (typedKey == 1) { + setKey(0); + return; + } + + newState.setState(newState.keyCode, KeyPreset.isCtrlKeyDown(), KeyPreset.isAltKeyDown(), KeyPreset.isShiftKeyDown()); + if (KeyPreset.isNotCtrlAltShift(typedKey)) + setKey(typedKey); + } + + public void setKey(int keyCode) { + if (keyCode == 0) + newState.clear(); + + newState.keyCode = keyCode; + newState.writeTo(key.currentState); + cancelEdit(); + manager.save(); + } + + public void cancelEdit() { + newState.clear(); + isEditing = false; + } + + public boolean conflicts() { + return list.stream().anyMatch(element -> key.shouldConflict && element.key.shouldConflict && element.key != this.key && element.key.equals(this.key)); + } + + public String getBoxKeyName() { + if (isEditing) { + String modifiers = ""; + if (KeyPreset.isCtrlKeyDown()) + modifiers += "CTRL "; + if (KeyPreset.isAltKeyDown()) + modifiers += "ALT "; + if (KeyPreset.isShiftKeyDown()) + modifiers += "SHIFT "; + + return !modifiers.isEmpty() ? modifiers : "Press a key"; + } + + return key.currentState.getName(); + } + + public boolean isMouseAboveBox(int mouseX, int mouseY) { + mouseX -= startX + 2; + mouseY -= startY + 5 - scroll.scrollY; + + float boxScaleX = 1.75f; + float screenY = boxScreenY + 10; + + return mouseX >= boxScreenX && mouseX < boxScreenX + 32 * boxScaleX && mouseY >= screenY && mouseY < screenY + 10; + } + + public boolean isMouseAboveReset(int mouseX, int mouseY) { + mouseX -= startX + 2; + mouseY -= startY + 5 - scroll.scrollY; + + float boxScaleX = 1.75f; + float resetWidth = 10, boxWidth = 32 * boxScaleX; + float screenX = boxScreenX + boxWidth + 2; + float screenY = boxScreenY + 10; + + return mouseX >= screenX && mouseX < screenX + resetWidth && mouseY >= screenY && mouseY < screenY + 10; + } + + public int getHeight() { + List wrappedDescription = font.listFormattedStringToWidth(String.format("- %s", key.description), getMaxStringWidth()); + + int nameHeight = font.FONT_HEIGHT; + int translations = font.FONT_HEIGHT; + int descriptionHeight = wrappedDescription.size() * (font.FONT_HEIGHT); + + return Math.round((nameHeight + descriptionHeight + translations) * scale) + elementSpacing; + } + + public int getMaxStringWidth() { + return (int) (getWidth() * 2.5f / 4); + } + } + + public class OverlayButton { + public int startX, startY, endX, width, height; + public int textureWidth = 252, textureHeight = 109; + public float scale = 0.25f; + + public OverlayButton() { + } + + public void initGui(int endX, int startY) { + this.endX = endX; + this.startY = startY; + + startX = (int) (endX - textureWidth * scale / 2); + } + + public void drawButton() { + Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); + + double screenX = startX / scale, screenY = startY / scale; + int oldMinFilter = GL11.glGetTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER); + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); + + GL11.glPushMatrix(); + float color = 0.55f; + GL11.glColor4f(color, color, color, 1); + GL11.glScalef(scale, scale, 1); + GuiUtil.drawTexturedModalRect(screenX, screenY, textureWidth, textureHeight, 0, 0); + GL11.glPopMatrix(); + + GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, oldMinFilter); + } + + public boolean isMouseAbove(int mouseX, int mouseY) { + return mouseX >= startX && mouseX < startX + textureWidth * scale / 2 && mouseY >= startY - 4 && mouseY < startY + textureHeight * scale / 2; + } + } +} diff --git a/src/main/java/noppes/npcs/client/key/KeyPreset.java b/src/main/java/noppes/npcs/client/key/KeyPreset.java new file mode 100644 index 000000000..62ec13690 --- /dev/null +++ b/src/main/java/noppes/npcs/client/key/KeyPreset.java @@ -0,0 +1,250 @@ +package noppes.npcs.client.key; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.settings.GameSettings; +import net.minecraft.nbt.NBTTagCompound; +import org.lwjgl.input.Keyboard; +import org.lwjgl.input.Mouse; + +import java.util.function.Consumer; + +public class KeyPreset { + /** + * @field SINGLE_PRESS => on releasing before 5 ticks press time + * @field PRESS_RELEASE => on pressing and releasing + */ + /** Press types for key events. */ + public enum PressType { PRESS, HOLD, RELEASE, SINGLE_PRESS, PRESS_RELEASE } + + public KeyState defaultState = new KeyState(); + public KeyState currentState = new KeyState(); + + public String name, description; + public boolean shouldConflict = true; + public Consumer task; + + public int pressTime; + public boolean isDown; + + + public KeyPreset(String name) { + this.name = name; + } + + public KeyPreset setDefaultState(int keyCode, boolean hasCtrl, boolean hasAlt, boolean hasShift) { + defaultState.setState(keyCode, hasCtrl, hasAlt, hasShift); + currentState.setState(keyCode, hasCtrl, hasAlt, hasShift); + return this; + } + + public KeyPreset setDescription(String description) { + this.description = description; + return this; + } + + public KeyPreset setTask(Consumer task) { + this.task = task; + return this; + } + + public KeyPreset shouldConflict(boolean shouldConflict) { + this.shouldConflict = shouldConflict; + return this; + } + + public void tick() { + int keyCode = keyCode(); + if (keyCode == -1 || keyCode == 0) + return; + + boolean isDown = isMouseKey() ? Mouse.isButtonDown(keyCode + 100) : Keyboard.isKeyDown(keyCode); + isDown = isDown && (hasCtrl() ? isCtrlKeyDown() : true) && (hasAlt() ? isAltKeyDown() : true) && (hasShift() ? isShiftKeyDown() : true); + + setDown(isDown); + } + + public void setDown(boolean down) { + if (down && !isDown) + onAction(PressType.PRESS, PressType.PRESS_RELEASE); + + if (!down && isDown) { + if (pressTime <= 5) + onAction(PressType.SINGLE_PRESS); + + onAction(PressType.RELEASE, PressType.PRESS_RELEASE); + pressTime = 0; + } + + if (isDown || down) //to fire in both press & release + onAction(PressType.HOLD); + + if (isDown) + pressTime++; + + this.isDown = down; + } + + public KeyPreset onAction(PressType... pressTypes) { + if (task != null) { + for (PressType pressType : pressTypes) + this.task.accept(pressType); + } + return this; + } + + public boolean isDefault() { + return currentState.equals(defaultState); + } + + public boolean isMouseKey() { + return keyCode() < -1; + } + + public int keyCode() { + return currentState.keyCode; + } + + public boolean hasCtrl() { + return currentState.hasCtrl; + } + + public boolean hasAlt() { + return currentState.hasAlt; + } + + public boolean hasShift() { + return currentState.hasShift; + } + + public String getKeyName() { + return currentState.getName(); + } + + public void clear() { + currentState.clear(); + } + + //call on the last key added, to load saved presets + public KeyPreset markDone(KeyPresetManager manager) { + manager.load(); + return this; + } + + public void writeToNbt(NBTTagCompound c) { + c.setTag(name, currentState.writeToNbt()); + } + + public void readFromNbt(NBTTagCompound compound) { + currentState.readFromNbt(compound.getCompoundTag(name)); + } + + public boolean equals(Object preset) { + if (preset == this) + return true; + + if (preset instanceof KeyPreset) { + KeyPreset key = (KeyPreset) preset; + return key.currentState.equals(currentState); + } + + return false; + } + + + public static class KeyState { + public int keyCode = -1; + public boolean hasCtrl, hasAlt, hasShift; + + public void setState(int keyCode, boolean hasCtrl, boolean hasAlt, boolean hasShift) { + this.keyCode = keyCode; + this.hasCtrl = hasCtrl; + this.hasAlt = hasAlt; + this.hasShift = hasShift; + } + + public void readFrom(KeyState state) { + keyCode = state.keyCode; + hasCtrl = state.hasCtrl; + hasShift = state.hasShift; + hasAlt = state.hasAlt; + } + + public void writeTo(KeyState state) { + state.setState(keyCode, hasCtrl, hasAlt, hasShift); + } + + public boolean hasState() { + return keyCode != -1; + } + + public void clear() { + this.keyCode = -1; + hasCtrl = hasAlt = hasShift = false; + } + + public boolean equals(Object preset) { + if (preset == this) + return true; + + if (preset instanceof KeyState) { + KeyState state = (KeyState) preset; + return state.keyCode == keyCode && state.hasCtrl == hasCtrl && state.hasAlt == hasAlt && state.hasShift == hasShift; + } + + return false; + } + + public NBTTagCompound writeToNbt() { + NBTTagCompound compound = new NBTTagCompound(); + compound.setInteger("keyCode", keyCode); + compound.setBoolean("hasCtrl", hasCtrl); + compound.setBoolean("hasShift", hasShift); + compound.setBoolean("hasAlt", hasAlt); + return compound; + } + + public void readFromNbt(NBTTagCompound compound) { + this.keyCode = compound.getInteger("keyCode"); + this.hasCtrl = compound.getBoolean("hasCtrl"); + this.hasShift = compound.getBoolean("hasShift"); + this.hasAlt = compound.getBoolean("hasAlt"); + } + + public String getName() { + int code = keyCode; + String name = ""; + + if (code == LEFT_MOUSE) + name = "Left Mouse"; + else if (code == RIGHT_MOUSE) + name = "Right Mouse"; + else if (code == MIDDLE_MOUSE) + name = "Middle Mouse"; + else { + name = code == -1 ? "" : GameSettings.getKeyDisplayString(code); + if (name.contains("Button")) + name = name.replace("Button", "Mouse"); + } + + return (hasCtrl ? "CTRL " : "") + (hasAlt ? "ALT " : "") + (hasShift ? "SHIFT " : "") + name; + } + } + + public static final int LEFT_MOUSE = -100, RIGHT_MOUSE = -99, MIDDLE_MOUSE = -98, MOUSE_4 = -97, MOUSE_5 = -96; + + public static boolean isCtrlKeyDown() { + return Minecraft.isRunningOnMac ? Keyboard.isKeyDown(219) || Keyboard.isKeyDown(220) : Keyboard.isKeyDown(29) || Keyboard.isKeyDown(157); + } + + public static boolean isAltKeyDown() { + return Keyboard.isKeyDown(56) || Keyboard.isKeyDown(184); + } + + public static boolean isShiftKeyDown() { + return Keyboard.isKeyDown(42) || Keyboard.isKeyDown(54); + } + + public static boolean isNotCtrlAltShift(int key) { + return key != 219 && key != 220 && key != 29 && key != 157 && key != 42 && key != 54 && key != 56 && key != 184; + } +} diff --git a/src/main/java/noppes/npcs/client/key/KeyPresetManager.java b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java new file mode 100644 index 000000000..61b74a610 --- /dev/null +++ b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java @@ -0,0 +1,77 @@ +package noppes.npcs.client.key; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.util.NBTJsonUtil; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class KeyPresetManager { + public List keys = new ArrayList<>(); + public String fileName; + + public KeyPresetManager(String fileName) { + this.fileName = fileName; + } + + public KeyPreset add(String name) { + KeyPreset preset = new KeyPreset(name); + keys.add(preset); + return preset; + } + + public void tick() { + for (KeyPreset key : keys) + key.tick(); + } + + + public void load() { + try { + File dir = getDir(); + File file = new File(dir, fileName + ".json"); + if (!file.exists()) + return; + + NBTTagCompound compound = NBTJsonUtil.LoadFile(file); + for (KeyPreset key : keys) + key.readFromNbt(compound); + + } catch (Exception e) { + LogWriter.except(e); + } + } + + public void save() { + try { + File dir = getDir(); + String filename = fileName + ".json"; + + File newFile = new File(dir, filename + "_new"); + File originalFile = new File(dir, filename); + + NBTTagCompound compound = new NBTTagCompound(); + for (KeyPreset key : keys) + key.writeToNbt(compound); + + NBTJsonUtil.SaveFile(newFile, compound); + + if (originalFile.exists()) + originalFile.delete(); + + newFile.renameTo(originalFile); + } catch (Exception e) { + LogWriter.except(e); + } + } + + public File getDir() { + File dir = new File(CustomNpcs.Dir, "keypresets"); + if (!dir.exists()) + dir.mkdir(); + return dir; + } +} From cb09deb495cb9ca05c48bb5d08b15b8b1baed8b1 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 12:42:11 +0200 Subject: [PATCH 085/358] Implemented KeyEvent for KeyPreset --- .../java/noppes/npcs/client/key/KeyEvent.java | 23 +++++++++++++++++++ .../noppes/npcs/client/key/KeyPreset.java | 12 +++------- 2 files changed, 26 insertions(+), 9 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/key/KeyEvent.java diff --git a/src/main/java/noppes/npcs/client/key/KeyEvent.java b/src/main/java/noppes/npcs/client/key/KeyEvent.java new file mode 100644 index 000000000..7dc7cac33 --- /dev/null +++ b/src/main/java/noppes/npcs/client/key/KeyEvent.java @@ -0,0 +1,23 @@ +package noppes.npcs.client.key; + +public class KeyEvent { + public final KeyPreset.PressType type; + public final KeyPreset preset; + public final int keyCode; + public final boolean isMouse; + public final long timestamp; + + public KeyEvent(KeyPreset.PressType type, KeyPreset preset) { + this.type = type; + this.preset = preset; + this.keyCode = preset.keyCode(); + this.isMouse = preset.isMouseKey(); + this.timestamp = System.currentTimeMillis(); + } + + public boolean is(KeyPreset.PressType t) { return this.type == t; } + public boolean isPress() { return is(KeyPreset.PressType.PRESS); } + public boolean isHold() { return is(KeyPreset.PressType.HOLD); } + public boolean isRelease() { return is(KeyPreset.PressType.RELEASE); } + public boolean isSinglePress() { return is(KeyPreset.PressType.SINGLE_PRESS); } +} diff --git a/src/main/java/noppes/npcs/client/key/KeyPreset.java b/src/main/java/noppes/npcs/client/key/KeyPreset.java index 62ec13690..12b135107 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPreset.java +++ b/src/main/java/noppes/npcs/client/key/KeyPreset.java @@ -21,7 +21,7 @@ public class KeyPreset { public String name, description; public boolean shouldConflict = true; - public Consumer task; + public Consumer task; public int pressTime; public boolean isDown; @@ -42,7 +42,7 @@ public class KeyPreset { return this; } - public KeyPreset setTask(Consumer task) { + public KeyPreset setTask(Consumer task) { this.task = task; return this; } @@ -87,7 +87,7 @@ public class KeyPreset { public KeyPreset onAction(PressType... pressTypes) { if (task != null) { for (PressType pressType : pressTypes) - this.task.accept(pressType); + this.task.accept(new KeyEvent(pressType, this)); } return this; } @@ -124,12 +124,6 @@ public class KeyPreset { currentState.clear(); } - //call on the last key added, to load saved presets - public KeyPreset markDone(KeyPresetManager manager) { - manager.load(); - return this; - } - public void writeToNbt(NBTTagCompound c) { c.setTag(name, currentState.writeToNbt()); } From d52e82bb5be17141fccd5cdf86c92d66f7482023 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 12:49:05 +0200 Subject: [PATCH 086/358] Improved KeyPreset.setDown logic --- .../java/noppes/npcs/client/key/KeyEvent.java | 1 + .../noppes/npcs/client/key/KeyPreset.java | 25 +++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/main/java/noppes/npcs/client/key/KeyEvent.java b/src/main/java/noppes/npcs/client/key/KeyEvent.java index 7dc7cac33..53011f2e8 100644 --- a/src/main/java/noppes/npcs/client/key/KeyEvent.java +++ b/src/main/java/noppes/npcs/client/key/KeyEvent.java @@ -20,4 +20,5 @@ public class KeyEvent { public boolean isHold() { return is(KeyPreset.PressType.HOLD); } public boolean isRelease() { return is(KeyPreset.PressType.RELEASE); } public boolean isSinglePress() { return is(KeyPreset.PressType.SINGLE_PRESS); } + public boolean isPressRelease() { return is(KeyPreset.PressType.PRESS_RELEASE); } } diff --git a/src/main/java/noppes/npcs/client/key/KeyPreset.java b/src/main/java/noppes/npcs/client/key/KeyPreset.java index 12b135107..bc9dc720c 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPreset.java +++ b/src/main/java/noppes/npcs/client/key/KeyPreset.java @@ -63,24 +63,27 @@ public class KeyPreset { setDown(isDown); } + private static final int SHORT_PRESS_TICKS = 5; + public void setDown(boolean down) { - if (down && !isDown) + if (down && !isDown) { // just pressed + pressTime = 0; onAction(PressType.PRESS, PressType.PRESS_RELEASE); - - if (!down && isDown) { - if (pressTime <= 5) + } + + if (!down && isDown) { // just released + if (pressTime <= SHORT_PRESS_TICKS) { onAction(PressType.SINGLE_PRESS); - - onAction(PressType.RELEASE, PressType.PRESS_RELEASE); + } + onAction(PressType.RELEASE); pressTime = 0; } - if (isDown || down) //to fire in both press & release - onAction(PressType.HOLD); - - if (isDown) + if (down) { // while held pressTime++; - + onAction(PressType.HOLD); + } + this.isDown = down; } From e5e8e8948a11fdfe30e72c06aa92bea0e7dd55c3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 12:57:15 +0200 Subject: [PATCH 087/358] Improved pressTime calculation logic --- .../noppes/npcs/client/key/KeyPreset.java | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/main/java/noppes/npcs/client/key/KeyPreset.java b/src/main/java/noppes/npcs/client/key/KeyPreset.java index bc9dc720c..cbc7cfb86 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPreset.java +++ b/src/main/java/noppes/npcs/client/key/KeyPreset.java @@ -23,7 +23,7 @@ public class KeyPreset { public boolean shouldConflict = true; public Consumer task; - public int pressTime; + private long pressStartMs = 0; public boolean isDown; @@ -63,29 +63,30 @@ public class KeyPreset { setDown(isDown); } - private static final int SHORT_PRESS_TICKS = 5; + private static final long SHORT_PRESS_MS = 250L; public void setDown(boolean down) { - if (down && !isDown) { // just pressed - pressTime = 0; + if (down && !isDown) { // just pressed + pressStartMs = System.currentTimeMillis(); onAction(PressType.PRESS, PressType.PRESS_RELEASE); } - - if (!down && isDown) { // just released - if (pressTime <= SHORT_PRESS_TICKS) { + + if (!down && isDown) { // just released + if (getPressTime() <= SHORT_PRESS_MS) onAction(PressType.SINGLE_PRESS); - } - onAction(PressType.RELEASE); - pressTime = 0; + onAction(PressType.RELEASE, PressType.PRESS_RELEASE); + pressStartMs = 0; } - if (down) { // while held - pressTime++; + if (down) // while held onAction(PressType.HOLD); - } this.isDown = down; } + + public long getPressTime() { + return pressStartMs > 0 ? (System.currentTimeMillis() - pressStartMs) : 0L; + } public KeyPreset onAction(PressType... pressTypes) { if (task != null) { From 255666607231f3ff458505c802639f44448d4921 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 14:09:55 +0200 Subject: [PATCH 088/358] Migrated Script Editor shortcut keys to proper ScriptEditorKeys handler with new KeyPreset API --- .../client/gui/util/GuiScriptTextArea.java | 364 +++++++++++++----- .../client/key/impl/ScriptEditorKeys.java | 26 ++ 2 files changed, 283 insertions(+), 107 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 38a83c840..d15c2d9a0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -8,12 +8,14 @@ import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.util.script.*; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import noppes.npcs.client.key.impl.ScriptEditorKeys; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import java.util.regex.Matcher; import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; @@ -67,7 +69,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { public List undoList = new ArrayList<>(); public List redoList = new ArrayList<>(); public boolean undoing = false; - + + // ==================== KEYS ==================== + public static final ScriptEditorKeys KEYS = new ScriptEditorKeys(); + // ==================== CONSTRUCTOR ==================== public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) { @@ -80,6 +85,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.undoing = true; this.setText(text); this.undoing = false; + initializeKeyBindings(); } // ==================== RENDERING ==================== @@ -420,7 +426,154 @@ public class GuiScriptTextArea extends GuiNpcTextField { scroll.scrollToLine(lineIdx, visible, maxScroll); } - + + // ==================== KEY BINDINGS INITIALIZATION ==================== + + /** + * Initialize key bindings for editor shortcuts using ScriptEditorKeys. + * Centralized active/enabled checking ensures shortcuts only fire when appropriate. + */ + private void initializeKeyBindings() { + // Helper: execute action only if text area is active and enabled + Consumer executeIfActive = (action) -> { + if (active && isEnabled()) { + action.run(); + } + }; + + // CUT: Copy selection to clipboard and delete it + KEYS.CUT.setTask(e -> { + if (!e.isSinglePress()) + return; + executeIfActive.accept(() -> { + if (selection.hasSelection()) { + NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); + String s = getSelectionBeforeText(); + setText(s + getSelectionAfterText()); + selection.reset(s.length()); + scrollToCursor(); + } + }); + }); + + // COPY: Copy selection to clipboard + KEYS.COPY.setTask(e -> { + if (!e.isSinglePress()) + return; + executeIfActive.accept(() -> { + if (selection.hasSelection()) { + NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); + } + }); + }); + + // PASTE: Insert clipboard contents at caret + KEYS.PASTE.setTask(e -> { + if (!e.isSinglePress()) + return; + executeIfActive.accept(() -> { + addText(NoppesStringUtils.getClipboardContents()); + scrollToCursor(); + }); + }); + + // UNDO: Restore last edit from undo list + KEYS.UNDO.setTask(e -> { + if (!e.isSinglePress()) + return; + executeIfActive.accept(() -> { + if (undoList.isEmpty()) + return; + undoing = true; + redoList.add(new UndoData(this.text, selection.getCursorPosition())); + UndoData data = undoList.remove(undoList.size() - 1); + setText(data.text); + selection.reset(data.cursorPosition); + undoing = false; + scrollToCursor(); + }); + }); + + // REDO: Restore last undone edit from redo list + KEYS.REDO.setTask(e -> { + if (!e.isSinglePress()) + return; + executeIfActive.accept(() -> { + if (redoList.isEmpty()) + return; + undoing = true; + undoList.add(new UndoData(this.text, selection.getCursorPosition())); + UndoData data = redoList.remove(redoList.size() - 1); + setText(data.text); + selection.reset(data.cursorPosition); + undoing = false; + scrollToCursor(); + }); + }); + + // FORMAT: Format/indent code + KEYS.FORMAT.setTask(e -> { + if (!e.isSinglePress()) + return; + executeIfActive.accept(this::formatText); + }); + + // TOGGLE_COMMENT: Toggle comment for selection or current line + KEYS.TOGGLE_COMMENT.setTask(e -> { + if (!e.isSinglePress()) + return; + executeIfActive.accept(() -> { + if (selection.hasSelection()) { + toggleCommentSelection(); + } else { + toggleCommentLineAtCursor(); + } + }); + }); + + // DUPLICATE: Duplicate selection or current line + KEYS.DUPLICATE.setTask(e -> { + if (!e.isSinglePress()) + return; + executeIfActive.accept(() -> { + if (selection.hasSelection()) { + // Multi-line selection duplication + LineData firstLine = null, lastLine = null; + for (LineData line : container.lines) { + if (line.end > selection.getStartSelection() && line.start < selection.getEndSelection()) { + if (firstLine == null) + firstLine = line; + lastLine = line; + } + } + if (firstLine != null && lastLine != null) { + String selectedText = text.substring(firstLine.start, lastLine.end); + int savedStart = selection.getStartSelection(); + int savedEnd = selection.getEndSelection(); + int insertAt = lastLine.end; + setText(text.substring(0, insertAt) + selectedText + text.substring(insertAt)); + selection.setStartSelection(savedStart); + selection.setEndSelection(savedEnd); + selection.setCursorPositionDirect(savedEnd); + } + } else { + // Duplicate current line + for (LineData line : container.lines) { + if (selection.getCursorPosition() >= line.start && selection.getCursorPosition() <= line.end) { + String lineText = text.substring(line.start, line.end); + String insertText = lineText.endsWith("\n") ? lineText : "\n" + lineText; + int insertionPoint = line.end; + setText(text.substring(0, insertionPoint) + insertText + text.substring(insertionPoint)); + int newCursor = insertionPoint + insertText.length() - (insertText.endsWith("\n") ? 1 : 0); + selection.reset(Math.max(0, Math.min(newCursor, this.text.length()))); + break; + } + } + } + }); + }); + } + // ==================== KEYBOARD INPUT HANDLING ==================== /** @@ -429,20 +582,18 @@ public class GuiScriptTextArea extends GuiNpcTextField { */ @Override public boolean textboxKeyTyped(char c, int i) { - if (!active) - return false; + if (!active) return false; if (this.isKeyComboCtrlA(i)) { selection.selectAll(text.length()); return true; } - if (!isEnabled()) - return false; + if (!isEnabled()) return false; if (handleNavigationKeys(i)) return true; + if (handleInsertionKeys(i)) return true; if (handleDeletionKeys(i)) return true; - if (handleShortcutKeys(i)) return true; if (handleCharacterInput(c)) return true; return true; @@ -510,6 +661,103 @@ public class GuiScriptTextArea extends GuiNpcTextField { return false; // not a navigation key } + /** + * Handles insertion-related keys such as Tab indentation and Enter behavior. + */ + private boolean handleInsertionKeys(int i) { + // TAB: indent or unindent depending on Shift + if (i == Keyboard.KEY_TAB) { + boolean shift = isShiftKeyDown(); + if (shift) { + handleShiftTab(); + } else { + handleTab(); + } + scrollToCursor(); + return true; + } + + // RETURN/ENTER: special handling when preceding char is an opening brace '{' + if (i == Keyboard.KEY_RETURN) { + int cursorPos = selection.getCursorPosition(); + int prevNonWs = cursorPos - 1; + while (prevNonWs >= 0 && prevNonWs < (text != null ? text.length() : 0) && Character.isWhitespace( + text.charAt(prevNonWs))) { + prevNonWs--; + } + + if (prevNonWs >= 0 && cursorPos <= (text != null ? text.length() : 0) && text.charAt(prevNonWs) == '{') { + String indent = ""; + for (LineData ld : this.container.lines) { + if (prevNonWs >= ld.start && prevNonWs < ld.end) { + indent = ld.text.substring(0, IndentHelper.getLineIndent(ld.text)); + break; + } + } + if (indent == null) + indent = ""; + String childIndent = indent + " "; + String before = getSelectionBeforeText(); + String after = getSelectionAfterText(); + + int firstNewline = after.indexOf('\n'); + String leadingSegment = firstNewline == -1 ? after : after.substring(0, firstNewline); + if (leadingSegment.trim().length() > 0) { + addText("\n" + childIndent); + scrollToCursor(); + return true; + } + + boolean hasMatchingCloseSameIndent = false; + try { + int openLineIdx = -1; + int bracePos = prevNonWs; + for (int li = 0; li < this.container.lines.size(); li++) { + LineData ld = this.container.lines.get(li); + if (bracePos >= ld.start && bracePos < ld.end) { + openLineIdx = li; + break; + } + } + + if (openLineIdx >= 0) { + List spans = BracketMatcher.computeBraceSpans(text, this.container.lines); + for (int[] span : spans) { + int spanOpen = span[1]; + int spanClose = span[2]; + if (spanOpen == openLineIdx) { + int closeIndent = IndentHelper.getLineIndent(this.container.lines.get(spanClose).text); + if (closeIndent == indent.length()) { + hasMatchingCloseSameIndent = true; + } + break; + } + } + } + } catch (Exception ex) { + hasMatchingCloseSameIndent = false; + } + + if (hasMatchingCloseSameIndent) { + addText("\n" + childIndent); + scrollToCursor(); + } else { + String insert = "\n" + childIndent + "\n" + indent + "}"; + setText(before + insert + after); + int newCursor = before.length() + 1 + childIndent.length(); + selection.reset(newCursor); + scrollToCursor(); + } + } else { + addText(Character.toString('\n') + getAutoIndentForEnter()); + scrollToCursor(); + } + return true; + } + + return false; + } + /** * Handles deletion keys: Delete, Backspace, and Ctrl+Backspace. * Includes smart backspace behavior for indentation-aware line merging, @@ -770,112 +1018,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { return true; } - // TAB: indent or unindent depending on Shift - if (i == Keyboard.KEY_TAB) { - boolean shift = isShiftKeyDown(); - if (shift) { - handleShiftTab(); - } else { - handleTab(); - } - scrollToCursor(); - return true; - } - // CTRL+F: format the text according to IndentHelper rules if (i == Keyboard.KEY_F && isCtrlKeyDown()) { formatText(); return true; } - // RETURN/ENTER: special handling when preceding char is an opening brace '{' - if (i == Keyboard.KEY_RETURN) { - // If there is an opening brace `{` before the caret (ignoring any - // intervening whitespace), treat Enter as creating an indented child - // block. This allows the user to type `{ ` and still - // receive the scaffold. - int cursorPos = selection.getCursorPosition(); - int prevNonWs = cursorPos - 1; - while (prevNonWs >= 0 && prevNonWs < (text != null ? text.length() : 0) && Character.isWhitespace(text.charAt(prevNonWs))) { - prevNonWs--; - } - - if (prevNonWs >= 0 && cursorPos <= (text != null ? text.length() : 0) && text.charAt(prevNonWs) == '{') { - // Compute indent using the line that contains the brace so trailing - // spaces after the brace do not affect the computed base indent. - String indent = ""; - for (LineData ld : this.container.lines) { - if (prevNonWs >= ld.start && prevNonWs < ld.end) { - indent = ld.text.substring(0, IndentHelper.getLineIndent(ld.text)); - break; - } - } - if (indent == null) indent = ""; - String childIndent = indent + " "; - String before = getSelectionBeforeText(); - String after = getSelectionAfterText(); - - // If there's non-empty code after the brace on the same line, - // it should be moved into the newly created inner line. - int firstNewline = after.indexOf('\n'); - String leadingSegment = firstNewline == -1 ? after : after.substring(0, firstNewline); - if (leadingSegment.trim().length() > 0) { - addText("\n" + childIndent); - scrollToCursor(); - return true; - } - - // Determine whether this opening brace already has a matching closing - // brace at the same scope (and indent). Use the brace position found - // above (prevNonWs) when computing spans. - boolean hasMatchingCloseSameIndent = false; - try { - int openLineIdx = -1; - int bracePos = prevNonWs; - for (int li = 0; li < this.container.lines.size(); li++) { - LineData ld = this.container.lines.get(li); - if (bracePos >= ld.start && bracePos < ld.end) { - openLineIdx = li; - break; - } - } - - if (openLineIdx >= 0) { - List spans = BracketMatcher.computeBraceSpans(text, this.container.lines); - for (int[] span : spans) { - int spanOpen = span[1]; - int spanClose = span[2]; - if (spanOpen == openLineIdx) { - int closeIndent = IndentHelper.getLineIndent(this.container.lines.get(spanClose).text); - if (closeIndent == indent.length()) { - hasMatchingCloseSameIndent = true; - } - break; - } - } - } - } catch (Exception ex) { - hasMatchingCloseSameIndent = false; - } - - if (hasMatchingCloseSameIndent) { - addText("\n" + childIndent); - scrollToCursor(); - } else { - String insert = "\n" + childIndent + "\n" + indent + "}"; - setText(before + insert + after); - int newCursor = before.length() + 1 + childIndent.length(); - selection.reset(newCursor); - scrollToCursor(); - } - } else { - // Normal enter: insert newline + auto indent for caret position - addText(Character.toString('\n') + getAutoIndentForEnter()); - scrollToCursor(); - } - return true; - } - // CTRL+/ : toggle comment for selection or current line if (i == Keyboard.KEY_SLASH && isCtrlKeyDown()) { if (selection.hasSelection()) { @@ -1261,7 +1409,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } + // Called from GuiScreen.updateScreen() public void updateCursorCounter() { + KEYS.tick(); ++this.cursorCounter; } diff --git a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java new file mode 100644 index 000000000..48e6df323 --- /dev/null +++ b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java @@ -0,0 +1,26 @@ +package noppes.npcs.client.key.impl; + +import noppes.npcs.client.key.KeyPreset; +import noppes.npcs.client.key.KeyPresetManager; +import org.lwjgl.input.Keyboard; + +public class ScriptEditorKeys extends KeyPresetManager { + + //////////////////// + // Editor shortcuts (mapped from GuiScriptTextArea.handleShortcutKeys) + public final KeyPreset CUT = add("Cut").setDefaultState(Keyboard.KEY_X, true, false, false).setDescription("Cut selection or current line"); + public final KeyPreset COPY = add("Copy").setDefaultState(Keyboard.KEY_C, true, false, false).setDescription("Copy selection"); + public final KeyPreset PASTE = add("Paste").setDefaultState(Keyboard.KEY_V, true, false, false).setDescription("Paste clipboard at caret"); + public final KeyPreset DUPLICATE = add("Duplicate").setDefaultState(Keyboard.KEY_D, true, false, false).setDescription("Duplicate selection or current line"); + + + public final KeyPreset UNDO = add("Undo").setDefaultState(Keyboard.KEY_Z, true, false, false).setDescription("Undo last edit"); + public final KeyPreset REDO = add("Redo").setDefaultState(Keyboard.KEY_Y, true, false, false).setDescription("Redo last undone edit"); + public final KeyPreset FORMAT = add("Format").setDefaultState(Keyboard.KEY_F, true, false, false).setDescription("Format code / indent"); + public final KeyPreset TOGGLE_COMMENT = add("Toggle Comment").setDefaultState(Keyboard.KEY_SLASH, true, false, false).setDescription("Toggle comment for selection or line"); + + public ScriptEditorKeys() { + super("script_editor"); + load(); + } +} From 7a74a599f210ff3fa7d099a6e2b543356f28669e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 16:01:19 +0200 Subject: [PATCH 089/358] Init'd GuiNpcTextField --- .../java/noppes/npcs/client/gui/util/GuiNPCInterface.java | 4 ++++ .../java/noppes/npcs/client/gui/util/GuiNpcTextField.java | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 1d453a395..daee2e905 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -104,6 +104,10 @@ public abstract class GuiNPCInterface extends GuiScreen { rotateLeft = new GuiNpcButton(0, guiLeft + 44 + xOffsetNpc + xOffsetButton, guiTop + yOffsetNpc + yOffsetButton, 20, 20, "<"); rotateRight = new GuiNpcButton(0, guiLeft + 66 + xOffsetNpc + xOffsetButton, guiTop + yOffsetNpc + yOffsetButton, 20, 20, ">"); } + + //Important for GuiScriptTextArea + for (GuiNpcTextField tf : textfields.values()) + tf.initGui(); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java index 0f26d9d36..1345a584f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java @@ -38,7 +38,10 @@ public class GuiNpcTextField extends GuiTextField { public GuiNpcTextField(int id, GuiScreen parent, int i, int j, int k, int l, String s) { this(id, parent, Minecraft.getMinecraft().fontRenderer, i, j, k, l, s); } - + + public void initGui() { + } + protected boolean charAllowed(char c, int i) { if (!integersOnly || Character.isDigit(c)) return true; From d717b00de0267f7120cdcab12decb493d587877d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 16:04:07 +0200 Subject: [PATCH 090/358] Fully implemented OverlayKeyPresetViewer to GuiScriptTextArea --- .../client/gui/util/GuiScriptTextArea.java | 30 ++++++++++++++---- .../gui/util/key/OverlayKeyPresetViewer.java | 14 ++++---- .../textures/gui/keypreset_highres.png | Bin 0 -> 33151 bytes 3 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 src/main/resources/assets/customnpcs/textures/gui/keypreset_highres.png diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index d15c2d9a0..29bbed9bd 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -6,6 +6,7 @@ import net.minecraft.client.gui.ScaledResolution; import net.minecraft.util.ChatAllowedCharacters; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; import noppes.npcs.client.gui.util.script.*; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; import noppes.npcs.client.key.impl.ScriptEditorKeys; @@ -72,6 +73,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== KEYS ==================== public static final ScriptEditorKeys KEYS = new ScriptEditorKeys(); + public OverlayKeyPresetViewer KEYS_OVERLAY = new OverlayKeyPresetViewer(KEYS); + // ==================== CONSTRUCTOR ==================== @@ -86,6 +89,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.setText(text); this.undoing = false; initializeKeyBindings(); + initGui(); + } + + public void initGui() { + int endX = x + width, endY = y + height; + KEYS_OVERLAY.initGui(endX - (width / 2) - 23, y + (height / 4), endX - 9, endY - 26); + + KEYS_OVERLAY.viewButton.scale = 0.4f; + KEYS_OVERLAY.viewButton.initGui(endX - 8, endY - 25); } // ==================== RENDERING ==================== @@ -119,9 +131,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { int maxScroll = Math.max(0, this.container.linesCount - container.visibleLines); // Handle mouse wheel scroll + int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); if (listener instanceof GuiNPCInterface) { - int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); - if (wheelDelta != 0) + ((GuiNPCInterface) listener).mouseScroll = wheelDelta; + if (wheelDelta != 0 && !KEYS_OVERLAY.showOverlay) scroll.applyWheelScroll(wheelDelta, maxScroll); } @@ -351,6 +364,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { drawRect(posX, posY, posX + 5, posY + sbSize + 2, 0xFFe0e0e0); } + + KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); } private void scissorViewport() { @@ -436,9 +451,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void initializeKeyBindings() { // Helper: execute action only if text area is active and enabled Consumer executeIfActive = (action) -> { - if (active && isEnabled()) { + if (active && isEnabled() && !KEYS_OVERLAY.showOverlay) action.run(); - } }; // CUT: Copy selection to clipboard and delete it @@ -582,7 +596,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { */ @Override public boolean textboxKeyTyped(char c, int i) { - if (!active) return false; + KEYS_OVERLAY.keyTyped(c, i); + + if (!active || KEYS_OVERLAY.showOverlay) return false; if (this.isKeyComboCtrlA(i)) { selection.selectAll(text.length()); @@ -1358,7 +1374,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { public void mouseClicked(int xMouse, int yMouse, int mouseButton) { // Determine whether click occurred inside the text area bounds this.active = xMouse >= this.x && xMouse < this.x + this.width && yMouse >= this.y && yMouse < this.y + this.height; - if (this.active) { + if (this.active && !KEYS_OVERLAY.showOverlay) { // Compute logical click position in text and reset selection/caret int clickPos = this.getSelectionPos(xMouse, yMouse); selection.reset(clickPos); @@ -1407,6 +1423,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.lastClicked = time; activeTextfield = this; } + + KEYS_OVERLAY.mouseClicked(xMouse, yMouse, mouseButton); } // Called from GuiScreen.updateScreen() diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java index 785f2425f..fc0cfe40a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -29,6 +29,8 @@ public class OverlayKeyPresetViewer { public OverlayButton viewButton = new OverlayButton(); public LinkedList list = new LinkedList<>(); + // from 0-1, where 0.5 is half overlay width; + public float RELATIVE_MAX_DESC_WIDTH = 0.5f; private final FontRenderer font = Minecraft.getMinecraft().fontRenderer; public OverlayKeyPresetViewer(KeyPresetManager manager) { @@ -65,7 +67,6 @@ public class OverlayKeyPresetViewer { if (!aboveOverlay && !aboveButton) showOverlay = false; } - viewButton.drawButton(); } @@ -74,8 +75,6 @@ public class OverlayKeyPresetViewer { GL11.glEnable(GL11.GL_SCISSOR_TEST); GuiUtil.drawGradientRect(startX, startY, endX, endY, 0x88000000, 0x88303030); - if (scroll.maxScroll > 0) - scroll.drawBar(); GuiUtil.setScissorClip(startX, startY, scroll.maxScroll > 0 ? width - scroll.barWidth : width, height); GL11.glPushMatrix(); @@ -95,6 +94,9 @@ public class OverlayKeyPresetViewer { GL11.glPopMatrix(); GL11.glDisable(GL11.GL_SCISSOR_TEST); + + if (scroll.maxScroll > 0) + scroll.drawBar(); } public void keyTyped(char c, int i) { @@ -189,7 +191,7 @@ public class OverlayKeyPresetViewer { public void drawBar() { GL11.glPushMatrix(); - GL11.glTranslatef(endX - barWidth - 2, startY + yStartSpacing + scrollY * scrollFactor, 0); + GL11.glTranslatef(endX - barWidth - 2, startY + yStartSpacing + scrollY * scrollFactor, 1); GuiUtil.drawRectD(0, 0, barWidth, scrollbarHeight, (isMouseAboveBar(mouseX, mouseY) || isMouseDragging) ? 0xffbababa : 0xff767676); GL11.glPopMatrix(); } @@ -221,7 +223,7 @@ public class OverlayKeyPresetViewer { font.drawString("(Conflict-free)", font.getStringWidth(name) + 5, 0, 0xface40); String description = String.format("- %s", key.description); - font.drawSplitString(description, 9, font.FONT_HEIGHT, getMaxStringWidth() - 9, 0x888888); + font.drawSplitString(description, 9, font.FONT_HEIGHT+3, getMaxStringWidth() - 9, 0x888888); } public void drawBox(int offsetY) { @@ -376,7 +378,7 @@ public class OverlayKeyPresetViewer { } public int getMaxStringWidth() { - return (int) (getWidth() * 2.5f / 4); + return (int) (getWidth() * RELATIVE_MAX_DESC_WIDTH); } } diff --git a/src/main/resources/assets/customnpcs/textures/gui/keypreset_highres.png b/src/main/resources/assets/customnpcs/textures/gui/keypreset_highres.png new file mode 100644 index 0000000000000000000000000000000000000000..bd393937b2555810b9e900abdda7ca84c0d19d9c GIT binary patch literal 33151 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 ~pDP%G!K>vI?)zr@$IHH;? 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 XWwjMMZ%Gpc@X&m$^XT_O^RWK|8$S4! literal 0 HcmV?d00001 From 37cd4f1b7a3adff03223ef57c4f4df6316c9ae33 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 16:39:53 +0200 Subject: [PATCH 091/358] OverlayKeyPreset tweaks --- .../client/gui/util/GuiScriptTextArea.java | 7 ++-- .../gui/util/key/OverlayKeyPresetViewer.java | 32 +++++++++++++------ .../client/key/impl/ScriptEditorKeys.java | 19 ++++++----- 3 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 29bbed9bd..5b0cf4830 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -94,10 +94,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { public void initGui() { int endX = x + width, endY = y + height; - KEYS_OVERLAY.initGui(endX - (width / 2) - 23, y + (height / 4), endX - 9, endY - 26); + KEYS_OVERLAY.elementSpacing = 0; + KEYS_OVERLAY.initGui(endX - (width / 2) , y + (height / 4), endX - 7, endY - 27); - KEYS_OVERLAY.viewButton.scale = 0.4f; - KEYS_OVERLAY.viewButton.initGui(endX - 8, endY - 25); + KEYS_OVERLAY.viewButton.scale = 0.45f; + KEYS_OVERLAY.viewButton.initGui(endX - 7, endY - 26); } // ==================== RENDERING ==================== diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java index fc0cfe40a..2bca001e3 100644 --- a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -154,13 +154,18 @@ public class OverlayKeyPresetViewer { totalHeight = 0; for (PresetElement element : list) totalHeight += element.getHeight(); + // contentTotal includes top spacing before first element + float contentTotal = totalHeight + yStartSpacing; - totalHeight -= yStartSpacing; - scroll.maxScroll = Math.max(0, totalHeight - height); + // maxScroll is how much content exceeds the visible area + maxScroll = Math.max(0f, contentTotal - height); - heightFactor = height / totalHeight; + // compute scrollbar height proportional to visible area vs content + heightFactor = contentTotal > 0f ? (float) height / contentTotal : 1f; scrollbarHeight = Math.max((int) (height * heightFactor), 20); - scrollFactor = (height - yStartSpacing - scrollbarHeight) / maxScroll; + + // scrollFactor maps scrollY -> pixel offset for scrollbar + scrollFactor = maxScroll > 0f ? (height - yStartSpacing - scrollbarHeight) / maxScroll : 0f; } public void update(int wheel) { @@ -216,14 +221,17 @@ public class OverlayKeyPresetViewer { } public void drawDescription() { + boolean hasDescription = key.description != null; String name = String.format("> %s", key.name); - font.drawString(name, 0, 0, 0xffffff); + font.drawString(name, 0, hasDescription ? 0 : 10, 0xffffff); if (!key.shouldConflict) font.drawString("(Conflict-free)", font.getStringWidth(name) + 5, 0, 0xface40); - String description = String.format("- %s", key.description); - font.drawSplitString(description, 9, font.FONT_HEIGHT+3, getMaxStringWidth() - 9, 0x888888); + if (hasDescription) { + String description = String.format("- %s", key.description); + font.drawSplitString(description, 9, font.FONT_HEIGHT + 3, getMaxStringWidth() - 9, 0x888888); + } } public void drawBox(int offsetY) { @@ -235,7 +243,13 @@ public class OverlayKeyPresetViewer { GL11.glColor4f(1, 1, 1, 1); float boxScaleX = 1.75f; float boxWidth = 32 * boxScaleX; - boxScreenX = getMaxStringWidth() * scale + 8; //remove scaling from maxStringWidth + // compute desired box X based on description width + float desiredBoxX = getMaxStringWidth() * scale + 8; //remove scaling from maxStringWidth + // clamp so the box + reset button always fit inside the content area + float resetWidth = 10f; + float contentAvailable = (scroll.maxScroll > 0 ? width - scroll.barWidth : width) - 4f; // small padding + float maxBoxX = Math.max(0f, contentAvailable - boxWidth - resetWidth - 4f); + boxScreenX = Math.min(desiredBoxX, maxBoxX); GL11.glScalef(boxScaleX, 1, 0); if (isEditing) @@ -249,7 +263,7 @@ public class OverlayKeyPresetViewer { //Reset box boolean isDefault = key.isDefault(); - float resetWidth = 10; + resetWidth = 10; float resetScreenX = boxScreenX + boxWidth + 2; if (isMouseAboveReset(mouseX, mouseY) && !isDefault) diff --git a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java index 48e6df323..cb6c58e66 100644 --- a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java +++ b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java @@ -8,16 +8,15 @@ public class ScriptEditorKeys extends KeyPresetManager { //////////////////// // Editor shortcuts (mapped from GuiScriptTextArea.handleShortcutKeys) - public final KeyPreset CUT = add("Cut").setDefaultState(Keyboard.KEY_X, true, false, false).setDescription("Cut selection or current line"); - public final KeyPreset COPY = add("Copy").setDefaultState(Keyboard.KEY_C, true, false, false).setDescription("Copy selection"); - public final KeyPreset PASTE = add("Paste").setDefaultState(Keyboard.KEY_V, true, false, false).setDescription("Paste clipboard at caret"); - public final KeyPreset DUPLICATE = add("Duplicate").setDefaultState(Keyboard.KEY_D, true, false, false).setDescription("Duplicate selection or current line"); - - - public final KeyPreset UNDO = add("Undo").setDefaultState(Keyboard.KEY_Z, true, false, false).setDescription("Undo last edit"); - public final KeyPreset REDO = add("Redo").setDefaultState(Keyboard.KEY_Y, true, false, false).setDescription("Redo last undone edit"); - public final KeyPreset FORMAT = add("Format").setDefaultState(Keyboard.KEY_F, true, false, false).setDescription("Format code / indent"); - public final KeyPreset TOGGLE_COMMENT = add("Toggle Comment").setDefaultState(Keyboard.KEY_SLASH, true, false, false).setDescription("Toggle comment for selection or line"); + public final KeyPreset COPY = add("Copy").setDefaultState(Keyboard.KEY_C, true, false, false); + public final KeyPreset PASTE = add("Paste").setDefaultState(Keyboard.KEY_V, true, false, false); + public final KeyPreset CUT = add("Cut").setDefaultState(Keyboard.KEY_X, true, false, false); + public final KeyPreset DUPLICATE = add("Duplicate").setDefaultState(Keyboard.KEY_D, true, false, false); + public final KeyPreset UNDO = add("Undo").setDefaultState(Keyboard.KEY_Z, true, false, false); + public final KeyPreset REDO = add("Redo").setDefaultState(Keyboard.KEY_Y, true, false, false); + + public final KeyPreset FORMAT = add("Format Code").setDefaultState(Keyboard.KEY_F, true, false, false); + public final KeyPreset TOGGLE_COMMENT = add("Toggle Comment").setDefaultState(Keyboard.KEY_SLASH, true, false, false); public ScriptEditorKeys() { super("script_editor"); From 9d8fd5bada8c57a2e9e7ac482f3863c3dc0d076e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 18:06:27 +0200 Subject: [PATCH 092/358] Drawn border for OverlayKeyPreset + accounted for text editor's scroll bar in init --- .../client/gui/util/GuiScriptTextArea.java | 7 ++-- .../noppes/npcs/client/gui/util/GuiUtil.java | 33 +++++++++++++++++++ .../gui/util/key/OverlayKeyPresetViewer.java | 21 ++++++++++-- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 5b0cf4830..dc2d40632 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -94,11 +94,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { public void initGui() { int endX = x + width, endY = y + height; - KEYS_OVERLAY.elementSpacing = 0; - KEYS_OVERLAY.initGui(endX - (width / 2) , y + (height / 4), endX - 7, endY - 27); + int xOffset = hasVerticalScrollbar() ? -8 : -2; + KEYS_OVERLAY.elementSpacing = KEYS_OVERLAY.yStartSpacing = 0; + KEYS_OVERLAY.initGui(endX - (width / 2), y + (height / 4) - 10, endX + xOffset - 2, endY - 28); KEYS_OVERLAY.viewButton.scale = 0.45f; - KEYS_OVERLAY.viewButton.initGui(endX - 7, endY - 26); + KEYS_OVERLAY.viewButton.initGui(endX + xOffset, endY - 26); } // ==================== RENDERING ==================== diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java b/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java index 10c86a40f..9a9fc71ca 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java @@ -112,6 +112,39 @@ public class GuiUtil { GL11.glEnable(GL11.GL_ALPHA_TEST); GL11.glEnable(GL11.GL_TEXTURE_2D); } + + public static void drawGradientRectHorizontal(int left, int top, int right, int bottom, int startColor, int endColor) { + double zLevel = 0; + + float sA = (float) (startColor >> 24 & 255) / 255.0F; + float sR = (float) (startColor >> 16 & 255) / 255.0F; + float sG = (float) (startColor >> 8 & 255) / 255.0F; + float sB = (float) (startColor & 255) / 255.0F; + + float eA = (float) (endColor >> 24 & 255) / 255.0F; + float eR = (float) (endColor >> 16 & 255) / 255.0F; + float eG = (float) (endColor >> 8 & 255) / 255.0F; + float eB = (float) (endColor & 255) / 255.0F; + + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glDisable(GL11.GL_ALPHA_TEST); + OpenGlHelper.glBlendFunc(770, 771, 1, 0); + GL11.glShadeModel(GL11.GL_SMOOTH); + Tessellator tessellator = Tessellator.instance; + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_F(sR, sG, sB, sA); + tessellator.addVertex(left, top, zLevel); + tessellator.addVertex(left, bottom, zLevel); + tessellator.setColorRGBA_F(eR, eG, eB, eA); + tessellator.addVertex(right, bottom, zLevel); + tessellator.addVertex(right, top, zLevel); + tessellator.draw(); + GL11.glShadeModel(GL11.GL_FLAT); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glEnable(GL11.GL_TEXTURE_2D); + } public static void drawTexturedModalRect(double x, double y, double width, double height, int textureX, int textureY) { double zLevel = 0; diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java index 2bca001e3..b1701bcee 100644 --- a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -33,6 +33,8 @@ public class OverlayKeyPresetViewer { public float RELATIVE_MAX_DESC_WIDTH = 0.5f; private final FontRenderer font = Minecraft.getMinecraft().fontRenderer; + public boolean hasBorder = true; + public OverlayKeyPresetViewer(KeyPresetManager manager) { this.manager = manager; @@ -72,10 +74,23 @@ public class OverlayKeyPresetViewer { public void drawOverlay(int wheel) { scroll.update(wheel); + + if (hasBorder) { + int fromCol = 0x22ffffff, toCol = 0xffffffff; //BORDER GRADIENT + // int fromCol = 0xffff00ff,toCol = 0xff00ffff; NEON + // Top + GuiUtil.drawGradientRectHorizontal(startX - 1, startY - 1, endX + 1, startY, toCol, fromCol); + // Bottom + GuiUtil.drawGradientRectHorizontal(startX - 1, endY, endX + 1, endY + 1, fromCol, toCol); + // Left + GuiUtil.drawGradientRect(startX - 1, startY - 1, startX, endY, toCol, fromCol); + //Right + GuiUtil.drawGradientRect(endX, startY, endX + 1, endY, fromCol, toCol); + } + + GuiUtil.drawGradientRect(startX + 1, startY + 1, endX - 1, endY - 1, 0x88000000, 0xcc303030); GL11.glEnable(GL11.GL_SCISSOR_TEST); - GuiUtil.drawGradientRect(startX, startY, endX, endY, 0x88000000, 0x88303030); - GuiUtil.setScissorClip(startX, startY, scroll.maxScroll > 0 ? width - scroll.barWidth : width, height); GL11.glPushMatrix(); GL11.glTranslatef((startX + 2), startY + yStartSpacing - scroll.scrollY, 0); @@ -419,7 +434,7 @@ public class OverlayKeyPresetViewer { GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); GL11.glPushMatrix(); - float color = 0.55f; + float color = 0.75f; GL11.glColor4f(color, color, color, 1); GL11.glScalef(scale, scale, 1); GuiUtil.drawTexturedModalRect(screenX, screenY, textureWidth, textureHeight, 0, 0); From 226b95af320a8f6d83b4935addaf57854eeda3b1 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 18:11:04 +0200 Subject: [PATCH 093/358] Improved keyboard button rendering --- .../npcs/client/gui/util/key/OverlayKeyPresetViewer.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java index b1701bcee..5bed55130 100644 --- a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -432,7 +432,8 @@ public class OverlayKeyPresetViewer { double screenX = startX / scale, screenY = startY / scale; int oldMinFilter = GL11.glGetTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); - + GL11.glDisable(GL11.GL_BLEND); + GL11.glPushMatrix(); float color = 0.75f; GL11.glColor4f(color, color, color, 1); @@ -440,6 +441,7 @@ public class OverlayKeyPresetViewer { GuiUtil.drawTexturedModalRect(screenX, screenY, textureWidth, textureHeight, 0, 0); GL11.glPopMatrix(); + GL11.glEnable(GL11.GL_BLEND); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, oldMinFilter); } From ba9b86e6d0ab41821bf0da6950ecb18e00879058 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 18:12:37 +0200 Subject: [PATCH 094/358] Correct mouseClicked logic --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index dc2d40632..0c29a816e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1374,9 +1374,14 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== MOUSE HANDLING ==================== public void mouseClicked(int xMouse, int yMouse, int mouseButton) { + if (KEYS_OVERLAY.showOverlay) { + KEYS_OVERLAY.mouseClicked(xMouse, yMouse, mouseButton); + return; + } + // Determine whether click occurred inside the text area bounds this.active = xMouse >= this.x && xMouse < this.x + this.width && yMouse >= this.y && yMouse < this.y + this.height; - if (this.active && !KEYS_OVERLAY.showOverlay) { + if (this.active) { // Compute logical click position in text and reset selection/caret int clickPos = this.getSelectionPos(xMouse, yMouse); selection.reset(clickPos); @@ -1425,8 +1430,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.lastClicked = time; activeTextfield = this; } - - KEYS_OVERLAY.mouseClicked(xMouse, yMouse, mouseButton); } // Called from GuiScreen.updateScreen() From 124d82889dbf281172fde64888ea41eecf993960 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 18:15:37 +0200 Subject: [PATCH 095/358] Fixed cursor rendering +1 Y offset --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 0c29a816e..18bb9c650 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -348,7 +348,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (active && isEnabled() && (recentInput || (cursorCounter / 10) % 2 == 0) && (selection.getCursorPosition() >= data.start && selection.getCursorPosition() < data.end || (i == list.size() - 1 && selection.getCursorPosition() == text.length()))) { int posX = x + LINE_NUMBER_GUTTER_WIDTH + ClientProxy.Font.width( line.substring(0, Math.min(selection.getCursorPosition() - data.start, line.length()))); - drawRect(posX + 1, yPos -1, posX + 2, yPos - 1 + container.lineHeight, 0xffffffff); + drawRect(posX + 1, posY, posX + 2, posY + container.lineHeight, 0xffffffff); } } } From cc0a329370062c6224aebc2e12f832a31e828dce Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 18:18:05 +0200 Subject: [PATCH 096/358] Changed Script Editor key press type from single press to just press --- .../npcs/client/gui/util/GuiScriptTextArea.java | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 18bb9c650..5d3869733 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -459,7 +459,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // CUT: Copy selection to clipboard and delete it KEYS.CUT.setTask(e -> { - if (!e.isSinglePress()) + if (!e.isPress()) return; executeIfActive.accept(() -> { if (selection.hasSelection()) { @@ -474,7 +474,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // COPY: Copy selection to clipboard KEYS.COPY.setTask(e -> { - if (!e.isSinglePress()) + if (!e.isPress()) return; executeIfActive.accept(() -> { if (selection.hasSelection()) { @@ -485,7 +485,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // PASTE: Insert clipboard contents at caret KEYS.PASTE.setTask(e -> { - if (!e.isSinglePress()) + if (!e.isPress()) return; executeIfActive.accept(() -> { addText(NoppesStringUtils.getClipboardContents()); @@ -495,7 +495,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // UNDO: Restore last edit from undo list KEYS.UNDO.setTask(e -> { - if (!e.isSinglePress()) + if (!e.isPress()) return; executeIfActive.accept(() -> { if (undoList.isEmpty()) @@ -512,7 +512,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // REDO: Restore last undone edit from redo list KEYS.REDO.setTask(e -> { - if (!e.isSinglePress()) + if (!e.isPress()) return; executeIfActive.accept(() -> { if (redoList.isEmpty()) @@ -529,14 +529,14 @@ public class GuiScriptTextArea extends GuiNpcTextField { // FORMAT: Format/indent code KEYS.FORMAT.setTask(e -> { - if (!e.isSinglePress()) + if (!e.isPress()) return; executeIfActive.accept(this::formatText); }); // TOGGLE_COMMENT: Toggle comment for selection or current line KEYS.TOGGLE_COMMENT.setTask(e -> { - if (!e.isSinglePress()) + if (!e.isPress()) return; executeIfActive.accept(() -> { if (selection.hasSelection()) { @@ -549,7 +549,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // DUPLICATE: Duplicate selection or current line KEYS.DUPLICATE.setTask(e -> { - if (!e.isSinglePress()) + if (!e.isPress()) return; executeIfActive.accept(() -> { if (selection.hasSelection()) { From 3e284376827db0ffe7a29f2ef5f95dc6d2006974 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 18:25:32 +0200 Subject: [PATCH 097/358] Switched to Supplier instead of Consumer --- .../client/gui/util/GuiScriptTextArea.java | 178 ++++++++---------- 1 file changed, 83 insertions(+), 95 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 5d3869733..75a8f5976 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -16,7 +16,7 @@ import org.lwjgl.opengl.GL11; import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; +import java.util.function.Supplier; import java.util.regex.Matcher; import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; @@ -452,141 +452,129 @@ public class GuiScriptTextArea extends GuiNpcTextField { */ private void initializeKeyBindings() { // Helper: execute action only if text area is active and enabled - Consumer executeIfActive = (action) -> { - if (active && isEnabled() && !KEYS_OVERLAY.showOverlay) - action.run(); - }; + Supplier isActive = () -> active && isEnabled() && !KEYS_OVERLAY.showOverlay; // CUT: Copy selection to clipboard and delete it KEYS.CUT.setTask(e -> { - if (!e.isPress()) + if (!e.isPress() || !isActive.get()) return; - executeIfActive.accept(() -> { - if (selection.hasSelection()) { - NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); - String s = getSelectionBeforeText(); - setText(s + getSelectionAfterText()); - selection.reset(s.length()); - scrollToCursor(); - } - }); + + if (selection.hasSelection()) { + NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); + String s = getSelectionBeforeText(); + setText(s + getSelectionAfterText()); + selection.reset(s.length()); + scrollToCursor(); + } }); // COPY: Copy selection to clipboard KEYS.COPY.setTask(e -> { - if (!e.isPress()) + if (!e.isPress() || !isActive.get()) return; - executeIfActive.accept(() -> { - if (selection.hasSelection()) { - NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); - } - }); + + if (selection.hasSelection()) + NoppesStringUtils.setClipboardContents(selection.getSelectedText(text)); }); // PASTE: Insert clipboard contents at caret KEYS.PASTE.setTask(e -> { - if (!e.isPress()) + if (!e.isPress() || !isActive.get()) return; - executeIfActive.accept(() -> { - addText(NoppesStringUtils.getClipboardContents()); - scrollToCursor(); - }); + + addText(NoppesStringUtils.getClipboardContents()); + scrollToCursor(); }); // UNDO: Restore last edit from undo list KEYS.UNDO.setTask(e -> { - if (!e.isPress()) + if (!e.isPress() || !isActive.get()) return; - executeIfActive.accept(() -> { - if (undoList.isEmpty()) - return; - undoing = true; - redoList.add(new UndoData(this.text, selection.getCursorPosition())); - UndoData data = undoList.remove(undoList.size() - 1); - setText(data.text); - selection.reset(data.cursorPosition); - undoing = false; - scrollToCursor(); - }); + + if (undoList.isEmpty()) + return; + undoing = true; + redoList.add(new UndoData(this.text, selection.getCursorPosition())); + UndoData data = undoList.remove(undoList.size() - 1); + setText(data.text); + selection.reset(data.cursorPosition); + undoing = false; + scrollToCursor(); }); // REDO: Restore last undone edit from redo list KEYS.REDO.setTask(e -> { - if (!e.isPress()) + if (!e.isPress() || !isActive.get()) return; - executeIfActive.accept(() -> { - if (redoList.isEmpty()) - return; - undoing = true; - undoList.add(new UndoData(this.text, selection.getCursorPosition())); - UndoData data = redoList.remove(redoList.size() - 1); - setText(data.text); - selection.reset(data.cursorPosition); - undoing = false; - scrollToCursor(); - }); + + if (redoList.isEmpty()) + return; + undoing = true; + undoList.add(new UndoData(this.text, selection.getCursorPosition())); + UndoData data = redoList.remove(redoList.size() - 1); + setText(data.text); + selection.reset(data.cursorPosition); + undoing = false; + scrollToCursor(); }); // FORMAT: Format/indent code KEYS.FORMAT.setTask(e -> { - if (!e.isPress()) + if (!e.isPress() || !isActive.get()) return; - executeIfActive.accept(this::formatText); + formatText(); }); // TOGGLE_COMMENT: Toggle comment for selection or current line KEYS.TOGGLE_COMMENT.setTask(e -> { - if (!e.isPress()) + if (!e.isPress() || !isActive.get()) return; - executeIfActive.accept(() -> { - if (selection.hasSelection()) { - toggleCommentSelection(); - } else { - toggleCommentLineAtCursor(); - } - }); + + if (selection.hasSelection()) + toggleCommentSelection(); + else + toggleCommentLineAtCursor(); }); // DUPLICATE: Duplicate selection or current line KEYS.DUPLICATE.setTask(e -> { - if (!e.isPress()) + if (!e.isPress() || !isActive.get()) return; - executeIfActive.accept(() -> { - if (selection.hasSelection()) { - // Multi-line selection duplication - LineData firstLine = null, lastLine = null; - for (LineData line : container.lines) { - if (line.end > selection.getStartSelection() && line.start < selection.getEndSelection()) { - if (firstLine == null) - firstLine = line; - lastLine = line; - } - } - if (firstLine != null && lastLine != null) { - String selectedText = text.substring(firstLine.start, lastLine.end); - int savedStart = selection.getStartSelection(); - int savedEnd = selection.getEndSelection(); - int insertAt = lastLine.end; - setText(text.substring(0, insertAt) + selectedText + text.substring(insertAt)); - selection.setStartSelection(savedStart); - selection.setEndSelection(savedEnd); - selection.setCursorPositionDirect(savedEnd); - } - } else { - // Duplicate current line - for (LineData line : container.lines) { - if (selection.getCursorPosition() >= line.start && selection.getCursorPosition() <= line.end) { - String lineText = text.substring(line.start, line.end); - String insertText = lineText.endsWith("\n") ? lineText : "\n" + lineText; - int insertionPoint = line.end; - setText(text.substring(0, insertionPoint) + insertText + text.substring(insertionPoint)); - int newCursor = insertionPoint + insertText.length() - (insertText.endsWith("\n") ? 1 : 0); - selection.reset(Math.max(0, Math.min(newCursor, this.text.length()))); - break; - } + + if (selection.hasSelection()) { + // Multi-line selection duplication + LineData firstLine = null, lastLine = null; + for (LineData line : container.lines) { + if (line.end > selection.getStartSelection() && line.start < selection.getEndSelection()) { + if (firstLine == null) + firstLine = line; + lastLine = line; } } - }); + if (firstLine != null && lastLine != null) { + String selectedText = text.substring(firstLine.start, lastLine.end); + int savedStart = selection.getStartSelection(); + int savedEnd = selection.getEndSelection(); + int insertAt = lastLine.end; + setText(text.substring(0, insertAt) + selectedText + text.substring(insertAt)); + selection.setStartSelection(savedStart); + selection.setEndSelection(savedEnd); + selection.setCursorPositionDirect(savedEnd); + } + } else { + // Duplicate current line + for (LineData line : container.lines) { + if (selection.getCursorPosition() >= line.start && selection.getCursorPosition() <= line.end) { + String lineText = text.substring(line.start, line.end); + String insertText = lineText.endsWith("\n") ? lineText : "\n" + lineText; + int insertionPoint = line.end; + setText(text.substring(0, insertionPoint) + insertText + text.substring(insertionPoint)); + int newCursor = insertionPoint + insertText.length() - (insertText.endsWith("\n") ? 1 : 0); + selection.reset(Math.max(0, Math.min(newCursor, this.text.length()))); + break; + } + } + } }); } From 94864fb19af6148e64562badc1ffd98a385066de Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 18:29:52 +0200 Subject: [PATCH 098/358] Fixed multi line comment changing cursor position --- src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 75a8f5976..48140f160 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1161,7 +1161,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(result.newText); selection.setStartSelection(result.newStartSelection); selection.setEndSelection(result.newEndSelection); - selection.setCursorPositionDirect(result.newEndSelection); } private void toggleCommentLineAtCursor() { From 993ce4b73f4bda09ae2f25b46ed4fa662d537b6f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 18:38:11 +0200 Subject: [PATCH 099/358] Fixed CTRL + Right not working. CTRL + X now cuts cursors sentence regardless of selection --- .../client/gui/util/GuiScriptTextArea.java | 70 ++++++++++++++++--- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 48140f160..05ac51fca 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -454,7 +454,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Helper: execute action only if text area is active and enabled Supplier isActive = () -> active && isEnabled() && !KEYS_OVERLAY.showOverlay; - // CUT: Copy selection to clipboard and delete it + // CUT: Copy selection to clipboard and delete it. If no selection, cut the current sentence. KEYS.CUT.setTask(e -> { if (!e.isPress() || !isActive.get()) return; @@ -465,6 +465,52 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(s + getSelectionAfterText()); selection.reset(s.length()); scrollToCursor(); + return; + } + + // No selection: cut the current sentence (heuristic based on .!? or newline) + if (text == null || text.isEmpty()) + return; + int cursor = selection.getCursorPosition(); + int start = cursor; + int pos = cursor - 1; + while (pos >= 0) { + char ch = text.charAt(pos); + if (ch == '.' || ch == '!' || ch == '?' || ch == '\n') { + start = pos + 1; + break; + } + pos--; + } + while (start < cursor && Character.isWhitespace(text.charAt(start))) start++; + + int end = cursor; + pos = cursor; + while (pos < text.length()) { + char ch = text.charAt(pos); + if (ch == '.' || ch == '!' || ch == '?' || ch == '\n') { + end = pos + 1; + break; + } + pos++; + } + if (end == cursor) end = text.length(); + while (end > start && Character.isWhitespace(text.charAt(end - 1))) end--; + + if (start >= end) { + // fallback: cut whole current line + int ls = text.lastIndexOf('\n', Math.max(0, cursor - 1)); + start = ls == -1 ? 0 : ls + 1; + int le = text.indexOf('\n', cursor); + end = le == -1 ? text.length() : le; + } + + if (start < end) { + String cut = text.substring(start, end); + NoppesStringUtils.setClipboardContents(cut); + setText(text.substring(0, start) + text.substring(end)); + selection.reset(start); + scrollToCursor(); } }); @@ -635,14 +681,22 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (i == Keyboard.KEY_RIGHT) { int j = 1; // default: move one character if (isCtrlKeyDown()) { - // With Ctrl, search for the next word boundary starting at the cursor. - // We look at the substring after the cursor and use the matcher to find - // the first match; its start offset indicates how many chars to skip. - Matcher m = container.regexWord.matcher(text.substring(selection.getCursorPosition())); - // m.find() called once is sufficient; the previous code attempted - // a redundant second find in some cases. Keep first positive find. + String after = text.substring(selection.getCursorPosition()); + Matcher m = container.regexWord.matcher(after); if (m.find()) { - j = m.start(); + if (m.start() == 0) { + // If the first match starts at 0 (cursor at word start), + // try to find the next match so we advance past the current word. + if (m.find()) + j = m.start(); + else + j = Math.max(1, after.length()); + } else { + j = m.start(); + } + } else { + // No word match found after cursor -> jump to end + j = Math.max(1, after.length()); } } int newPos = Math.min(selection.getCursorPosition() + j, text.length()); From c7c516d97e6d01ecd76702a0cc5664c1f5ca496a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 18:44:09 +0200 Subject: [PATCH 100/358] Overlay KeyPreset adjustments --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 2 +- .../noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 05ac51fca..2b02f8219 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -96,7 +96,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { int endX = x + width, endY = y + height; int xOffset = hasVerticalScrollbar() ? -8 : -2; KEYS_OVERLAY.elementSpacing = KEYS_OVERLAY.yStartSpacing = 0; - KEYS_OVERLAY.initGui(endX - (width / 2), y + (height / 4) - 10, endX + xOffset - 2, endY - 28); + KEYS_OVERLAY.initGui(endX - (width / 2)-3, y + (height / 4) - 10, endX + xOffset - 2, endY - 28); KEYS_OVERLAY.viewButton.scale = 0.45f; KEYS_OVERLAY.viewButton.initGui(endX + xOffset, endY - 26); diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java index 5bed55130..13518c079 100644 --- a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -212,7 +212,7 @@ public class OverlayKeyPresetViewer { public void drawBar() { GL11.glPushMatrix(); GL11.glTranslatef(endX - barWidth - 2, startY + yStartSpacing + scrollY * scrollFactor, 1); - GuiUtil.drawRectD(0, 0, barWidth, scrollbarHeight, (isMouseAboveBar(mouseX, mouseY) || isMouseDragging) ? 0xffbababa : 0xff767676); + GuiUtil.drawRectD(0, 1, barWidth, scrollbarHeight-1, (isMouseAboveBar(mouseX, mouseY) || isMouseDragging) ? 0xffbababa : 0xff767676); GL11.glPopMatrix(); } From 0fce002b52dcd5ccf0f8808cc66226d94e1afa3f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 19:48:08 +0200 Subject: [PATCH 101/358] Search/Replace BAR FOR SCRIPT EDITOR FUUUUUUUUH --- .../client/gui/util/GuiScriptTextArea.java | 95 ++ .../gui/util/script/SearchReplaceBar.java | 909 ++++++++++++++++++ .../client/key/impl/ScriptEditorKeys.java | 5 + 3 files changed, 1009 insertions(+) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 2b02f8219..375dfb2e7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -75,6 +75,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { public static final ScriptEditorKeys KEYS = new ScriptEditorKeys(); public OverlayKeyPresetViewer KEYS_OVERLAY = new OverlayKeyPresetViewer(KEYS); + // ==================== SEARCH/REPLACE ==================== + private final SearchReplaceBar searchBar = new SearchReplaceBar(); // ==================== CONSTRUCTOR ==================== @@ -100,6 +102,46 @@ public class GuiScriptTextArea extends GuiNpcTextField { KEYS_OVERLAY.viewButton.scale = 0.45f; KEYS_OVERLAY.viewButton.initGui(endX + xOffset, endY - 26); + + // Initialize search bar + searchBar.initGui(x, y, width); + searchBar.setCallback(new SearchReplaceBar.SearchCallback() { + @Override + public String getText() { + return text; + } + + @Override + public void setText(String newText) { + GuiScriptTextArea.this.setText(newText); + } + + @Override + public void scrollToPosition(int position) { + // Find line containing position and scroll to it + if (container == null || container.lines == null) return; + for (int i = 0; i < container.lines.size(); i++) { + LineData ld = container.lines.get(i); + if (position >= ld.start && position < ld.end) { + int visible = Math.max(1, container.visibleLines); + int maxScroll = Math.max(0, container.linesCount - visible); + scroll.scrollToLine(i, visible, maxScroll); + break; + } + } + } + + @Override + public void setSelection(int start, int end) { + selection.setSelection(start, end); + selection.setCursorPositionDirect(end); + } + + @Override + public int getGutterWidth() { + return LINE_NUMBER_GUTTER_WIDTH; + } + }); } // ==================== RENDERING ==================== @@ -289,6 +331,29 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } + + // Highlight search matches + List searchMatches = searchBar.getMatches(); + int currentMatchIdx = searchBar.getCurrentMatchIndex(); + for (int mi = 0; mi < searchMatches.size(); mi++) { + int[] match = searchMatches.get(mi); + // Check if match overlaps with this line + if (match[1] > data.start && match[0] < data.end) { + int matchStart = Math.max(match[0] - data.start, 0); + int matchEnd = Math.min(match[1] - data.start, line.length()); + if (matchStart < matchEnd) { + int s = ClientProxy.Font.width(line.substring(0, matchStart)); + int e = ClientProxy.Font.width(line.substring(0, matchEnd)) + 1; + // Current match gets brighter highlight, others get dimmer + int highlightColor = (mi == currentMatchIdx) ? 0xBB4488ff : 0x662266aa; + if (searchBar.isMatchExcluded(mi)) { + highlightColor = 0x33666666; // Dimmer for excluded matches + } + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, highlightColor); + } + } + } + // Highlight the current line (light gray) under any selection if (active && isEnabled() && (selection.getCursorPosition() >= data.start && selection.getCursorPosition() < data.end || (i == list.size() - 1 && selection.getCursorPosition() == text.length()))) { drawRect(x , posY, x + width - 1, posY + container.lineHeight, 0x22e0e0e0); @@ -367,6 +432,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { drawRect(posX, posY, posX + 5, posY + sbSize + 2, 0xFFe0e0e0); } + // Draw search/replace bar (overlays viewport) + searchBar.draw(xMouse, yMouse); + KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); } @@ -622,6 +690,20 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } }); + + // SEARCH: Open search bar (Ctrl+R) + KEYS.SEARCH.setTask(e -> { + if (!e.isPress() || !active || !isEnabled()) + return; + searchBar.toggle(); + }); + + // SEARCH_REPLACE: Open search+replace bar (Ctrl+Shift+R) + KEYS.SEARCH_REPLACE.setTask(e -> { + if (!e.isPress() || !active || !isEnabled()) + return; + searchBar.toggleReplace(); + }); } // ==================== KEYBOARD INPUT HANDLING ==================== @@ -634,6 +716,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { public boolean textboxKeyTyped(char c, int i) { KEYS_OVERLAY.keyTyped(c, i); + // Handle search bar input first if it has focus + if (searchBar.hasFocus()) { + if (searchBar.keyTyped(c, i)) { + return true; + } + } + if (!active || KEYS_OVERLAY.showOverlay) return false; if (this.isKeyComboCtrlA(i)) { @@ -1415,6 +1504,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== MOUSE HANDLING ==================== public void mouseClicked(int xMouse, int yMouse, int mouseButton) { + // Check search bar clicks first + if (searchBar.isVisible() && searchBar.mouseClicked(xMouse, yMouse, mouseButton)) { + return; + } + if (KEYS_OVERLAY.showOverlay) { KEYS_OVERLAY.mouseClicked(xMouse, yMouse, mouseButton); return; @@ -1476,6 +1570,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Called from GuiScreen.updateScreen() public void updateCursorCounter() { KEYS.tick(); + searchBar.updateCursor(); ++this.cursorCounter; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java new file mode 100644 index 000000000..aca29e933 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -0,0 +1,909 @@ +package noppes.npcs.client.gui.util.script; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.util.ChatAllowedCharacters; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.util.GuiUtil; +import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; +import org.lwjgl.input.Keyboard; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Search and Replace bar component for the script editor. + * Provides IntelliJ-like search functionality with: + * - Search text field with rounded edges + * - Match case (Cc) toggle + * - Whole word (W) toggle + * - Previous/Next match navigation + * - Replace functionality with Replace/Replace All/Exclude buttons + * - Auto-scroll to matches + * - All matches highlighting + */ +public class SearchReplaceBar { + + // ==================== TEXTURE ==================== + public static final ResourceLocation TEXTURE = OverlayKeyPresetViewer.TEXTURE; + + // ==================== DIMENSIONS ==================== + private int x, y, width; + private int barHeight = 24; + private int replaceBarHeight = 22; + private int textFieldWidth = 150; + private int textFieldHeight = 16; + private int buttonSize = 16; + private int buttonSpacing = 2; + private int padding = 4; + + // ==================== STATE ==================== + private boolean visible = false; + private boolean showReplace = false; + private boolean searchFieldFocused = true; + private boolean replaceFieldFocused = false; + + // ==================== SEARCH OPTIONS ==================== + private boolean matchCase = false; + private boolean wholeWord = false; + + // ==================== TEXT FIELDS ==================== + private String searchText = ""; + private String replaceText = ""; + private int searchCursor = 0; + private int replaceCursor = 0; + private int searchSelectionStart = 0; + private int searchSelectionEnd = 0; + private int replaceSelectionStart = 0; + private int replaceSelectionEnd = 0; + + // ==================== MATCHES ==================== + private List matches = new ArrayList<>(); + private int currentMatchIndex = -1; + private List excludedMatches = new ArrayList<>(); + + // ==================== HOVER STATE ==================== + private boolean hoverClose = false; + private boolean hoverMatchCase = false; + private boolean hoverWholeWord = false; + private boolean hoverPrev = false; + private boolean hoverNext = false; + private boolean hoverReplace = false; + private boolean hoverReplaceAll = false; + private boolean hoverExclude = false; + + // ==================== CURSOR BLINK ==================== + private int cursorCounter = 0; + + // ==================== CALLBACK ==================== + private SearchCallback callback; + + private final FontRenderer font = Minecraft.getMinecraft().fontRenderer; + + /** + * Callback interface for search/replace operations + */ + public interface SearchCallback { + String getText(); + void setText(String text); + void scrollToPosition(int position); + void setSelection(int start, int end); + int getGutterWidth(); + } + + public SearchReplaceBar() { + } + + public void setCallback(SearchCallback callback) { + this.callback = callback; + } + + /** + * Initialize bar position based on text area bounds + */ + public void initGui(int textAreaX, int textAreaY, int textAreaWidth) { + this.x = textAreaX; + this.y = textAreaY; + this.width = textAreaWidth; + } + + /** + * Open search bar (Ctrl+R) + */ + public void openSearch() { + visible = true; + showReplace = false; + searchFieldFocused = true; + replaceFieldFocused = false; + searchSelectionStart = 0; + searchSelectionEnd = searchText.length(); + searchCursor = searchText.length(); + updateMatches(); + } + + /** + * Open search+replace bar (Ctrl+Shift+R) + */ + public void openSearchReplace() { + visible = true; + showReplace = true; + searchFieldFocused = true; + replaceFieldFocused = false; + searchSelectionStart = 0; + searchSelectionEnd = searchText.length(); + searchCursor = searchText.length(); + updateMatches(); + } + + /** + * Close the search bar + */ + public void close() { + visible = false; + } + + /** + * Toggle visibility + */ + public void toggle() { + if (visible && !showReplace) { + close(); + } else { + openSearch(); + } + } + + /** + * Toggle replace mode + */ + public void toggleReplace() { + if (visible && showReplace) { + close(); + } else { + openSearchReplace(); + } + } + + public boolean isVisible() { + return visible; + } + + public boolean isSearchFocused() { + return visible && searchFieldFocused; + } + + public boolean isReplaceFocused() { + return visible && replaceFieldFocused; + } + + public boolean hasFocus() { + return visible && (searchFieldFocused || replaceFieldFocused); + } + + /** + * Get total height of the bar (for viewport offset) + */ + public int getTotalHeight() { + if (!visible) return 0; + return showReplace ? barHeight + replaceBarHeight : barHeight; + } + + /** + * Get all current matches for highlighting + */ + public List getMatches() { + return matches; + } + + /** + * Get current match index + */ + public int getCurrentMatchIndex() { + return currentMatchIndex; + } + + /** + * Check if a match is excluded + */ + public boolean isMatchExcluded(int index) { + return excludedMatches.contains(index); + } + + /** + * Update cursor blink counter + */ + public void updateCursor() { + cursorCounter++; + } + + // ==================== DRAWING ==================== + + /** + * Draw the search/replace bar + */ + public void draw(int mouseX, int mouseY) { + if (!visible) return; + + updateHoverStates(mouseX, mouseY); + + int totalHeight = getTotalHeight(); + Gui.drawRect(x, y, x + width, y + totalHeight, 0xFF3c3f41); + Gui.drawRect(x, y + totalHeight - 1, x + width, y + totalHeight, 0xFF515151); + + drawSearchRow(mouseX, mouseY); + + if (showReplace) { + drawReplaceRow(mouseX, mouseY); + } + } + + private void drawSearchRow(int mouseX, int mouseY) { + int rowY = y + padding; + int currentX = x + padding; + + drawTextField(currentX, rowY, textFieldWidth, textFieldHeight, + searchText, searchCursor, searchSelectionStart, searchSelectionEnd, + searchFieldFocused, "Search..."); + currentX += textFieldWidth + buttonSpacing + 4; + + String matchInfo = matches.isEmpty() ? "No matches" : + (currentMatchIndex + 1) + "/" + matches.size(); + int matchInfoWidth = font.getStringWidth(matchInfo); + font.drawString(matchInfo, currentX, rowY + 4, matches.isEmpty() ? 0xFF888888 : 0xFFcccccc); + currentX += matchInfoWidth + 8; + + drawToggleButton(currentX, rowY, "Cc", matchCase, hoverMatchCase, "Match Case"); + currentX += buttonSize + buttonSpacing; + + drawToggleButton(currentX, rowY, "W", wholeWord, hoverWholeWord, "Whole Word"); + currentX += buttonSize + buttonSpacing + 4; + + boolean prevEnabled = !matches.isEmpty(); + drawNavButton(currentX, rowY, "▲", prevEnabled, hoverPrev); + currentX += buttonSize + buttonSpacing; + + boolean nextEnabled = !matches.isEmpty(); + drawNavButton(currentX, rowY, "▼", nextEnabled, hoverNext); + currentX += buttonSize + buttonSpacing; + + int closeX = x + width - buttonSize - padding; + drawCloseButton(closeX, rowY, hoverClose); + } + + private void drawReplaceRow(int mouseX, int mouseY) { + int rowY = y + barHeight + 2; + int currentX = x + padding; + + drawTextField(currentX, rowY, textFieldWidth, textFieldHeight, + replaceText, replaceCursor, replaceSelectionStart, replaceSelectionEnd, + replaceFieldFocused, "Replace..."); + currentX += textFieldWidth + buttonSpacing + 4; + + drawActionButton(currentX, rowY, "Replace", hoverReplace, !matches.isEmpty()); + currentX += font.getStringWidth("Replace") + 12 + buttonSpacing; + + drawActionButton(currentX, rowY, "Replace All", hoverReplaceAll, !matches.isEmpty()); + currentX += font.getStringWidth("Replace All") + 12 + buttonSpacing; + + drawActionButton(currentX, rowY, "Exclude", hoverExclude, currentMatchIndex >= 0); + } + + private void drawTextField(int fieldX, int fieldY, int fieldWidth, int fieldHeight, + String text, int cursor, int selStart, int selEnd, + boolean focused, String placeholder) { + Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); + GL11.glPushMatrix(); + GL11.glColor4f(1, 1, 1, 1); + + float boxScaleX = fieldWidth / 32f; + GL11.glScalef(boxScaleX, 1, 1); + + if (focused) { + GL11.glColor4f(0.28f, 0.45f, 0.7f, 1f); + } else { + GL11.glColor4f(0.33f, 0.33f, 0.33f, 1f); + } + GuiUtil.drawTexturedModalRect(fieldX / boxScaleX, fieldY - 2, 32, 20, 0, 492); + GL11.glPopMatrix(); + + int textX = fieldX + 4; + int textY = fieldY + (fieldHeight - font.FONT_HEIGHT) / 2; + + GL11.glEnable(GL11.GL_SCISSOR_TEST); + GuiUtil.setScissorClip(fieldX + 2, fieldY, fieldWidth - 4, fieldHeight); + + if (text.isEmpty() && !focused) { + font.drawString(placeholder, textX, textY, 0xFF808080); + } else { + if (selStart != selEnd && focused) { + int startX = textX + font.getStringWidth(text.substring(0, Math.min(selStart, text.length()))); + int endX = textX + font.getStringWidth(text.substring(0, Math.min(selEnd, text.length()))); + Gui.drawRect(startX, textY - 1, endX, textY + font.FONT_HEIGHT, 0xFF2d5ca6); + } + + font.drawString(text, textX, textY, 0xFFe0e0e0); + + if (focused && (cursorCounter / 10) % 2 == 0) { + int cursorX = textX + font.getStringWidth(text.substring(0, Math.min(cursor, text.length()))); + Gui.drawRect(cursorX, textY - 1, cursorX + 1, textY + font.FONT_HEIGHT, 0xFFffffff); + } + } + + GL11.glDisable(GL11.GL_SCISSOR_TEST); + } + + private void drawToggleButton(int btnX, int btnY, String label, boolean active, boolean hovered, String tooltip) { + int bgColor = active ? 0xFF4a6da7 : (hovered ? 0xFF505050 : 0xFF3c3c3c); + Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + buttonSize, bgColor); + Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + 1, 0xFF606060); + Gui.drawRect(btnX, btnY + buttonSize - 1, btnX + buttonSize, btnY + buttonSize, 0xFF2a2a2a); + + int textColor = active ? 0xFFffffff : (hovered ? 0xFFcccccc : 0xFF999999); + int labelWidth = font.getStringWidth(label); + font.drawString(label, btnX + (buttonSize - labelWidth) / 2, btnY + (buttonSize - font.FONT_HEIGHT) / 2 + 1, textColor); + } + + private void drawNavButton(int btnX, int btnY, String arrow, boolean enabled, boolean hovered) { + int bgColor = !enabled ? 0xFF2a2a2a : (hovered ? 0xFF505050 : 0xFF3c3c3c); + Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + buttonSize, bgColor); + + int textColor = !enabled ? 0xFF555555 : (hovered ? 0xFFffffff : 0xFFaaaaaa); + int arrowWidth = font.getStringWidth(arrow); + font.drawString(arrow, btnX + (buttonSize - arrowWidth) / 2, btnY + (buttonSize - font.FONT_HEIGHT) / 2, textColor); + } + + private void drawCloseButton(int btnX, int btnY, boolean hovered) { + int bgColor = hovered ? 0xFFc75050 : 0xFF3c3c3c; + Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + buttonSize, bgColor); + + int textColor = hovered ? 0xFFffffff : 0xFFaaaaaa; + String xChar = "X"; + int xWidth = font.getStringWidth(xChar); + font.drawString(xChar, btnX + (buttonSize - xWidth) / 2, btnY + (buttonSize - font.FONT_HEIGHT) / 2 + 1, textColor); + } + + private void drawActionButton(int btnX, int btnY, String label, boolean hovered, boolean enabled) { + int btnWidth = font.getStringWidth(label) + 10; + int btnHeight = 14; + + int bgColor = !enabled ? 0xFF2a2a2a : (hovered ? 0xFF505050 : 0xFF3c3c3c); + Gui.drawRect(btnX, btnY + 1, btnX + btnWidth, btnY + btnHeight + 1, bgColor); + Gui.drawRect(btnX, btnY + 1, btnX + btnWidth, btnY + 2, 0xFF555555); + Gui.drawRect(btnX, btnY + btnHeight, btnX + btnWidth, btnY + btnHeight + 1, 0xFF2a2a2a); + + int textColor = !enabled ? 0xFF555555 : (hovered ? 0xFFffffff : 0xFFaaaaaa); + font.drawString(label, btnX + 5, btnY + (btnHeight - font.FONT_HEIGHT) / 2 + 2, textColor); + } + + // ==================== HOVER DETECTION ==================== + + private void updateHoverStates(int mouseX, int mouseY) { + int rowY = y + padding; + int currentX = x + padding; + + int searchFieldEndX = currentX + textFieldWidth; + currentX = searchFieldEndX + buttonSpacing + 4; + + String matchInfo = matches.isEmpty() ? "No matches" : (currentMatchIndex + 1) + "/" + matches.size(); + currentX += font.getStringWidth(matchInfo) + 8; + + int matchCaseX = currentX; + hoverMatchCase = isMouseOver(mouseX, mouseY, matchCaseX, rowY, buttonSize, buttonSize); + currentX += buttonSize + buttonSpacing; + + int wholeWordX = currentX; + hoverWholeWord = isMouseOver(mouseX, mouseY, wholeWordX, rowY, buttonSize, buttonSize); + currentX += buttonSize + buttonSpacing + 4; + + int prevX = currentX; + hoverPrev = isMouseOver(mouseX, mouseY, prevX, rowY, buttonSize, buttonSize); + currentX += buttonSize + buttonSpacing; + + int nextX = currentX; + hoverNext = isMouseOver(mouseX, mouseY, nextX, rowY, buttonSize, buttonSize); + + int closeX = x + width - buttonSize - padding; + hoverClose = isMouseOver(mouseX, mouseY, closeX, rowY, buttonSize, buttonSize); + + if (showReplace) { + int replaceRowY = y + barHeight + 2; + currentX = x + padding + textFieldWidth + buttonSpacing + 4; + + int replaceWidth = font.getStringWidth("Replace") + 12; + hoverReplace = isMouseOver(mouseX, mouseY, currentX, replaceRowY + 1, replaceWidth, 14); + currentX += replaceWidth + buttonSpacing; + + int replaceAllWidth = font.getStringWidth("Replace All") + 12; + hoverReplaceAll = isMouseOver(mouseX, mouseY, currentX, replaceRowY + 1, replaceAllWidth, 14); + currentX += replaceAllWidth + buttonSpacing; + + int excludeWidth = font.getStringWidth("Exclude") + 12; + hoverExclude = isMouseOver(mouseX, mouseY, currentX, replaceRowY + 1, excludeWidth, 14); + } else { + hoverReplace = hoverReplaceAll = hoverExclude = false; + } + } + + private boolean isMouseOver(int mouseX, int mouseY, int bx, int by, int bw, int bh) { + return mouseX >= bx && mouseX < bx + bw && mouseY >= by && mouseY < by + bh; + } + + // ==================== MOUSE INPUT ==================== + + /** + * Handle mouse click + * @return true if click was consumed + */ + public boolean mouseClicked(int mouseX, int mouseY, int button) { + if (!visible) return false; + + int totalHeight = getTotalHeight(); + if (mouseY < y || mouseY > y + totalHeight) return false; + + int rowY = y + padding; + int currentX = x + padding; + + if (isMouseOver(mouseX, mouseY, currentX, rowY - 2, textFieldWidth, textFieldHeight + 4)) { + searchFieldFocused = true; + replaceFieldFocused = false; + searchCursor = getTextCursorPosition(searchText, mouseX - currentX - 4); + searchSelectionStart = searchSelectionEnd = searchCursor; + return true; + } + currentX += textFieldWidth + buttonSpacing + 4; + + String matchInfo = matches.isEmpty() ? "No matches" : (currentMatchIndex + 1) + "/" + matches.size(); + currentX += font.getStringWidth(matchInfo) + 8; + + if (hoverMatchCase) { + matchCase = !matchCase; + updateMatches(); + return true; + } + currentX += buttonSize + buttonSpacing; + + if (hoverWholeWord) { + wholeWord = !wholeWord; + updateMatches(); + return true; + } + currentX += buttonSize + buttonSpacing + 4; + + if (hoverPrev && !matches.isEmpty()) { + goToPreviousMatch(); + return true; + } + currentX += buttonSize + buttonSpacing; + + if (hoverNext && !matches.isEmpty()) { + goToNextMatch(); + return true; + } + + if (hoverClose) { + close(); + return true; + } + + if (showReplace) { + int replaceRowY = y + barHeight + 2; + + if (isMouseOver(mouseX, mouseY, x + padding, replaceRowY - 2, textFieldWidth, textFieldHeight + 4)) { + searchFieldFocused = false; + replaceFieldFocused = true; + replaceCursor = getTextCursorPosition(replaceText, mouseX - x - padding - 4); + replaceSelectionStart = replaceSelectionEnd = replaceCursor; + return true; + } + + if (hoverReplace && !matches.isEmpty()) { + replaceCurrent(); + return true; + } + + if (hoverReplaceAll && !matches.isEmpty()) { + replaceAll(); + return true; + } + + if (hoverExclude && currentMatchIndex >= 0) { + excludeCurrentMatch(); + return true; + } + } + + return mouseY < y + totalHeight; + } + + private int getTextCursorPosition(String text, int clickX) { + if (text.isEmpty() || clickX <= 0) return 0; + + for (int i = 1; i <= text.length(); i++) { + int w = font.getStringWidth(text.substring(0, i)); + if (clickX < w) { + int prevW = font.getStringWidth(text.substring(0, i - 1)); + return clickX < (prevW + w) / 2 ? i - 1 : i; + } + } + return text.length(); + } + + // ==================== KEYBOARD INPUT ==================== + + /** + * Handle keyboard input + * @return true if input was consumed + */ + public boolean keyTyped(char c, int keyCode) { + if (!visible || (!searchFieldFocused && !replaceFieldFocused)) return false; + + if (keyCode == Keyboard.KEY_ESCAPE) { + close(); + return true; + } + + if (keyCode == Keyboard.KEY_TAB && showReplace) { + searchFieldFocused = !searchFieldFocused; + replaceFieldFocused = !replaceFieldFocused; + return true; + } + + if (keyCode == Keyboard.KEY_RETURN) { + if (replaceFieldFocused && !matches.isEmpty()) { + replaceCurrent(); + } else if (!matches.isEmpty()) { + goToNextMatch(); + } + return true; + } + + if (searchFieldFocused) { + handleTextFieldInput(c, keyCode, true); + } else if (replaceFieldFocused) { + handleTextFieldInput(c, keyCode, false); + } + + return true; + } + + private void handleTextFieldInput(char c, int keyCode, boolean isSearchField) { + String text = isSearchField ? searchText : replaceText; + int cursor = isSearchField ? searchCursor : replaceCursor; + int selStart = isSearchField ? searchSelectionStart : replaceSelectionStart; + int selEnd = isSearchField ? searchSelectionEnd : replaceSelectionEnd; + + boolean hasSelection = selStart != selEnd; + boolean ctrl = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); + boolean shift = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); + + if (ctrl && keyCode == Keyboard.KEY_A) { + selStart = 0; + selEnd = text.length(); + cursor = text.length(); + } + else if (ctrl && keyCode == Keyboard.KEY_C) { + if (hasSelection) { + String selected = text.substring(Math.min(selStart, selEnd), Math.max(selStart, selEnd)); + setClipboard(selected); + } + } + else if (ctrl && keyCode == Keyboard.KEY_X) { + if (hasSelection) { + String selected = text.substring(Math.min(selStart, selEnd), Math.max(selStart, selEnd)); + setClipboard(selected); + text = text.substring(0, Math.min(selStart, selEnd)) + text.substring(Math.max(selStart, selEnd)); + cursor = Math.min(selStart, selEnd); + selStart = selEnd = cursor; + } + } + else if (ctrl && keyCode == Keyboard.KEY_V) { + String clipboard = getClipboard(); + if (clipboard != null) { + clipboard = clipboard.replace("\n", "").replace("\r", ""); + if (hasSelection) { + text = text.substring(0, Math.min(selStart, selEnd)) + clipboard + text.substring(Math.max(selStart, selEnd)); + cursor = Math.min(selStart, selEnd) + clipboard.length(); + } else { + text = text.substring(0, cursor) + clipboard + text.substring(cursor); + cursor += clipboard.length(); + } + selStart = selEnd = cursor; + } + } + else if (keyCode == Keyboard.KEY_BACK) { + if (hasSelection) { + text = text.substring(0, Math.min(selStart, selEnd)) + text.substring(Math.max(selStart, selEnd)); + cursor = Math.min(selStart, selEnd); + } else if (cursor > 0) { + text = text.substring(0, cursor - 1) + text.substring(cursor); + cursor--; + } + selStart = selEnd = cursor; + } + else if (keyCode == Keyboard.KEY_DELETE) { + if (hasSelection) { + text = text.substring(0, Math.min(selStart, selEnd)) + text.substring(Math.max(selStart, selEnd)); + cursor = Math.min(selStart, selEnd); + } else if (cursor < text.length()) { + text = text.substring(0, cursor) + text.substring(cursor + 1); + } + selStart = selEnd = cursor; + } + else if (keyCode == Keyboard.KEY_LEFT) { + if (shift) { + if (cursor > 0) { + cursor--; + if (selStart == selEnd) { + selEnd = cursor + 1; + selStart = cursor; + } else if (cursor < selStart) { + selStart = cursor; + } else { + selEnd = cursor; + } + } + } else { + if (hasSelection) { + cursor = Math.min(selStart, selEnd); + } else if (cursor > 0) { + cursor--; + } + selStart = selEnd = cursor; + } + } + else if (keyCode == Keyboard.KEY_RIGHT) { + if (shift) { + if (cursor < text.length()) { + cursor++; + if (selStart == selEnd) { + selStart = cursor - 1; + selEnd = cursor; + } else if (cursor > selEnd) { + selEnd = cursor; + } else { + selStart = cursor; + } + } + } else { + if (hasSelection) { + cursor = Math.max(selStart, selEnd); + } else if (cursor < text.length()) { + cursor++; + } + selStart = selEnd = cursor; + } + } + else if (keyCode == Keyboard.KEY_HOME) { + if (shift) { + selEnd = selStart == selEnd ? cursor : selEnd; + selStart = 0; + } else { + selStart = selEnd = 0; + } + cursor = 0; + } + else if (keyCode == Keyboard.KEY_END) { + if (shift) { + selStart = selStart == selEnd ? cursor : selStart; + selEnd = text.length(); + } else { + selStart = selEnd = text.length(); + } + cursor = text.length(); + } + else if (ChatAllowedCharacters.isAllowedCharacter(c)) { + if (hasSelection) { + text = text.substring(0, Math.min(selStart, selEnd)) + c + text.substring(Math.max(selStart, selEnd)); + cursor = Math.min(selStart, selEnd) + 1; + } else { + text = text.substring(0, cursor) + c + text.substring(cursor); + cursor++; + } + selStart = selEnd = cursor; + } + + if (selStart > selEnd) { + int temp = selStart; + selStart = selEnd; + selEnd = temp; + } + + if (isSearchField) { + searchText = text; + searchCursor = cursor; + searchSelectionStart = selStart; + searchSelectionEnd = selEnd; + updateMatches(); + } else { + replaceText = text; + replaceCursor = cursor; + replaceSelectionStart = selStart; + replaceSelectionEnd = selEnd; + } + } + + private void setClipboard(String text) { + try { + java.awt.datatransfer.StringSelection selection = new java.awt.datatransfer.StringSelection(text); + java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null); + } catch (Exception ignored) {} + } + + private String getClipboard() { + try { + return (String) java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().getData(java.awt.datatransfer.DataFlavor.stringFlavor); + } catch (Exception e) { + return null; + } + } + + // ==================== SEARCH LOGIC ==================== + + /** + * Update matches based on current search text and options + */ + public void updateMatches() { + matches.clear(); + excludedMatches.clear(); + currentMatchIndex = -1; + + if (callback == null || searchText.isEmpty()) return; + + String sourceText = callback.getText(); + if (sourceText == null || sourceText.isEmpty()) return; + + String searchPattern = searchText; + + if (wholeWord) { + searchPattern = "\\b" + Pattern.quote(searchPattern) + "\\b"; + } else { + searchPattern = Pattern.quote(searchPattern); + } + + int flags = matchCase ? 0 : Pattern.CASE_INSENSITIVE; + + try { + Pattern pattern = Pattern.compile(searchPattern, flags); + Matcher matcher = pattern.matcher(sourceText); + + while (matcher.find()) { + matches.add(new int[]{matcher.start(), matcher.end()}); + } + } catch (Exception ignored) { + } + + if (!matches.isEmpty()) { + currentMatchIndex = 0; + navigateToCurrentMatch(); + } + } + + /** + * Navigate to next match + */ + public void goToNextMatch() { + if (matches.isEmpty()) return; + + int startIndex = currentMatchIndex; + do { + currentMatchIndex = (currentMatchIndex + 1) % matches.size(); + } while (excludedMatches.contains(currentMatchIndex) && currentMatchIndex != startIndex); + + navigateToCurrentMatch(); + } + + /** + * Navigate to previous match + */ + public void goToPreviousMatch() { + if (matches.isEmpty()) return; + + int startIndex = currentMatchIndex; + do { + currentMatchIndex = (currentMatchIndex - 1 + matches.size()) % matches.size(); + } while (excludedMatches.contains(currentMatchIndex) && currentMatchIndex != startIndex); + + navigateToCurrentMatch(); + } + + /** + * Navigate viewport to current match and select it + */ + private void navigateToCurrentMatch() { + if (callback == null || currentMatchIndex < 0 || currentMatchIndex >= matches.size()) return; + + int[] match = matches.get(currentMatchIndex); + callback.scrollToPosition(match[0]); + callback.setSelection(match[0], match[1]); + } + + // ==================== REPLACE LOGIC ==================== + + /** + * Replace current match + */ + public void replaceCurrent() { + if (callback == null || currentMatchIndex < 0 || matches.isEmpty()) return; + if (excludedMatches.contains(currentMatchIndex)) { + goToNextMatch(); + return; + } + + String sourceText = callback.getText(); + int[] match = matches.get(currentMatchIndex); + + String newText = sourceText.substring(0, match[0]) + replaceText + sourceText.substring(match[1]); + callback.setText(newText); + + updateMatches(); + + if (!matches.isEmpty()) { + int replacementEnd = match[0] + replaceText.length(); + for (int i = 0; i < matches.size(); i++) { + if (matches.get(i)[0] >= replacementEnd) { + currentMatchIndex = i; + break; + } + } + navigateToCurrentMatch(); + } + } + + /** + * Replace all matches + */ + public void replaceAll() { + if (callback == null || matches.isEmpty()) return; + + String sourceText = callback.getText(); + StringBuilder result = new StringBuilder(); + int lastEnd = 0; + + for (int i = 0; i < matches.size(); i++) { + if (excludedMatches.contains(i)) { + continue; + } + int[] match = matches.get(i); + result.append(sourceText, lastEnd, match[0]); + result.append(replaceText); + lastEnd = match[1]; + } + result.append(sourceText.substring(lastEnd)); + + callback.setText(result.toString()); + updateMatches(); + } + + /** + * Exclude current match from replacement + */ + public void excludeCurrentMatch() { + if (currentMatchIndex >= 0 && !excludedMatches.contains(currentMatchIndex)) { + excludedMatches.add(currentMatchIndex); + goToNextMatch(); + } + } + + // ==================== UTILITY ==================== + + /** + * Get search text for external highlighting + */ + public String getSearchText() { + return searchText; + } + + /** + * Check if bar consumes click at position + */ + public boolean isMouseOverBar(int mouseX, int mouseY) { + if (!visible) return false; + int totalHeight = getTotalHeight(); + return mouseX >= x && mouseX < x + width && mouseY >= y && mouseY < y + totalHeight; + } +} diff --git a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java index cb6c58e66..28d12b226 100644 --- a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java +++ b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java @@ -17,9 +17,14 @@ public class ScriptEditorKeys extends KeyPresetManager { public final KeyPreset FORMAT = add("Format Code").setDefaultState(Keyboard.KEY_F, true, false, false); public final KeyPreset TOGGLE_COMMENT = add("Toggle Comment").setDefaultState(Keyboard.KEY_SLASH, true, false, false); + + // Search/Replace + public final KeyPreset SEARCH = add("Search").setDefaultState(Keyboard.KEY_R, true, false, false); + public final KeyPreset SEARCH_REPLACE = add("Search & Replace").setDefaultState(Keyboard.KEY_R, true, false, true); public ScriptEditorKeys() { super("script_editor"); load(); } } + From c7bab53bdc460a358990017f84da98ff43d1b213 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 21:42:00 +0200 Subject: [PATCH 102/358] Huge bunch of SearcheReplaceBar fixes and QoL --- .../client/gui/util/GuiScriptTextArea.java | 64 ++++- .../gui/util/script/SearchReplaceBar.java | 264 +++++++++++++++--- 2 files changed, 290 insertions(+), 38 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 375dfb2e7..3a03ed5b0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -103,7 +103,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { KEYS_OVERLAY.viewButton.scale = 0.45f; KEYS_OVERLAY.viewButton.initGui(endX + xOffset, endY - 26); - // Initialize search bar + // Initialize search bar (preserves state across initGui calls) searchBar.initGui(x, y, width); searchBar.setCallback(new SearchReplaceBar.SearchCallback() { @Override @@ -120,12 +120,32 @@ public class GuiScriptTextArea extends GuiNpcTextField { public void scrollToPosition(int position) { // Find line containing position and scroll to it if (container == null || container.lines == null) return; + + // Calculate offset to account for search bar height + int searchBarOffset = searchBar.getTotalHeight(); + int effectiveHeight = height - searchBarOffset; + int visibleLines = effectiveHeight / container.lineHeight; + // Calculate how many lines the search bar covers + int linesHiddenBySRB = searchBarOffset > 0 ? (int) Math.ceil((double) searchBarOffset / container.lineHeight) : 0; + for (int i = 0; i < container.lines.size(); i++) { LineData ld = container.lines.get(i); if (position >= ld.start && position < ld.end) { - int visible = Math.max(1, container.visibleLines); + int visible = Math.max(1, visibleLines); int maxScroll = Math.max(0, container.linesCount - visible); - scroll.scrollToLine(i, visible, maxScroll); + int targetLine = i; + + // If search bar is visible and would hide this line, scroll down so it's visible + // The target line should appear below the search bar, not under it + int currentScroll = scroll.getScrolledLine(); + int firstVisibleLine = currentScroll + linesHiddenBySRB; + + if (searchBarOffset > 0 && targetLine < firstVisibleLine) { + // Force scroll so target line appears just below the search bar + scroll.setTargetScroll(Math.max(0, targetLine - linesHiddenBySRB), maxScroll); + } else { + scroll.scrollToLine(targetLine, visible, maxScroll); + } break; } } @@ -141,6 +161,23 @@ public class GuiScriptTextArea extends GuiNpcTextField { public int getGutterWidth() { return LINE_NUMBER_GUTTER_WIDTH; } + + @Override + public void unfocusMainEditor() { + // Save position but unfocus + active = false; + } + + @Override + public void focusMainEditor() { + active = true; + selection.markActivity(); + } + + @Override + public void onMatchesUpdated() { + // Called when matches change - could be used for UI updates + } }); } @@ -344,12 +381,21 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (matchStart < matchEnd) { int s = ClientProxy.Font.width(line.substring(0, matchStart)); int e = ClientProxy.Font.width(line.substring(0, matchEnd)) + 1; + boolean isExcluded = searchBar.isMatchExcluded(mi); // Current match gets brighter highlight, others get dimmer int highlightColor = (mi == currentMatchIdx) ? 0xBB4488ff : 0x662266aa; - if (searchBar.isMatchExcluded(mi)) { + if (isExcluded) { highlightColor = 0x33666666; // Dimmer for excluded matches } - drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, highlightColor); + int highlightX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + s; + int highlightEndX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + e; + drawRect(highlightX, posY, highlightEndX, posY + container.lineHeight, highlightColor); + + // Draw strikethrough line for excluded matches + if (isExcluded) { + int strikeY = posY + container.lineHeight / 2; + drawRect(highlightX, strikeY, highlightEndX, strikeY + 1, 0xFFaa4444); + } } } } @@ -614,6 +660,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { selection.reset(data.cursorPosition); undoing = false; scrollToCursor(); + searchBar.updateMatches(); }); // REDO: Restore last undone edit from redo list @@ -630,6 +677,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { selection.reset(data.cursorPosition); undoing = false; scrollToCursor(); + searchBar.updateMatches(); }); // FORMAT: Format/indent code @@ -1509,6 +1557,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { return; } + // If search bar is visible but click was outside it, unfocus the search bar + if (searchBar.isVisible()) { + searchBar.unfocus(); + } + if (KEYS_OVERLAY.showOverlay) { KEYS_OVERLAY.mouseClicked(xMouse, yMouse, mouseButton); return; @@ -1571,6 +1624,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { public void updateCursorCounter() { KEYS.tick(); searchBar.updateCursor(); + goToLineDialog.updateCursor(); ++this.cursorCounter; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index aca29e933..02e2849b5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -5,7 +5,6 @@ import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.Gui; import net.minecraft.util.ChatAllowedCharacters; import net.minecraft.util.ResourceLocation; -import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.util.GuiUtil; import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; import org.lwjgl.input.Keyboard; @@ -61,6 +60,9 @@ public class SearchReplaceBar { private int searchSelectionEnd = 0; private int replaceSelectionStart = 0; private int replaceSelectionEnd = 0; + // Text scroll offsets for when text exceeds field width + private int searchScrollOffset = 0; + private int replaceScrollOffset = 0; // ==================== MATCHES ==================== private List matches = new ArrayList<>(); @@ -79,6 +81,12 @@ public class SearchReplaceBar { // ==================== CURSOR BLINK ==================== private int cursorCounter = 0; + private long lastInputTime = 0; // For cursor blink pause on activity + + // ==================== DOUBLE/TRIPLE CLICK ==================== + private long lastClickTime = 0; + private int clickCount = 0; + private boolean lastClickWasSearch = true; // ==================== CALLBACK ==================== private SearchCallback callback; @@ -94,6 +102,9 @@ public class SearchReplaceBar { void scrollToPosition(int position); void setSelection(int start, int end); int getGutterWidth(); + void unfocusMainEditor(); + void focusMainEditor(); + void onMatchesUpdated(); // Called when matches change (for undo/redo sync) } public SearchReplaceBar() { @@ -104,12 +115,14 @@ public class SearchReplaceBar { } /** - * Initialize bar position based on text area bounds + * Initialize bar position based on text area bounds. + * Preserves existing state (search text, matches, etc.) across initGui calls. */ public void initGui(int textAreaX, int textAreaY, int textAreaWidth) { this.x = textAreaX; this.y = textAreaY; this.width = textAreaWidth; + // State is preserved - don't reset searchText, matches, visible, etc. } /** @@ -123,6 +136,8 @@ public class SearchReplaceBar { searchSelectionStart = 0; searchSelectionEnd = searchText.length(); searchCursor = searchText.length(); + markActivity(); + if (callback != null) callback.unfocusMainEditor(); updateMatches(); } @@ -137,14 +152,27 @@ public class SearchReplaceBar { searchSelectionStart = 0; searchSelectionEnd = searchText.length(); searchCursor = searchText.length(); + markActivity(); + if (callback != null) callback.unfocusMainEditor(); updateMatches(); } /** - * Close the search bar + * Close the search bar and restore focus to main editor */ public void close() { visible = false; + searchFieldFocused = false; + replaceFieldFocused = false; + if (callback != null) callback.focusMainEditor(); + } + + /** + * Unfocus the search bar (when clicking main editor) + */ + public void unfocus() { + searchFieldFocused = false; + replaceFieldFocused = false; } /** @@ -221,6 +249,27 @@ public class SearchReplaceBar { cursorCounter++; } + /** + * Mark activity to pause cursor blinking + */ + private void markActivity() { + lastInputTime = System.currentTimeMillis(); + } + + /** + * Check if there was recent input (cursor stays visible during typing) + */ + private boolean hadRecentInput() { + return System.currentTimeMillis() - lastInputTime < 500; + } + + /** + * Check if cursor should be visible (solid during typing, blinking otherwise) + */ + private boolean shouldShowCursor() { + return hadRecentInput() || (cursorCounter / 10) % 2 == 0; + } + // ==================== DRAWING ==================== /** @@ -245,16 +294,17 @@ public class SearchReplaceBar { private void drawSearchRow(int mouseX, int mouseY) { int rowY = y + padding; int currentX = x + padding; - + textFieldWidth=100; drawTextField(currentX, rowY, textFieldWidth, textFieldHeight, searchText, searchCursor, searchSelectionStart, searchSelectionEnd, - searchFieldFocused, "Search..."); + searchFieldFocused, "Search...", searchScrollOffset, true); currentX += textFieldWidth + buttonSpacing + 4; - String matchInfo = matches.isEmpty() ? "No matches" : + // "0 results" in red, otherwise "X/Y" in normal color + String matchInfo = matches.isEmpty() ? "0 results" : (currentMatchIndex + 1) + "/" + matches.size(); int matchInfoWidth = font.getStringWidth(matchInfo); - font.drawString(matchInfo, currentX, rowY + 4, matches.isEmpty() ? 0xFF888888 : 0xFFcccccc); + font.drawString(matchInfo, currentX, rowY + 4, matches.isEmpty() ? 0xFFff6666 : 0xFFcccccc); currentX += matchInfoWidth + 8; drawToggleButton(currentX, rowY, "Cc", matchCase, hoverMatchCase, "Match Case"); @@ -281,7 +331,7 @@ public class SearchReplaceBar { drawTextField(currentX, rowY, textFieldWidth, textFieldHeight, replaceText, replaceCursor, replaceSelectionStart, replaceSelectionEnd, - replaceFieldFocused, "Replace..."); + replaceFieldFocused, "Replace...", replaceScrollOffset, false); currentX += textFieldWidth + buttonSpacing + 4; drawActionButton(currentX, rowY, "Replace", hoverReplace, !matches.isEmpty()); @@ -295,24 +345,39 @@ public class SearchReplaceBar { private void drawTextField(int fieldX, int fieldY, int fieldWidth, int fieldHeight, String text, int cursor, int selStart, int selEnd, - boolean focused, String placeholder) { + boolean focused, String placeholder, int scrollOffset, boolean isSearchField) { Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); GL11.glPushMatrix(); GL11.glColor4f(1, 1, 1, 1); - - float boxScaleX = fieldWidth / 32f; - GL11.glScalef(boxScaleX, 1, 1); + + float boxScaleX = fieldWidth / 32f, boxScaleY = 1.5f; + GL11.glScalef(boxScaleX, boxScaleY, 1); if (focused) { GL11.glColor4f(0.28f, 0.45f, 0.7f, 1f); } else { GL11.glColor4f(0.33f, 0.33f, 0.33f, 1f); } - GuiUtil.drawTexturedModalRect(fieldX / boxScaleX, fieldY - 2, 32, 20, 0, 492); + GuiUtil.drawTexturedModalRect(fieldX / boxScaleX, (fieldY - 16) / boxScaleY, 32, 20, 0, 492); GL11.glPopMatrix(); int textX = fieldX + 4; int textY = fieldY + (fieldHeight - font.FONT_HEIGHT) / 2; + int visibleWidth = fieldWidth - 8; + + // Calculate scroll offset to keep cursor visible + int cursorPosX = font.getStringWidth(text.substring(0, Math.min(cursor, text.length()))); + if (cursorPosX - scrollOffset > visibleWidth - 2) { + scrollOffset = cursorPosX - visibleWidth + 2; + } else if (cursorPosX - scrollOffset < 0) { + scrollOffset = Math.max(0, cursorPosX); + } + // Update stored scroll offset + if (isSearchField) { + searchScrollOffset = scrollOffset; + } else { + replaceScrollOffset = scrollOffset; + } GL11.glEnable(GL11.GL_SCISSOR_TEST); GuiUtil.setScissorClip(fieldX + 2, fieldY, fieldWidth - 4, fieldHeight); @@ -320,17 +385,20 @@ public class SearchReplaceBar { if (text.isEmpty() && !focused) { font.drawString(placeholder, textX, textY, 0xFF808080); } else { + // Draw selection highlight if (selStart != selEnd && focused) { - int startX = textX + font.getStringWidth(text.substring(0, Math.min(selStart, text.length()))); - int endX = textX + font.getStringWidth(text.substring(0, Math.min(selEnd, text.length()))); + int startX = textX + font.getStringWidth(text.substring(0, Math.min(selStart, text.length()))) - scrollOffset; + int endX = textX + font.getStringWidth(text.substring(0, Math.min(selEnd, text.length()))) - scrollOffset; Gui.drawRect(startX, textY - 1, endX, textY + font.FONT_HEIGHT, 0xFF2d5ca6); } - font.drawString(text, textX, textY, 0xFFe0e0e0); + // Draw text with scroll offset + font.drawString(text, textX - scrollOffset, textY, 0xFFe0e0e0); - if (focused && (cursorCounter / 10) % 2 == 0) { - int cursorX = textX + font.getStringWidth(text.substring(0, Math.min(cursor, text.length()))); - Gui.drawRect(cursorX, textY - 1, cursorX + 1, textY + font.FONT_HEIGHT, 0xFFffffff); + // Draw cursor (stays visible during typing) + if (focused && shouldShowCursor()) { + int cursorDrawX = textX + cursorPosX - scrollOffset; + Gui.drawRect(cursorDrawX, textY - 1, cursorDrawX + 1, textY + font.FONT_HEIGHT, 0xFFffffff); } } @@ -389,7 +457,7 @@ public class SearchReplaceBar { int searchFieldEndX = currentX + textFieldWidth; currentX = searchFieldEndX + buttonSpacing + 4; - String matchInfo = matches.isEmpty() ? "No matches" : (currentMatchIndex + 1) + "/" + matches.size(); + String matchInfo = matches.isEmpty() ? "0 results" : (currentMatchIndex + 1) + "/" + matches.size(); currentX += font.getStringWidth(matchInfo) + 8; int matchCaseX = currentX; @@ -436,28 +504,61 @@ public class SearchReplaceBar { // ==================== MOUSE INPUT ==================== /** - * Handle mouse click + * Handle mouse click - supports double-click for word selection, triple-click for all * @return true if click was consumed */ public boolean mouseClicked(int mouseX, int mouseY, int button) { if (!visible) return false; + if (button != 0) return false; // Only handle left clicks int totalHeight = getTotalHeight(); - if (mouseY < y || mouseY > y + totalHeight) return false; + // If click is outside the bar, don't consume it (let main editor handle) + if (mouseX < x || mouseX > x + width || mouseY < y || mouseY > y + totalHeight) { + return false; + } int rowY = y + padding; int currentX = x + padding; - if (isMouseOver(mouseX, mouseY, currentX, rowY - 2, textFieldWidth, textFieldHeight + 4)) { + // Handle click timing for double/triple click + long now = System.currentTimeMillis(); + boolean isSearchFieldClick = isMouseOver(mouseX, mouseY, currentX, rowY - 2, textFieldWidth, textFieldHeight + 4); + boolean isReplaceFieldClick = showReplace && isMouseOver(mouseX, mouseY, x + padding, y + barHeight, textFieldWidth, textFieldHeight + 4); + + if (isSearchFieldClick) { + boolean sameField = lastClickWasSearch; + if (sameField && now - lastClickTime < 300) { + clickCount++; + } else { + clickCount = 1; + } + lastClickTime = now; + lastClickWasSearch = true; + searchFieldFocused = true; replaceFieldFocused = false; - searchCursor = getTextCursorPosition(searchText, mouseX - currentX - 4); - searchSelectionStart = searchSelectionEnd = searchCursor; + markActivity(); + + if (clickCount == 1) { + // Single click - position cursor + int clickX = mouseX - currentX - 4 + searchScrollOffset; + searchCursor = getTextCursorPosition(searchText, clickX); + searchSelectionStart = searchSelectionEnd = searchCursor; + } else if (clickCount == 2) { + // Double click - select word + selectWordAt(searchText, getTextCursorPosition(searchText, mouseX - currentX - 4 + searchScrollOffset), true); + } else if (clickCount >= 3) { + // Triple click - select all + searchSelectionStart = 0; + searchSelectionEnd = searchText.length(); + searchCursor = searchText.length(); + clickCount = 0; + } return true; } currentX += textFieldWidth + buttonSpacing + 4; - String matchInfo = matches.isEmpty() ? "No matches" : (currentMatchIndex + 1) + "/" + matches.size(); + String matchInfo = matches.isEmpty() ? "0 results" : (currentMatchIndex + 1) + "/" + matches.size(); currentX += font.getStringWidth(matchInfo) + 8; if (hoverMatchCase) { @@ -493,11 +594,33 @@ public class SearchReplaceBar { if (showReplace) { int replaceRowY = y + barHeight + 2; - if (isMouseOver(mouseX, mouseY, x + padding, replaceRowY - 2, textFieldWidth, textFieldHeight + 4)) { + if (isReplaceFieldClick || isMouseOver(mouseX, mouseY, x + padding, replaceRowY - 2, textFieldWidth, textFieldHeight + 4)) { + // Handle click timing for replace field + boolean sameField = !lastClickWasSearch; + if (sameField && now - lastClickTime < 300) { + clickCount++; + } else { + clickCount = 1; + } + lastClickTime = now; + lastClickWasSearch = false; + searchFieldFocused = false; replaceFieldFocused = true; - replaceCursor = getTextCursorPosition(replaceText, mouseX - x - padding - 4); - replaceSelectionStart = replaceSelectionEnd = replaceCursor; + markActivity(); + + if (clickCount == 1) { + int clickX = mouseX - x - padding - 4 + replaceScrollOffset; + replaceCursor = getTextCursorPosition(replaceText, clickX); + replaceSelectionStart = replaceSelectionEnd = replaceCursor; + } else if (clickCount == 2) { + selectWordAt(replaceText, getTextCursorPosition(replaceText, mouseX - x - padding - 4 + replaceScrollOffset), false); + } else if (clickCount >= 3) { + replaceSelectionStart = 0; + replaceSelectionEnd = replaceText.length(); + replaceCursor = replaceText.length(); + clickCount = 0; + } return true; } @@ -517,7 +640,8 @@ public class SearchReplaceBar { } } - return mouseY < y + totalHeight; + // Click was within bar bounds but not on any control - consume it to prevent MTA from getting focus + return true; } private int getTextCursorPosition(String text, int clickX) { @@ -533,6 +657,40 @@ public class SearchReplaceBar { return text.length(); } + /** + * Select word at cursor position (for double-click) + */ + private void selectWordAt(String text, int pos, boolean isSearchField) { + if (text.isEmpty()) return; + + // Find word boundaries + int start = pos; + int end = pos; + + // Expand left + while (start > 0 && isWordChar(text.charAt(start - 1))) { + start--; + } + // Expand right + while (end < text.length() && isWordChar(text.charAt(end))) { + end++; + } + + if (isSearchField) { + searchSelectionStart = start; + searchSelectionEnd = end; + searchCursor = end; + } else { + replaceSelectionStart = start; + replaceSelectionEnd = end; + replaceCursor = end; + } + } + + private boolean isWordChar(char c) { + return Character.isLetterOrDigit(c) || c == '_'; + } + // ==================== KEYBOARD INPUT ==================== /** @@ -542,6 +700,18 @@ public class SearchReplaceBar { public boolean keyTyped(char c, int keyCode) { if (!visible || (!searchFieldFocused && !replaceFieldFocused)) return false; + // Check modifier keys - don't let them trigger first-match issue + boolean ctrl = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); + boolean shift = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); + boolean alt = Keyboard.isKeyDown(Keyboard.KEY_LMENU) || Keyboard.isKeyDown(Keyboard.KEY_RMENU); + + // Ignore lone modifier key presses + if (keyCode == Keyboard.KEY_LCONTROL || keyCode == Keyboard.KEY_RCONTROL || + keyCode == Keyboard.KEY_LSHIFT || keyCode == Keyboard.KEY_RSHIFT || + keyCode == Keyboard.KEY_LMENU || keyCode == Keyboard.KEY_RMENU) { + return true; // Consume but don't process + } + if (keyCode == Keyboard.KEY_ESCAPE) { close(); return true; @@ -550,11 +720,18 @@ public class SearchReplaceBar { if (keyCode == Keyboard.KEY_TAB && showReplace) { searchFieldFocused = !searchFieldFocused; replaceFieldFocused = !replaceFieldFocused; + markActivity(); return true; } + // Enter/Shift+Enter for next/previous match if (keyCode == Keyboard.KEY_RETURN) { - if (replaceFieldFocused && !matches.isEmpty()) { + if (shift) { + // Shift+Enter = previous match + if (!matches.isEmpty()) { + goToPreviousMatch(); + } + } else if (replaceFieldFocused && !matches.isEmpty()) { replaceCurrent(); } else if (!matches.isEmpty()) { goToNextMatch(); @@ -562,6 +739,16 @@ public class SearchReplaceBar { return true; } + // Up/Down arrows cycle through matches when SRB is focused + if (keyCode == Keyboard.KEY_UP && !matches.isEmpty()) { + goToPreviousMatch(); + return true; + } + if (keyCode == Keyboard.KEY_DOWN && !matches.isEmpty()) { + goToNextMatch(); + return true; + } + if (searchFieldFocused) { handleTextFieldInput(c, keyCode, true); } else if (replaceFieldFocused) { @@ -713,6 +900,9 @@ public class SearchReplaceBar { selEnd = temp; } + // Mark activity on any input to keep cursor visible + markActivity(); + if (isSearchField) { searchText = text; searchCursor = cursor; @@ -752,10 +942,16 @@ public class SearchReplaceBar { excludedMatches.clear(); currentMatchIndex = -1; - if (callback == null || searchText.isEmpty()) return; + if (callback == null || searchText.isEmpty()) { + if (callback != null) callback.onMatchesUpdated(); + return; + } String sourceText = callback.getText(); - if (sourceText == null || sourceText.isEmpty()) return; + if (sourceText == null || sourceText.isEmpty()) { + callback.onMatchesUpdated(); + return; + } String searchPattern = searchText; @@ -781,6 +977,8 @@ public class SearchReplaceBar { currentMatchIndex = 0; navigateToCurrentMatch(); } + + callback.onMatchesUpdated(); } /** From 825f3b4a39437b49a37d3e7d9dd205a9ad7d5194 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 22:38:26 +0200 Subject: [PATCH 103/358] Script Editor GoToLine GUI --- .../client/gui/util/GuiScriptTextArea.java | 86 ++- .../gui/util/script/GoToLineDialog.java | 614 ++++++++++++++++++ .../client/key/impl/ScriptEditorKeys.java | 3 + 3 files changed, 702 insertions(+), 1 deletion(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 3a03ed5b0..fe9274fed 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -77,6 +77,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== SEARCH/REPLACE ==================== private final SearchReplaceBar searchBar = new SearchReplaceBar(); + + // ==================== GO TO LINE ==================== + private final GoToLineDialog goToLineDialog; // ==================== CONSTRUCTOR ==================== @@ -90,6 +93,54 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.undoing = true; this.setText(text); this.undoing = false; + + // Initialize Go To Line dialog with callback + this.goToLineDialog = new GoToLineDialog(new GoToLineDialog.GoToLineCallback() { + @Override + public int getLineCount() { + return container != null ? container.linesCount : 0; + } + + @Override + public int getColumnCount(int lineIndex) { + if (container == null || container.lines == null || lineIndex < 0 || lineIndex >= container.lines.size()) { + return 0; + } + LineData ld = container.lines.get(lineIndex); + return ld.end - ld.start; + } + + @Override + public void goToLineColumn(int line, int column) { + if (container == null || container.lines == null) return; + + // Convert 1-indexed line to 0-indexed + int lineIdx = line - 1; + if (lineIdx < 0 || lineIdx >= container.lines.size()) return; + + LineData ld = container.lines.get(lineIdx); + int lineLength = ld.end - ld.start; + + // Convert 1-indexed column to 0-indexed, clamp to line length + int col = Math.max(0, Math.min(column - 1, lineLength)); + int position = ld.start + col; + + // Set cursor position + selection.reset(position); + + // Scroll to make the line visible + int visible = height / (container != null ? container.lineHeight : 12); + int maxScroll = Math.max(0, container.linesCount - visible); + scroll.scrollToLine(lineIdx, visible, maxScroll); + } + + @Override + public void onDialogClose() { + active = true; + selection.markActivity(); + } + }); + initializeKeyBindings(); initGui(); } @@ -179,6 +230,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Called when matches change - could be used for UI updates } }); + + // Initialize Go To Line dialog + goToLineDialog.initGui(x, y, width); } // ==================== RENDERING ==================== @@ -481,6 +535,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Draw search/replace bar (overlays viewport) searchBar.draw(xMouse, yMouse); + // Draw go to line dialog (overlays everything) + goToLineDialog.draw(xMouse, yMouse); + KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); } @@ -752,6 +809,17 @@ public class GuiScriptTextArea extends GuiNpcTextField { return; searchBar.toggleReplace(); }); + + // GO_TO_LINE: Open go to line dialog (Ctrl+G) + KEYS.GO_TO_LINE.setTask(e -> { + if (!e.isPress() || !active || !isEnabled()) + return; + // Close search bar if open + if (searchBar.isVisible()) { + searchBar.close(); + } + goToLineDialog.toggle(); + }); } // ==================== KEYBOARD INPUT HANDLING ==================== @@ -764,6 +832,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { public boolean textboxKeyTyped(char c, int i) { KEYS_OVERLAY.keyTyped(c, i); + // Handle Go To Line dialog input first if it has focus + if (goToLineDialog.hasFocus()) { + if (goToLineDialog.keyTyped(c, i)) { + return true; + } + } + // Handle search bar input first if it has focus if (searchBar.hasFocus()) { if (searchBar.keyTyped(c, i)) { @@ -1552,6 +1627,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== MOUSE HANDLING ==================== public void mouseClicked(int xMouse, int yMouse, int mouseButton) { + // Check go to line dialog clicks first + if (goToLineDialog.isVisible() && goToLineDialog.mouseClicked(xMouse, yMouse, mouseButton)) { + return; + } + // Check search bar clicks first if (searchBar.isVisible() && searchBar.mouseClicked(xMouse, yMouse, mouseButton)) { return; @@ -1622,7 +1702,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Called from GuiScreen.updateScreen() public void updateCursorCounter() { - KEYS.tick(); + // Only process KeyPresets if search bar and go-to-line dialog don't have focus + // This prevents COPY, PASTE, UNDO, etc. from firing when typing in dialogs + if (!searchBar.hasFocus() && !goToLineDialog.hasFocus()) { + KEYS.tick(); + } searchBar.updateCursor(); goToLineDialog.updateCursor(); ++this.cursorCounter; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java new file mode 100644 index 000000000..84329cad0 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java @@ -0,0 +1,614 @@ +package noppes.npcs.client.gui.util.script; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.Gui; +import net.minecraft.util.ChatAllowedCharacters; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.gui.util.GuiUtil; +import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; +import org.lwjgl.input.Keyboard; +import org.lwjgl.opengl.GL11; + +/** + * Go To Line:Column dialog for the script editor. + * Provides IntelliJ-like "Go to Line" functionality (Ctrl+G). + * + * Input format: "line" or "line:column" + * Example: "42" goes to line 42, column 1 + * Example: "42:15" goes to line 42, column 15 + */ +public class GoToLineDialog { + + // ==================== TEXTURE ==================== + public static final ResourceLocation TEXTURE = OverlayKeyPresetViewer.TEXTURE; + + // ==================== DIMENSIONS ==================== + private int x, y, width; + private int dialogWidth = 200; + private int dialogHeight = 51; + private int textFieldWidth = 180; + private int textFieldHeight = 16; + private int padding = 8; + + // ==================== STATE ==================== + private boolean visible = false; + private boolean focused = true; + + // ==================== TEXT FIELD ==================== + private String inputText = ""; + private int cursor = 0; + private int selectionStart = 0; + private int selectionEnd = 0; + private int scrollOffset = 0; + + // ==================== CURSOR BLINK ==================== + private int cursorCounter = 0; + private long lastInputTime = 0; + + // ==================== CALLBACK ==================== + private GoToLineCallback callback; + + private final FontRenderer font = Minecraft.getMinecraft().fontRenderer; + + /** + * Callback interface for go-to-line operations + */ + public interface GoToLineCallback { + /** Get total line count */ + int getLineCount(); + + /** Get the column count for a specific line (0-indexed) */ + int getColumnCount(int lineIndex); + + /** Navigate to specific line and column (both 1-indexed for user display) */ + void goToLineColumn(int line, int column); + + /** Called when dialog closes - should restore focus to editor */ + void onDialogClose(); + } + + /** + * Initialize the dialog with callback + */ + public GoToLineDialog(GoToLineCallback callback) { + this.callback = callback; + } + + /** + * Initialize/update position for the dialog + */ + public void initGui(int guiX, int guiY, int guiWidth) { + this.width = guiWidth; + // Center the dialog horizontally + this.x = guiX + (guiWidth - dialogWidth) / 2; + // Position near top of editor + this.y = guiY + 40; + } + + // ==================== VISIBILITY ==================== + + public boolean isVisible() { + return visible; + } + + public void show() { + visible = true; + focused = true; + inputText = ""; + cursor = 0; + selectionStart = 0; + selectionEnd = 0; + scrollOffset = 0; + markActivity(); + } + + public void close() { + visible = false; + focused = false; + if (callback != null) { + callback.onDialogClose(); + } + } + + public void toggle() { + if (visible) { + close(); + } else { + show(); + } + } + + public boolean hasFocus() { + return visible && focused; + } + + // ==================== RENDERING ==================== + + public void draw(int mouseX, int mouseY) { + if (!visible) + return; + + ParseResult result = null; + if (!inputText.isEmpty()) + result = parseInput(inputText); + + GL11.glPushMatrix(); + GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + + // Draw dialog background with shadow + drawDialogBackground(result != null && !result.valid); + + // Draw title + String title = "Go to Line"; + font.drawString(title, x + padding + 1, y + padding + 4, 0xFFe0e0e0); + + // Draw hint text showing valid range + int lineCount = callback != null ? callback.getLineCount() : 0; + String hint = "[" + lineCount + " lines]"; + int hintWidth = font.getStringWidth(hint); + dialogWidth = 200; + font.drawString(hint, x + dialogWidth - padding - hintWidth, y + padding + 4, 0xFF888888); + + + // Draw text field + int fieldX = x + padding + 0; + int fieldY = y + padding + 14 + 5; + textFieldWidth = dialogWidth - padding * 2; + drawTextField(fieldX, fieldY, textFieldWidth, textFieldHeight, inputText, cursor, + selectionStart, selectionEnd, scrollOffset, true); + + if (inputText.isEmpty()) + font.drawString("[Line][:Column]", x + padding + 6, y + padding + 23, 0xFF888888); + + // Draw error/validation hint if input is invalid + if (result != null && !result.valid) + font.drawString(result.error, x + padding + 2, y + dialogHeight - 3, 0xFFff6666); + + GL11.glDisable(GL11.GL_BLEND); + GL11.glPopMatrix(); + } + + private void drawDialogBackground(boolean resultInvalid) { + int dialogHeight = this.dialogHeight; + if (resultInvalid) + dialogHeight += 5; // Pad by 6 + + int y = this.y + 5; + // Draw shadow + Gui.drawRect(x + 2, y + 2, x + dialogWidth + 2, y + dialogHeight + 2, 0x80000000); + + // Draw main background + Gui.drawRect(x, y, x + dialogWidth, y + dialogHeight, 0xFF2d2d30); + + // Draw border + Gui.drawRect(x, y, x + dialogWidth, y + 1, 0xFF3c3c3c); + Gui.drawRect(x, y + dialogHeight - 1, x + dialogWidth, y + dialogHeight, 0xFF3c3c3c); + Gui.drawRect(x, y, x + 1, y + dialogHeight, 0xFF3c3c3c); + Gui.drawRect(x + dialogWidth - 1, y, x + dialogWidth, y + dialogHeight, 0xFF3c3c3c); + } + + private void drawTextField(int fx, int fy, int fw, int fh, String text, int cursor, + int selStart, int selEnd, int scrollOff, boolean isFocused) { + // Draw text field background + Gui.drawRect(fx, fy, fx + fw, fy + fh, 0xFF1e1e1e); + Gui.drawRect(fx, fy, fx + fw, fy + 1, isFocused ? 0xFF007acc : 0xFF3c3c3c); + Gui.drawRect(fx, fy + fh - 1, fx + fw, fy + fh, isFocused ? 0xFF007acc : 0xFF3c3c3c); + Gui.drawRect(fx, fy, fx + 1, fy + fh, isFocused ? 0xFF007acc : 0xFF3c3c3c); + Gui.drawRect(fx + fw - 1, fy, fx + fw, fy + fh, isFocused ? 0xFF007acc : 0xFF3c3c3c); + + // Calculate visible area + int textX = fx + 4; + int textY = fy + (fh - 8) / 2; + int visibleWidth = fw - 8; + + // Apply scroll offset + String visibleText = text; + int adjustedCursor = cursor; + int adjustedSelStart = selStart; + int adjustedSelEnd = selEnd; + + if (scrollOff > 0 && scrollOff < text.length()) { + visibleText = text.substring(scrollOff); + adjustedCursor = cursor - scrollOff; + adjustedSelStart = Math.max(0, selStart - scrollOff); + adjustedSelEnd = Math.max(0, selEnd - scrollOff); + } + + // Draw selection highlight + if (adjustedSelStart != adjustedSelEnd) { + int minSel = Math.min(adjustedSelStart, adjustedSelEnd); + int maxSel = Math.max(adjustedSelStart, adjustedSelEnd); + String beforeSel = visibleText.substring(0, Math.min(minSel, visibleText.length())); + String inSel = visibleText.substring(Math.min(minSel, visibleText.length()), + Math.min(maxSel, visibleText.length())); + int selX = textX + font.getStringWidth(beforeSel); + int selW = font.getStringWidth(inSel); + Gui.drawRect(selX, textY - 1, selX + selW, textY + 9, 0xFF264f78); + } + + // Draw text + font.drawString(visibleText, textX, textY, 0xFFe0e0e0); + + // Draw cursor + if (isFocused && shouldShowCursor()) { + String beforeCursor = adjustedCursor > 0 && adjustedCursor <= visibleText.length() + ? visibleText.substring(0, adjustedCursor) : ""; + int cursorX = textX + font.getStringWidth(beforeCursor); + Gui.drawRect(cursorX, textY - 1, cursorX + 1, textY + 9, 0xFFe0e0e0); + } + } + + // ==================== INPUT HANDLING ==================== + + public boolean mouseClicked(int mouseX, int mouseY, int button) { + if (!visible) + return false; + + // Check if click is inside dialog + if (mouseX >= x && mouseX < x + dialogWidth && mouseY >= y && mouseY < y + dialogHeight) { + // Check if click is in text field + int fieldX = x + padding; + int fieldY = y + padding + 14; + if (mouseX >= fieldX && mouseX < fieldX + textFieldWidth && + mouseY >= fieldY && mouseY < fieldY + textFieldHeight) { + focused = true; + // Position cursor based on click + int clickX = mouseX - fieldX - 4; + String visibleText = scrollOffset > 0 && scrollOffset < inputText.length() + ? inputText.substring(scrollOffset) : inputText; + int newCursor = getCharIndexAtX(visibleText, clickX) + scrollOffset; + cursor = Math.max(0, Math.min(newCursor, inputText.length())); + selectionStart = cursor; + selectionEnd = cursor; + markActivity(); + } + return true; + } + + // Click outside dialog - close it + close(); + return false; + } + + private int getCharIndexAtX(String text, int targetX) { + int x = 0; + for (int i = 0; i < text.length(); i++) { + int charWidth = font.getCharWidth(text.charAt(i)); + if (x + charWidth / 2 > targetX) { + return i; + } + x += charWidth; + } + return text.length(); + } + + public boolean keyTyped(char c, int keyCode) { + if (!visible || !focused) + return false; + + boolean ctrl = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); + boolean shift = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); + + // Escape - close dialog + if (keyCode == Keyboard.KEY_ESCAPE) { + close(); + return true; + } + + // Enter - execute go to line + if (keyCode == Keyboard.KEY_RETURN || keyCode == Keyboard.KEY_NUMPADENTER) { + executeGoToLine(); + return true; + } + + // Select all (Ctrl+A) + if (ctrl && keyCode == Keyboard.KEY_A) { + selectionStart = 0; + selectionEnd = inputText.length(); + cursor = inputText.length(); + markActivity(); + return true; + } + + // Copy (Ctrl+C) + if (ctrl && keyCode == Keyboard.KEY_C) { + if (selectionStart != selectionEnd) { + int minSel = Math.min(selectionStart, selectionEnd); + int maxSel = Math.max(selectionStart, selectionEnd); + noppes.npcs.NoppesStringUtils.setClipboardContents(inputText.substring(minSel, maxSel)); + } + return true; + } + + // Cut (Ctrl+X) + if (ctrl && keyCode == Keyboard.KEY_X) { + if (selectionStart != selectionEnd) { + int minSel = Math.min(selectionStart, selectionEnd); + int maxSel = Math.max(selectionStart, selectionEnd); + noppes.npcs.NoppesStringUtils.setClipboardContents(inputText.substring(minSel, maxSel)); + inputText = inputText.substring(0, minSel) + inputText.substring(maxSel); + cursor = minSel; + selectionStart = cursor; + selectionEnd = cursor; + updateScrollOffset(); + markActivity(); + } + return true; + } + + // Paste (Ctrl+V) + if (ctrl && keyCode == Keyboard.KEY_V) { + String clipboard = noppes.npcs.NoppesStringUtils.getClipboardContents(); + if (clipboard != null) { + // Only allow digits and colon + StringBuilder filtered = new StringBuilder(); + for (char ch : clipboard.toCharArray()) { + if (Character.isDigit(ch) || ch == ':') { + filtered.append(ch); + } + } + insertText(filtered.toString()); + } + return true; + } + + // Backspace + if (keyCode == Keyboard.KEY_BACK) { + if (selectionStart != selectionEnd) { + deleteSelection(); + } else if (cursor > 0) { + inputText = inputText.substring(0, cursor - 1) + inputText.substring(cursor); + cursor--; + selectionStart = cursor; + selectionEnd = cursor; + updateScrollOffset(); + } + markActivity(); + return true; + } + + // Delete + if (keyCode == Keyboard.KEY_DELETE) { + if (selectionStart != selectionEnd) { + deleteSelection(); + } else if (cursor < inputText.length()) { + inputText = inputText.substring(0, cursor) + inputText.substring(cursor + 1); + } + markActivity(); + return true; + } + + // Left arrow + if (keyCode == Keyboard.KEY_LEFT) { + if (shift) { + if (cursor > 0) { + cursor--; + selectionEnd = cursor; + } + } else { + if (selectionStart != selectionEnd) { + cursor = Math.min(selectionStart, selectionEnd); + } else if (cursor > 0) { + cursor--; + } + selectionStart = cursor; + selectionEnd = cursor; + } + updateScrollOffset(); + markActivity(); + return true; + } + + // Right arrow + if (keyCode == Keyboard.KEY_RIGHT) { + if (shift) { + if (cursor < inputText.length()) { + cursor++; + selectionEnd = cursor; + } + } else { + if (selectionStart != selectionEnd) { + cursor = Math.max(selectionStart, selectionEnd); + } else if (cursor < inputText.length()) { + cursor++; + } + selectionStart = cursor; + selectionEnd = cursor; + } + updateScrollOffset(); + markActivity(); + return true; + } + + // Home + if (keyCode == Keyboard.KEY_HOME) { + if (shift) { + selectionEnd = 0; + cursor = 0; + } else { + cursor = 0; + selectionStart = cursor; + selectionEnd = cursor; + } + updateScrollOffset(); + markActivity(); + return true; + } + + // End + if (keyCode == Keyboard.KEY_END) { + if (shift) { + selectionEnd = inputText.length(); + cursor = inputText.length(); + } else { + cursor = inputText.length(); + selectionStart = cursor; + selectionEnd = cursor; + } + updateScrollOffset(); + markActivity(); + return true; + } + + // Type character - only allow digits and colon + if (ChatAllowedCharacters.isAllowedCharacter(c)) { + if (Character.isDigit(c) || c == ':') { + insertText(String.valueOf(c)); + markActivity(); + return true; + } + } + + return true; // Consume all other keys when dialog is focused + } + + private void insertText(String text) { + if (selectionStart != selectionEnd) { + deleteSelection(); + } + inputText = inputText.substring(0, cursor) + text + inputText.substring(cursor); + cursor += text.length(); + selectionStart = cursor; + selectionEnd = cursor; + updateScrollOffset(); + } + + private void deleteSelection() { + int minSel = Math.min(selectionStart, selectionEnd); + int maxSel = Math.max(selectionStart, selectionEnd); + inputText = inputText.substring(0, minSel) + inputText.substring(maxSel); + cursor = minSel; + selectionStart = cursor; + selectionEnd = cursor; + updateScrollOffset(); + } + + private void updateScrollOffset() { + int fieldWidth = textFieldWidth - 8; + String beforeCursor = cursor > 0 ? inputText.substring(0, cursor) : ""; + int cursorX = font.getStringWidth(beforeCursor); + + // Scroll to keep cursor visible + if (cursorX - scrollOffset * 6 > fieldWidth) { + scrollOffset = Math.max(0, cursor - fieldWidth / 6); + } else if (cursor < scrollOffset) { + scrollOffset = cursor; + } + } + + private void executeGoToLine() { + ParseResult result = parseInput(inputText); + if (result.valid && callback != null) { + callback.goToLineColumn(result.line, result.column); + close(); + } + } + + // ==================== PARSING ==================== + + private static class ParseResult { + boolean valid; + int line; + int column; + String error; + + static ParseResult invalid(String error) { + ParseResult r = new ParseResult(); + r.valid = false; + r.error = error; + return r; + } + + static ParseResult of(int line, int column) { + ParseResult r = new ParseResult(); + r.valid = true; + r.line = line; + r.column = column; + return r; + } + } + + private ParseResult parseInput(String input) { + if (input == null || input.trim().isEmpty()) { + return ParseResult.invalid("Enter line number"); + } + + input = input.trim(); + int line, column = 1; + + int colonIndex = input.indexOf(':'); + if (colonIndex >= 0) { + // Format: line:column + String linePart = input.substring(0, colonIndex); + String colPart = input.substring(colonIndex + 1); + + if (linePart.isEmpty()) { + return ParseResult.invalid("Invalid line number"); + } + + try { + line = Integer.parseInt(linePart); + } catch (NumberFormatException e) { + return ParseResult.invalid("Invalid line number"); + } + + if (!colPart.isEmpty()) { + try { + column = Integer.parseInt(colPart); + } catch (NumberFormatException e) { + return ParseResult.invalid("Invalid column number"); + } + } + } else { + // Format: just line + try { + line = Integer.parseInt(input); + } catch (NumberFormatException e) { + return ParseResult.invalid("Invalid line number"); + } + } + + // Validate ranges + int lineCount = callback != null ? callback.getLineCount() : 0; + if (line < 1) { + return ParseResult.invalid("Line must be >= 1"); + } + if (line > lineCount) { + return ParseResult.invalid("Line must be <= " + lineCount); + } + if (column < 1) { + return ParseResult.invalid("Column must be >= 1"); + } + // Column validation happens at callback - depends on actual line length + + return ParseResult.of(line, column); + } + + // ==================== CURSOR BLINK ==================== + + public void updateCursor() { + cursorCounter++; + } + + private void markActivity() { + lastInputTime = System.currentTimeMillis(); + } + + private boolean shouldShowCursor() { + // Keep cursor visible for 500ms after any input + if (System.currentTimeMillis() - lastInputTime < 500) { + return true; + } + // Then blink every 500ms + return (cursorCounter / 10) % 2 == 0; + } + + // ==================== GETTERS ==================== + + public int getTotalHeight() { + return visible ? dialogHeight + 10 : 0; + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java index 28d12b226..4dc7c7c54 100644 --- a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java +++ b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java @@ -21,6 +21,9 @@ public class ScriptEditorKeys extends KeyPresetManager { // Search/Replace public final KeyPreset SEARCH = add("Search").setDefaultState(Keyboard.KEY_R, true, false, false); public final KeyPreset SEARCH_REPLACE = add("Search & Replace").setDefaultState(Keyboard.KEY_R, true, false, true); + + // Navigation + public final KeyPreset GO_TO_LINE = add("Go to Line").setDefaultState(Keyboard.KEY_G, true, false, false); public ScriptEditorKeys() { super("script_editor"); From 7ec0ce6f94a8b2e7d7b8214a720c2a8223678d16 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 22:47:51 +0200 Subject: [PATCH 104/358] Implemented main editor focus/unfocus callbacks to GoToLine/SearchReplaceBar --- .../npcs/client/gui/util/GuiScriptTextArea.java | 12 ++++++++++++ .../npcs/client/gui/util/script/GoToLineDialog.java | 7 +++++++ .../client/gui/util/script/SearchReplaceBar.java | 4 +++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index fe9274fed..c1e8e44f9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -133,6 +133,18 @@ public class GuiScriptTextArea extends GuiNpcTextField { int maxScroll = Math.max(0, container.linesCount - visible); scroll.scrollToLine(lineIdx, visible, maxScroll); } + + @Override + public void unfocusMainEditor() { + // Save position but unfocus + active = false; + } + + @Override + public void focusMainEditor() { + active = true; + selection.markActivity(); + } @Override public void onDialogClose() { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java index 84329cad0..941d42551 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java @@ -66,6 +66,10 @@ public class GoToLineDialog { /** Called when dialog closes - should restore focus to editor */ void onDialogClose(); + + void unfocusMainEditor(); + + void focusMainEditor(); } /** @@ -101,6 +105,7 @@ public class GoToLineDialog { selectionEnd = 0; scrollOffset = 0; markActivity(); + if (callback != null) callback.unfocusMainEditor(); } public void close() { @@ -108,6 +113,7 @@ public class GoToLineDialog { focused = false; if (callback != null) { callback.onDialogClose(); + callback.focusMainEditor(); } } @@ -255,6 +261,7 @@ public class GoToLineDialog { if (mouseX >= fieldX && mouseX < fieldX + textFieldWidth && mouseY >= fieldY && mouseY < fieldY + textFieldHeight) { focused = true; + if (callback != null) callback.unfocusMainEditor(); // Position cursor based on click int clickX = mouseX - fieldX - 4; String visibleText = scrollOffset > 0 && scrollOffset < inputText.length() diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index 02e2849b5..f073ad1d7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -524,7 +524,9 @@ public class SearchReplaceBar { long now = System.currentTimeMillis(); boolean isSearchFieldClick = isMouseOver(mouseX, mouseY, currentX, rowY - 2, textFieldWidth, textFieldHeight + 4); boolean isReplaceFieldClick = showReplace && isMouseOver(mouseX, mouseY, x + padding, y + barHeight, textFieldWidth, textFieldHeight + 4); - + + if ((isSearchFieldClick || isReplaceFieldClick) && callback != null) callback.unfocusMainEditor(); + if (isSearchFieldClick) { boolean sameField = lastClickWasSearch; if (sameField && now - lastClickTime < 300) { From 78fc2bd3fd4d999867e304d74fbbd1317ec06843 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 19 Dec 2025 23:22:54 +0200 Subject: [PATCH 105/358] Colored search bar/results found in red when no matches --- .../gui/util/script/SearchReplaceBar.java | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index f073ad1d7..22717623b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -304,9 +304,9 @@ public class SearchReplaceBar { String matchInfo = matches.isEmpty() ? "0 results" : (currentMatchIndex + 1) + "/" + matches.size(); int matchInfoWidth = font.getStringWidth(matchInfo); - font.drawString(matchInfo, currentX, rowY + 4, matches.isEmpty() ? 0xFFff6666 : 0xFFcccccc); + font.drawString(matchInfo, currentX, rowY + 4, + matches.isEmpty() && !searchText.isEmpty() ? 0xFFff6666 : 0xFFcccccc); currentX += matchInfoWidth + 8; - drawToggleButton(currentX, rowY, "Cc", matchCase, hoverMatchCase, "Match Case"); currentX += buttonSize + buttonSpacing; @@ -381,10 +381,8 @@ public class SearchReplaceBar { GL11.glEnable(GL11.GL_SCISSOR_TEST); GuiUtil.setScissorClip(fieldX + 2, fieldY, fieldWidth - 4, fieldHeight); - - if (text.isEmpty() && !focused) { - font.drawString(placeholder, textX, textY, 0xFF808080); - } else { + + { // Draw selection highlight if (selStart != selEnd && focused) { int startX = textX + font.getStringWidth(text.substring(0, Math.min(selStart, text.length()))) - scrollOffset; @@ -393,7 +391,12 @@ public class SearchReplaceBar { } // Draw text with scroll offset - font.drawString(text, textX - scrollOffset, textY, 0xFFe0e0e0); + boolean noMatches = !text.isEmpty() && matches.isEmpty(); + int col = text.isEmpty() ? 0xFFa0a0a0 : 0xFFe0e0e0; + if (isSearchField && noMatches) + col = 0xFFff6666; // Red + + font.drawString(text.isEmpty() ? placeholder : text, textX - scrollOffset, textY, col); // Draw cursor (stays visible during typing) if (focused && shouldShowCursor()) { From aca9cec48c551c27faf875557de423f8b34d33af Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Fri, 19 Dec 2025 22:38:43 +0100 Subject: [PATCH 106/358] Make it shift down the viewport if search is --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index c1e8e44f9..407be1454 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -262,6 +262,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { } // Draw outer border around entire area drawRect(x - 1, y - 1, x + width + 1, y + height + 1, 0xffa0a0a0); + + int searchHeight = searchBar.getTotalHeight(); + y += searchHeight; + height -= searchHeight; + // Draw line number gutter background drawRect(x, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff000000); // Draw text viewport background (starts after gutter) @@ -551,6 +556,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { goToLineDialog.draw(xMouse, yMouse); KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); + y -= searchHeight; + height += searchHeight; } private void scissorViewport() { From 00eefb0887b19cb7eede1dd5bb8c0ccd0065672d Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Fri, 19 Dec 2025 22:39:36 +0100 Subject: [PATCH 107/358] Cap scroll to amount of visible lines, currently was capped to amount of lines + 1 --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 407be1454..2f31a86b7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -277,8 +277,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Enable scissor test to clip drawing to the TEXT viewport rectangle (excludes gutter) GL11.glEnable(GL11.GL_SCISSOR_TEST); scissorViewport(); - - container.visibleLines = height / container.lineHeight-1; + + container.visibleLines = (height / container.lineHeight); int maxScroll = Math.max(0, this.container.linesCount - container.visibleLines); From 3568fb8a868a07c5265bde66ab3605fc108e54f3 Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:08:37 +0100 Subject: [PATCH 108/358] Give priority of keybinds with special keys Stop searching if found match. --- .../noppes/npcs/client/key/KeyPreset.java | 22 ++++++++++++++++++- .../npcs/client/key/KeyPresetManager.java | 13 ++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/key/KeyPreset.java b/src/main/java/noppes/npcs/client/key/KeyPreset.java index cbc7cfb86..27ed9485b 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPreset.java +++ b/src/main/java/noppes/npcs/client/key/KeyPreset.java @@ -8,7 +8,7 @@ import org.lwjgl.input.Mouse; import java.util.function.Consumer; -public class KeyPreset { +public class KeyPreset implements Comparable { /** * @field SINGLE_PRESS => on releasing before 5 ticks press time * @field PRESS_RELEASE => on pressing and releasing @@ -148,6 +148,26 @@ public class KeyPreset { return false; } + @Override + public int compareTo(KeyPreset o) { + int mySpecialCount = getSpecialKeyCount(this); + int otherSpecialCount = getSpecialKeyCount(o); + + return mySpecialCount - otherSpecialCount; + } + + private static int getSpecialKeyCount(KeyPreset preset) { + int res = 0; + if (preset.hasAlt()) + res += 1; + if (preset.hasCtrl()) + res += 1; + if (preset.hasShift()) + res += 1; + + return res; + } + public static class KeyState { public int keyCode = -1; diff --git a/src/main/java/noppes/npcs/client/key/KeyPresetManager.java b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java index 61b74a610..355c07b54 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPresetManager.java +++ b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java @@ -13,6 +13,8 @@ public class KeyPresetManager { public List keys = new ArrayList<>(); public String fileName; + private boolean wasSorted = false; + public KeyPresetManager(String fileName) { this.fileName = fileName; } @@ -20,12 +22,21 @@ public class KeyPresetManager { public KeyPreset add(String name) { KeyPreset preset = new KeyPreset(name); keys.add(preset); + wasSorted = false; return preset; } public void tick() { - for (KeyPreset key : keys) + if (!wasSorted) { + keys.sort(KeyPreset::compareTo); + wasSorted = true; + } + + for (KeyPreset key : keys) { key.tick(); + if (key.isDown) + return; + } } From 0cbfe526cbc7a824d9429486d17502252a462583 Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:09:17 +0100 Subject: [PATCH 109/358] Add search / replace unwind button --- .../client/gui/util/script/SearchReplaceBar.java | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index f073ad1d7..ae01c430e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -71,6 +71,7 @@ public class SearchReplaceBar { // ==================== HOVER STATE ==================== private boolean hoverClose = false; + private boolean hoverSearchUnwind = false; private boolean hoverMatchCase = false; private boolean hoverWholeWord = false; private boolean hoverPrev = false; @@ -323,6 +324,9 @@ public class SearchReplaceBar { int closeX = x + width - buttonSize - padding; drawCloseButton(closeX, rowY, hoverClose); + + int replaceUnwindButton = closeX - buttonSize - buttonSpacing; + drawActionButton(replaceUnwindButton, rowY, showReplace ? "▼" : "◀", hoverSearchUnwind, true); } private void drawReplaceRow(int mouseX, int mouseY) { @@ -477,7 +481,10 @@ public class SearchReplaceBar { int closeX = x + width - buttonSize - padding; hoverClose = isMouseOver(mouseX, mouseY, closeX, rowY, buttonSize, buttonSize); - + + int replaceUnwindButton = closeX - buttonSize - buttonSpacing; + hoverSearchUnwind = isMouseOver(mouseX, mouseY, replaceUnwindButton, rowY, buttonSize, buttonSize); + if (showReplace) { int replaceRowY = y + barHeight + 2; currentX = x + padding + textFieldWidth + buttonSpacing + 4; @@ -592,7 +599,12 @@ public class SearchReplaceBar { close(); return true; } - + + if (hoverSearchUnwind) { + showReplace = !showReplace; + return true; + } + if (showReplace) { int replaceRowY = y + barHeight + 2; From 2efdafe2546abc18d9fe1f31234d712fe01b5fd6 Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:15:48 +0100 Subject: [PATCH 110/358] Fix priority calculations. --- src/main/java/noppes/npcs/client/key/KeyPreset.java | 2 +- src/main/java/noppes/npcs/client/key/KeyPresetManager.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/key/KeyPreset.java b/src/main/java/noppes/npcs/client/key/KeyPreset.java index 27ed9485b..8c21d86c5 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPreset.java +++ b/src/main/java/noppes/npcs/client/key/KeyPreset.java @@ -153,7 +153,7 @@ public class KeyPreset implements Comparable { int mySpecialCount = getSpecialKeyCount(this); int otherSpecialCount = getSpecialKeyCount(o); - return mySpecialCount - otherSpecialCount; + return otherSpecialCount - mySpecialCount; } private static int getSpecialKeyCount(KeyPreset preset) { diff --git a/src/main/java/noppes/npcs/client/key/KeyPresetManager.java b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java index 355c07b54..96cf63731 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPresetManager.java +++ b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java @@ -34,7 +34,7 @@ public class KeyPresetManager { for (KeyPreset key : keys) { key.tick(); - if (key.isDown) + if (key.isDown && key.shouldConflict) return; } } From 780dc4ca76366d0e9f325f8523310d41115f44b7 Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Fri, 19 Dec 2025 23:23:39 +0100 Subject: [PATCH 111/358] Stop sorting keys list, create sorted copy instead. Made it so the rendering for them would still work for visual grouping, but the ticking logic wouldn't suffer --- src/main/java/noppes/npcs/client/key/KeyPresetManager.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/key/KeyPresetManager.java b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java index 96cf63731..8d25c43ac 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPresetManager.java +++ b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java @@ -11,6 +11,7 @@ import java.util.List; public class KeyPresetManager { public List keys = new ArrayList<>(); + public List keysSorted = new ArrayList<>(); public String fileName; private boolean wasSorted = false; @@ -22,17 +23,18 @@ public class KeyPresetManager { public KeyPreset add(String name) { KeyPreset preset = new KeyPreset(name); keys.add(preset); + keysSorted.add(preset); wasSorted = false; return preset; } public void tick() { if (!wasSorted) { - keys.sort(KeyPreset::compareTo); + keysSorted.sort(KeyPreset::compareTo); wasSorted = true; } - for (KeyPreset key : keys) { + for (KeyPreset key : keysSorted) { key.tick(); if (key.isDown && key.shouldConflict) return; From 170225262975fb696fd8ca7133705d675e13babe Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 00:31:30 +0200 Subject: [PATCH 112/358] Fixed resizing game window/gui initi'ing newly creating the text editor --- .../noppes/npcs/client/gui/GuiScript.java | 14 +- .../client/gui/script/GuiScriptEffect.java | 14 +- .../client/gui/script/GuiScriptInterface.java | 16 +- .../gui/script/GuiScriptLinkedItem.java | 13 +- .../client/gui/script/GuiScriptRecipe.java | 13 +- .../client/gui/util/GuiScriptTextArea.java | 175 +++++++++--------- 6 files changed, 137 insertions(+), 108 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index ea9941b69..8b0d7d24a 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -44,6 +44,8 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall private static int activeConsole = 0; boolean loaded = false; + private GuiScriptTextArea textArea; + public GuiScript(EntityNPCInterface npc) { super(npc); script = npc.script; @@ -96,10 +98,14 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall ScriptContainer container = script.getNPCScript(activeTab); - GuiScriptTextArea ta = new GuiScriptTextArea(this, 2, guiLeft + 74, guiTop + 4, 239, 208, container == null ? "" : container.script); - ta.enableCodeHighlighting(); - ta.setListener(this); - this.addTextField(ta); + if (textArea == null) + textArea = new GuiScriptTextArea(this, 2, guiLeft + 74, guiTop + 4, 239, 208, + container == null ? "" : container.script); + else + textArea.init(guiLeft + 74, guiTop + 4, 239, 208, container == null ? "" : container.script); + textArea.enableCodeHighlighting(); + textArea.setListener(this); + this.addTextField(textArea); addButton(new GuiNpcButton(102, guiLeft + 315, guiTop + 4, 50, 20, "gui.clear")); addButton(new GuiNpcButton(101, guiLeft + 366, guiTop + 4, 50, 20, "gui.paste")); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java index 33582ad88..f3a4d219d 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java @@ -48,6 +48,9 @@ public class GuiScriptEffect extends GuiNPCInterface implements GuiYesNoCallback private final EffectScript scriptHandler; boolean loaded = false; + private GuiScriptTextArea textArea; + + public GuiScriptEffect(GuiNPCManageEffects parent, CustomEffect effect) { this.drawDefaultBackground = true; this.closeOnEsc = true; @@ -109,10 +112,13 @@ public class GuiScriptEffect extends GuiNPCInterface implements GuiYesNoCallback hookLabel.color = 11184810; this.addLabel(hookLabel); ScriptContainer container = this.scriptHandler.container; - GuiScriptTextArea ta = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - ta.enableCodeHighlighting(); - ta.setListener(this); - this.addTextField(ta); + if (textArea == null) + textArea = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); + else + textArea.init(this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); + textArea.enableCodeHighlighting(); + textArea.setListener(this); + this.addTextField(textArea); int left1 = this.guiLeft + this.xSize - 104; this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 185eb90fd..4afa22144 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -44,6 +44,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb public List hookList = new ArrayList(); protected boolean loaded = false; + private GuiScriptTextArea textArea; public GuiScriptInterface() { this.drawDefaultBackground = true; this.closeOnEsc = true; @@ -108,11 +109,18 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb addLabel(hookLabel); ScriptContainer container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); - GuiScriptTextArea ta = new GuiScriptTextArea(this, 2, guiLeft + 1 + yoffset, guiTop + yoffset, xSize - 108 - yoffset, (int) (ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - ta.enableCodeHighlighting(); - ta.setListener(this); + if (textArea == null) + textArea = new GuiScriptTextArea(this, 2, guiLeft + 1 + yoffset, guiTop + yoffset, + xSize - 108 - yoffset, (int) (ySize * 0.96) - yoffset * 2, + container == null ? "" : container.script); + else + textArea.init(guiLeft + 1 + yoffset, guiTop + yoffset, xSize - 108 - yoffset, + (int) (ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - this.addTextField(ta); + textArea.enableCodeHighlighting(); + textArea.setListener(this); + + this.addTextField(textArea); int left1 = this.guiLeft + this.xSize - 104; this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java index 664c08d1f..a6803d2c4 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java @@ -49,6 +49,8 @@ public class GuiScriptLinkedItem extends GuiNPCInterface implements GuiYesNoCall private final LinkedItemScript scriptHandler; boolean loaded = false; + private GuiScriptTextArea textArea; + public GuiScriptLinkedItem(GuiNPCManageLinked parent, LinkedItem linkedItem) { this.drawDefaultBackground = true; this.closeOnEsc = true; @@ -121,10 +123,13 @@ public class GuiScriptLinkedItem extends GuiNPCInterface implements GuiYesNoCall hookLabel.color = 11184810; this.addLabel(hookLabel); ScriptContainer container = this.scriptHandler.container; - GuiScriptTextArea ta = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - ta.enableCodeHighlighting(); - ta.setListener(this); - this.addTextField(ta); + if (textArea == null) + textArea = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); + else + textArea.init(this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); + textArea.enableCodeHighlighting(); + textArea.setListener(this); + this.addTextField(textArea); int left1 = this.guiLeft + this.xSize - 104; this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java index bb93052d7..fd93d8fe3 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java @@ -50,6 +50,8 @@ public class GuiScriptRecipe extends GuiNPCInterface implements GuiYesNoCallback private final RecipeScript scriptHandler; boolean loaded = false; + private GuiScriptTextArea textArea; + public GuiScriptRecipe(GuiNpcManageRecipes parent, RecipeCarpentry recipe) { this.drawDefaultBackground = true; this.closeOnEsc = true; @@ -129,10 +131,13 @@ public class GuiScriptRecipe extends GuiNPCInterface implements GuiYesNoCallback hookLabel.color = 11184810; this.addLabel(hookLabel); ScriptContainer container = this.scriptHandler.container; - GuiScriptTextArea ta = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - ta.enableCodeHighlighting(); - ta.setListener(this); - this.addTextField(ta); + if (textArea == null) + textArea = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); + else + textArea.init(this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); + textArea.enableCodeHighlighting(); + textArea.setListener(this); + this.addTextField(textArea); int left1 = this.guiLeft + this.xSize - 104; this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index c1e8e44f9..d68735f91 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -36,11 +36,8 @@ import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; public class GuiScriptTextArea extends GuiNpcTextField { // ==================== DIMENSIONS & POSITION ==================== - public int id; public int x; public int y; - public int width; - public int height; // ==================== STATE FLAGS ==================== public boolean active = false; @@ -85,15 +82,81 @@ public class GuiScriptTextArea extends GuiNpcTextField { public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) { super(id, guiScreen, x, y, width, height, null); - this.id = id; - this.x = x; - this.y = y; - this.width = width; - this.height = height; - this.undoing = true; - this.setText(text); - this.undoing = false; - + + searchBar.setCallback(new SearchReplaceBar.SearchCallback() { + @Override + public String getText() { + return text; + } + + @Override + public void setText(String newText) { + GuiScriptTextArea.this.setText(newText); + } + + @Override + public void scrollToPosition(int position) { + // Find line containing position and scroll to it + if (container == null || container.lines == null) return; + + // Calculate offset to account for search bar height + int searchBarOffset = searchBar.getTotalHeight(); + int effectiveHeight = height - searchBarOffset; + int visibleLines = effectiveHeight / container.lineHeight; + // Calculate how many lines the search bar covers + int linesHiddenBySRB = searchBarOffset > 0 ? (int) Math.ceil((double) searchBarOffset / container.lineHeight) : 0; + + for (int i = 0; i < container.lines.size(); i++) { + LineData ld = container.lines.get(i); + if (position >= ld.start && position < ld.end) { + int visible = Math.max(1, visibleLines); + int maxScroll = Math.max(0, container.linesCount - visible); + int targetLine = i; + + // If search bar is visible and would hide this line, scroll down so it's visible + // The target line should appear below the search bar, not under it + int currentScroll = scroll.getScrolledLine(); + int firstVisibleLine = currentScroll + linesHiddenBySRB; + + if (searchBarOffset > 0 && targetLine < firstVisibleLine) { + // Force scroll so target line appears just below the search bar + scroll.setTargetScroll(Math.max(0, targetLine - linesHiddenBySRB), maxScroll); + } else { + scroll.scrollToLine(targetLine, visible, maxScroll); + } + break; + } + } + } + + @Override + public void setSelection(int start, int end) { + selection.setSelection(start, end); + selection.setCursorPositionDirect(end); + } + + @Override + public int getGutterWidth() { + return LINE_NUMBER_GUTTER_WIDTH; + } + + @Override + public void unfocusMainEditor() { + // Save position but unfocus + active = false; + } + + @Override + public void focusMainEditor() { + active = true; + selection.markActivity(); + } + + @Override + public void onMatchesUpdated() { + // Called when matches change - could be used for UI updates + } + }); // Initialize Go To Line dialog with callback this.goToLineDialog = new GoToLineDialog(new GoToLineDialog.GoToLineCallback() { @Override @@ -154,9 +217,19 @@ public class GuiScriptTextArea extends GuiNpcTextField { }); initializeKeyBindings(); - initGui(); + init(x, y, width, height, text); } + public void init(int x, int y, int width, int height, String text) { + this.x = xPosition = x; + this.y = yPosition = y; + this.width = width; + this.height = height; + this.undoing = true; + this.setText(text); + this.undoing = false; + initGui(); + } public void initGui() { int endX = x + width, endY = y + height; int xOffset = hasVerticalScrollbar() ? -8 : -2; @@ -168,80 +241,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Initialize search bar (preserves state across initGui calls) searchBar.initGui(x, y, width); - searchBar.setCallback(new SearchReplaceBar.SearchCallback() { - @Override - public String getText() { - return text; - } - - @Override - public void setText(String newText) { - GuiScriptTextArea.this.setText(newText); - } - - @Override - public void scrollToPosition(int position) { - // Find line containing position and scroll to it - if (container == null || container.lines == null) return; - - // Calculate offset to account for search bar height - int searchBarOffset = searchBar.getTotalHeight(); - int effectiveHeight = height - searchBarOffset; - int visibleLines = effectiveHeight / container.lineHeight; - // Calculate how many lines the search bar covers - int linesHiddenBySRB = searchBarOffset > 0 ? (int) Math.ceil((double) searchBarOffset / container.lineHeight) : 0; - - for (int i = 0; i < container.lines.size(); i++) { - LineData ld = container.lines.get(i); - if (position >= ld.start && position < ld.end) { - int visible = Math.max(1, visibleLines); - int maxScroll = Math.max(0, container.linesCount - visible); - int targetLine = i; - - // If search bar is visible and would hide this line, scroll down so it's visible - // The target line should appear below the search bar, not under it - int currentScroll = scroll.getScrolledLine(); - int firstVisibleLine = currentScroll + linesHiddenBySRB; - - if (searchBarOffset > 0 && targetLine < firstVisibleLine) { - // Force scroll so target line appears just below the search bar - scroll.setTargetScroll(Math.max(0, targetLine - linesHiddenBySRB), maxScroll); - } else { - scroll.scrollToLine(targetLine, visible, maxScroll); - } - break; - } - } - } - - @Override - public void setSelection(int start, int end) { - selection.setSelection(start, end); - selection.setCursorPositionDirect(end); - } - - @Override - public int getGutterWidth() { - return LINE_NUMBER_GUTTER_WIDTH; - } - - @Override - public void unfocusMainEditor() { - // Save position but unfocus - active = false; - } - - @Override - public void focusMainEditor() { - active = true; - selection.markActivity(); - } - - @Override - public void onMatchesUpdated() { - // Called when matches change - could be used for UI updates - } - }); // Initialize Go To Line dialog goToLineDialog.initGui(x, y, width); @@ -1774,7 +1773,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } public boolean hasVerticalScrollbar() { - return this.container.visibleLines < this.container.linesCount; + return this.container != null && this.container.visibleLines < this.container.linesCount; } public void enableCodeHighlighting() { From 950a749be643ce4bab94155e1aa98ef7b90b431b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 00:51:33 +0200 Subject: [PATCH 113/358] Fixed KeyPreset not fully respecting SHIFT/ALT/CTRL modifiers + removed redundant code --- .../noppes/npcs/client/key/KeyPreset.java | 26 ++----------------- .../npcs/client/key/KeyPresetManager.java | 14 +--------- 2 files changed, 3 insertions(+), 37 deletions(-) diff --git a/src/main/java/noppes/npcs/client/key/KeyPreset.java b/src/main/java/noppes/npcs/client/key/KeyPreset.java index 8c21d86c5..6f433c9aa 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPreset.java +++ b/src/main/java/noppes/npcs/client/key/KeyPreset.java @@ -8,7 +8,7 @@ import org.lwjgl.input.Mouse; import java.util.function.Consumer; -public class KeyPreset implements Comparable { +public class KeyPreset { /** * @field SINGLE_PRESS => on releasing before 5 ticks press time * @field PRESS_RELEASE => on pressing and releasing @@ -58,8 +58,7 @@ public class KeyPreset implements Comparable { return; boolean isDown = isMouseKey() ? Mouse.isButtonDown(keyCode + 100) : Keyboard.isKeyDown(keyCode); - isDown = isDown && (hasCtrl() ? isCtrlKeyDown() : true) && (hasAlt() ? isAltKeyDown() : true) && (hasShift() ? isShiftKeyDown() : true); - + isDown = isDown && (isCtrlKeyDown() == hasCtrl()) && (isAltKeyDown() == hasAlt()) && (isShiftKeyDown() == hasShift()); setDown(isDown); } @@ -148,27 +147,6 @@ public class KeyPreset implements Comparable { return false; } - @Override - public int compareTo(KeyPreset o) { - int mySpecialCount = getSpecialKeyCount(this); - int otherSpecialCount = getSpecialKeyCount(o); - - return otherSpecialCount - mySpecialCount; - } - - private static int getSpecialKeyCount(KeyPreset preset) { - int res = 0; - if (preset.hasAlt()) - res += 1; - if (preset.hasCtrl()) - res += 1; - if (preset.hasShift()) - res += 1; - - return res; - } - - public static class KeyState { public int keyCode = -1; public boolean hasCtrl, hasAlt, hasShift; diff --git a/src/main/java/noppes/npcs/client/key/KeyPresetManager.java b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java index 8d25c43ac..6f973e2b5 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPresetManager.java +++ b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java @@ -11,10 +11,8 @@ import java.util.List; public class KeyPresetManager { public List keys = new ArrayList<>(); - public List keysSorted = new ArrayList<>(); public String fileName; - private boolean wasSorted = false; public KeyPresetManager(String fileName) { this.fileName = fileName; @@ -23,22 +21,12 @@ public class KeyPresetManager { public KeyPreset add(String name) { KeyPreset preset = new KeyPreset(name); keys.add(preset); - keysSorted.add(preset); - wasSorted = false; return preset; } public void tick() { - if (!wasSorted) { - keysSorted.sort(KeyPreset::compareTo); - wasSorted = true; - } - - for (KeyPreset key : keysSorted) { + for (KeyPreset key : keys) key.tick(); - if (key.isDown && key.shouldConflict) - return; - } } From df210367e07406c93848e1629281c49b97d49308 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 00:57:36 +0200 Subject: [PATCH 114/358] Fixed search bar closing when goToLine pane opens --- .../npcs/client/gui/util/GuiScriptTextArea.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 3fe7de850..56cc1d5fc 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -816,26 +816,23 @@ public class GuiScriptTextArea extends GuiNpcTextField { // SEARCH: Open search bar (Ctrl+R) KEYS.SEARCH.setTask(e -> { - if (!e.isPress() || !active || !isEnabled()) + if (!e.isPress() || !isActive.get()) return; searchBar.toggle(); }); // SEARCH_REPLACE: Open search+replace bar (Ctrl+Shift+R) KEYS.SEARCH_REPLACE.setTask(e -> { - if (!e.isPress() || !active || !isEnabled()) + if (!e.isPress() || !isActive.get()) return; searchBar.toggleReplace(); }); // GO_TO_LINE: Open go to line dialog (Ctrl+G) KEYS.GO_TO_LINE.setTask(e -> { - if (!e.isPress() || !active || !isEnabled()) + if (!e.isPress() || !isActive.get()) return; - // Close search bar if open - if (searchBar.isVisible()) { - searchBar.close(); - } + goToLineDialog.toggle(); }); } From 1c7f614c3f224b4151f1a38ec6ca74141771d9b6 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 01:04:31 +0200 Subject: [PATCH 115/358] Highlighted search match words if search bar is visible --- .../client/gui/util/GuiScriptTextArea.java | 52 ++++++++++--------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 56cc1d5fc..65fc0680a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -440,31 +440,33 @@ public class GuiScriptTextArea extends GuiNpcTextField { } // Highlight search matches - List searchMatches = searchBar.getMatches(); - int currentMatchIdx = searchBar.getCurrentMatchIndex(); - for (int mi = 0; mi < searchMatches.size(); mi++) { - int[] match = searchMatches.get(mi); - // Check if match overlaps with this line - if (match[1] > data.start && match[0] < data.end) { - int matchStart = Math.max(match[0] - data.start, 0); - int matchEnd = Math.min(match[1] - data.start, line.length()); - if (matchStart < matchEnd) { - int s = ClientProxy.Font.width(line.substring(0, matchStart)); - int e = ClientProxy.Font.width(line.substring(0, matchEnd)) + 1; - boolean isExcluded = searchBar.isMatchExcluded(mi); - // Current match gets brighter highlight, others get dimmer - int highlightColor = (mi == currentMatchIdx) ? 0xBB4488ff : 0x662266aa; - if (isExcluded) { - highlightColor = 0x33666666; // Dimmer for excluded matches - } - int highlightX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + s; - int highlightEndX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + e; - drawRect(highlightX, posY, highlightEndX, posY + container.lineHeight, highlightColor); - - // Draw strikethrough line for excluded matches - if (isExcluded) { - int strikeY = posY + container.lineHeight / 2; - drawRect(highlightX, strikeY, highlightEndX, strikeY + 1, 0xFFaa4444); + if (searchBar.isVisible()) { + List searchMatches = searchBar.getMatches(); + int currentMatchIdx = searchBar.getCurrentMatchIndex(); + for (int mi = 0; mi < searchMatches.size(); mi++) { + int[] match = searchMatches.get(mi); + // Check if match overlaps with this line + if (match[1] > data.start && match[0] < data.end) { + int matchStart = Math.max(match[0] - data.start, 0); + int matchEnd = Math.min(match[1] - data.start, line.length()); + if (matchStart < matchEnd) { + int s = ClientProxy.Font.width(line.substring(0, matchStart)); + int e = ClientProxy.Font.width(line.substring(0, matchEnd)) + 1; + boolean isExcluded = searchBar.isMatchExcluded(mi); + // Current match gets brighter highlight, others get dimmer + int highlightColor = (mi == currentMatchIdx) ? 0xBB4488ff : 0x662266aa; + if (isExcluded) { + highlightColor = 0x33666666; // Dimmer for excluded matches + } + int highlightX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + s; + int highlightEndX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + e; + drawRect(highlightX, posY, highlightEndX, posY + container.lineHeight, highlightColor); + + // Draw strikethrough line for excluded matches + if (isExcluded) { + int strikeY = posY + container.lineHeight / 2; + drawRect(highlightX, strikeY, highlightEndX, strikeY + 1, 0xFFaa4444); + } } } } From cf1afc9fcaa78c13f4413ca3b09a8e1a85d9cf1e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 02:00:13 +0200 Subject: [PATCH 116/358] Fixed mouse click being on main text area when search bar open (still WIP) --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 65fc0680a..cf799ca76 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -313,7 +313,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { doubleClicked = false; tripleClicked = false; } - + // y += searchHeight; + // height -= searchHeight; // Calculate braces next to cursor to highlight int startBracket = 0, endBracket = 0; if (selection.getStartSelection() >= 0 && text != null && text.length() > 0 && @@ -557,8 +558,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { goToLineDialog.draw(xMouse, yMouse); KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); - y -= searchHeight; - height += searchHeight; + // y -= searchHeight; + // height += searchHeight; } private void scissorViewport() { From abd639438e68514196870d0be2877f0d35c869ae Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 02:00:58 +0200 Subject: [PATCH 117/358] Fixed search bar matches not updating when new text updates happen in main editor --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index cf799ca76..ba63f5e09 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -86,7 +86,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { searchBar.setCallback(new SearchReplaceBar.SearchCallback() { @Override public String getText() { - return text; + return GuiScriptTextArea.this.text; } @Override @@ -1767,6 +1767,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Consider text changes user activity to pause caret blinking briefly selection.markActivity(); + searchBar.updateMatches(); } } From 8514b9f9809856ff2eff1fe8eca65908add2129f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 02:02:43 +0200 Subject: [PATCH 118/358] Search bar now auto-opens on highlighted word --- .../client/gui/util/GuiScriptTextArea.java | 16 +++++++++------ .../gui/util/script/SearchReplaceBar.java | 20 +++++++++---------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index ba63f5e09..f5f79ed0d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -51,6 +51,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== TEXT & CONTAINER ==================== public String text = null; + public String highlightedWord; private JavaTextContainer container = null; private boolean enableCodeHighlighting = false; @@ -89,6 +90,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { return GuiScriptTextArea.this.text; } + public String getHighlightedWord() { + return GuiScriptTextArea.this.highlightedWord; + } + @Override public void setText(String newText) { GuiScriptTextArea.this.setText(newText); @@ -263,8 +268,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { drawRect(x - 1, y - 1, x + width + 1, y + height + 1, 0xffa0a0a0); int searchHeight = searchBar.getTotalHeight(); - y += searchHeight; - height -= searchHeight; + // Draw line number gutter background drawRect(x, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff000000); @@ -356,12 +360,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } - String wordHightLight = null; + highlightedWord = null; if (selection.hasSelection()) { Matcher m = container.regexWord.matcher(text); while (m.find()) { if (m.start() == selection.getStartSelection() && m.end() == selection.getEndSelection()) { - wordHightLight = text.substring(selection.getStartSelection(), selection.getEndSelection()); + highlightedWord = text.substring(selection.getStartSelection(), selection.getEndSelection()); } } } @@ -429,10 +433,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } //Highlight words - if (wordHightLight != null) { + if (highlightedWord != null) { Matcher m = container.regexWord.matcher(line); while (m.find()) { - if (line.substring(m.start(), m.end()).equals(wordHightLight)) { + if (line.substring(m.start(), m.end()).equals(highlightedWord)) { int s = ClientProxy.Font.width(line.substring(0, m.start())); int e = ClientProxy.Font.width(line.substring(0, m.end())) + 1; drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0x99004c00); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index bab0de897..9308f55d5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -99,6 +99,8 @@ public class SearchReplaceBar { */ public interface SearchCallback { String getText(); + + String getHighlightedWord(); void setText(String text); void scrollToPosition(int position); void setSelection(int start, int end); @@ -134,11 +136,17 @@ public class SearchReplaceBar { showReplace = false; searchFieldFocused = true; replaceFieldFocused = false; + if (callback != null) { + callback.unfocusMainEditor(); + String highlight = callback.getHighlightedWord(); + if (highlight != null) { + searchText = highlight; + } + } searchSelectionStart = 0; searchSelectionEnd = searchText.length(); searchCursor = searchText.length(); markActivity(); - if (callback != null) callback.unfocusMainEditor(); updateMatches(); } @@ -146,16 +154,8 @@ public class SearchReplaceBar { * Open search+replace bar (Ctrl+Shift+R) */ public void openSearchReplace() { - visible = true; showReplace = true; - searchFieldFocused = true; - replaceFieldFocused = false; - searchSelectionStart = 0; - searchSelectionEnd = searchText.length(); - searchCursor = searchText.length(); - markActivity(); - if (callback != null) callback.unfocusMainEditor(); - updateMatches(); + openSearch(); } /** From ed15ae861a525c6d27dbd0825ba7d063412412df Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 02:03:02 +0200 Subject: [PATCH 119/358] Ctrl+F now only opens/updates search bar, not close. --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index f5f79ed0d..3978723ac 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -825,7 +825,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { KEYS.SEARCH.setTask(e -> { if (!e.isPress() || !isActive.get()) return; - searchBar.toggle(); + searchBar.openSearch(); }); // SEARCH_REPLACE: Open search+replace bar (Ctrl+Shift+R) From 69c87404e741c601b689c4ac9ec80da51ae3fa85 Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Sat, 20 Dec 2025 01:10:26 +0100 Subject: [PATCH 120/358] Clean up class filter registration --- src/main/java/noppes/npcs/CustomNpcs.java | 42 ++++++++++++++- .../janino/AdvancedClassFilter.java | 54 +++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 src/main/java/somehussar/janino/AdvancedClassFilter.java diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 6ce129262..8192c065a 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -13,9 +13,11 @@ import cpw.mods.fml.common.event.FMLServerStoppedEvent; import cpw.mods.fml.common.network.NetworkRegistry; import cpw.mods.fml.common.registry.EntityRegistry; import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; import foxz.command.CommandNoppes; import io.github.somehussar.janinoloader.api.IDynamicCompiler; import io.github.somehussar.janinoloader.api.IDynamicCompilerBuilder; +import io.github.somehussar.janinoloader.api.delegates.LoadClassCondition; import kamkeel.npcs.addon.AddonManager; import kamkeel.npcs.command.CommandKamkeel; import kamkeel.npcs.command.profile.CommandProfile; @@ -98,9 +100,16 @@ import noppes.npcs.entity.old.EntityNpcNagaFemale; import noppes.npcs.entity.old.EntityNpcNagaMale; import noppes.npcs.entity.old.EntityNpcSkeleton; import noppes.npcs.scripted.NpcAPI; +import somehussar.janino.AdvancedClassFilter; import java.io.File; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.function.Consumer; +import java.util.regex.Pattern; +import java.util.stream.Collectors; @Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.10.2") public class CustomNpcs { @@ -139,12 +148,21 @@ public class CustomNpcs { public static MinecraftServer Server; + private static IDynamicCompiler jls; + @SideOnly(Side.CLIENT) + private static IDynamicCompiler clientJaninoCompiler; + @SideOnly(Side.CLIENT) + private final static Set> clientClassFilterConsumer = new HashSet<>(); + + @SideOnly(Side.CLIENT) + public static void addClassesToClientClassFilter(Consumer consumer) { + clientClassFilterConsumer.add(consumer); + } + public CustomNpcs() { instance = this; } - private static IDynamicCompiler jls; - public static IDynamicCompiler getDynamicCompiler() { if (jls == null) { jls = IDynamicCompilerBuilder.createBuilder().getCompiler(); @@ -153,6 +171,26 @@ public class CustomNpcs { return jls; } + @SideOnly(Side.CLIENT) + public static IDynamicCompiler getClientCompiler() { + + if (clientJaninoCompiler == null) { + AdvancedClassFilter filter = new AdvancedClassFilter() + .addRegexes("noppes\\.npcs\\.api\\..*") + .banRegexes(".*ClassLoader.*", + ".*File*."); + + for (Consumer consumer : clientClassFilterConsumer) { + consumer.accept(filter); + } + + + clientJaninoCompiler = IDynamicCompilerBuilder.createBuilder().setClassFilter(filter).getCompiler(); + } + + return clientJaninoCompiler; + } + @EventHandler public void load(FMLPreInitializationEvent ev) { PacketHandler.Instance = new PacketHandler(); diff --git a/src/main/java/somehussar/janino/AdvancedClassFilter.java b/src/main/java/somehussar/janino/AdvancedClassFilter.java new file mode 100644 index 000000000..fb1fac6e8 --- /dev/null +++ b/src/main/java/somehussar/janino/AdvancedClassFilter.java @@ -0,0 +1,54 @@ +package somehussar.janino; + +import io.github.somehussar.janinoloader.api.delegates.LoadClassCondition; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public class AdvancedClassFilter implements LoadClassCondition { + + private final Set allowedClasses = new HashSet<>(); + private final Set allowedWildCards = new HashSet<>(); + private final Set bannedClasses = new HashSet<>(); + private final Set bannedWildCards = new HashSet<>(); + + public AdvancedClassFilter() { + addClasses("java.io.Serializable", "java.util.Iterator"); + addRegexes("java\\.lang\\.."); + } + + public AdvancedClassFilter addRegexes(String... classRegexes) { + allowedWildCards.addAll(Arrays.stream(classRegexes).map(Pattern::compile).collect(Collectors.toList())); + return this; + } + public AdvancedClassFilter banRegexes(String... classRegexes) { + bannedWildCards.addAll(Arrays.stream(classRegexes).map(Pattern::compile).collect(Collectors.toList())); + return this; + } + public AdvancedClassFilter addClasses(String... classPath) { + allowedClasses.addAll(Arrays.asList(classPath)); + return this; + } + public AdvancedClassFilter banClasses(String... classPath) { + bannedClasses.addAll(Arrays.asList(classPath)); + return this; + } + + + @Override + public boolean isValid(String name) { + if (bannedClasses.contains(name)) + return false; + if (bannedWildCards.stream().anyMatch(pattern -> pattern.matcher(name).matches())) + return false; + + if (name.startsWith("java.lang")) return true; + + if (allowedClasses.contains(name)) return true; + + return allowedWildCards.stream().anyMatch(pattern -> pattern.matcher(name).matches()); + } +} From b150cd060850d4ad63d5e752853988c19c0e4abe Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Sat, 20 Dec 2025 01:21:35 +0100 Subject: [PATCH 121/358] Rename compilers and add clearing logic. --- src/main/java/noppes/npcs/CustomNpcs.java | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 8192c065a..ad42bc419 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -17,7 +17,6 @@ import cpw.mods.fml.relauncher.SideOnly; import foxz.command.CommandNoppes; import io.github.somehussar.janinoloader.api.IDynamicCompiler; import io.github.somehussar.janinoloader.api.IDynamicCompilerBuilder; -import io.github.somehussar.janinoloader.api.delegates.LoadClassCondition; import kamkeel.npcs.addon.AddonManager; import kamkeel.npcs.command.CommandKamkeel; import kamkeel.npcs.command.profile.CommandProfile; @@ -103,13 +102,9 @@ import noppes.npcs.scripted.NpcAPI; import somehussar.janino.AdvancedClassFilter; import java.io.File; -import java.util.Arrays; import java.util.HashSet; -import java.util.List; import java.util.Set; import java.util.function.Consumer; -import java.util.regex.Pattern; -import java.util.stream.Collectors; @Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.10.2") public class CustomNpcs { @@ -148,7 +143,7 @@ public class CustomNpcs { public static MinecraftServer Server; - private static IDynamicCompiler jls; + private static IDynamicCompiler globalJaninoCompiler; @SideOnly(Side.CLIENT) private static IDynamicCompiler clientJaninoCompiler; @SideOnly(Side.CLIENT) @@ -164,11 +159,11 @@ public class CustomNpcs { } public static IDynamicCompiler getDynamicCompiler() { - if (jls == null) { - jls = IDynamicCompilerBuilder.createBuilder().getCompiler(); + if (globalJaninoCompiler == null) { + globalJaninoCompiler = IDynamicCompilerBuilder.createBuilder().getCompiler(); } - return jls; + return globalJaninoCompiler; } @SideOnly(Side.CLIENT) @@ -307,6 +302,10 @@ public class CustomNpcs { @EventHandler public void setAboutToStart(FMLServerAboutToStartEvent event) { + globalJaninoCompiler = null; + if (FMLCommonHandler.instance().getSide().isClient()) + clientJaninoCompiler = null; + Server = event.getServer(); ChunkController.Instance.clear(); FactionController.getInstance().load(); @@ -319,6 +318,7 @@ public class CustomNpcs { new LinkedNpcController(); new AnimationController(); + LinkedItemController.getInstance().load(); // Custom Effects @@ -372,6 +372,9 @@ public class CustomNpcs { ScriptController.Instance.saveForgeScripts(); ScriptController.Instance.savePlayerScripts(); ScriptController.Instance.saveGlobalNpcScripts(); + if (FMLCommonHandler.instance().getSide().isClient()) + clientJaninoCompiler = null; + globalJaninoCompiler = null; } @EventHandler From 92431ab2a9f4be3a22f1a35bdc4ba0edc67d2694 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 02:29:42 +0200 Subject: [PATCH 122/358] Replace key now only opens/updates replace search bar, not close. --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 3978723ac..d7e74b1bb 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -832,7 +832,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { KEYS.SEARCH_REPLACE.setTask(e -> { if (!e.isPress() || !isActive.get()) return; - searchBar.toggleReplace(); + searchBar.openSearchReplace(); }); // GO_TO_LINE: Open go to line dialog (Ctrl+G) From 7f79609c18275f31cedb4b25196c5734c0922587 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 02:34:00 +0200 Subject: [PATCH 123/358] Improved SearchReplaceBar rendering, it's template now matches GoToLineDialog --- .../gui/util/script/SearchReplaceBar.java | 101 ++++++++++++++++-- 1 file changed, 93 insertions(+), 8 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index 9308f55d5..42e289f61 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -154,8 +154,8 @@ public class SearchReplaceBar { * Open search+replace bar (Ctrl+Shift+R) */ public void openSearchReplace() { - showReplace = true; openSearch(); + showReplace = true; } /** @@ -282,8 +282,8 @@ public class SearchReplaceBar { updateHoverStates(mouseX, mouseY); int totalHeight = getTotalHeight(); - Gui.drawRect(x, y, x + width, y + totalHeight, 0xFF3c3f41); - Gui.drawRect(x, y + totalHeight - 1, x + width, y + totalHeight, 0xFF515151); + Gui.drawRect(x, y, x + width, y + totalHeight, 0xFF2d2d30); + Gui.drawRect(x, y + totalHeight - 1, x + width, y + totalHeight, 0xFF414141); drawSearchRow(mouseX, mouseY); @@ -332,7 +332,7 @@ public class SearchReplaceBar { private void drawReplaceRow(int mouseX, int mouseY) { int rowY = y + barHeight + 2; int currentX = x + padding; - + drawTextField(currentX, rowY, textFieldWidth, textFieldHeight, replaceText, replaceCursor, replaceSelectionStart, replaceSelectionEnd, replaceFieldFocused, "Replace...", replaceScrollOffset, false); @@ -346,8 +346,91 @@ public class SearchReplaceBar { drawActionButton(currentX, rowY, "Exclude", hoverExclude, currentMatchIndex >= 0); } - + private void drawTextField(int fieldX, int fieldY, int fieldWidth, int fieldHeight, + String text, int cursor, int selStart, int selEnd, + boolean focused, String placeholder, int scrollOffset, boolean isSearchField) { + // Draw text field background + Gui.drawRect(fieldX, fieldY, fieldX + fieldWidth, fieldY + fieldHeight, 0xFF1e1e1e); + Gui.drawRect(fieldX, fieldY, fieldX + fieldWidth, fieldY + 1, focused ? 0xFF007acc : 0xFF3c3c3c); + Gui.drawRect(fieldX, fieldY + fieldHeight - 1, fieldX + fieldWidth, fieldY + fieldHeight, + focused ? 0xFF007acc : 0xFF3c3c3c); + Gui.drawRect(fieldX, fieldY, fieldX + 1, fieldY + fieldHeight, focused ? 0xFF007acc : 0xFF3c3c3c); + Gui.drawRect(fieldX + fieldWidth - 1, fieldY, fieldX + fieldWidth, fieldY + fieldHeight, + focused ? 0xFF007acc : 0xFF3c3c3c); + + // Calculate visible area + int textX = fieldX + 4; + int textY = fieldY + (fieldHeight - 8) / 2; + int visibleWidth = fieldWidth - 8; + + // Apply scroll offset + String visibleText = text; + int adjustedSelStart = selStart; + int adjustedSelEnd = selEnd; + + if (scrollOffset > 0 && scrollOffset < text.length()) { + visibleText = text.substring(scrollOffset); + adjustedSelStart = Math.max(0, selStart - scrollOffset); + adjustedSelEnd = Math.max(0, selEnd - scrollOffset); + } + + // Draw selection highlight + if (adjustedSelStart != adjustedSelEnd) { + int minSel = Math.min(adjustedSelStart, adjustedSelEnd); + int maxSel = Math.max(adjustedSelStart, adjustedSelEnd); + String beforeSel = visibleText.substring(0, Math.min(minSel, visibleText.length())); + String inSel = visibleText.substring(Math.min(minSel, visibleText.length()), + Math.min(maxSel, visibleText.length())); + int selX = textX + font.getStringWidth(beforeSel); + int selW = font.getStringWidth(inSel); + Gui.drawRect(selX, textY - 1, selX + selW, textY + 9, 0xFF264f78); + } + + int cursorPosX = font.getStringWidth(text.substring(0, Math.min(cursor, text.length()))); + if (cursorPosX - scrollOffset > visibleWidth - 2) { + scrollOffset = cursorPosX - visibleWidth + 2; + } else if (cursorPosX - scrollOffset < 0) { + scrollOffset = Math.max(0, cursorPosX); + } + // Update stored scroll offset + if (isSearchField) { + searchScrollOffset = scrollOffset; + } else { + replaceScrollOffset = scrollOffset; + } + + GL11.glEnable(GL11.GL_SCISSOR_TEST); + GuiUtil.setScissorClip(fieldX + 2, fieldY, fieldWidth - 4, fieldHeight); + + + // Draw selection highlight + if (selStart != selEnd && focused) { + int startX = textX + font.getStringWidth( + text.substring(0, Math.min(selStart, text.length()))) - scrollOffset; + int endX = textX + font.getStringWidth(text.substring(0, Math.min(selEnd, text.length()))) - scrollOffset; + Gui.drawRect(startX, textY - 1, endX, textY + font.FONT_HEIGHT, 0xFF2d5ca6); + } + + // Draw text with scroll offset + boolean noMatches = !text.isEmpty() && matches.isEmpty(); + int col = text.isEmpty() ? 0xFFa0a0a0 : 0xFFe0e0e0; + if (isSearchField && noMatches) + col = 0xFFff6666; // Red + + font.drawString(text.isEmpty() ? placeholder : text, textX - scrollOffset, textY, col); + + // Draw cursor (stays visible during typing) + if (focused && shouldShowCursor()) { + int cursorDrawX = textX + cursorPosX - scrollOffset; + Gui.drawRect(cursorDrawX, textY - 1, cursorDrawX + 1, textY + font.FONT_HEIGHT, 0xFFffffff); + } + + + GL11.glDisable(GL11.GL_SCISSOR_TEST); + } + + private void oldDrawTextField(int fieldX, int fieldY, int fieldWidth, int fieldHeight, String text, int cursor, int selStart, int selEnd, boolean focused, String placeholder, int scrollOffset, boolean isSearchField) { Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); @@ -424,9 +507,11 @@ public class SearchReplaceBar { } private void drawNavButton(int btnX, int btnY, String arrow, boolean enabled, boolean hovered) { - int bgColor = !enabled ? 0xFF2a2a2a : (hovered ? 0xFF505050 : 0xFF3c3c3c); + int bgColor = !enabled ? 0xFF393939 : (hovered ? 0xFF505050 : 0xFF3c3c3c); Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + buttonSize, bgColor); - + Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + 1, 0xFF606060); + + int textColor = !enabled ? 0xFF555555 : (hovered ? 0xFFffffff : 0xFFaaaaaa); int arrowWidth = font.getStringWidth(arrow); font.drawString(arrow, btnX + (buttonSize - arrowWidth) / 2, btnY + (buttonSize - font.FONT_HEIGHT) / 2, textColor); @@ -446,7 +531,7 @@ public class SearchReplaceBar { int btnWidth = font.getStringWidth(label) + 10; int btnHeight = 14; - int bgColor = !enabled ? 0xFF2a2a2a : (hovered ? 0xFF505050 : 0xFF3c3c3c); + int bgColor = !enabled ? 0xFF393939 : (hovered ? 0xFF505050 : 0xFF3c3c3c); Gui.drawRect(btnX, btnY + 1, btnX + btnWidth, btnY + btnHeight + 1, bgColor); Gui.drawRect(btnX, btnY + 1, btnX + btnWidth, btnY + 2, 0xFF555555); Gui.drawRect(btnX, btnY + btnHeight, btnX + btnWidth, btnY + btnHeight + 1, 0xFF2a2a2a); From ec40a1c580daa68ce7efaed27a94234628f8875c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 02:34:56 +0200 Subject: [PATCH 124/358] Renamed Search & Replace to just Replace --- src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java index 4dc7c7c54..329758cfc 100644 --- a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java +++ b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java @@ -20,7 +20,7 @@ public class ScriptEditorKeys extends KeyPresetManager { // Search/Replace public final KeyPreset SEARCH = add("Search").setDefaultState(Keyboard.KEY_R, true, false, false); - public final KeyPreset SEARCH_REPLACE = add("Search & Replace").setDefaultState(Keyboard.KEY_R, true, false, true); + public final KeyPreset SEARCH_REPLACE = add("Replace").setDefaultState(Keyboard.KEY_R, true, false, true); // Navigation public final KeyPreset GO_TO_LINE = add("Go to Line").setDefaultState(Keyboard.KEY_G, true, false, false); From 762d52f658c20b2363970229888502e3a5f8cf90 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 03:11:17 +0200 Subject: [PATCH 125/358] Reset selection of search bar when unfocusing --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 2 +- .../noppes/npcs/client/gui/util/script/SearchReplaceBar.java | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index d7e74b1bb..172184f35 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -154,7 +154,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { @Override public void focusMainEditor() { active = true; - selection.markActivity(); + searchBar.resetSelection(); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index 42e289f61..7f0f4a010 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -596,6 +596,10 @@ public class SearchReplaceBar { return mouseX >= bx && mouseX < bx + bw && mouseY >= by && mouseY < by + bh; } + public void resetSelection() { + searchSelectionStart = searchSelectionEnd = searchCursor; + replaceSelectionStart = replaceSelectionEnd = replaceCursor; + } // ==================== MOUSE INPUT ==================== /** @@ -609,6 +613,7 @@ public class SearchReplaceBar { int totalHeight = getTotalHeight(); // If click is outside the bar, don't consume it (let main editor handle) if (mouseX < x || mouseX > x + width || mouseY < y || mouseY > y + totalHeight) { + resetSelection(); return false; } From d06e7df1a0d497eb7b46c960b43f152613708b6c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 03:11:46 +0200 Subject: [PATCH 126/358] Improved interaction between SearchReplaceBar and GoToLineDialog --- .../client/gui/util/GuiScriptTextArea.java | 25 +++++++++++++------ .../gui/util/script/GoToLineDialog.java | 7 ++++-- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 172184f35..67aa5f748 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -822,28 +822,40 @@ public class GuiScriptTextArea extends GuiNpcTextField { }); // SEARCH: Open search bar (Ctrl+R) + // Works in search bar. KEYS.SEARCH.setTask(e -> { - if (!e.isPress() || !isActive.get()) + if (!e.isPress() || !isActive.get() && !searchBar.isVisible()) return; + + unfocusAll(); searchBar.openSearch(); }); // SEARCH_REPLACE: Open search+replace bar (Ctrl+Shift+R) + // Works in search bar. KEYS.SEARCH_REPLACE.setTask(e -> { - if (!e.isPress() || !isActive.get()) + if (!e.isPress() || !isActive.get() && !searchBar.isVisible()) return; + + unfocusAll(); searchBar.openSearchReplace(); }); // GO_TO_LINE: Open go to line dialog (Ctrl+G) + // Works in search bar. KEYS.GO_TO_LINE.setTask(e -> { - if (!e.isPress() || !isActive.get()) + if (!e.isPress() || !isActive.get() && !searchBar.isVisible()) return; - + + unfocusAll(); goToLineDialog.toggle(); }); } + public void unfocusAll() { + if (searchBar.hasFocus()) searchBar.unfocus(); + if (goToLineDialog.hasFocus()) goToLineDialog.unfocus(); + } // ==================== KEYBOARD INPUT HANDLING ==================== /** @@ -1726,9 +1738,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { public void updateCursorCounter() { // Only process KeyPresets if search bar and go-to-line dialog don't have focus // This prevents COPY, PASTE, UNDO, etc. from firing when typing in dialogs - if (!searchBar.hasFocus() && !goToLineDialog.hasFocus()) { - KEYS.tick(); - } + KEYS.tick(); + searchBar.updateCursor(); goToLineDialog.updateCursor(); ++this.cursorCounter; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java index 941d42551..6f252c375 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java @@ -118,7 +118,7 @@ public class GoToLineDialog { } public void toggle() { - if (visible) { + if (visible && focused) { close(); } else { show(); @@ -129,6 +129,9 @@ public class GoToLineDialog { return visible && focused; } + public void unfocus() { + focused = false; + } // ==================== RENDERING ==================== public void draw(int mouseX, int mouseY) { @@ -164,7 +167,7 @@ public class GoToLineDialog { int fieldY = y + padding + 14 + 5; textFieldWidth = dialogWidth - padding * 2; drawTextField(fieldX, fieldY, textFieldWidth, textFieldHeight, inputText, cursor, - selectionStart, selectionEnd, scrollOffset, true); + selectionStart, selectionEnd, scrollOffset, focused); if (inputText.isEmpty()) font.drawString("[Line][:Column]", x + padding + 6, y + padding + 23, 0xFF888888); From 7e62dbc3fa9cdcc7c4c7feaeb64f4d1156e4bb53 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 11:53:11 +0200 Subject: [PATCH 127/358] closeOnEsc supplier for GuiNpcInterface --- .../npcs/client/gui/util/GuiNPCInterface.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index daee2e905..204bece7e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -27,6 +27,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; public abstract class GuiNPCInterface extends GuiScreen { public EntityClientPlayerMP player; @@ -48,6 +49,7 @@ public abstract class GuiNPCInterface extends GuiScreen { public String title; private ResourceLocation background = null; public boolean closeOnEsc = false; + public Supplier closeOnEscSupplier = null; public int guiLeft, guiTop, xSize, ySize; private SubGuiInterface subgui; public int mouseX, mouseY, mouseScroll; @@ -256,7 +258,15 @@ public abstract class GuiNPCInterface extends GuiScreen { */ boolean isSub = this instanceof SubGuiInterface; - if (closeOnEsc && !isSub && (i == 1 || !GuiNpcTextField.isFieldActive() && isInventoryKey(i))) { + boolean shouldClose = closeOnEsc; + if (closeOnEscSupplier != null) { + try { + shouldClose = closeOnEscSupplier.get(); + } catch (Exception ignored) { + } + } + + if (shouldClose && !isSub && (i == 1 || (!GuiNpcTextField.isFieldActive() && isInventoryKey(i)))) { SubGuiInterface sub = getSubGui(); if (sub != null) sub.close(); @@ -269,6 +279,10 @@ public abstract class GuiNPCInterface extends GuiScreen { GuiNpcTextField.unfocus(); } + public void closeOnEsc(Supplier close) { + this.closeOnEscSupplier = close; + } + public void close() { if(GuiNpcTextField.activeTextfield != null) GuiNpcTextField.unfocus(); From 4715e2ffbf1200142f84631d8886dc2c23210dae Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 11:53:26 +0200 Subject: [PATCH 128/358] Implemented closeOnEsc supplier for ScriptTextArea --- src/main/java/noppes/npcs/client/gui/GuiScript.java | 1 + .../java/noppes/npcs/client/gui/script/GuiScriptEffect.java | 2 ++ .../noppes/npcs/client/gui/script/GuiScriptInterface.java | 2 +- .../noppes/npcs/client/gui/script/GuiScriptLinkedItem.java | 2 ++ .../java/noppes/npcs/client/gui/script/GuiScriptRecipe.java | 2 ++ .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 4 ++++ 6 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index 8b0d7d24a..206526c65 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -105,6 +105,7 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall textArea.init(guiLeft + 74, guiTop + 4, 239, 208, container == null ? "" : container.script); textArea.enableCodeHighlighting(); textArea.setListener(this); + this.closeOnEsc(textArea::closeOnEsc); this.addTextField(textArea); addButton(new GuiNpcButton(102, guiLeft + 315, guiTop + 4, 50, 20, "gui.clear")); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java index f3a4d219d..22db14a2e 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java @@ -118,7 +118,9 @@ public class GuiScriptEffect extends GuiNPCInterface implements GuiYesNoCallback textArea.init(this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); textArea.enableCodeHighlighting(); textArea.setListener(this); + this.closeOnEsc(textArea::closeOnEsc); this.addTextField(textArea); + int left1 = this.guiLeft + this.xSize - 104; this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 4afa22144..99fbf7d3e 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -119,7 +119,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb textArea.enableCodeHighlighting(); textArea.setListener(this); - + this.closeOnEsc(textArea::closeOnEsc); this.addTextField(textArea); int left1 = this.guiLeft + this.xSize - 104; diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java index a6803d2c4..8c22c2dd8 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java @@ -129,7 +129,9 @@ public class GuiScriptLinkedItem extends GuiNPCInterface implements GuiYesNoCall textArea.init(this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); textArea.enableCodeHighlighting(); textArea.setListener(this); + this.closeOnEsc(textArea::closeOnEsc); this.addTextField(textArea); + int left1 = this.guiLeft + this.xSize - 104; this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java index fd93d8fe3..0215fd403 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java @@ -137,7 +137,9 @@ public class GuiScriptRecipe extends GuiNPCInterface implements GuiYesNoCallback textArea.init(this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); textArea.enableCodeHighlighting(); textArea.setListener(this); + this.closeOnEsc(textArea::closeOnEsc); this.addTextField(textArea); + int left1 = this.guiLeft + this.xSize - 104; this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 67aa5f748..55cb47cb7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1470,6 +1470,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { setCursor(result.newCursorPosition, false); } + public boolean closeOnEsc(){ + return !searchBar.isVisible() && !goToLineDialog.isVisible(); + } + // ==================== KEYBOARD MODIFIERS ==================== private boolean isAltKeyDown() { From db0e7da8348f0034c2cb64a23c209cb2d5f4cb3e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 11:55:40 +0200 Subject: [PATCH 129/358] Calculated closeOnEsc logic first thing in method (to fix closeOnEsc closing searchBar & entire gui in the same click) --- .../npcs/client/gui/util/GuiNPCInterface.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 204bece7e..37dce6f79 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -243,16 +243,7 @@ public abstract class GuiNPCInterface extends GuiScreen { @Override public void keyTyped(char c, int i) { - if (subgui != null) - subgui.keyTyped(c, i); - for (GuiNpcTextField tf : textfields.values()) - tf.textboxKeyTyped(c, i); - - for (GuiScrollWindow guiScrollableComponent : scrollWindows.values()) { - guiScrollableComponent.keyTyped(c, i); - } - - /* + /* Fixes closing sub with escape closes all of its parents. The outermost GUI (root of all subs) closes the deepest open sub on ESC. */ @@ -273,6 +264,17 @@ public abstract class GuiNPCInterface extends GuiScreen { else close(); } + + + if (subgui != null) + subgui.keyTyped(c, i); + for (GuiNpcTextField tf : textfields.values()) + tf.textboxKeyTyped(c, i); + + for (GuiScrollWindow guiScrollableComponent : scrollWindows.values()) { + guiScrollableComponent.keyTyped(c, i); + } + } public void onGuiClosed() { From a91039c026a2e99e2597f56005b4fb4bf6ad5ab0 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 11:59:34 +0200 Subject: [PATCH 130/358] Fixed ESC not closing searchBar/goToDialog when they are visible but not focused --- .../npcs/client/gui/util/GuiScriptTextArea.java | 12 +++--------- .../npcs/client/gui/util/script/GoToLineDialog.java | 12 ++++++------ .../client/gui/util/script/SearchReplaceBar.java | 9 +++++---- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 55cb47cb7..f4f1566cb 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -867,18 +867,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { KEYS_OVERLAY.keyTyped(c, i); // Handle Go To Line dialog input first if it has focus - if (goToLineDialog.hasFocus()) { - if (goToLineDialog.keyTyped(c, i)) { + if (goToLineDialog.isVisible() &&goToLineDialog.keyTyped(c, i)) return true; - } - } // Handle search bar input first if it has focus - if (searchBar.hasFocus()) { - if (searchBar.keyTyped(c, i)) { - return true; - } - } + if (searchBar.isVisible() && searchBar.keyTyped(c, i)) + return true; if (!active || KEYS_OVERLAY.showOverlay) return false; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java index 6f252c375..36fb52196 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java @@ -296,18 +296,18 @@ public class GoToLineDialog { } public boolean keyTyped(char c, int keyCode) { + // Escape - close dialog + if (keyCode == Keyboard.KEY_ESCAPE && visible) { + close(); + return true; + } + if (!visible || !focused) return false; boolean ctrl = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); boolean shift = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); - // Escape - close dialog - if (keyCode == Keyboard.KEY_ESCAPE) { - close(); - return true; - } - // Enter - execute go to line if (keyCode == Keyboard.KEY_RETURN || keyCode == Keyboard.KEY_NUMPADENTER) { executeGoToLine(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index 7f0f4a010..72f5c52ac 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -805,6 +805,11 @@ public class SearchReplaceBar { * @return true if input was consumed */ public boolean keyTyped(char c, int keyCode) { + if (keyCode == Keyboard.KEY_ESCAPE && visible) { + close(); + return true; + } + if (!visible || (!searchFieldFocused && !replaceFieldFocused)) return false; // Check modifier keys - don't let them trigger first-match issue @@ -819,10 +824,6 @@ public class SearchReplaceBar { return true; // Consume but don't process } - if (keyCode == Keyboard.KEY_ESCAPE) { - close(); - return true; - } if (keyCode == Keyboard.KEY_TAB && showReplace) { searchFieldFocused = !searchFieldFocused; From 6b8b3a0596c8e1b6d80d6e2942505de986638821 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 12:19:32 +0200 Subject: [PATCH 131/358] Implemented Undo/Redo functionality for Search/ReplaceBar --- .../client/gui/util/GuiScriptTextArea.java | 56 +++++---- .../gui/util/script/SearchReplaceBar.java | 108 +++++++++++++++++- 2 files changed, 136 insertions(+), 28 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index f4f1566cb..e23b3c164 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -729,37 +729,47 @@ public class GuiScriptTextArea extends GuiNpcTextField { }); // UNDO: Restore last edit from undo list + // Works in search bar. KEYS.UNDO.setTask(e -> { - if (!e.isPress() || !isActive.get()) + if (!e.isPress() || !isActive.get() && !searchBar.isVisible()) return; - if (undoList.isEmpty()) - return; - undoing = true; - redoList.add(new UndoData(this.text, selection.getCursorPosition())); - UndoData data = undoList.remove(undoList.size() - 1); - setText(data.text); - selection.reset(data.cursorPosition); - undoing = false; - scrollToCursor(); - searchBar.updateMatches(); + if (searchBar.hasFocus()) { + searchBar.undo(); + } else { + if (undoList.isEmpty()) + return; + undoing = true; + redoList.add(new UndoData(this.text, selection.getCursorPosition())); + UndoData data = undoList.remove(undoList.size() - 1); + setText(data.text); + selection.reset(data.cursorPosition); + undoing = false; + scrollToCursor(); + searchBar.updateMatches(); + } }); // REDO: Restore last undone edit from redo list + // Works in search bar. KEYS.REDO.setTask(e -> { - if (!e.isPress() || !isActive.get()) + if (!e.isPress() || !isActive.get() && !searchBar.isVisible()) return; - if (redoList.isEmpty()) - return; - undoing = true; - undoList.add(new UndoData(this.text, selection.getCursorPosition())); - UndoData data = redoList.remove(redoList.size() - 1); - setText(data.text); - selection.reset(data.cursorPosition); - undoing = false; - scrollToCursor(); - searchBar.updateMatches(); + if (searchBar.hasFocus()) { + searchBar.redo(); + } else { + if (redoList.isEmpty()) + return; + undoing = true; + undoList.add(new UndoData(this.text, selection.getCursorPosition())); + UndoData data = redoList.remove(redoList.size() - 1); + setText(data.text); + selection.reset(data.cursorPosition); + undoing = false; + scrollToCursor(); + searchBar.updateMatches(); + } }); // FORMAT: Format/indent code @@ -1812,7 +1822,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== INNER CLASSES ==================== - class UndoData { + public static class UndoData { public String text; public int cursorPosition; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index 72f5c52ac..f2fefd544 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -15,6 +15,8 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; +import noppes.npcs.client.gui.util.GuiScriptTextArea.UndoData; + /** * Search and Replace bar component for the script editor. * Provides IntelliJ-like search functionality with: @@ -64,6 +66,12 @@ public class SearchReplaceBar { private int searchScrollOffset = 0; private int replaceScrollOffset = 0; + // ==================== UNDO/REDO ==================== + private List searchUndoList = new ArrayList<>(); + private List searchRedoList = new ArrayList<>(); + private List replaceUndoList = new ArrayList<>(); + private List replaceRedoList = new ArrayList<>(); + // ==================== MATCHES ==================== private List matches = new ArrayList<>(); private int currentMatchIndex = -1; @@ -176,6 +184,92 @@ public class SearchReplaceBar { replaceFieldFocused = false; } + /** + * Save current state to undo list before making changes + */ + private void saveState(boolean isSearchField) { + if (isSearchField) { + searchUndoList.add(new UndoData(searchText, searchCursor)); + searchRedoList.clear(); // Clear redo on new change + } else { + replaceUndoList.add(new UndoData(replaceText, replaceCursor)); + replaceRedoList.clear(); + } + } + + /** + * Undo last change in focused field + */ + public void undo() { + if (searchFieldFocused && !searchUndoList.isEmpty()) { + UndoData data = searchUndoList.remove(searchUndoList.size() - 1); + searchRedoList.add(new UndoData(searchText, searchCursor)); + searchText = data.text; + searchCursor = data.cursorPosition; + searchSelectionStart = searchCursor; + searchSelectionEnd = searchCursor; + updateScrollOffset(true); + updateMatches(); + } else if (replaceFieldFocused && !replaceUndoList.isEmpty()) { + UndoData data = replaceUndoList.remove(replaceUndoList.size() - 1); + replaceRedoList.add(new UndoData(replaceText, replaceCursor)); + replaceText = data.text; + replaceCursor = data.cursorPosition; + replaceSelectionStart = replaceCursor; + replaceSelectionEnd = replaceCursor; + updateScrollOffset(false); + } + } + + /** + * Redo last undone change in focused field + */ + public void redo() { + if (searchFieldFocused && !searchRedoList.isEmpty()) { + UndoData data = searchRedoList.remove(searchRedoList.size() - 1); + searchUndoList.add(new UndoData(searchText, searchCursor)); + searchText = data.text; + searchCursor = data.cursorPosition; + searchSelectionStart = searchCursor; + searchSelectionEnd = searchCursor; + updateScrollOffset(true); + updateMatches(); + } else if (replaceFieldFocused && !replaceRedoList.isEmpty()) { + UndoData data = replaceRedoList.remove(replaceRedoList.size() - 1); + replaceUndoList.add(new UndoData(replaceText, replaceCursor)); + replaceText = data.text; + replaceCursor = data.cursorPosition; + replaceSelectionStart = replaceCursor; + replaceSelectionEnd = replaceCursor; + updateScrollOffset(false); + } + } + + /** + * Update scroll offset for text field to keep cursor visible + */ + private void updateScrollOffset(boolean isSearchField) { + int fieldWidth = textFieldWidth - 8; + String text = isSearchField ? searchText : replaceText; + int cursor = isSearchField ? searchCursor : replaceCursor; + String beforeCursor = cursor > 0 ? text.substring(0, cursor) : ""; + int cursorX = font.getStringWidth(beforeCursor); + int scrollOffset = isSearchField ? searchScrollOffset : replaceScrollOffset; + + // Scroll to keep cursor visible + if (cursorX - scrollOffset * 6 > fieldWidth) { + scrollOffset = Math.max(0, cursor - fieldWidth / 6); + } else if (cursor < scrollOffset) { + scrollOffset = cursor; + } + + if (isSearchField) { + searchScrollOffset = scrollOffset; + } else { + replaceScrollOffset = scrollOffset; + } + } + /** * Toggle visibility */ @@ -805,11 +899,6 @@ public class SearchReplaceBar { * @return true if input was consumed */ public boolean keyTyped(char c, int keyCode) { - if (keyCode == Keyboard.KEY_ESCAPE && visible) { - close(); - return true; - } - if (!visible || (!searchFieldFocused && !replaceFieldFocused)) return false; // Check modifier keys - don't let them trigger first-match issue @@ -824,6 +913,10 @@ public class SearchReplaceBar { return true; // Consume but don't process } + if (keyCode == Keyboard.KEY_ESCAPE) { + close(); + return true; + } if (keyCode == Keyboard.KEY_TAB && showReplace) { searchFieldFocused = !searchFieldFocused; @@ -891,6 +984,7 @@ public class SearchReplaceBar { if (hasSelection) { String selected = text.substring(Math.min(selStart, selEnd), Math.max(selStart, selEnd)); setClipboard(selected); + saveState(isSearchField); text = text.substring(0, Math.min(selStart, selEnd)) + text.substring(Math.max(selStart, selEnd)); cursor = Math.min(selStart, selEnd); selStart = selEnd = cursor; @@ -900,6 +994,7 @@ public class SearchReplaceBar { String clipboard = getClipboard(); if (clipboard != null) { clipboard = clipboard.replace("\n", "").replace("\r", ""); + saveState(isSearchField); if (hasSelection) { text = text.substring(0, Math.min(selStart, selEnd)) + clipboard + text.substring(Math.max(selStart, selEnd)); cursor = Math.min(selStart, selEnd) + clipboard.length(); @@ -911,6 +1006,7 @@ public class SearchReplaceBar { } } else if (keyCode == Keyboard.KEY_BACK) { + saveState(isSearchField); if (hasSelection) { text = text.substring(0, Math.min(selStart, selEnd)) + text.substring(Math.max(selStart, selEnd)); cursor = Math.min(selStart, selEnd); @@ -921,6 +1017,7 @@ public class SearchReplaceBar { selStart = selEnd = cursor; } else if (keyCode == Keyboard.KEY_DELETE) { + saveState(isSearchField); if (hasSelection) { text = text.substring(0, Math.min(selStart, selEnd)) + text.substring(Math.max(selStart, selEnd)); cursor = Math.min(selStart, selEnd); @@ -992,6 +1089,7 @@ public class SearchReplaceBar { cursor = text.length(); } else if (ChatAllowedCharacters.isAllowedCharacter(c)) { + saveState(isSearchField); if (hasSelection) { text = text.substring(0, Math.min(selStart, selEnd)) + c + text.substring(Math.max(selStart, selEnd)); cursor = Math.min(selStart, selEnd) + 1; From 05bb9c0081c9552036d94e31385f3e4c9ddd2cda Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 12:51:49 +0200 Subject: [PATCH 132/358] Undo/Redo keys can now be held down. Implemented throttling intervals for KeyPresets for both first press & all subsequent presses --- .../client/gui/util/GuiScriptTextArea.java | 4 +-- .../noppes/npcs/client/key/KeyPreset.java | 36 ++++++++++++++++++- 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index e23b3c164..dc08df7ea 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -731,7 +731,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // UNDO: Restore last edit from undo list // Works in search bar. KEYS.UNDO.setTask(e -> { - if (!e.isPress() || !isActive.get() && !searchBar.isVisible()) + if ((!e.isPress() && !e.isHold()) || !isActive.get() && !searchBar.isVisible()) return; if (searchBar.hasFocus()) { @@ -753,7 +753,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // REDO: Restore last undone edit from redo list // Works in search bar. KEYS.REDO.setTask(e -> { - if (!e.isPress() || !isActive.get() && !searchBar.isVisible()) + if ((!e.isPress() && !e.isHold()) || !isActive.get() && !searchBar.isVisible()) return; if (searchBar.hasFocus()) { diff --git a/src/main/java/noppes/npcs/client/key/KeyPreset.java b/src/main/java/noppes/npcs/client/key/KeyPreset.java index 6f433c9aa..5b2b72517 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPreset.java +++ b/src/main/java/noppes/npcs/client/key/KeyPreset.java @@ -23,6 +23,12 @@ public class KeyPreset { public boolean shouldConflict = true; public Consumer task; + // Throttling for hold-down behavior + private long lastActionTime = 0; + private long throttleInterval = 25; + private long firstThrottleInterval = 500; + private boolean isFirstAction = true; + private long pressStartMs = 0; public boolean isDown; @@ -52,6 +58,33 @@ public class KeyPreset { return this; } + /** + * Fire every X ms when holding + */ + public KeyPreset setThrottleInterval(long ms) { + this.throttleInterval = ms; + return this; + } + + /** + * Set throttle interval for the first action (initial press) + */ + public KeyPreset setFirstThrottleInterval(long ms) { + this.firstThrottleInterval = ms; + return this; + } + + public boolean shouldThrottle() { + if (throttleInterval <= 0) return false; + long currentTime = System.currentTimeMillis(); + long intervalToUse = isFirstAction && firstThrottleInterval > 0 ? + firstThrottleInterval : throttleInterval; + if (currentTime - lastActionTime < intervalToUse) return true; + lastActionTime = currentTime; + isFirstAction = false; + return false; + } + public void tick() { int keyCode = keyCode(); if (keyCode == -1 || keyCode == 0) @@ -68,6 +101,7 @@ public class KeyPreset { if (down && !isDown) { // just pressed pressStartMs = System.currentTimeMillis(); onAction(PressType.PRESS, PressType.PRESS_RELEASE); + isFirstAction = true; // Reset for new press sequence } if (!down && isDown) { // just released @@ -88,7 +122,7 @@ public class KeyPreset { } public KeyPreset onAction(PressType... pressTypes) { - if (task != null) { + if (task != null && !shouldThrottle()) { for (PressType pressType : pressTypes) this.task.accept(new KeyEvent(pressType, this)); } From 4b7effe2d78f502460ebc85ac7fcfa5ef779c066 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 13:28:31 +0200 Subject: [PATCH 133/358] Improved replace exclusion QoL --- .../gui/util/script/SearchReplaceBar.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index f2fefd544..6076b7a8d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -1192,12 +1192,10 @@ public class SearchReplaceBar { */ public void goToNextMatch() { if (matches.isEmpty()) return; - - int startIndex = currentMatchIndex; - do { - currentMatchIndex = (currentMatchIndex + 1) % matches.size(); - } while (excludedMatches.contains(currentMatchIndex) && currentMatchIndex != startIndex); - + + if (currentMatchIndex < 0) currentMatchIndex = 0; + else currentMatchIndex = (currentMatchIndex + 1) % matches.size(); + navigateToCurrentMatch(); } @@ -1206,12 +1204,10 @@ public class SearchReplaceBar { */ public void goToPreviousMatch() { if (matches.isEmpty()) return; - - int startIndex = currentMatchIndex; - do { - currentMatchIndex = (currentMatchIndex - 1 + matches.size()) % matches.size(); - } while (excludedMatches.contains(currentMatchIndex) && currentMatchIndex != startIndex); - + + if (currentMatchIndex < 0) currentMatchIndex = matches.size() - 1; + else currentMatchIndex = (currentMatchIndex - 1 + matches.size()) % matches.size(); + navigateToCurrentMatch(); } @@ -1287,7 +1283,13 @@ public class SearchReplaceBar { * Exclude current match from replacement */ public void excludeCurrentMatch() { - if (currentMatchIndex >= 0 && !excludedMatches.contains(currentMatchIndex)) { + if (currentMatchIndex < 0) return; + + if (excludedMatches.contains(currentMatchIndex)) { + // If already excluded, remove the exclusion and keep selection on this match + excludedMatches.remove(Integer.valueOf(currentMatchIndex)); + } else { + // Otherwise exclude and move to the next match excludedMatches.add(currentMatchIndex); goToNextMatch(); } From af0553ba0d0b34fee703e4ea2c5330acee0fbd6e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 13:55:42 +0200 Subject: [PATCH 134/358] Fixed KeyPreset throttling logic so ONLY HOLD presses are affected --- .../java/noppes/npcs/client/key/KeyPreset.java | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/noppes/npcs/client/key/KeyPreset.java b/src/main/java/noppes/npcs/client/key/KeyPreset.java index 5b2b72517..7bae2c58c 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPreset.java +++ b/src/main/java/noppes/npcs/client/key/KeyPreset.java @@ -74,10 +74,13 @@ public class KeyPreset { return this; } - public boolean shouldThrottle() { + /** + * Determine whether the given press type should be throttled. Only HOLD events are throttled. + */ + public boolean shouldThrottle(PressType pressType) { if (throttleInterval <= 0) return false; long currentTime = System.currentTimeMillis(); - long intervalToUse = isFirstAction && firstThrottleInterval > 0 ? + long intervalToUse = pressType == PressType.HOLD && isFirstAction && firstThrottleInterval > 0 ? firstThrottleInterval : throttleInterval; if (currentTime - lastActionTime < intervalToUse) return true; lastActionTime = currentTime; @@ -122,10 +125,13 @@ public class KeyPreset { } public KeyPreset onAction(PressType... pressTypes) { - if (task != null && !shouldThrottle()) { - for (PressType pressType : pressTypes) + if (task == null) + return this; + + for (PressType pressType : pressTypes) + if (!shouldThrottle(pressType)) this.task.accept(new KeyEvent(pressType, this)); - } + return this; } From 49a51598108387c6b65d1fbf36e0e49c3de7049d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 14:18:35 +0200 Subject: [PATCH 135/358] Drawn all unclosed/missing braces in RED --- .../client/gui/util/GuiScriptTextArea.java | 18 ++++- .../gui/util/script/BracketMatcher.java | 78 +++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index dc08df7ea..288734d1b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -331,10 +331,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { } List list = new ArrayList<>(container.lines); - + // Build brace spans: {origDepth, open line, close line, adjustedDepth} List braceSpans = BracketMatcher.computeBraceSpans(text, list); - + // Always highlight unmatched braces (positions in text) + List unmatchedBraces = BracketMatcher.findUnmatchedBracePositions(text); + // Determine which exact brace span (openLine/closeLine) should be highlighted based on bracket under caret int highlightedOpenLine = -1; int highlightedCloseLine = -1; @@ -432,6 +434,18 @@ public class GuiScriptTextArea extends GuiNpcTextField { drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight + 0, 0x9900cc00); } } + + // Highlight unmatched braces in this line (always red) + if (unmatchedBraces != null && !unmatchedBraces.isEmpty()) { + for (int ubPos : unmatchedBraces) { + if (ubPos >= data.start && ubPos < data.end) { + int rel = ubPos - data.start; + int s = ClientProxy.Font.width(line.substring(0, rel)); + int e = ClientProxy.Font.width(line.substring(0, rel + 1)) + 1; + drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0xffcc0000); + } + } + } //Highlight words if (highlightedWord != null) { Matcher m = container.regexWord.matcher(line); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java b/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java index a4c0e7a30..96fe2a7ba 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java @@ -52,6 +52,84 @@ public class BracketMatcher { return null; } + /** + * Find all unmatched brace positions (both opens and closes) in the text. + * Returns a list of character indices into the text for braces that have no matching pair. + */ + public static List findUnmatchedBracePositions(String text) { + List unmatched = new ArrayList<>(); + if (text == null || text.isEmpty()) return unmatched; + + List stack = new ArrayList<>(); // {char, pos} + + boolean inLineComment = false; + boolean inBlockComment = false; + boolean inString = false; + boolean escape = false; + char stringDelimiter = 0; + + for (int pos = 0; pos < text.length(); pos++) { + char c = text.charAt(pos); + char next = pos + 1 < text.length() ? text.charAt(pos + 1) : 0; + + if (inString) { + if (escape) { escape = false; } + else if (c == '\\') { escape = true; } + else if (c == stringDelimiter) { inString = false; } + else if (c == '\n') { inString = false; } + continue; + } + + if (inBlockComment) { + if (c == '*' && next == '/') { inBlockComment = false; pos++; } + continue; + } + + if (inLineComment) { + if (c == '\n') { inLineComment = false; } + continue; + } + + if (c == '/' && next == '/') { inLineComment = true; pos++; continue; } + if (c == '/' && next == '*') { inBlockComment = true; pos++; continue; } + + if (c == '"' || c == '\'') { + inString = true; + stringDelimiter = c; + escape = false; + continue; + } + + if (c == '{' || c == '[' || c == '(') { + stack.add(new int[]{c, pos}); + } else if (c == '}' || c == ']' || c == ')') { + if (!stack.isEmpty()) { + int[] top = stack.get(stack.size() - 1); + char topc = (char) top[0]; + char expectOpen = (c == '}') ? '{' : (c == ']') ? '[' : '('; + if (topc == expectOpen) { + stack.remove(stack.size() - 1); + continue; + } else { + // mismatched: mark this close as unmatched + unmatched.add(pos); + continue; + } + } else { + // closing with no opens + unmatched.add(pos); + } + } + } + + // any remaining opens on stack are unmatched + for (int[] open : stack) { + unmatched.add(open[1]); + } + + return unmatched; + } + /** * Find closing bracket, returns offset from start or 0 if not found */ From fad17fa3bd064974c4ad6a7892aefa3179b4c408 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 14:19:32 +0200 Subject: [PATCH 136/358] Closed unfocused search bar on escape (again) --- .../npcs/client/gui/util/script/SearchReplaceBar.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index 6076b7a8d..da0eacde4 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -899,6 +899,11 @@ public class SearchReplaceBar { * @return true if input was consumed */ public boolean keyTyped(char c, int keyCode) { + if (keyCode == Keyboard.KEY_ESCAPE && visible) { + close(); + return true; + } + if (!visible || (!searchFieldFocused && !replaceFieldFocused)) return false; // Check modifier keys - don't let them trigger first-match issue @@ -913,11 +918,6 @@ public class SearchReplaceBar { return true; // Consume but don't process } - if (keyCode == Keyboard.KEY_ESCAPE) { - close(); - return true; - } - if (keyCode == Keyboard.KEY_TAB && showReplace) { searchFieldFocused = !searchFieldFocused; replaceFieldFocused = !replaceFieldFocused; From 4973203f72bb3bde1e72c380fd3ee2331c39716e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 14:21:53 +0200 Subject: [PATCH 137/358] Removed unused --- .../gui/util/script/JavaTextContainer.java | 90 +------------------ 1 file changed, 1 insertion(+), 89 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 8dae4b782..48b198db5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -54,95 +54,7 @@ public class JavaTextContainer extends TextContainer { super(text); this.text = text == null ? "" : text.replaceAll("\\r?\\n|\\r", "\n"); } - - public synchronized List getBraceSpans() { - if (braceSpansCache != null) - return braceSpansCache; - List spans = new ArrayList<>(); - if (text == null || text.isEmpty() || lines == null || lines.isEmpty()) { - braceSpansCache = spans; - return spans; - } - - // Heuristics limits to avoid runaway spans when braces are unbalanced. - final int MAX_SPAN_LINES = 200; // cap span length to this many lines - final int MAX_STACK_DEPTH = 512; // avoid excessive stack growth - - List excluded = MethodBlock.getExcludedRanges(text); - int exIdx = 0; - java.util.ArrayList openStack = new java.util.ArrayList<>(); - int len = text.length(); - for (int p = 0; p < len; p++) { - while (exIdx < excluded.size() && p >= excluded.get(exIdx)[1]) - exIdx++; - if (exIdx < excluded.size()) { - int[] r = excluded.get(exIdx); - if (p >= r[0] && p < r[1]) { - p = r[1] - 1; - continue; - } - } - char c = text.charAt(p); - if (c == '{') { - if (openStack.size() < MAX_STACK_DEPTH) { - // Look ahead to see if this open has a plausible closing brace within limits. - int openLine = getLineIndex(p); - int lookaheadChars = Math.max(1000, MAX_SPAN_LINES * 120); - int searchEnd = Math.min(len, p + lookaheadChars); - boolean foundClose = false; - int exIdx2 = exIdx; // local index for scanning ahead - for (int q = p + 1; q < searchEnd; q++) { - while (exIdx2 < excluded.size() && q >= excluded.get(exIdx2)[1]) exIdx2++; - if (exIdx2 < excluded.size()) { - int[] r2 = excluded.get(exIdx2); - if (q >= r2[0] && q < r2[1]) { q = r2[1] - 1; continue; } - } - if (text.charAt(q) == '}') { - int closeLineCandidate = getLineIndex(q); - if (closeLineCandidate - openLine <= MAX_SPAN_LINES) { - foundClose = true; - break; - } - } - } - if (foundClose) openStack.add(p); - } - } else if (c == '}') { - if (!openStack.isEmpty()) { - int openPos = openStack.remove(openStack.size() - 1); - int level = openStack.size() + 1; - int openLine = getLineIndex(openPos); - int closeLine = getLineIndex(p); - // Cap spans to avoid drawing across the whole document when syntax is broken - if (closeLine - openLine > MAX_SPAN_LINES) { - closeLine = Math.min(openLine + MAX_SPAN_LINES, lines.size() - 1); - } - // Only add spans where closeLine > openLine (multi-line blocks) - if (closeLine > openLine) { - spans.add(new int[]{level, openLine, closeLine}); - } - } - } - } - - braceSpansCache = spans; - return spans; - } - - private int getLineIndex(int pos) { - if (lines == null || lines.isEmpty()) - return 0; - int length = text == null ? 0 : text.length(); - for (int i = 0; i < lines.size(); i++) { - LineData d = lines.get(i); - int s = Math.max(0, Math.min(d.start, length)); - int e = Math.max(0, Math.min(d.end, length)); - if (pos >= s && pos <= e) - return i; - } - return Math.max(0, lines.size() - 1); - } - + public void init(int width, int height) { // lineHeight = ClientProxy.Font.height(); // if (lineHeight == 0) From 67b2ba16dd4a06d7820a40eab32c2fc9aa1423dc Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 14:49:11 +0200 Subject: [PATCH 138/358] Documented JavaTextContainer.init --- .../gui/util/script/JavaTextContainer.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 48b198db5..5b2c96f2d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -65,13 +65,21 @@ public class JavaTextContainer extends TextContainer { for (int lineIndex = 0; lineIndex < split.length; lineIndex++) { String l = split[lineIndex]; StringBuilder line = new StringBuilder(); + // Break the source line `l` into layout-friendly segments using `regexWord`. + // `regexWord` finds word tokens (letters/numbers/underscore/hyphen), newlines + // or the end-of-line. The loop takes substrings from the previous match + // index `i` up to the current `m.start()` so each `word` contains the + // next token plus any following delimiters (spaces, punctuation). + // This lets us measure and wrap at token boundaries rather than mid-word. Matcher m = regexWord.matcher(l); int i = 0; while (m.find()) { String word = l.substring(i, m.start()); if (ClientProxy.Font.width(line + word) > width - 10) { - lines.add(new LineData(line.toString(), totalChars, totalChars + line.length())); - totalChars += line.length(); + // Note: `end` is an exclusive offset into the full text (start..end). + // For wrapped lines we record the current `totalChars` as the start + // and compute the exclusive end as `start + line.length()`. + lines.add(new LineData(line.toString(), totalChars, (totalChars += line.length()))); line = new StringBuilder(); } line.append(word); @@ -88,9 +96,9 @@ public class JavaTextContainer extends TextContainer { continue; } int endOffset = isLastLine ? 0 : 1; - lines.add(new LineData(line.toString(), totalChars, totalChars + line.length() + endOffset)); - - totalChars += line.length() + endOffset; + // Note: `end` is exclusive. For non-last lines we include the trailing + // newline by adding `endOffset` (1), so previous line end == next line start. + lines.add(new LineData(line.toString(), totalChars, (totalChars += line.length() + endOffset))); } linesCount = lines.size(); totalHeight = linesCount * lineHeight; From b4fb4765f53bad8002e31c1c0b814008ae33746f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 14:49:46 +0200 Subject: [PATCH 139/358] Fixed GoToLine line's last column going to first col in next line --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 288734d1b..4b844435f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -190,7 +190,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { int lineLength = ld.end - ld.start; // Convert 1-indexed column to 0-indexed, clamp to line length - int col = Math.max(0, Math.min(column - 1, lineLength)); + // lineLength - 1 because the line's end is the start of the next line + int col = Math.max(0, Math.min(column - 1, lineLength - 1)); int position = ld.start + col; // Set cursor position From 16f894baca8661b1ff36cae75da453e40388b6d3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 18:23:52 +0200 Subject: [PATCH 140/358] FULLY fixed phantom trailing line bug (Backspace logic was adding an extra index) --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 4b844435f..a9c9153f3 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1174,13 +1174,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { // intuitive on blank/indented lines outside any recognized scope. LineData currCheck = selection.findCurrentLine(container.lines); if (currCheck != null && currCheck.text.trim().length() == 0) { - int removeEnd = text.indexOf('\n', currCheck.start); + int removeEnd = text.indexOf('\n', currCheck.start-1); if (removeEnd == -1) { removeEnd = text.length(); } else { removeEnd = removeEnd + 1; // include the newline } - String before = text.substring(0, currCheck.start); + String before = text.substring(0, currCheck.start-1); String after = removeEnd <= text.length() ? text.substring(removeEnd) : ""; setText(before + after); int newCursor = Math.max(0, currCheck.start - 1); From c091a31022d2dbc6ad2c84960da775274b93c9e1 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 18:42:48 +0200 Subject: [PATCH 141/358] Reverted JavaTextContainer.init to its original state before phantom line bug destroyed it lol --- .../client/gui/util/GuiScriptTextArea.java | 10 +-- .../npcs/client/gui/util/TextContainer.java | 2 +- .../gui/util/script/JavaTextContainer.java | 62 +++++-------------- 3 files changed, 21 insertions(+), 53 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index a9c9153f3..cd16b9c65 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1791,11 +1791,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.text = text; //this.container = new TextContainer(text); - this.container = new JavaTextContainer(text); - this.container.init(this.width, this.height); - if (this.enableCodeHighlighting) { + if (this.container == null) + this.container = new JavaTextContainer(text); + + this.container.init(text, this.width, this.height); + + if (this.enableCodeHighlighting) this.container.formatCodeText(); - } // Ensure scroll state stays in bounds after text change int maxScroll = Math.max(0, this.container.linesCount - this.container.visibleLines); diff --git a/src/main/java/noppes/npcs/client/gui/util/TextContainer.java b/src/main/java/noppes/npcs/client/gui/util/TextContainer.java index 2ea02cfd6..2dc588c0f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/TextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/TextContainer.java @@ -25,7 +25,7 @@ public class TextContainer { public List makeup = new ArrayList(); public List lines = new ArrayList(); - public int lineHeight; + public int lineHeight = 13; public int totalHeight; public int visibleLines = 1; public int linesCount; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 5b2c96f2d..88e647703 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -9,7 +9,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; public class JavaTextContainer extends TextContainer { - public static final char colorChar = '\u00A7'; public static final Pattern MODIFIER = Pattern.compile( "\\b(public|protected|private|static|final|abstract|synchronized|native|default)\\b"); @@ -41,29 +40,20 @@ public class JavaTextContainer extends TextContainer { public static final Pattern NUMBER = Pattern.compile( "\\b-?(?:0[xX][\\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?(?:[fFbBdDlLsS])?|NaN|null|Infinity|true|false)\\b"); - public String text; public List lines = new ArrayList<>(); public List methodBlocks = new ArrayList<>(); - public int lineHeight; - public int totalHeight; - public int visibleLines = 1; - // Cached brace spans: list of {level, startLine, endLine} - private List braceSpansCache = null; public JavaTextContainer(String text) { super(text); - this.text = text == null ? "" : text.replaceAll("\\r?\\n|\\r", "\n"); } - - public void init(int width, int height) { - // lineHeight = ClientProxy.Font.height(); - // if (lineHeight == 0) - lineHeight = 13; - - String[] split = text.split("\\n", -1); // -1 preserves empty lines "" + + public void init(String text,int width, int height) { + this.text = text == null ? "" : text.replaceAll("\\r?\\n|\\r", "\n"); + lines.clear(); + String[] split = text.split("\n",-1); + int totalChars = 0; - for (int lineIndex = 0; lineIndex < split.length; lineIndex++) { - String l = split[lineIndex]; + for (String l : split) { StringBuilder line = new StringBuilder(); // Break the source line `l` into layout-friendly segments using `regexWord`. // `regexWord` finds word tokens (letters/numbers/underscore/hyphen), newlines @@ -79,36 +69,20 @@ public class JavaTextContainer extends TextContainer { // Note: `end` is an exclusive offset into the full text (start..end). // For wrapped lines we record the current `totalChars` as the start // and compute the exclusive end as `start + line.length()`. - lines.add(new LineData(line.toString(), totalChars, (totalChars += line.length()))); + lines.add(new LineData(line.toString(), totalChars, totalChars += line.length())); line = new StringBuilder(); } line.append(word); i = m.start(); } - // Only add +1 for the newline if this is NOT the last line - // The last line has no newline after it. If the source text - // ends with a trailing '\n' split(...) produces a final empty - // element — skip creating a LineData for that trailing empty - // split so there's no phantom clickable empty line. - boolean isLastLine = (lineIndex == split.length - 1); - if (isLastLine && l.isEmpty() && text.endsWith("\n") && split.length > 1) { - // skip trailing empty split caused by final newline - continue; - } - int endOffset = isLastLine ? 0 : 1; - // Note: `end` is exclusive. For non-last lines we include the trailing - // newline by adding `endOffset` (1), so previous line end == next line start. - lines.add(new LineData(line.toString(), totalChars, (totalChars += line.length() + endOffset))); + lines.add(new LineData(line.toString(), totalChars, totalChars += line.length() + 1)); } linesCount = lines.size(); totalHeight = linesCount * lineHeight; - // Number of fully-visible lines that fit in the given height. + // Number of fully-visible lines that fit in the given viewport height. // Use floor division and ensure at least 1 line is visible. // Don't forget -1, fixes enter auto-scrolling properly - visibleLines = Math.max(height / lineHeight - 1, 1); - - // Invalidate caches that depend on line layout - invalidateCaches(); + visibleLines = Math.max(height / lineHeight - 1, 1); } private List globalFields = new ArrayList<>(); @@ -180,10 +154,7 @@ public class JavaTextContainer extends TextContainer { } } } - - public synchronized void invalidateCaches() { - this.braceSpansCache = null; - } + public void formatCodeText() { // Step 1: Tokenize the full text List marks = new ArrayList<>(); @@ -208,12 +179,7 @@ public class JavaTextContainer extends TextContainer { // Compute indent guides based on matched braces, ignoring strings/comments computeIndentGuides(marks); - - // Invalidate cached brace spans; will be recomputed on demand - invalidateCaches(); - - - + // Step 2: Clear existing tokens for (LineData line : lines) { line.tokens.clear(); @@ -320,7 +286,7 @@ public class JavaTextContainer extends TextContainer { return result; } - public class LineData { + public static class LineData { public String text; public int start, end; public List tokens = new ArrayList<>(); From ce9eb610599c1bbba42a216103135f510e796ac3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 18:56:58 +0200 Subject: [PATCH 142/358] Highlighted indent lines in green for selected braces only, not[] or () pairs --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index cd16b9c65..5a0d6a588 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -338,11 +338,17 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Always highlight unmatched braces (positions in text) List unmatchedBraces = BracketMatcher.findUnmatchedBracePositions(text); - // Determine which exact brace span (openLine/closeLine) should be highlighted based on bracket under caret + // Determine which exact brace span (openLine/closeLine) to highlight indent guides for int highlightedOpenLine = -1; int highlightedCloseLine = -1; if (startBracket != endBracket && startBracket >= 0) { int bracketLineIdx = -1; + // Only consider curly braces for highlighting the indent guides + boolean isCurlyBracket = false; + if (text != null && startBracket >= 0 && startBracket < text.length()) { + char bc = text.charAt(startBracket); + if (bc == '{' || bc == '}') isCurlyBracket = true; + } for (int li = 0; li < list.size(); li++) { LineData ld = list.get(li); if (startBracket >= ld.start && startBracket < ld.end) { @@ -350,7 +356,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { break; } } - if (bracketLineIdx >= 0) { + if (bracketLineIdx >= 0 && isCurlyBracket) { for (int[] span : braceSpans) { int openLine = span[1]; int closeLine = span[2]; From 9a03ccf56946c266da3caa55baab3d68d7896edb Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 18:59:17 +0200 Subject: [PATCH 143/358] Cut indent lines height by 2 pixels if highlighted --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 5a0d6a588..45a9888bd 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -543,10 +543,14 @@ public class GuiScriptTextArea extends GuiNpcTextField { int px = ClientProxy.Font.width(sb.toString()); int gx = x + LINE_NUMBER_GUTTER_WIDTH + 4 + px - 2; // shift left ~2px for the IntelliJ feel + boolean highlighted = (openLine == highlightedOpenLine && closeLine == highlightedCloseLine); + int guideColor = highlighted ? 0x9933cc00 : 0x33FFFFFF; + int topY = y + (drawStart - scroll.getScrolledLine()) * container.lineHeight; int bottomY = y + (endLine - scroll.getScrolledLine() + 1) * container.lineHeight; - int guideColor = (openLine == highlightedOpenLine && closeLine == highlightedCloseLine) ? 0x9933cc00 : 0x33FFFFFF; - drawRect(gx, topY, gx + 1, bottomY, guideColor); + if(highlighted) + bottomY-=2; + drawRect(gx, topY, gx + 1, bottomY, guideColor); } } int yPos = posY + stringYOffset; From 85b2fe24a257b1c3371e90a514b380f12f817da5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 19:50:28 +0200 Subject: [PATCH 144/358] SearchReplaceBar QoL: * Shifted text editor viewport appropriately when opening/closing bar * Bar is now maintained across multiple script tabs with its focus * Can open bar without having main editor viewport focused --- .../client/gui/util/GuiScriptTextArea.java | 28 +++++++++++++++---- .../gui/util/script/SearchReplaceBar.java | 22 +++++++++++---- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 45a9888bd..7befbf6d3 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -161,6 +161,14 @@ public class GuiScriptTextArea extends GuiNpcTextField { public void onMatchesUpdated() { // Called when matches change - could be used for UI updates } + + // Shift text editor viewport up or down bar + public void resizeEditor(boolean open) { + int scrollHeight = searchBar.getTotalHeight(); + GuiScriptTextArea.this.y += open ? scrollHeight : -scrollHeight; + GuiScriptTextArea.this.height += open ? -scrollHeight : scrollHeight; + container.visibleLines = Math.max(GuiScriptTextArea.this.height / container.lineHeight - 1, 1); + } }); // Initialize Go To Line dialog with callback this.goToLineDialog = new GoToLineDialog(new GoToLineDialog.GoToLineCallback() { @@ -247,6 +255,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Initialize search bar (preserves state across initGui calls) searchBar.initGui(x, y, width); + if (searchBar.isVisible()) { // If open + // Shift viewport down again + searchBar.callback.resizeEditor(true); + if (!active) // Focus search if opening another script tab & bar is open + searchBar.focus(false); + } // Initialize Go To Line dialog goToLineDialog.initGui(x, y, width); @@ -587,8 +601,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { goToLineDialog.draw(xMouse, yMouse); KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); - // y -= searchHeight; - // height += searchHeight; } private void scissorViewport() { @@ -855,11 +867,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } }); - + + + // Check if can open just for SearchReplaceBar and GoToLine + Supplier openBoxes = () -> !KEYS_OVERLAY.showOverlay; + // SEARCH: Open search bar (Ctrl+R) // Works in search bar. KEYS.SEARCH.setTask(e -> { - if (!e.isPress() || !isActive.get() && !searchBar.isVisible()) + if (!e.isPress() || !openBoxes.get()) return; unfocusAll(); @@ -869,7 +885,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // SEARCH_REPLACE: Open search+replace bar (Ctrl+Shift+R) // Works in search bar. KEYS.SEARCH_REPLACE.setTask(e -> { - if (!e.isPress() || !isActive.get() && !searchBar.isVisible()) + if (!e.isPress() || !openBoxes.get()) return; unfocusAll(); @@ -879,7 +895,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // GO_TO_LINE: Open go to line dialog (Ctrl+G) // Works in search bar. KEYS.GO_TO_LINE.setTask(e -> { - if (!e.isPress() || !isActive.get() && !searchBar.isVisible()) + if (!e.isPress() || !openBoxes.get()) return; unfocusAll(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index da0eacde4..b85efe52d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -98,7 +98,7 @@ public class SearchReplaceBar { private boolean lastClickWasSearch = true; // ==================== CALLBACK ==================== - private SearchCallback callback; + public SearchCallback callback; private final FontRenderer font = Minecraft.getMinecraft().fontRenderer; @@ -116,6 +116,7 @@ public class SearchReplaceBar { void unfocusMainEditor(); void focusMainEditor(); void onMatchesUpdated(); // Called when matches change (for undo/redo sync) + void resizeEditor(boolean open); } public SearchReplaceBar() { @@ -140,16 +141,18 @@ public class SearchReplaceBar { * Open search bar (Ctrl+R) */ public void openSearch() { + boolean oldVisible = this.visible; visible = true; showReplace = false; searchFieldFocused = true; replaceFieldFocused = false; if (callback != null) { + if (!oldVisible) //if wasnt open before, do it once. + callback.resizeEditor(true); callback.unfocusMainEditor(); String highlight = callback.getHighlightedWord(); - if (highlight != null) { + if (highlight != null) searchText = highlight; - } } searchSelectionStart = 0; searchSelectionEnd = searchText.length(); @@ -170,12 +173,21 @@ public class SearchReplaceBar { * Close the search bar and restore focus to main editor */ public void close() { + if (callback != null) { + callback.resizeEditor(false); + callback.focusMainEditor(); + } visible = false; searchFieldFocused = false; replaceFieldFocused = false; - if (callback != null) callback.focusMainEditor(); } - + + public void focus(boolean replace) { + if (replace) + replaceFieldFocused = true; + else + searchFieldFocused = true; + } /** * Unfocus the search bar (when clicking main editor) */ From 244424ea0db50b84cbd2b9a534e11d0858c6208f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 19:51:19 +0200 Subject: [PATCH 145/358] Corrected args passed to callback --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 7befbf6d3..b4280fecb 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -106,7 +106,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Calculate offset to account for search bar height int searchBarOffset = searchBar.getTotalHeight(); - int effectiveHeight = height - searchBarOffset; + int effectiveHeight = GuiScriptTextArea.this.height - searchBarOffset; int visibleLines = effectiveHeight / container.lineHeight; // Calculate how many lines the search bar covers int linesHiddenBySRB = searchBarOffset > 0 ? (int) Math.ceil((double) searchBarOffset / container.lineHeight) : 0; @@ -206,7 +206,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { selection.reset(position); // Scroll to make the line visible - int visible = height / (container != null ? container.lineHeight : 12); + int visible = GuiScriptTextArea.this.height / (container != null ? container.lineHeight : 12); int maxScroll = Math.max(0, container.linesCount - visible); scroll.scrollToLine(lineIdx, visible, maxScroll); } From b8be3afa5679cf8d939f02850dda79742beaa532 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 20:07:06 +0200 Subject: [PATCH 146/358] Fixed searchBar always selecting the first match when found matches are updating, so selected one is maintained. --- .../npcs/client/gui/util/script/SearchReplaceBar.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index b85efe52d..e3a183e64 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -7,6 +7,7 @@ import net.minecraft.util.ChatAllowedCharacters; import net.minecraft.util.ResourceLocation; import noppes.npcs.client.gui.util.GuiUtil; import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; +import noppes.npcs.util.ValueUtil; import org.lwjgl.input.Keyboard; import org.lwjgl.opengl.GL11; @@ -1158,7 +1159,6 @@ public class SearchReplaceBar { public void updateMatches() { matches.clear(); excludedMatches.clear(); - currentMatchIndex = -1; if (callback == null || searchText.isEmpty()) { if (callback != null) callback.onMatchesUpdated(); @@ -1191,11 +1191,13 @@ public class SearchReplaceBar { } catch (Exception ignored) { } + // No need to navigate right now if (!matches.isEmpty()) { - currentMatchIndex = 0; - navigateToCurrentMatch(); + // currentMatchIndex = 0; + // navigateToCurrentMatch(); } + currentMatchIndex = ValueUtil.clamp(currentMatchIndex, 0, matches.size() - 1); callback.onMatchesUpdated(); } From 7b800694dbc2520eeae3908f88eb29f29ea48304 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 20:09:51 +0200 Subject: [PATCH 147/358] TAB now switches focus from search bar to main editor --- .../client/gui/util/script/SearchReplaceBar.java | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index e3a183e64..43a87b12f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -930,10 +930,15 @@ public class SearchReplaceBar { keyCode == Keyboard.KEY_LMENU || keyCode == Keyboard.KEY_RMENU) { return true; // Consume but don't process } - - if (keyCode == Keyboard.KEY_TAB && showReplace) { - searchFieldFocused = !searchFieldFocused; - replaceFieldFocused = !replaceFieldFocused; + + if (keyCode == Keyboard.KEY_TAB) { + if (showReplace) { + searchFieldFocused = !searchFieldFocused; + replaceFieldFocused = !replaceFieldFocused; + } else if (searchFieldFocused && callback != null) { + searchFieldFocused = false; + callback.focusMainEditor(); + } markActivity(); return true; } From 0124ad4fd4553f0f5258834bf7406f65b94f48dc Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 20:30:04 +0200 Subject: [PATCH 148/358] Opening search bar now sets highlighted word as currentMatch --- .../client/gui/util/GuiScriptTextArea.java | 10 +++++ .../gui/util/script/SearchReplaceBar.java | 39 ++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index b4280fecb..54b8c3de2 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -94,6 +94,16 @@ public class GuiScriptTextArea extends GuiNpcTextField { return GuiScriptTextArea.this.highlightedWord; } + @Override + public int getSelectionStart() { + return GuiScriptTextArea.this.selection.getStartSelection(); + } + + @Override + public int getSelectionEnd() { + return GuiScriptTextArea.this.selection.getEndSelection(); + } + @Override public void setText(String newText) { GuiScriptTextArea.this.setText(newText); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index 43a87b12f..24de5f316 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -110,6 +110,11 @@ public class SearchReplaceBar { String getText(); String getHighlightedWord(); + + // Return current selection bounds in the main editor (start inclusive, end exclusive) + int getSelectionStart(); + + int getSelectionEnd(); void setText(String text); void scrollToPosition(int position); void setSelection(int start, int end); @@ -147,19 +152,28 @@ public class SearchReplaceBar { showReplace = false; searchFieldFocused = true; replaceFieldFocused = false; + boolean matchHighlight = false; if (callback != null) { if (!oldVisible) //if wasnt open before, do it once. callback.resizeEditor(true); callback.unfocusMainEditor(); + String highlight = callback.getHighlightedWord(); - if (highlight != null) + if (highlight != null) { searchText = highlight; + matchHighlight = true; + } } searchSelectionStart = 0; searchSelectionEnd = searchText.length(); searchCursor = searchText.length(); markActivity(); updateMatches(); + if (matchHighlight) { + setCurrentMatchToHighlight(); + navigateToCurrentMatch(); + } + } /** @@ -1205,6 +1219,29 @@ public class SearchReplaceBar { currentMatchIndex = ValueUtil.clamp(currentMatchIndex, 0, matches.size() - 1); callback.onMatchesUpdated(); } + + /** + * If the editor currently has a selection, prefer the match that contains that selection + * and navigate to it. + */ + private void setCurrentMatchToHighlight() { + if (callback == null || matches.isEmpty()) + return; + try { + int selStart = callback.getSelectionStart(); + int selEnd = callback.getSelectionEnd(); + if (selStart >= 0 && selEnd > selStart) { + for (int i = 0; i < matches.size(); i++) { + int[] m = matches.get(i); + if (m[0] <= selStart && m[1] >= selEnd) { + currentMatchIndex = i; + return; + } + } + } + } catch (Exception ignored) { + } + } /** * Navigate to next match From 918c3acf7ff5e0884cf67308eb6b6d21a37043fc Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 20 Dec 2025 20:59:39 +0200 Subject: [PATCH 149/358] Add line padding at the bottom of the text editor --- .../client/gui/util/GuiScriptTextArea.java | 45 +++++++++++++------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 54b8c3de2..f43e86988 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -54,6 +54,19 @@ public class GuiScriptTextArea extends GuiNpcTextField { public String highlightedWord; private JavaTextContainer container = null; private boolean enableCodeHighlighting = false; + // Extra empty lines to allow padding at the bottom of the editor viewport + private int bottomPaddingLines = 6; + + private int getPaddedLineCount() { + if (container == null) return 0; + // Only add bottom padding when the content is already scrollable. This avoids + // introducing a scrollbar when there's nothing to scroll for. + if (container.linesCount > container.visibleLines - bottomPaddingLines) { + return Math.max(0, container.linesCount + bottomPaddingLines); + } else { + return container.linesCount; + } + } // ==================== HELPER CLASS INSTANCES ==================== private final ScrollState scroll = new ScrollState(); @@ -125,7 +138,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { LineData ld = container.lines.get(i); if (position >= ld.start && position < ld.end) { int visible = Math.max(1, visibleLines); - int maxScroll = Math.max(0, container.linesCount - visible); + int effectiveVisible = Math.max(1, visible - bottomPaddingLines); + int maxScroll = Math.max(0, getPaddedLineCount() - visible); int targetLine = i; // If search bar is visible and would hide this line, scroll down so it's visible @@ -137,7 +151,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Force scroll so target line appears just below the search bar scroll.setTargetScroll(Math.max(0, targetLine - linesHiddenBySRB), maxScroll); } else { - scroll.scrollToLine(targetLine, visible, maxScroll); + scroll.scrollToLine(targetLine, effectiveVisible, maxScroll); } break; } @@ -217,8 +231,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Scroll to make the line visible int visible = GuiScriptTextArea.this.height / (container != null ? container.lineHeight : 12); - int maxScroll = Math.max(0, container.linesCount - visible); - scroll.scrollToLine(lineIdx, visible, maxScroll); + int effectiveVisible = Math.max(1, visible - bottomPaddingLines); + int maxScroll = Math.max(0, getPaddedLineCount() - visible); + scroll.scrollToLine(lineIdx, effectiveVisible, maxScroll); } @Override @@ -308,7 +323,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { container.visibleLines = (height / container.lineHeight); - int maxScroll = Math.max(0, this.container.linesCount - container.visibleLines); + int maxScroll = Math.max(0, getPaddedLineCount() - container.visibleLines); // Handle mouse wheel scroll int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); @@ -320,7 +335,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Handle scrollbar dragging (delegated to ScrollState) if (scroll.isClickScrolling()) - scroll.handleClickScrolling(yMouse, x, y, height, container.visibleLines, container.linesCount, maxScroll); + scroll.handleClickScrolling(yMouse, x, y, height, container.visibleLines, getPaddedLineCount(), maxScroll); // Update scroll animation scroll.initializeIfNeeded(scroll.getScrolledLine()); @@ -595,10 +610,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (hasVerticalScrollbar()) { Minecraft.getMinecraft().renderEngine.bindTexture(GuiCustomScroll.resource); - int sbSize = Math.max((int) (1f * (container.visibleLines) / container.linesCount * height), 2); + int effLines = Math.max(1, getPaddedLineCount()); + int sbSize = Math.max((int) (1f * (container.visibleLines) / effLines * height), 2); int posX = x + width - 6; - double linesCount = Math.max(1, (double) container.linesCount); + double linesCount = (double) effLines; int posY = (int) (y + 1f * scroll.getScrollPos() / linesCount * (height - 4)) + 1; drawRect(posX, posY, posX + 5, posY + sbSize + 2, 0xFFe0e0e0); @@ -682,9 +698,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { int lineIdx = getCursorLineIndex(); int visible = Math.max(1, container.visibleLines); - int maxScroll = Math.max(0, container.linesCount - visible); + int effectiveVisible = Math.max(1, visible - bottomPaddingLines); + int maxScroll = Math.max(0, getPaddedLineCount() - visible); - scroll.scrollToLine(lineIdx, visible, maxScroll); + scroll.scrollToLine(lineIdx, effectiveVisible, maxScroll); } // ==================== KEY BINDINGS INITIALIZATION ==================== @@ -1757,10 +1774,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Prefer delegating scrollbar-start logic to ScrollState. If the click // is on the scrollbar area and the scrollbar can be dragged, we start // click-scrolling mode and cancel the normal text click-drag behavior. - if (this.clicked && this.container.linesCount * this.container.lineHeight > this.height && xMouse > this.x + this.width - 8) { + if (this.clicked && getPaddedLineCount() * this.container.lineHeight > this.height && xMouse > this.x + this.width - 8) { // We consumed the mouse-down as a scrollbar drag start this.clicked = false; - scroll.startScrollbarDrag(yMouse,this.y,this.height,container.linesCount); + scroll.startScrollbarDrag(yMouse,this.y,this.height, getPaddedLineCount()); } else { // Handle double/triple click selection counting if (time - this.lastClicked < 300L) { @@ -1836,7 +1853,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.container.formatCodeText(); // Ensure scroll state stays in bounds after text change - int maxScroll = Math.max(0, this.container.linesCount - this.container.visibleLines); + int maxScroll = Math.max(0, getPaddedLineCount() - this.container.visibleLines); scroll.clampToBounds(maxScroll); selection.clamp(this.text.length()); @@ -1857,7 +1874,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } public boolean hasVerticalScrollbar() { - return this.container != null && this.container.visibleLines < this.container.linesCount; + return this.container != null && this.container.visibleLines < getPaddedLineCount(); } public void enableCodeHighlighting() { From 6f0ccace42f3d3ceefc1e94a48630898aa01bc85 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 00:49:02 +0200 Subject: [PATCH 150/358] All new FormatHelper; * Built in FormatSettings * takes care of all appropriate formatting operations needed in this editor's context --- .../client/gui/util/script/FormatHelper.java | 1124 +++++++++++++++++ .../client/gui/util/script/IndentHelper.java | 84 +- .../gui/util/script/JavaTextContainer.java | 91 +- .../client/gui/util/script/MethodBlock.java | 31 + 4 files changed, 1303 insertions(+), 27 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java b/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java new file mode 100644 index 000000000..675a6b097 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java @@ -0,0 +1,1124 @@ +package noppes.npcs.client.gui.util.script; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Configurable code formatter with IntelliJ-like settings. + * + * Features: + * - Operator spacing (a+b → a + b) + * - Whitespace normalization (a b → a b) + * - Long line wrapping + * - Bracket spacing configuration + * - Preserves string content and comments + */ +public class FormatHelper { + + // ==================== SETTINGS ==================== + + /** + * Formatting configuration - similar to IntelliJ Code Style settings + */ + public static class FormatSettings { + // Spaces around operators + public boolean spaceAroundAssignment = true; // a = b + public boolean spaceAroundArithmetic = true; // a + b - c * d / e + public boolean spaceAroundComparison = true; // a == b, a != b, a <= b + public boolean spaceAroundLogical = true; // a && b || c + public boolean spaceAroundBitwise = true; // a & b | c ^ d + + // Spaces within brackets + public boolean spaceWithinParens = false; // (a) vs ( a ) + public boolean spaceWithinBrackets = false; // [a] vs [ a ] + public boolean spaceWithinBraces = true; // {a} vs { a } (for initializers) + + // Spaces before/after + public boolean spaceAfterComma = true; // a, b vs a,b + public boolean spaceBeforeComma = false; // a, b vs a , b + public boolean spaceAfterSemicolon = true; // for (a; b; c) vs for (a;b;c) + public boolean spaceBeforeSemicolon = false; + public boolean spaceAfterColon = true; // case a: b vs case a:b + public boolean spaceBeforeColon = false; + + // Method calls + public boolean spaceBeforeMethodParens = false; // foo() vs foo () + public boolean spaceAfterTypeCast = true; // (int) x vs (int)x + + // Control statements + public boolean spaceBeforeIfParens = true; // if (...) vs if(...) + public boolean spaceBeforeWhileParens = true; + public boolean spaceBeforeForParens = true; + public boolean spaceBeforeSwitchParens = true; + public boolean spaceBeforeCatchParens = true; + + // Braces + public boolean spaceBeforeOpenBrace = true; // if () { vs if (){ + + // Line length + public int maxLineLength = 120; + public boolean wrapLongLines = false; // Auto-wrap lines longer than maxLineLength + public int wrapIndentSpaces = 4; // Extra indent for wrapped lines + public boolean wrapComments = true; // Also wrap long comments + + // Wrap break preferences (where to break long lines) + public boolean wrapAfterComma = true; // Prefer breaking after commas + public boolean wrapAfterOperator = true; // Prefer breaking after operators + public boolean wrapBeforeDot = true; // Break before . for chained calls + public boolean wrapMethodArguments = true; // Each arg on new line when wrapping method calls + + // Whitespace normalization + public boolean normalizeWhitespace = true; // Multiple spaces → single space + public boolean trimTrailingWhitespace = true; + + // Unary operators + public boolean spaceAroundUnaryNot = false; // !a vs ! a + public boolean spaceAroundUnaryIncDec = false; // ++i vs ++ i + + /** + * Create default settings (similar to IntelliJ defaults) + */ + public static FormatSettings defaults() { + return new FormatSettings(); + } + + /** + * Create compact settings (minimal spacing) + */ + public static FormatSettings compact() { + FormatSettings s = new FormatSettings(); + s.spaceWithinParens = false; + s.spaceWithinBrackets = false; + s.spaceWithinBraces = false; + s.wrapLongLines = false; + return s; + } + } + + private FormatSettings settings; + + public FormatHelper() { + this.settings = FormatSettings.defaults(); + } + + public FormatHelper(FormatSettings settings) { + this.settings = settings != null ? settings : FormatSettings.defaults(); + } + + public void setSettings(FormatSettings settings) { + this.settings = settings; + } + + public FormatSettings getSettings() { + return settings; + } + + // ==================== MAIN FORMAT METHOD ==================== + + /** + * Format the entire text with all configured options + */ + public String format(String text) { + if (text == null || text.isEmpty()) return text; + + // Process line by line to preserve structure + String[] lines = text.split("\n", -1); + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < lines.length; i++) { + String line = formatLine(lines[i], text, getLineStartOffset(lines, i)); + + if (settings.trimTrailingWhitespace) { + line = trimTrailing(line); + } + + result.append(line); + if (i < lines.length - 1) { + result.append("\n"); + } + } + + return result.toString(); + } + + private int getLineStartOffset(String[] lines, int lineIndex) { + int offset = 0; + for (int i = 0; i < lineIndex; i++) { + offset += lines[i].length() + 1; // +1 for \n + } + return offset; + } + + /** + * Format a single line of code + */ + private String formatLine(String line, String fullText, int lineOffset) { + if (line.trim().isEmpty()) return line; + + // Preserve leading indentation + int indent = 0; + while (indent < line.length() && (line.charAt(indent) == ' ' || line.charAt(indent) == '\t')) { + indent++; + } + String indentation = line.substring(0, indent); + String content = line.substring(indent); + + if (content.isEmpty()) return line; + + // Skip formatting for lines that are entirely comments + String trimmed = content.trim(); + if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) { + return line; + } + + // Format the content + String formatted = formatContent(content, fullText, lineOffset + indent); + + return indentation + formatted; + } + + /** + * Format line content, preserving strings and comments + */ + private String formatContent(String content, String fullText, int offset) { + // Find string/comment regions to preserve + List preserveRegions = findPreserveRegions(content); + + if (preserveRegions.isEmpty()) { + // No strings/comments, format everything + return formatCodeSegment(content); + } + + // Build result by formatting non-preserved regions + StringBuilder result = new StringBuilder(); + int lastEnd = 0; + + for (int[] region : preserveRegions) { + // Format code before this preserved region + if (region[0] > lastEnd) { + String codeSegment = content.substring(lastEnd, region[0]); + result.append(formatCodeSegment(codeSegment)); + } + + // Append preserved region unchanged + result.append(content.substring(region[0], region[1])); + lastEnd = region[1]; + } + + // Format remaining code after last preserved region + if (lastEnd < content.length()) { + result.append(formatCodeSegment(content.substring(lastEnd))); + } + + return result.toString(); + } + + /** + * Find string literals and comments that should not be formatted + */ + private List findPreserveRegions(String content) { + List regions = new ArrayList<>(); + + int i = 0; + while (i < content.length()) { + char c = content.charAt(i); + + // String literal + if (c == '"') { + int end = findStringEnd(content, i, '"'); + regions.add(new int[]{i, end}); + i = end; + continue; + } + + // Char literal + if (c == '\'') { + int end = findStringEnd(content, i, '\''); + regions.add(new int[]{i, end}); + i = end; + continue; + } + + // Line comment + if (c == '/' && i + 1 < content.length() && content.charAt(i + 1) == '/') { + regions.add(new int[]{i, content.length()}); + break; + } + + // Block comment (could span multiple lines but we process line-by-line) + if (c == '/' && i + 1 < content.length() && content.charAt(i + 1) == '*') { + int end = content.indexOf("*/", i + 2); + if (end >= 0) { + regions.add(new int[]{i, end + 2}); + i = end + 2; + } else { + regions.add(new int[]{i, content.length()}); + break; + } + continue; + } + + i++; + } + + return regions; + } + + /** + * Find end of string/char literal + */ + private int findStringEnd(String content, int start, char quote) { + int i = start + 1; + while (i < content.length()) { + char c = content.charAt(i); + if (c == '\\' && i + 1 < content.length()) { + i += 2; // Skip escape sequence + continue; + } + if (c == quote) { + return i + 1; + } + i++; + } + return content.length(); + } + + // ==================== CODE SEGMENT FORMATTING ==================== + + /** + * Format a code segment (no strings/comments) + */ + private String formatCodeSegment(String code) { + if (code.isEmpty()) return code; + + String result = code; + + // Normalize whitespace first + if (settings.normalizeWhitespace) { + result = normalizeWhitespace(result); + } + + // Apply operator spacing + result = formatOperators(result); + + // Apply bracket spacing + result = formatBrackets(result); + + // Apply keyword spacing + result = formatKeywords(result); + + // Apply punctuation spacing + result = formatPunctuation(result); + + return result; + } + + /** + * Normalize whitespace (multiple spaces → single space) + */ + private String normalizeWhitespace(String code) { + // Replace multiple spaces with single space, but preserve indentation + return code.replaceAll(" +", " "); + } + + /** + * Format operator spacing + */ + private String formatOperators(String code) { + String result = code; + + // Assignment operators + if (settings.spaceAroundAssignment) { + // Compound assignments first (longer patterns first) + result = formatBinaryOp(result, "+=", " += "); + result = formatBinaryOp(result, "-=", " -= "); + result = formatBinaryOp(result, "*=", " *= "); + result = formatBinaryOp(result, "/=", " /= "); + result = formatBinaryOp(result, "%=", " %= "); + result = formatBinaryOp(result, "&=", " &= "); + result = formatBinaryOp(result, "|=", " |= "); + result = formatBinaryOp(result, "^=", " ^= "); + result = formatBinaryOp(result, "<<=", " <<= "); + result = formatBinaryOp(result, ">>=", " >>= "); + result = formatBinaryOp(result, ">>>=", " >>>= "); + + // Simple assignment (but not ==, !=, <=, >=) + result = formatSimpleAssignment(result); + } + + // Comparison operators + if (settings.spaceAroundComparison) { + result = formatBinaryOp(result, "==", " == "); + result = formatBinaryOp(result, "!=", " != "); + result = formatBinaryOp(result, "<=", " <= "); + result = formatBinaryOp(result, ">=", " >= "); + // Simple < > (but not generics) + result = formatComparisonOperators(result); + } + + // Logical operators + if (settings.spaceAroundLogical) { + result = formatBinaryOp(result, "&&", " && "); + result = formatBinaryOp(result, "||", " || "); + } + + // Arithmetic operators + if (settings.spaceAroundArithmetic) { + // Handle ++ and -- first to avoid conflicts + result = formatArithmeticOperators(result); + } + + // Bitwise operators (but not in generics) + if (settings.spaceAroundBitwise) { + result = formatBitwiseOperators(result); + } + + return result; + } + + /** + * Format a binary operator with proper spacing + */ + private String formatBinaryOp(String code, String op, String replacement) { + // First normalize: remove extra spaces around the operator + String pattern = "\\s*" + Pattern.quote(op) + "\\s*"; + return code.replaceAll(pattern, replacement); + } + + /** + * Format simple assignment = (not ==, !=, <=, >=, etc.) + */ + private String formatSimpleAssignment(String code) { + StringBuilder result = new StringBuilder(); + int i = 0; + while (i < code.length()) { + char c = code.charAt(i); + if (c == '=') { + // Check it's not part of ==, !=, <=, >=, +=, etc. + char prev = i > 0 ? code.charAt(i - 1) : ' '; + char next = i + 1 < code.length() ? code.charAt(i + 1) : ' '; + + boolean isCompound = prev == '!' || prev == '<' || prev == '>' || + prev == '+' || prev == '-' || prev == '*' || + prev == '/' || prev == '%' || prev == '&' || + prev == '|' || prev == '^'; + boolean isEquality = next == '='; + + if (!isCompound && !isEquality) { + // Ensure space before = + if (result.length() > 0 && result.charAt(result.length() - 1) != ' ') { + result.append(' '); + } + result.append('='); + // Ensure space after = + if (next != ' ') { + result.append(' '); + } + } else { + result.append(c); + } + } else { + result.append(c); + } + i++; + } + return result.toString(); + } + + /** + * Format < and > comparison operators (avoiding generics) + */ + private String formatComparisonOperators(String code) { + StringBuilder result = new StringBuilder(); + int i = 0; + while (i < code.length()) { + char c = code.charAt(i); + + if ((c == '<' || c == '>') && !isLikelyGeneric(code, i, c)) { + char prev = i > 0 ? code.charAt(i - 1) : ' '; + char next = i + 1 < code.length() ? code.charAt(i + 1) : ' '; + + // Skip if part of <<, >>, <=, >= + if ((c == '<' && next == '<') || (c == '>' && next == '>') || + next == '=' || prev == '<' || prev == '>') { + result.append(c); + } else { + // Add spacing + if (result.length() > 0 && result.charAt(result.length() - 1) != ' ') { + result.append(' '); + } + result.append(c); + if (next != ' ' && next != '=') { + result.append(' '); + } + } + } else { + result.append(c); + } + i++; + } + return result.toString(); + } + + /** + * Check if < or > is likely part of a generic type declaration + */ + private boolean isLikelyGeneric(String code, int pos, char bracket) { + if (bracket == '<') { + // Check for pattern like List<, Map<, etc. + if (pos > 0) { + int start = pos - 1; + while (start > 0 && Character.isLetterOrDigit(code.charAt(start - 1))) { + start--; + } + String before = code.substring(start, pos); + // Common generic types or capital letter start (type name) + if (!before.isEmpty() && Character.isUpperCase(before.charAt(0))) { + return true; + } + } + // Check if there's a matching > + int depth = 1; + for (int i = pos + 1; i < code.length() && depth > 0; i++) { + char c = code.charAt(i); + if (c == '<') depth++; + else if (c == '>') depth--; + else if (c == ';' || c == '{' || c == '}') break; // Not a generic + } + return depth == 0; + } else { // '>' + // Check if there's a matching < before + int depth = 1; + for (int i = pos - 1; i >= 0 && depth > 0; i--) { + char c = code.charAt(i); + if (c == '>') depth++; + else if (c == '<') depth--; + else if (c == ';' || c == '{' || c == '}') break; + } + return depth == 0; + } + } + + /** + * Format arithmetic operators (+, -, *, /, %) + */ + private String formatArithmeticOperators(String code) { + StringBuilder result = new StringBuilder(); + int i = 0; + while (i < code.length()) { + char c = code.charAt(i); + char next = i + 1 < code.length() ? code.charAt(i + 1) : ' '; + char prev = i > 0 ? code.charAt(i - 1) : ' '; + + boolean isArithOp = c == '+' || c == '-' || c == '*' || c == '/' || c == '%'; + + if (isArithOp) { + // Skip ++, --, +=, -=, *=, /=, %=, and unary +/- + if ((c == '+' && next == '+') || (c == '-' && next == '-') || + (c == '+' && prev == '+') || (c == '-' && prev == '-') || + next == '=') { + result.append(c); + } else if (isUnaryContext(code, i)) { + // Unary + or - (after operator, open paren, comma, etc.) + result.append(c); + } else { + // Binary operator - add spacing + if (result.length() > 0 && result.charAt(result.length() - 1) != ' ') { + result.append(' '); + } + result.append(c); + if (next != ' ' && next != '=' && !((c == '+' && next == '+') || (c == '-' && next == '-'))) { + result.append(' '); + } + } + } else { + result.append(c); + } + i++; + } + return result.toString(); + } + + /** + * Check if position is in a unary context (operator is unary, not binary) + */ + private boolean isUnaryContext(String code, int pos) { + if (pos == 0) return true; + + // Look back for context + int i = pos - 1; + while (i >= 0 && code.charAt(i) == ' ') { + i--; + } + + if (i < 0) return true; + + char prev = code.charAt(i); + // Unary context after: ( [ { , ; = + - * / % < > & | ^ ? : + return prev == '(' || prev == '[' || prev == '{' || prev == ',' || + prev == ';' || prev == '=' || prev == '+' || prev == '-' || + prev == '*' || prev == '/' || prev == '%' || prev == '<' || + prev == '>' || prev == '&' || prev == '|' || prev == '^' || + prev == '?' || prev == ':' || prev == '!'; + } + + /** + * Format bitwise operators + */ + private String formatBitwiseOperators(String code) { + String result = code; + + // Shift operators first + result = formatBinaryOp(result, ">>>", " >>> "); + result = formatBinaryOp(result, "<<", " << "); + result = formatBinaryOp(result, ">>", " >> "); + + // Single & | ^ (not && ||) + StringBuilder sb = new StringBuilder(); + int i = 0; + while (i < result.length()) { + char c = result.charAt(i); + char next = i + 1 < result.length() ? result.charAt(i + 1) : ' '; + char prev = i > 0 ? result.charAt(i - 1) : ' '; + + if ((c == '&' && next != '&' && prev != '&' && next != '=') || + (c == '|' && next != '|' && prev != '|' && next != '=') || + (c == '^' && next != '=')) { + if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') { + sb.append(' '); + } + sb.append(c); + if (next != ' ') { + sb.append(' '); + } + } else { + sb.append(c); + } + i++; + } + + return sb.toString(); + } + + // ==================== BRACKET FORMATTING ==================== + + /** + * Format bracket spacing + */ + private String formatBrackets(String code) { + String result = code; + + // Parentheses + if (settings.spaceWithinParens) { + result = result.replaceAll("\\(\\s*", "( "); + result = result.replaceAll("\\s*\\)", " )"); + } else { + result = result.replaceAll("\\(\\s+", "("); + result = result.replaceAll("\\s+\\)", ")"); + } + + // Square brackets + if (settings.spaceWithinBrackets) { + result = result.replaceAll("\\[\\s*", "[ "); + result = result.replaceAll("\\s*\\]", " ]"); + } else { + result = result.replaceAll("\\[\\s+", "["); + result = result.replaceAll("\\s+\\]", "]"); + } + + // Curly braces (for array initializers inline) + // Note: standalone braces for blocks are handled by indentation logic + + return result; + } + + // ==================== KEYWORD FORMATTING ==================== + + /** + * Format keyword spacing (if, while, for, etc.) + */ + private String formatKeywords(String code) { + String result = code; + + if (settings.spaceBeforeIfParens) { + result = result.replaceAll("\\bif\\s*\\(", "if ("); + } else { + result = result.replaceAll("\\bif\\s+\\(", "if("); + } + + if (settings.spaceBeforeWhileParens) { + result = result.replaceAll("\\bwhile\\s*\\(", "while ("); + } else { + result = result.replaceAll("\\bwhile\\s+\\(", "while("); + } + + if (settings.spaceBeforeForParens) { + result = result.replaceAll("\\bfor\\s*\\(", "for ("); + } else { + result = result.replaceAll("\\bfor\\s+\\(", "for("); + } + + if (settings.spaceBeforeSwitchParens) { + result = result.replaceAll("\\bswitch\\s*\\(", "switch ("); + } else { + result = result.replaceAll("\\bswitch\\s+\\(", "switch("); + } + + if (settings.spaceBeforeCatchParens) { + result = result.replaceAll("\\bcatch\\s*\\(", "catch ("); + } else { + result = result.replaceAll("\\bcatch\\s+\\(", "catch("); + } + + // Space before opening brace + if (settings.spaceBeforeOpenBrace) { + result = result.replaceAll("\\)\\s*\\{", ") {"); + } else { + result = result.replaceAll("\\)\\s+\\{", "){"); + } + + return result; + } + + // ==================== PUNCTUATION FORMATTING ==================== + + /** + * Format punctuation spacing (commas, semicolons, colons) + */ + private String formatPunctuation(String code) { + String result = code; + + // Commas + if (settings.spaceAfterComma && !settings.spaceBeforeComma) { + result = result.replaceAll("\\s*,\\s*", ", "); + } else if (settings.spaceAfterComma && settings.spaceBeforeComma) { + result = result.replaceAll("\\s*,\\s*", " , "); + } else if (!settings.spaceAfterComma && settings.spaceBeforeComma) { + result = result.replaceAll("\\s*,\\s*", " ,"); + } else { + result = result.replaceAll("\\s*,\\s*", ","); + } + + // Semicolons (but not in for loops - handle more carefully) + // This is tricky because for(;;) should be handled differently + // For now, just handle trailing semicolons + if (settings.trimTrailingWhitespace) { + result = result.replaceAll("\\s+;", ";"); + } + + return result; + } + + /** + * Trim trailing whitespace from a line + */ + private String trimTrailing(String line) { + int end = line.length(); + while (end > 0 && (line.charAt(end - 1) == ' ' || line.charAt(end - 1) == '\t')) { + end--; + } + return line.substring(0, end); + } + + // ==================== STATIC UTILITIES ==================== + + /** + * Format code with default settings + */ + public static String formatCode(String code) { + return new FormatHelper().format(code); + } + + /** + * Format code with custom settings + */ + public static String formatCode(String code, FormatSettings settings) { + return new FormatHelper(settings).format(code); + } + + // ==================== LINE WRAPPING ==================== + + /** + * Wrap long lines according to settings. + * Call this after format() if wrapLongLines is enabled. + * + * @param text The text to wrap + * @param maxWidth Maximum line width (use settings.maxLineLength if 0) + * @return Text with wrapped lines + */ + public String wrapLines(String text, int maxWidth) { + if (text == null || text.isEmpty()) return text; + + int width = maxWidth > 0 ? maxWidth : settings.maxLineLength; + String[] lines = text.split("\n", -1); + StringBuilder result = new StringBuilder(); + + for (int i = 0; i < lines.length; i++) { + String line = lines[i]; + + if (getVisualLength(line) <= width) { + result.append(line); + } else { + // Determine if this is a comment line + String trimmed = line.trim(); + if (trimmed.startsWith("//")) { + result.append(wrapCommentLine(line, width)); + } else if (trimmed.startsWith("/*") || trimmed.startsWith("*")) { + result.append(wrapBlockCommentLine(line, width)); + } else { + result.append(wrapCodeLine(line, width)); + } + } + + if (i < lines.length - 1) { + result.append("\n"); + } + } + + return result.toString(); + } + + /** + * Get visual length of a line (tabs count as 4 spaces) + */ + private int getVisualLength(String line) { + int len = 0; + for (char c : line.toCharArray()) { + if (c == '\t') { + len += 4; + } else { + len++; + } + } + return len; + } + + /** + * Extract leading indentation from a line + */ + private String extractIndent(String line) { + int i = 0; + while (i < line.length() && (line.charAt(i) == ' ' || line.charAt(i) == '\t')) { + i++; + } + return line.substring(0, i); + } + + /** + * Wrap a single-line comment (// style) + */ + private String wrapCommentLine(String line, int maxWidth) { + if (!settings.wrapComments) return line; + + String indent = extractIndent(line); + String content = line.substring(indent.length()); + + // Must start with // + if (!content.startsWith("//")) return line; + + String commentContent = content.substring(2).trim(); + String prefix = indent + "// "; + int prefixLen = getVisualLength(prefix); + int contentWidth = maxWidth - prefixLen; + + if (contentWidth <= 10) return line; // Too narrow to wrap + + StringBuilder result = new StringBuilder(); + result.append(indent).append("// "); + + String[] words = commentContent.split("\\s+"); + int lineLen = prefixLen; + + for (int i = 0; i < words.length; i++) { + String word = words[i]; + int wordLen = word.length(); + + if (lineLen + wordLen > maxWidth && lineLen > prefixLen) { + // Start new line + result.append("\n").append(prefix); + lineLen = prefixLen; + } + + if (lineLen > prefixLen) { + result.append(" "); + lineLen++; + } + + result.append(word); + lineLen += wordLen; + } + + return result.toString(); + } + + /** + * Wrap a block comment line (* style) + */ + private String wrapBlockCommentLine(String line, int maxWidth) { + if (!settings.wrapComments) return line; + + String indent = extractIndent(line); + String content = line.substring(indent.length()).trim(); + + // Determine prefix for wrapped lines + String prefix; + String textContent; + + if (content.startsWith("/**")) { + prefix = indent + " * "; + textContent = content.substring(3).trim(); + } else if (content.startsWith("/*")) { + prefix = indent + " * "; + textContent = content.substring(2).trim(); + } else if (content.startsWith("*")) { + prefix = indent + " * "; + textContent = content.substring(1).trim(); + } else { + return line; + } + + // Handle closing */ + boolean hasClose = textContent.endsWith("*/"); + if (hasClose) { + textContent = textContent.substring(0, textContent.length() - 2).trim(); + } + + int prefixLen = getVisualLength(prefix); + int contentWidth = maxWidth - prefixLen; + + if (contentWidth <= 10) return line; + + StringBuilder result = new StringBuilder(); + + // First line keeps original prefix + if (content.startsWith("/**")) { + result.append(indent).append("/** "); + } else if (content.startsWith("/*")) { + result.append(indent).append("/* "); + } else { + result.append(indent).append(" * "); + } + + String[] words = textContent.split("\\s+"); + int lineLen = prefixLen; + + for (int i = 0; i < words.length; i++) { + String word = words[i]; + int wordLen = word.length(); + + if (lineLen + wordLen > maxWidth && lineLen > prefixLen) { + result.append("\n").append(prefix); + lineLen = prefixLen; + } + + if (lineLen > prefixLen) { + result.append(" "); + lineLen++; + } + + result.append(word); + lineLen += wordLen; + } + + if (hasClose) { + result.append(" */"); + } + + return result.toString(); + } + + /** + * Wrap a code line at appropriate break points + */ + private String wrapCodeLine(String line, int maxWidth) { + String indent = extractIndent(line); + String content = line.substring(indent.length()); + + // Additional indent for wrapped lines + String wrapIndent = indent + spaces(settings.wrapIndentSpaces); + + // Find break points - positions where we can break the line + List breakPoints = findBreakPoints(content); + + if (breakPoints.isEmpty()) { + // No good break points, just return as-is + return line; + } + + StringBuilder result = new StringBuilder(); + result.append(indent); + + int currentLen = getVisualLength(indent); + int lastBreak = 0; + boolean isFirstLine = true; + + for (BreakPoint bp : breakPoints) { + int segmentEnd = bp.position; + String segment = content.substring(lastBreak, segmentEnd); + int segmentLen = getVisualLength(segment); + + // Check if adding this segment would exceed max width + if (currentLen + segmentLen > maxWidth && !isFirstLine) { + // Break here + result.append("\n").append(wrapIndent); + currentLen = getVisualLength(wrapIndent); + isFirstLine = false; + + // Trim leading space from continuation if any + segment = trimLeading(segment); + } else if (currentLen + segmentLen > maxWidth && isFirstLine && lastBreak > 0) { + // Even first segment is too long after indent, break before it + result.append("\n").append(wrapIndent); + currentLen = getVisualLength(wrapIndent); + isFirstLine = false; + } + + result.append(segment); + currentLen += getVisualLength(segment); + lastBreak = segmentEnd; + isFirstLine = false; + } + + // Append remaining content + if (lastBreak < content.length()) { + String remaining = content.substring(lastBreak); + int remLen = getVisualLength(remaining); + + if (currentLen + remLen > maxWidth && !isFirstLine) { + result.append("\n").append(wrapIndent); + remaining = trimLeading(remaining); + } + + result.append(remaining); + } + + return result.toString(); + } + + /** + * Trim leading whitespace (Java 8 compatible) + */ + private String trimLeading(String s) { + int start = 0; + while (start < s.length() && Character.isWhitespace(s.charAt(start))) { + start++; + } + return s.substring(start); + } + + /** + * A potential line break point + */ + private static class BreakPoint { + int position; // Position in string (after this char) + @SuppressWarnings("unused") + int priority; // Lower = better place to break (for future sorting) + + BreakPoint(int pos, int prio) { + this.position = pos; + this.priority = prio; + } + } + + /** + * Find good positions to break a line + */ + private List findBreakPoints(String content) { + List points = new ArrayList<>(); + + int parenDepth = 0; + int bracketDepth = 0; + int braceDepth = 0; + boolean inString = false; + char stringChar = 0; + + for (int i = 0; i < content.length(); i++) { + char c = content.charAt(i); + char prev = i > 0 ? content.charAt(i - 1) : ' '; + + // Track string state + if ((c == '"' || c == '\'') && prev != '\\') { + if (!inString) { + inString = true; + stringChar = c; + } else if (c == stringChar) { + inString = false; + } + } + + if (inString) continue; + + // Track bracket depth + if (c == '(') parenDepth++; + else if (c == ')') parenDepth--; + else if (c == '[') bracketDepth++; + else if (c == ']') bracketDepth--; + else if (c == '{') braceDepth++; + else if (c == '}') braceDepth--; + + // Calculate total nesting depth (prefer breaking at lower depth) + int depth = parenDepth + bracketDepth + braceDepth; + + // Find break points + if (settings.wrapAfterComma && c == ',') { + // After comma is a good break point (better at lower depth) + points.add(new BreakPoint(i + 1, 1 + depth)); + } + + if (settings.wrapAfterOperator) { + // After binary operators + if ((c == '+' || c == '-') && i > 0 && i < content.length() - 1) { + char next = content.charAt(i + 1); + if (prev != '(' && prev != '[' && prev != ',' && prev != '=' && + next != '+' && next != '-' && next != '=') { + points.add(new BreakPoint(i + 1, 3 + depth)); + } + } + if (c == '&' && i + 1 < content.length() && content.charAt(i + 1) == '&') { + points.add(new BreakPoint(i + 2, 2 + depth)); + } + if (c == '|' && i + 1 < content.length() && content.charAt(i + 1) == '|') { + points.add(new BreakPoint(i + 2, 2 + depth)); + } + } + + if (settings.wrapBeforeDot && c == '.') { + // Before dot (for method chaining) + points.add(new BreakPoint(i, 2 + depth)); + } + + // After opening brace + if (c == '{' && i + 1 < content.length()) { + points.add(new BreakPoint(i + 1, 1)); + } + } + + // Sort by position + points.sort((a, b) -> Integer.compare(a.position, b.position)); + + return points; + } + + /** + * Create a string of n spaces + */ + private String spaces(int n) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < n; i++) { + sb.append(' '); + } + return sb.toString(); + } + + /** + * Format and wrap code in one step + */ + public String formatAndWrap(String text, int maxWidth) { + String formatted = format(text); + if (settings.wrapLongLines) { + return wrapLines(formatted, maxWidth); + } + return formatted; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java b/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java index 8a7acaa2a..88bc354b8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java @@ -145,36 +145,86 @@ public class IndentHelper { } /** - * Format entire text with proper indentation + * Format entire text with proper indentation and operator spacing */ public static FormatResult formatText(String text, int cursorPosition) { - String[] linesArr = text.split("\n", -1); - StringBuilder out = new StringBuilder(text.length() + 32); + return formatText(text, cursorPosition, 0); + } + + /** + * Format entire text with proper indentation, operator spacing, and optional line wrapping + * @param text The text to format + * @param cursorPosition Current cursor position + * @param viewportWidth Viewport width for line wrapping (0 = no wrapping) + */ + public static FormatResult formatText(String text, int cursorPosition, int viewportWidth) { + // First pass: fix indentation + String indented = formatIndentation(text, cursorPosition); + + // Second pass: apply operator spacing and whitespace normalization + FormatHelper helper = new FormatHelper(); + FormatHelper.FormatSettings settings = helper.getSettings(); + + // Enable wrapping if viewport width is provided + if (viewportWidth > 0) { + settings.wrapLongLines = true; + settings.wrapComments = true; + // Use 75% of viewport width as max line length for some margin + int charWidth = 6; // Approximate character width + settings.maxLineLength = Math.max(60, (int)(viewportWidth * 0.75 / charWidth)); + } + + String formatted = helper.format(indented); + + // Apply wrapping if enabled + if (viewportWidth > 0 && settings.wrapLongLines) { + formatted = helper.wrapLines(formatted, settings.maxLineLength); + } + + // Recalculate cursor position (try to maintain relative position) + int newCursorPos = Math.min(cursorPosition, formatted.length()); + + // Try to find cursor on same line with same content context + String[] origLines = text.split("\n", -1); + String[] newLines = formatted.split("\n", -1); - // Track cursor position in formatted output - int cursorLine = -1; - int cursorColInContent = 0; int lineStartPos = 0; + int cursorLine = -1; + int cursorColInLine = 0; - // Find cursor's line and column - for (int li = 0; li < linesArr.length; li++) { - String line = linesArr[li]; - int lineEndPos = lineStartPos + line.length(); - + for (int li = 0; li < origLines.length; li++) { + int lineEndPos = lineStartPos + origLines[li].length(); if (cursorPosition >= lineStartPos && cursorPosition <= lineEndPos) { cursorLine = li; - int leadingSpaces = line.length() - line.replaceAll("^[ \\t]+", "").length(); - cursorColInContent = Math.max(0, cursorPosition - lineStartPos - leadingSpaces); + cursorColInLine = cursorPosition - lineStartPos; break; } lineStartPos = lineEndPos + 1; } + if (cursorLine >= 0 && cursorLine < newLines.length) { + int newLineStart = 0; + for (int li = 0; li < cursorLine; li++) { + newLineStart += newLines[li].length() + 1; + } + // Clamp cursor to new line length + newCursorPos = newLineStart + Math.min(cursorColInLine, newLines[cursorLine].length()); + } + + return new FormatResult(formatted, newCursorPos); + } + + /** + * Format indentation only (first pass) + */ + private static String formatIndentation(String text, int cursorPosition) { + String[] linesArr = text.split("\n", -1); + StringBuilder out = new StringBuilder(text.length() + 32); + boolean inString = false; boolean escape = false; boolean inBlockComment = false; int depth = 0; - int newCursorPos = 0; for (int li = 0; li < linesArr.length; li++) { String line = linesArr[li]; @@ -218,17 +268,13 @@ public class IndentHelper { if (startsWithClose) indentLevel = Math.max(0, indentLevel - 1); int targetIndent = indentLevel * TAB_SIZE; - if (li == cursorLine) { - newCursorPos = out.length() + targetIndent + Math.min(cursorColInContent, trimmedLeading.length()); - } - out.append(spaces(targetIndent)).append(trimmedLeading); if (li < linesArr.length - 1) out.append('\n'); depth = Math.max(0, depth + opens - closes); } - return new FormatResult(out.toString(), newCursorPos); + return out.toString(); } /** diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 88e647703..8873fbc0a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -139,22 +139,95 @@ public class JavaTextContainer extends TextContainer { } private void highlightVariableReferences(List marks) { + // Known Java keywords and types that shouldn't be flagged as undefined + java.util.Set knownIdentifiers = new java.util.HashSet<>(java.util.Arrays.asList( + // Primitive types + "boolean", "int", "float", "double", "long", "char", "byte", "short", "void", + // Keywords + "null", "true", "false", "if", "else", "switch", "case", "for", "while", "do", + "try", "catch", "finally", "return", "throw", "var", "let", "const", "function", + "continue", "break", "this", "new", "typeof", "instanceof", "class", "interface", + "extends", "implements", "import", "package", "public", "private", "protected", + "static", "final", "abstract", "synchronized", "native", "default", "enum", + "throws", "super", "assert", "volatile", "transient", "strictfp", "goto", + // Common JS/scripting keywords + "undefined", "NaN", "Infinity", "arguments", "prototype", "constructor", + // Common types (first letter uppercase pattern handles most) + "String", "Object", "Array", "Math", "System", "Integer", "Double", "Float", + "Boolean", "Long", "Byte", "Short", "Character", "List", "Map", "Set" + )); + Pattern identifier = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); Matcher m = identifier.matcher(text); while (m.find()) { String name = m.group(1); int position = m.start(1); - - // Check if this variable is local to the method it's in + + // Skip known keywords and types + if (knownIdentifiers.contains(name)) continue; + + // Skip type names (first letter uppercase) + if (Character.isUpperCase(name.charAt(0))) continue; + + // Check if inside a method MethodBlock methodBlock = findMethodBlockAtPosition(position); - if (methodBlock != null && methodBlock.localVariables.contains(name)) { - marks.add(new Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD)); - } else if (globalFields.contains(name)) { - marks.add(new Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); + + if (methodBlock != null) { + // Inside a method - check in order: parameter, local, global + if (methodBlock.parameters.contains(name)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.PARAMETER)); + } else if (methodBlock.localVariables.contains(name)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD)); + } else if (globalFields.contains(name)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); + } else { + // Unknown variable - mark as undefined (Bug 11) + // But only if it looks like a variable reference (not a method call) + if (!isMethodCall(position) && !isTypeReference(name, position)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR)); + } + } + } else { + // Outside any method - check global fields + if (globalFields.contains(name)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); + } + // Don't mark as undefined outside methods - could be a type name or declaration } } } + /** + * Check if the identifier at this position is a method call (followed by parenthesis) + */ + private boolean isMethodCall(int position) { + // Skip whitespace after identifier + int i = position; + while (i < text.length() && Character.isLetterOrDigit(text.charAt(i)) || text.charAt(i) == '_') { + i++; + } + // Skip whitespace + while (i < text.length() && Character.isWhitespace(text.charAt(i))) { + i++; + } + // Check for opening paren + return i < text.length() && text.charAt(i) == '('; + } + + /** + * Check if this looks like a type reference (e.g., part of a declaration or generic) + */ + private boolean isTypeReference(String name, int position) { + // Check if preceded by 'new ', '<', or ',Type' + if (position > 4) { + String before = text.substring(Math.max(0, position - 5), position); + if (before.endsWith("new ") || before.endsWith("< ") || before.endsWith("<")) { + return true; + } + } + return false; + } + public void formatCodeText() { // Step 1: Tokenize the full text List marks = new ArrayList<>(); @@ -366,8 +439,10 @@ public class JavaTextContainer extends TextContainer { METHOD_CALL('a', 50), NUMBER('7', 40), VARIABLE('f', 30), - GLOBAL_FIELD('b', 35), // new - LOCAL_FIELD('e', 25), // new + GLOBAL_FIELD('b', 35), // aqua - class-level fields + LOCAL_FIELD('e', 25), // yellow - local variables + PARAMETER('9', 36), // blue - method parameters (Bug 10) + UNDEFINED_VAR('4', 10), // dark red - undefined variables (Bug 11) DEFAULT('f', 0); public final char color; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java index a85b2ce35..e16fdb764 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java @@ -16,18 +16,49 @@ public class MethodBlock { // Pattern for variable declarations (same as in JavaTextContainer) public static final Pattern LOCAL_VAR_DECL = Pattern.compile( "\\b([A-Z][a-zA-Z0-9_<>\\[\\]]*|[a-z][a-zA-Z0-9_]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + + // Pattern for method parameters: Type name (with optional ... for varargs) + public static final Pattern PARAMETER_PATTERN = Pattern.compile( + "([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*(?:\\.{3})?)\\s+([a-zA-Z_][a-zA-Z0-9_]*)"); public int startOffset; public int endOffset; public String text; public List localVariables = new ArrayList<>(); + public List parameters = new ArrayList<>(); // Method parameters public MethodBlock(int start, int end, String text) { this.startOffset = start; this.endOffset = end; this.text = text; + extractParameters(); extractLocalVariables(); } + + // Extract method parameters from the method signature + private void extractParameters() { + parameters.clear(); + + // Find the parameter list between ( and ) + int parenStart = text.indexOf('('); + int parenEnd = text.indexOf(')'); + + if (parenStart < 0 || parenEnd <= parenStart) return; + + String paramList = text.substring(parenStart + 1, parenEnd); + + // Skip if empty + if (paramList.trim().isEmpty()) return; + + // Split by comma (but be careful of generic types like List) + Matcher m = PARAMETER_PATTERN.matcher(paramList); + while (m.find()) { + String paramName = m.group(2); + if (!parameters.contains(paramName)) { + parameters.add(paramName); + } + } + } // Extract local variables declared within this method private void extractLocalVariables() { From e75e87dd436182cc58f880929a57dc9f4a4d10c2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 00:49:34 +0200 Subject: [PATCH 151/358] Refactore/Rename KeyPrest + functionality --- .../client/gui/util/GuiScriptTextArea.java | 217 ++++- .../util/script/RenameRefactorHandler.java | 846 ++++++++++++++++++ .../client/key/impl/ScriptEditorKeys.java | 3 + 3 files changed, 1063 insertions(+), 3 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index f43e86988..716d6c538 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -92,6 +92,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== GO TO LINE ==================== private final GoToLineDialog goToLineDialog; + // ==================== RENAME REFACTOR ==================== + private final RenameRefactorHandler renameHandler = new RenameRefactorHandler(); + // ==================== CONSTRUCTOR ==================== public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) { @@ -254,6 +257,113 @@ public class GuiScriptTextArea extends GuiNpcTextField { selection.markActivity(); } }); + + // Initialize Rename Refactor handler with callback + renameHandler.setCallback(new RenameRefactorHandler.RenameCallback() { + @Override + public String getText() { + return GuiScriptTextArea.this.text; + } + + @Override + public void setText(String newText) { + GuiScriptTextArea.this.setText(newText); + } + + @Override + public List getLines() { + return container != null ? container.lines : new ArrayList<>(); + } + + @Override + public int getCursorPosition() { + return selection.getCursorPosition(); + } + + public SelectionState getSelectionState() { + return selection; + } + + @Override + public void setCursorPosition(int pos) { + selection.reset(pos); + } + + @Override + public void unfocusMainEditor() { + active = false; + } + + @Override + public void focusMainEditor() { + active = true; + selection.markActivity(); + } + + @Override + public int getGutterWidth() { + return LINE_NUMBER_GUTTER_WIDTH; + } + + @Override + public int getLineHeight() { + return container != null ? container.lineHeight : 12; + } + + @Override + public int getScrolledLine() { + return scroll.getScrolledLine(); + } + + @Override + public double getFractionalOffset() { + return scroll.getFractionalOffset(); + } + + @Override + public void scrollToPosition(int pos) { + if (container == null || container.lines == null) + return; + for (int i = 0; i < container.lines.size(); i++) { + LineData ld = container.lines.get(i); + if (pos >= ld.start && pos < ld.end) { + int visible = Math.max(1, container.visibleLines); + int effectiveVisible = Math.max(1, visible - bottomPaddingLines); + int maxScroll = Math.max(0, getPaddedLineCount() - visible); + scroll.scrollToLine(i, effectiveVisible, maxScroll); + break; + } + } + } + + @Override + public JavaTextContainer getContainer() { + return container; + } + + @Override + public void setTextWithoutUndo(String newText) { + // Set text without creating an undo entry (for live rename preview) + boolean wasUndoing = undoing; + undoing = true; + setText(newText); + undoing = wasUndoing; + } + + @Override + public void pushUndoState(String textState) { + // Push a specific text state to the undo list + if (!undoing) { + undoList.add(new UndoData(textState, selection.getCursorPosition())); + redoList.clear(); + } + } + + @Override + public int getViewportWidth() { + return width - LINE_NUMBER_GUTTER_WIDTH - 8; // Account for gutter and scrollbar + } + }); initializeKeyBindings(); init(x, y, width, height, text); @@ -536,6 +646,56 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } + + // Highlight rename refactor occurrences + if (renameHandler.isActive()) { + List renameOccurrences = renameHandler.getOccurrences(); + for (int[] occ : renameOccurrences) { + // Check if occurrence overlaps with this line + if (occ[1] > data.start && occ[0] < data.end) { + int occStart = Math.max(occ[0] - data.start, 0); + int occEnd = Math.min(occ[1] - data.start, line.length()); + if (occStart < occEnd) { + int s = ClientProxy.Font.width(line.substring(0, occStart)); + int e = ClientProxy.Font.width(line.substring(0, occEnd)) + 1; + int occX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + s; + int occEndX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + e; + boolean isPrimary = renameHandler.isPrimaryOccurrence(occ[0]); + + // Draw background highlight + int bgColor = isPrimary ? 0x55335577 : 0x33224466; + drawRect(occX, posY, occEndX, posY + container.lineHeight, bgColor); + + // Draw white border for primary occurrence (IntelliJ-like) + if (isPrimary) { + int borderColor = 0xDDFFFFFF; + // Top border + drawRect(occX, posY, occEndX, posY + 1, borderColor); + // Bottom border + drawRect(occX, posY + container.lineHeight - 1, occEndX, + posY + container.lineHeight, borderColor); + // Left border + drawRect(occX, posY, occX + 1, posY + container.lineHeight, borderColor); + // Right border + drawRect(occEndX - 1, posY, occEndX, posY + container.lineHeight, borderColor); + + // Draw cursor inside the primary occurrence + if (renameHandler.shouldShowCursor()) { + int cursorInWord = renameHandler.getCursorInWord(); + String currentWord = renameHandler.getCurrentWord(); + if (currentWord != null && cursorInWord >= 0 && cursorInWord <= currentWord.length()) { + String beforeCursor = currentWord.substring(0, + Math.min(cursorInWord, currentWord.length())); + int cursorX = occX + ClientProxy.Font.width(beforeCursor); + drawRect(cursorX, posY + 1, cursorX + 1, posY + container.lineHeight - 1, + 0xFFFFFFFF); + } + } + } + } + } + } + } // Highlight the current line (light gray) under any selection if (active && isEnabled() && (selection.getCursorPosition() >= data.start && selection.getCursorPosition() < data.end || (i == list.size() - 1 && selection.getCursorPosition() == text.length()))) { @@ -625,6 +785,27 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Draw go to line dialog (overlays everything) goToLineDialog.draw(xMouse, yMouse); + + // Draw rename status indicator + if (renameHandler.isActive()) { + String status = renameHandler.getScopeDescription(); + String hint = "Enter \u2713 | Esc \u2715"; // Enter to confirm, Esc to cancel + int statusWidth = Math.max(ClientProxy.Font.width(status), ClientProxy.Font.width(hint)) + 8; + int statusX = x + width - statusWidth - 8; + int statusY = y + height - 30; + // Semi-transparent background + drawRect(statusX - 2, statusY - 2, statusX + statusWidth + 2, statusY + 24, 0xDD1a1a2e); + // Border (purple/blue for rename mode) + int borderColor = 0xFF6677dd; + drawRect(statusX - 2, statusY - 2, statusX + statusWidth + 2, statusY - 1, borderColor); + drawRect(statusX - 2, statusY + 23, statusX + statusWidth + 2, statusY + 24, borderColor); + drawRect(statusX - 2, statusY - 2, statusX - 1, statusY + 24, borderColor); + drawRect(statusX + statusWidth + 1, statusY - 2, statusX + statusWidth + 2, statusY + 24, borderColor); + // Status text (top line) + ClientProxy.Font.drawString(status, statusX + 4, statusY + 2, 0xFFccddff); + // Hint text (bottom line - dimmer) + ClientProxy.Font.drawString(hint, statusX + 4, statusY + 13, 0xAA88aacc); + } KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); } @@ -928,11 +1109,24 @@ public class GuiScriptTextArea extends GuiNpcTextField { unfocusAll(); goToLineDialog.toggle(); }); + + // RENAME: Start rename refactoring (Shift+F6) + KEYS.RENAME.setTask(e -> { + if (!e.isPress() || !openBoxes.get()) + return; + + unfocusAll(); + if (!renameHandler.isActive()) { + renameHandler.startRename(); + } + }); } public void unfocusAll() { if (searchBar.hasFocus()) searchBar.unfocus(); if (goToLineDialog.hasFocus()) goToLineDialog.unfocus(); + if (renameHandler.isActive()) + renameHandler.cancel(); } // ==================== KEYBOARD INPUT HANDLING ==================== @@ -943,6 +1137,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { @Override public boolean textboxKeyTyped(char c, int i) { KEYS_OVERLAY.keyTyped(c, i); + + // Handle rename refactor input first if active + if (renameHandler.isActive() && renameHandler.keyTyped(c, i)) + return true; // Handle Go To Line dialog input first if it has focus if (goToLineDialog.isVisible() &&goToLineDialog.keyTyped(c, i)) @@ -1543,7 +1741,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } public boolean closeOnEsc(){ - return !searchBar.isVisible() && !goToLineDialog.isVisible(); + return !searchBar.isVisible() && !goToLineDialog.isVisible() && !renameHandler.isActive(); } // ==================== KEYBOARD MODIFIERS ==================== @@ -1603,7 +1801,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { } private void formatText() { - IndentHelper.FormatResult result = IndentHelper.formatText(text, selection.getCursorPosition()); + // Calculate viewport width for line wrapping (account for gutter and scrollbar) + int viewportWidth = this.width - LINE_NUMBER_GUTTER_WIDTH - 10; + IndentHelper.FormatResult result = IndentHelper.formatText(text, selection.getCursorPosition(), viewportWidth); setText(result.text); selection.reset(Math.max(0, Math.min(result.cursorPosition, this.text.length()))); } @@ -1760,8 +1960,18 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Determine whether click occurred inside the text area bounds this.active = xMouse >= this.x && xMouse < this.x + this.width && yMouse >= this.y && yMouse < this.y + this.height; if (this.active) { - // Compute logical click position in text and reset selection/caret + // Compute logical click position in text int clickPos = this.getSelectionPos(xMouse, yMouse); + + // Check if rename refactoring is active and click is in the rename box + if (renameHandler.isActive() && renameHandler.handleClick(clickPos)) { + // Click was handled by rename handler - don't reset selection or do other click handling + this.clicked = false; + activeTextfield = this; + return; + } + + // Normal click handling - reset selection/caret selection.reset(clickPos); selection.markActivity(); @@ -1818,6 +2028,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { searchBar.updateCursor(); goToLineDialog.updateCursor(); + renameHandler.updateCursor(); ++this.cursorCounter; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java new file mode 100644 index 000000000..b8a9bfe26 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -0,0 +1,846 @@ +package noppes.npcs.client.gui.util.script; + +import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Handles IntelliJ-like rename refactoring with scope-aware renaming. + * + * Features: + * - Scope detection: local variables rename only within their block, global fields rename everywhere + * - Visual feedback: primary occurrence has white border, others just highlighted + * - ESC cancels, Enter confirms + * - Live preview as you type the new name + * - Full word selection by default when starting rename + * - Allows empty word during editing (shows empty box) + * - Click support within rename box + */ +public class RenameRefactorHandler { + + // ==================== STATE ==================== + private boolean active = false; + private String initialWord = ""; // The word when rename started (for undo grouping) + private String originalWord = ""; // Current baseline for live rename + private String currentWord = ""; // Current word being typed + private int primaryOccurrenceStart = -1; + private int primaryOccurrenceEnd = -1; + private List allOccurrences = new ArrayList<>(); + private ScopeInfo scope = null; + private int cursorInWord = 0; // Cursor position within the word being renamed + private boolean wordFullySelected = true; // Start with word fully selected + + // For restoring original text on cancel + private String originalText = ""; + private int originalCursorPos = 0; + + // Callback interface + private RenameCallback callback; + + // ==================== CURSOR BLINK ==================== + private int cursorCounter = 0; + private long lastInputTime = 0; + + // Pattern for identifiers + private static final Pattern IDENTIFIER = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*"); + + /** + * Callback interface for rename operations + */ + public interface RenameCallback { + String getText(); + + void setText(String text); + + void setTextWithoutUndo(String text); // Set text without creating undo entry + + void pushUndoState(String text); // Push an undo state + + List getLines(); + + int getCursorPosition(); + + SelectionState getSelectionState(); + + void setCursorPosition(int pos); + + void unfocusMainEditor(); + + void focusMainEditor(); + + int getGutterWidth(); + + int getLineHeight(); + + int getScrolledLine(); + + double getFractionalOffset(); + + void scrollToPosition(int pos); + + JavaTextContainer getContainer(); + + int getViewportWidth(); // For determining if clicks are in rename box + } + + /** + * Scope information for a variable + */ + public static class ScopeInfo { + public final int startOffset; + public final int endOffset; + public final boolean isGlobal; + public final String scopeType; // "global", "method", "block" + + public ScopeInfo(int start, int end, boolean isGlobal, String type) { + this.startOffset = start; + this.endOffset = end; + this.isGlobal = isGlobal; + this.scopeType = type; + } + + public boolean containsPosition(int pos) { + return pos >= startOffset && pos < endOffset; + } + } + + public void setCallback(RenameCallback callback) { + this.callback = callback; + } + + /** + * Start rename refactoring at the current cursor position. + * Selects the word under cursor and finds all occurrences within scope. + * + * @return true if rename mode was successfully started + */ + public boolean startRename() { + if (callback == null) + return false; + + String text = callback.getText(); + int cursor = callback.getCursorPosition(); + + if (text == null || text.isEmpty()) + return false; + + // Find word under cursor + int[] wordBounds = findWordAtPosition(text, cursor); + if (wordBounds == null) + return false; + + // Store original state for cancel AND for undo grouping + originalText = text; + originalCursorPos = cursor; + + initialWord = text.substring(wordBounds[0], wordBounds[1]); // Remember for undo + originalWord = initialWord; + currentWord = initialWord; + setSelection(wordBounds[0], wordBounds[1]); + + // Start with FULL WORD SELECTED (cursor at end, word selected) + cursorInWord = currentWord.length(); + wordFullySelected = true; + + // Determine scope for this identifier - MUST check if local shadows global + scope = determineScope(text, wordBounds[0], originalWord, callback.getContainer()); + + // Find all occurrences within scope + findOccurrences(text, originalWord); + + if (allOccurrences.isEmpty()) + return false; + + active = true; + markActivity(); + callback.unfocusMainEditor(); + + // Push initial undo state so we can restore to this point + callback.pushUndoState(text); + + return true; + } + + /** + * Cancel rename operation and restore original state + */ + public void cancel() { + if (!active) + return; + + // Restore original text + if (callback != null && !originalText.isEmpty()) { + callback.setText(originalText); + callback.setCursorPosition(originalCursorPos); + } + + resetState(); + + if (callback != null) { + callback.focusMainEditor(); + } + } + + /** + * Confirm rename operation and apply changes + */ + public void confirm() { + if (!active || callback == null) + return; + + // If word is empty, restore original + if (currentWord.isEmpty()) { + cancel(); + return; + } + + // Final text is already applied - push as single undo state + // The text was modified via setTextWithoutUndo, so push final state + String finalText = callback.getText(); + callback.pushUndoState(finalText); + + // Changes are already applied live, just exit rename mode + int newCursorPos = primaryOccurrenceStart + cursorInWord; + + resetState(); + + callback.focusMainEditor(); + callback.setCursorPosition(Math.max(0, newCursorPos)); + } + + private void resetState() { + active = false; + initialWord = ""; + originalWord = ""; + currentWord = ""; + primaryOccurrenceStart = -1; + primaryOccurrenceEnd = -1; + allOccurrences.clear(); + scope = null; + originalText = ""; + originalCursorPos = 0; + wordFullySelected = false; + } + + /** + * Handle keyboard input during rename + * @return true if input was consumed + */ + public boolean keyTyped(char c, int keyCode) { + if (!active) + return false; + + // ESC - cancel + if (keyCode == org.lwjgl.input.Keyboard.KEY_ESCAPE) { + cancel(); + return true; + } + + // Enter - confirm + if (keyCode == org.lwjgl.input.Keyboard.KEY_RETURN) { + confirm(); + return true; + } + + // Tab - confirm and move on + if (keyCode == org.lwjgl.input.Keyboard.KEY_TAB) { + confirm(); + return true; + } + + // Any navigation or editing key clears full selection mode + boolean wasFullySelected = wordFullySelected; + + // Backspace + if (keyCode == org.lwjgl.input.Keyboard.KEY_BACK) { + if (wasFullySelected) { + // Delete entire word + currentWord = ""; + cursorInWord = 0; + wordFullySelected = false; + applyLiveRename(); + markActivity(); + } else if (cursorInWord > 0 && currentWord.length() > 0) { + String before = currentWord.substring(0, cursorInWord - 1); + String after = currentWord.substring(cursorInWord); + currentWord = before + after; + cursorInWord--; + applyLiveRename(); + markActivity(); + } + return true; + } + + // Delete + if (keyCode == org.lwjgl.input.Keyboard.KEY_DELETE) { + if (wasFullySelected) { + // Delete entire word + currentWord = ""; + cursorInWord = 0; + wordFullySelected = false; + applyLiveRename(); + markActivity(); + } else if (cursorInWord < currentWord.length()) { + String before = currentWord.substring(0, cursorInWord); + String after = currentWord.substring(cursorInWord + 1); + currentWord = before + after; + applyLiveRename(); + markActivity(); + } + return true; + } + + // Left arrow + if (keyCode == org.lwjgl.input.Keyboard.KEY_LEFT) { + wordFullySelected = false; + if (cursorInWord > 0) { + cursorInWord--; + markActivity(); + } + return true; + } + + // Right arrow + if (keyCode == org.lwjgl.input.Keyboard.KEY_RIGHT) { + wordFullySelected = false; + if (cursorInWord < currentWord.length()) { + cursorInWord++; + markActivity(); + } + return true; + } + + // Home + if (keyCode == org.lwjgl.input.Keyboard.KEY_HOME) { + wordFullySelected = false; + cursorInWord = 0; + markActivity(); + return true; + } + + // End + if (keyCode == org.lwjgl.input.Keyboard.KEY_END) { + wordFullySelected = false; + cursorInWord = currentWord.length(); + markActivity(); + return true; + } + + // Ctrl+A - select all + if (keyCode == org.lwjgl.input.Keyboard.KEY_A && + org.lwjgl.input.Keyboard.isKeyDown(org.lwjgl.input.Keyboard.KEY_LCONTROL)) { + wordFullySelected = true; + cursorInWord = currentWord.length(); + markActivity(); + return true; + } + + // Character input - only valid identifier characters + if (isValidIdentifierChar(c, (wasFullySelected || currentWord.isEmpty()) && cursorInWord == 0)) { + if (wasFullySelected) { + // Replace entire word with this character + currentWord = String.valueOf(c); + cursorInWord = 1; + wordFullySelected = false; + } else { + String before = currentWord.substring(0, cursorInWord); + String after = currentWord.substring(cursorInWord); + currentWord = before + c + after; + cursorInWord++; + } + applyLiveRename(); + markActivity(); + return true; + } + + // Block other input while in rename mode + return true; + } + + /** + * Check if string is a valid identifier + */ + private boolean isValidIdentifier(String s) { + if (s == null || s.isEmpty()) + return false; + return IDENTIFIER.matcher(s).matches(); + } + + /** + * Check if character is valid for an identifier + */ + private boolean isValidIdentifierChar(char c, boolean isFirst) { + if (isFirst) { + return Character.isLetter(c) || c == '_'; + } + return Character.isLetterOrDigit(c) || c == '_'; + } + + /** + * Apply rename changes live as user types (without creating undo entries) + */ + private void applyLiveRename() { + if (callback == null) + return; + + String text = callback.getText(); + + // Calculate new primary position after rename + int occurrencesBefore = 0; + for (int[] occ : allOccurrences) { + if (occ[0] < primaryOccurrenceStart) { + occurrencesBefore++; + } + } + int lengthDiff = currentWord.length() - originalWord.length(); + int newPrimaryStart = primaryOccurrenceStart + (occurrencesBefore * lengthDiff); + + // Apply rename (even if currentWord is empty - we allow temporary empty state) + String newText = applyRename(text, currentWord); + callback.setTextWithoutUndo(newText); // Don't create undo entry for each keystroke + + // Update tracking for current word + originalWord = currentWord; + + // Recalculate occurrences based on the new word + // If empty, we still keep the positions tracked (just with empty strings) + if (!currentWord.isEmpty()) { + findOccurrences(newText, currentWord); + } else { + // For empty word, update positions based on length difference + updateOccurrencePositionsForEmpty(newPrimaryStart); + } + + // Update primary occurrence tracking + if (!allOccurrences.isEmpty()) { + for (int[] occ : allOccurrences) { + if (Math.abs(occ[0] - newPrimaryStart) < 2) { + setSelection(occ[0], occ[1]); + break; + } + } + } else if (currentWord.isEmpty()) { + // Keep tracking the position even when empty + setSelection(newPrimaryStart, newPrimaryStart); + } + } + + private void setSelection(int start, int end) { + primaryOccurrenceStart = start; + primaryOccurrenceEnd = end; + if (callback != null) + callback.getSelectionState().setSelection(start, end); + } + + /** + * Update occurrence positions when word is empty (for visual tracking) + */ + private void updateOccurrencePositionsForEmpty(int newPrimaryStart) { + // Just keep the primary occurrence position for rendering the empty box + allOccurrences.clear(); + allOccurrences.add(new int[]{newPrimaryStart, newPrimaryStart}); + } + + /** + * Apply rename to all occurrences in text + */ + private String applyRename(String text, String newName) { + if (allOccurrences.isEmpty() || newName.isEmpty()) + return text; + + StringBuilder result = new StringBuilder(); + int lastEnd = 0; + + // Sort occurrences by position + List sorted = new ArrayList<>(allOccurrences); + sorted.sort((a, b) -> Integer.compare(a[0], b[0])); + + for (int[] occ : sorted) { + if (occ[0] >= lastEnd && occ[1] <= text.length()) { + result.append(text, lastEnd, occ[0]); + result.append(newName); + lastEnd = occ[1]; + } + } + result.append(text.substring(lastEnd)); + + return result.toString(); + } + + /** + * Find all occurrences of the identifier within scope + */ + private void findOccurrences(String text, String word) { + allOccurrences.clear(); + + if (word.isEmpty() || scope == null) + return; + + // Get excluded ranges (strings and comments) + List excluded = MethodBlock.getExcludedRanges(text); + + Pattern pattern = Pattern.compile("\\b" + Pattern.quote(word) + "\\b"); + Matcher m = pattern.matcher(text); + + while (m.find()) { + int start = m.start(); + int end = m.end(); + + // Skip if in string or comment + if (isInExcludedRange(start, excluded)) + continue; + + // Check if within scope + if (scope.isGlobal || scope.containsPosition(start)) { + allOccurrences.add(new int[]{start, end}); + } + } + } + + /** + * Check if position is in an excluded range + */ + private boolean isInExcludedRange(int pos, List ranges) { + for (int[] range : ranges) { + if (pos >= range[0] && pos < range[1]) + return true; + } + return false; + } + + /** + * Find word boundaries at a given position + */ + private int[] findWordAtPosition(String text, int pos) { + if (pos < 0 || pos > text.length()) + return null; + + // Find start of word + int start = pos; + while (start > 0 && isIdentifierChar(text.charAt(start - 1))) { + start--; + } + + // Find end of word + int end = pos; + while (end < text.length() && isIdentifierChar(text.charAt(end))) { + end++; + } + + if (start == end) + return null; + + // Validate it's a proper identifier + String word = text.substring(start, end); + if (!IDENTIFIER.matcher(word).matches()) + return null; + + return new int[]{start, end}; + } + + private boolean isIdentifierChar(char c) { + return Character.isLetterOrDigit(c) || c == '_'; + } + + /** + * Determine the scope for a variable at the given position. + * Key insight: Local variables shadow global variables within their scope. + * We need to check if the cursor is within the scope of a local declaration. + */ + private ScopeInfo determineScope(String text, int position, String varName, JavaTextContainer container) { + List methods = MethodBlock.collectMethodBlocks(text); + + // First, check if this is a global field declaration (outside any method) + boolean isGlobalField = isGlobalFieldDeclaration(text, varName, methods); + + // Check if cursor is outside all methods - definitely global scope + boolean insideMethod = false; + MethodBlock containingMethod = null; + for (MethodBlock method : methods) { + if (method.containsPosition(position)) { + insideMethod = true; + containingMethod = method; + break; + } + } + + if (!insideMethod || containingMethod == null) { + // Cursor is outside methods - must be global scope + return new ScopeInfo(0, text.length(), true, "global"); + } + + // Cursor is inside a method + // Check if it's a method parameter first + String methodHeader = text.substring(containingMethod.startOffset, + Math.min(containingMethod.startOffset + 500, containingMethod.endOffset)); + int parenStart = methodHeader.indexOf('('); + int parenEnd = methodHeader.indexOf(')'); + if (parenStart >= 0 && parenEnd > parenStart) { + String params = methodHeader.substring(parenStart + 1, parenEnd); + Pattern paramPattern = Pattern.compile("\\b" + Pattern.quote(varName) + "\\b"); + if (paramPattern.matcher(params).find()) { + // It's a parameter - scope to method + return new ScopeInfo(containingMethod.startOffset, containingMethod.endOffset, false, "parameter"); + } + } + + // Check if there's a local variable with this name in the method + if (containingMethod.localVariables.contains(varName)) { + // Find where the local variable is declared + int localDeclPosition = findLocalDeclarationPosition(text, varName, containingMethod); + + if (localDeclPosition >= 0 && position >= localDeclPosition) { + // Cursor is at or after the local declaration - this is the local variable + int[] blockBounds = findInnermostBlockWithDeclaration(text, position, varName, containingMethod); + if (blockBounds != null) { + return new ScopeInfo(blockBounds[0], blockBounds[1], false, "block"); + } + return new ScopeInfo(containingMethod.startOffset, containingMethod.endOffset, false, "local"); + } else if (isGlobalField) { + // Cursor is BEFORE the local declaration - must be referring to global + // This is the shadowing case + return new ScopeInfo(0, text.length(), true, "global"); + } + } + + // No local variable with this name - check if it's a global field + if (isGlobalField) { + return new ScopeInfo(0, text.length(), true, "global"); + } + + // Unknown variable - default to method scope as fallback + return new ScopeInfo(containingMethod.startOffset, containingMethod.endOffset, false, "method"); + } + + /** + * Check if a variable is declared as a global field (outside any method) + */ + private boolean isGlobalFieldDeclaration(String text, String varName, List methods) { + Matcher m = JavaTextContainer.GLOBAL_FIELD_DECL.matcher(text); + while (m.find()) { + if (m.group(2).equals(varName)) { + int pos = m.start(2); + // Verify it's not inside a method + boolean insideMethod = false; + for (MethodBlock method : methods) { + if (method.containsPosition(pos)) { + insideMethod = true; + break; + } + } + if (!insideMethod) { + return true; + } + } + } + return false; + } + + /** + * Find the position of the first local variable declaration with the given name + */ + private int findLocalDeclarationPosition(String text, String varName, MethodBlock method) { + // Pattern for local variable declaration: Type varName = or Type varName; + Pattern localDecl = Pattern.compile("\\b\\w+\\s+" + Pattern.quote(varName) + "\\s*[=;,)]"); + String methodText = text.substring(method.startOffset, Math.min(method.endOffset, text.length())); + Matcher m = localDecl.matcher(methodText); + + // Skip past the method header (parameters) to find true local declarations + int bodyStart = methodText.indexOf('{'); + if (bodyStart < 0) + bodyStart = 0; + + while (m.find()) { + if (m.start() >= bodyStart) { + return method.startOffset + m.start(); + } + } + return -1; + } + + /** + * Find the innermost brace block that contains both the position and a declaration of varName + */ + private int[] findInnermostBlockWithDeclaration(String text, int position, String varName, MethodBlock method) { + List excluded = MethodBlock.getExcludedRanges(text); + + // Find all brace blocks within the method + List blocks = new ArrayList<>(); + List braceStack = new ArrayList<>(); + + for (int i = method.startOffset; i < method.endOffset && i < text.length(); i++) { + if (isInExcludedRange(i, excluded)) + continue; + + char c = text.charAt(i); + if (c == '{') { + braceStack.add(i); + } else if (c == '}') { + if (!braceStack.isEmpty()) { + int openPos = braceStack.remove(braceStack.size() - 1); + blocks.add(new int[]{openPos, i + 1}); + } + } + } + + // Find smallest block containing position that has a declaration of varName + int[] best = null; + int bestSize = Integer.MAX_VALUE; + + Pattern declPattern = Pattern.compile("\\b\\w+\\s+" + Pattern.quote(varName) + "\\s*[=;,)]"); + + for (int[] block : blocks) { + if (block[0] <= position && block[1] >= position) { + int size = block[1] - block[0]; + if (size < bestSize) { + // Check if this block contains a declaration of varName + String blockText = text.substring(block[0], Math.min(block[1], text.length())); + if (declPattern.matcher(blockText).find()) { + best = block; + bestSize = size; + } + } + } + } + + return best; + } + + // ==================== RENDERING ==================== + + /** + * Get all occurrences for rendering highlights + */ + public List getOccurrences() { + return allOccurrences; + } + + /** + * Get primary occurrence (the one under cursor when rename started) + */ + public int[] getPrimaryOccurrence() { + if (!active) + return null; + return new int[]{primaryOccurrenceStart, primaryOccurrenceEnd}; + } + + /** + * Check if a given occurrence is the primary one + */ + public boolean isPrimaryOccurrence(int start) { + return start == primaryOccurrenceStart; + } + + /** + * Get cursor position within the rename word for rendering + */ + public int getCursorInWord() { + return cursorInWord; + } + + /** + * Check if word is fully selected (for visual rendering) + */ + public boolean isWordFullySelected() { + return wordFullySelected; + } + + /** + * Get the current word being renamed + */ + public String getCurrentWord() { + return currentWord; + } + + // ==================== MOUSE HANDLING ==================== + + /** + * Handle mouse click within the rename box area. + * + * @param clickPosInText The text position where the click occurred + * @return true if the click was within the primary occurrence box, false otherwise + */ + public boolean handleClick(int clickPosInText) { + if (!active) + return false; + + // Check if click is within the primary occurrence bounds + if (clickPosInText >= primaryOccurrenceStart && + clickPosInText <= primaryOccurrenceEnd) { + + // Calculate position within the word + int relativePos = clickPosInText - primaryOccurrenceStart; + + // Clamp to valid range + cursorInWord = Math.max(0, Math.min(relativePos, currentWord.length())); + + // Clear full selection mode on click + wordFullySelected = false; + + markActivity(); + return true; + } + + return false; + } + + /** + * Check if a text position is within the primary rename occurrence + */ + public boolean isPositionInPrimaryOccurrence(int pos) { + if (!active) + return false; + return pos >= primaryOccurrenceStart && pos <= primaryOccurrenceEnd; + } + + // ==================== STATUS ==================== + + public boolean isActive() { + return active; + } + + public void updateCursor() { + cursorCounter++; + } + + private void markActivity() { + lastInputTime = System.currentTimeMillis(); + } + + public boolean shouldShowCursor() { + boolean recentInput = System.currentTimeMillis() - lastInputTime < 500; + return recentInput || (cursorCounter / 10) % 2 == 0; + } + + /** + * Get scope type description for status display + */ + public String getScopeDescription() { + if (scope == null) + return ""; + switch (scope.scopeType) { + case "global": + return "Renaming in file (" + allOccurrences.size() + " occurrences)"; + case "parameter": + return "Renaming parameter (" + allOccurrences.size() + " occurrences)"; + case "local": + return "Renaming local variable (" + allOccurrences.size() + " occurrences)"; + case "method": + return "Renaming in method (" + allOccurrences.size() + " occurrences)"; + case "block": + return "Renaming in block (" + allOccurrences.size() + " occurrences)"; + default: + return "Renaming (" + allOccurrences.size() + " occurrences)"; + } + } + + /** + * Get number of occurrences that will be renamed + */ + public int getOccurrenceCount() { + return allOccurrences.size(); + } +} diff --git a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java index 329758cfc..2e2fa46f4 100644 --- a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java +++ b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java @@ -24,6 +24,9 @@ public class ScriptEditorKeys extends KeyPresetManager { // Navigation public final KeyPreset GO_TO_LINE = add("Go to Line").setDefaultState(Keyboard.KEY_G, true, false, false); + + // Refactoring + public final KeyPreset RENAME = add("Rename/Refactor").setDefaultState(Keyboard.KEY_F6, false, true, false); public ScriptEditorKeys() { super("script_editor"); From 06c0a364a95a975b236ddba0f8736aeb3ad8d11c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 12:11:46 +0200 Subject: [PATCH 152/358] RenameRefactor bug fixes and QoL adjustments + improved code formatting --- .../client/gui/util/GuiScriptTextArea.java | 19 +- .../gui/util/script/JavaTextContainer.java | 21 ++ .../util/script/RenameRefactorHandler.java | 258 ++++++++++++------ 3 files changed, 209 insertions(+), 89 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 716d6c538..12fcef87e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -652,12 +652,17 @@ public class GuiScriptTextArea extends GuiNpcTextField { List renameOccurrences = renameHandler.getOccurrences(); for (int[] occ : renameOccurrences) { // Check if occurrence overlaps with this line - if (occ[1] > data.start && occ[0] < data.end) { + if (occ[1] >= data.start && occ[0] <= data.end) { int occStart = Math.max(occ[0] - data.start, 0); int occEnd = Math.min(occ[1] - data.start, line.length()); - if (occStart < occEnd) { + + // Handle empty word case - draw 1 pixel wide box + boolean isEmpty = (occ[0] == occ[1]); + + if (occStart <= occEnd) { // Changed from < to <= to handle empty case int s = ClientProxy.Font.width(line.substring(0, occStart)); - int e = ClientProxy.Font.width(line.substring(0, occEnd)) + 1; + int e = isEmpty ? s + 2 : ClientProxy.Font.width( + line.substring(0, occEnd)) + 1; // 2px wide for empty int occX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + s; int occEndX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + e; boolean isPrimary = renameHandler.isPrimaryOccurrence(occ[0]); @@ -687,8 +692,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { String beforeCursor = currentWord.substring(0, Math.min(cursorInWord, currentWord.length())); int cursorX = occX + ClientProxy.Font.width(beforeCursor); - drawRect(cursorX, posY + 1, cursorX + 1, posY + container.lineHeight - 1, - 0xFFFFFFFF); + // drawRect(cursorX, posY + 1, cursorX + 1, posY + container.lineHeight - 1, + // 0xFFFFFFFF); } } } @@ -1115,8 +1120,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (!e.isPress() || !openBoxes.get()) return; - unfocusAll(); + if (!renameHandler.isActive()) { + unfocusAll(); + active = true; renameHandler.startRename(); } }); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 8873fbc0a..6ca5a4281 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -169,6 +169,10 @@ public class JavaTextContainer extends TextContainer { // Skip type names (first letter uppercase) if (Character.isUpperCase(name.charAt(0))) continue; + // Skip field access (identifier preceded by a dot) - these should be gray/default + // e.g., in "container.lines", the "lines" part should not be marked + if (isFieldAccess(position)) continue; + // Check if inside a method MethodBlock methodBlock = findMethodBlockAtPosition(position); @@ -197,6 +201,23 @@ public class JavaTextContainer extends TextContainer { } } + /** + * Check if the identifier at this position is a field access (preceded by a dot) + * e.g., in "obj.field", the "field" part is a field access + */ + private boolean isFieldAccess(int position) { + if (position <= 0) return false; + + // Look backwards from position, skipping any whitespace + int i = position - 1; + while (i >= 0 && Character.isWhitespace(text.charAt(i))) { + i--; + } + + // Check if preceded by a dot + return i >= 0 && text.charAt(i) == '.'; + } + /** * Check if the identifier at this position is a method call (followed by parenthesis) */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java index b8a9bfe26..da6cbec43 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -30,8 +30,9 @@ public class RenameRefactorHandler { private int primaryOccurrenceEnd = -1; private List allOccurrences = new ArrayList<>(); private ScopeInfo scope = null; - private int cursorInWord = 0; // Cursor position within the word being renamed - private boolean wordFullySelected = true; // Start with word fully selected + + // For global scope, track positions that have local shadowing (to exclude them) + private List localShadowedPositions = new ArrayList<>(); // For restoring original text on cancel private String originalText = ""; @@ -40,10 +41,6 @@ public class RenameRefactorHandler { // Callback interface private RenameCallback callback; - // ==================== CURSOR BLINK ==================== - private int cursorCounter = 0; - private long lastInputTime = 0; - // Pattern for identifiers private static final Pattern IDENTIFIER = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*"); @@ -139,14 +136,15 @@ public class RenameRefactorHandler { initialWord = text.substring(wordBounds[0], wordBounds[1]); // Remember for undo originalWord = initialWord; currentWord = initialWord; - setSelection(wordBounds[0], wordBounds[1]); - - // Start with FULL WORD SELECTED (cursor at end, word selected) - cursorInWord = currentWord.length(); - wordFullySelected = true; - + // Determine scope for this identifier - MUST check if local shadows global scope = determineScope(text, wordBounds[0], originalWord, callback.getContainer()); + + // If global scope, find all positions where local variables shadow this name + localShadowedPositions.clear(); + if (scope != null && scope.isGlobal) { + findLocalShadowedPositions(text, originalWord); + } // Find all occurrences within scope findOccurrences(text, originalWord); @@ -155,11 +153,13 @@ public class RenameRefactorHandler { return false; active = true; - markActivity(); - callback.unfocusMainEditor(); - - // Push initial undo state so we can restore to this point - callback.pushUndoState(text); + + // Set selection to full word (uses GuiScriptTextArea's selection) + primaryOccurrenceStart = wordBounds[0]; + primaryOccurrenceEnd = wordBounds[1]; + callback.getSelectionState().setSelection(wordBounds[0], wordBounds[1]); + callback.setCursorPosition(wordBounds[1]); + callback.getSelectionState().markActivity(); return true; } @@ -202,7 +202,9 @@ public class RenameRefactorHandler { String finalText = callback.getText(); callback.pushUndoState(finalText); - // Changes are already applied live, just exit rename mode + // Calculate cursor position relative to primary occurrence + int cursorInWord = callback.getCursorPosition() - primaryOccurrenceStart; + cursorInWord = Math.max(0, Math.min(cursorInWord, currentWord.length())); int newCursorPos = primaryOccurrenceStart + cursorInWord; resetState(); @@ -219,10 +221,10 @@ public class RenameRefactorHandler { primaryOccurrenceStart = -1; primaryOccurrenceEnd = -1; allOccurrences.clear(); + localShadowedPositions.clear(); scope = null; originalText = ""; originalCursorPos = 0; - wordFullySelected = false; } /** @@ -230,7 +232,7 @@ public class RenameRefactorHandler { * @return true if input was consumed */ public boolean keyTyped(char c, int keyCode) { - if (!active) + if (!active || callback == null) return false; // ESC - cancel @@ -251,108 +253,114 @@ public class RenameRefactorHandler { return true; } - // Any navigation or editing key clears full selection mode - boolean wasFullySelected = wordFullySelected; + // Get selection state + SelectionState sel = callback.getSelectionState(); + boolean hasSelection = sel.hasSelection(); + + // Calculate cursor position within the word + int cursorInWord = callback.getCursorPosition() - primaryOccurrenceStart; + cursorInWord = Math.max(0, Math.min(cursorInWord, currentWord.length())); // Backspace if (keyCode == org.lwjgl.input.Keyboard.KEY_BACK) { - if (wasFullySelected) { - // Delete entire word + if (hasSelection) { + // Delete selected text (entire word if fully selected) currentWord = ""; - cursorInWord = 0; - wordFullySelected = false; applyLiveRename(); - markActivity(); - } else if (cursorInWord > 0 && currentWord.length() > 0) { + sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart); + callback.setCursorPosition(primaryOccurrenceStart); + } else if (cursorInWord > 0) { String before = currentWord.substring(0, cursorInWord - 1); String after = currentWord.substring(cursorInWord); currentWord = before + after; - cursorInWord--; applyLiveRename(); - markActivity(); + // Update cursor after the text change + callback.setCursorPosition(primaryOccurrenceStart + cursorInWord - 1); } + sel.markActivity(); return true; } // Delete if (keyCode == org.lwjgl.input.Keyboard.KEY_DELETE) { - if (wasFullySelected) { - // Delete entire word + if (hasSelection) { + // Delete selected text (entire word if fully selected) currentWord = ""; - cursorInWord = 0; - wordFullySelected = false; applyLiveRename(); - markActivity(); + sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart); + callback.setCursorPosition(primaryOccurrenceStart); } else if (cursorInWord < currentWord.length()) { String before = currentWord.substring(0, cursorInWord); String after = currentWord.substring(cursorInWord + 1); currentWord = before + after; applyLiveRename(); - markActivity(); } + sel.markActivity(); return true; } // Left arrow if (keyCode == org.lwjgl.input.Keyboard.KEY_LEFT) { - wordFullySelected = false; if (cursorInWord > 0) { - cursorInWord--; - markActivity(); + callback.setCursorPosition(primaryOccurrenceStart + cursorInWord - 1); + sel.setSelection(0, 0); // Clear selection } + sel.markActivity(); return true; } // Right arrow if (keyCode == org.lwjgl.input.Keyboard.KEY_RIGHT) { - wordFullySelected = false; if (cursorInWord < currentWord.length()) { - cursorInWord++; - markActivity(); + callback.setCursorPosition(primaryOccurrenceStart + cursorInWord + 1); + sel.setSelection(0, 0); // Clear selection } + sel.markActivity(); return true; } // Home if (keyCode == org.lwjgl.input.Keyboard.KEY_HOME) { - wordFullySelected = false; - cursorInWord = 0; - markActivity(); + callback.setCursorPosition(primaryOccurrenceStart); + sel.setSelection(0, 0); // Clear selection + sel.markActivity(); return true; } // End if (keyCode == org.lwjgl.input.Keyboard.KEY_END) { - wordFullySelected = false; - cursorInWord = currentWord.length(); - markActivity(); + callback.setCursorPosition(primaryOccurrenceStart + currentWord.length()); + sel.setSelection(0, 0); // Clear selection + sel.markActivity(); return true; } - // Ctrl+A - select all + // Ctrl+A - select all (select the whole word) if (keyCode == org.lwjgl.input.Keyboard.KEY_A && org.lwjgl.input.Keyboard.isKeyDown(org.lwjgl.input.Keyboard.KEY_LCONTROL)) { - wordFullySelected = true; - cursorInWord = currentWord.length(); - markActivity(); + sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart + currentWord.length()); + callback.setCursorPosition(primaryOccurrenceStart + currentWord.length()); + sel.markActivity(); return true; } // Character input - only valid identifier characters - if (isValidIdentifierChar(c, (wasFullySelected || currentWord.isEmpty()) && cursorInWord == 0)) { - if (wasFullySelected) { - // Replace entire word with this character + boolean isFirstChar = hasSelection || (currentWord.isEmpty() && cursorInWord == 0); + if (isValidIdentifierChar(c, isFirstChar)) { + if (hasSelection) { + // Replace entire selection with this character currentWord = String.valueOf(c); - cursorInWord = 1; - wordFullySelected = false; + applyLiveRename(); + callback.setCursorPosition(primaryOccurrenceStart + 1); + sel.setSelection(0, 0); // Clear selection } else { String before = currentWord.substring(0, cursorInWord); String after = currentWord.substring(cursorInWord); currentWord = before + c + after; - cursorInWord++; + applyLiveRename(); + callback.setCursorPosition(primaryOccurrenceStart + cursorInWord + 1); } - applyLiveRename(); - markActivity(); + sel.markActivity(); return true; } @@ -448,7 +456,7 @@ public class RenameRefactorHandler { * Apply rename to all occurrences in text */ private String applyRename(String text, String newName) { - if (allOccurrences.isEmpty() || newName.isEmpty()) + if (allOccurrences.isEmpty()) return text; StringBuilder result = new StringBuilder(); @@ -461,7 +469,7 @@ public class RenameRefactorHandler { for (int[] occ : sorted) { if (occ[0] >= lastEnd && occ[1] <= text.length()) { result.append(text, lastEnd, occ[0]); - result.append(newName); + result.append(newName); // Append newName, even if empty (which deletes the occurrence) lastEnd = occ[1]; } } @@ -494,11 +502,90 @@ public class RenameRefactorHandler { continue; // Check if within scope - if (scope.isGlobal || scope.containsPosition(start)) { + if (scope.isGlobal) { + // For global scope, skip positions where local variables shadow the global + if (!isInLocalShadowedRange(start)) { + allOccurrences.add(new int[]{start, end}); + } + } else if (scope.containsPosition(start)) { allOccurrences.add(new int[]{start, end}); } } } + + /** + * Find all positions where local variables with the same name shadow the global variable. + * This populates localShadowedPositions with ranges where local declarations exist. + */ + private void findLocalShadowedPositions(String text, String varName) { + localShadowedPositions.clear(); + + List methods = MethodBlock.collectMethodBlocks(text); + List excluded = MethodBlock.getExcludedRanges(text); + + for (MethodBlock method : methods) { + // Check if this method has a local variable or parameter with the same name + boolean hasLocalDecl = method.localVariables.contains(varName); + boolean hasParamDecl = isParameterInMethod(text, varName, method); + + if (hasLocalDecl || hasParamDecl) { + // Find where the local/param scope starts + int scopeStart; + if (hasParamDecl) { + // Parameter scope starts at method start + scopeStart = method.startOffset; + } else { + // Local variable scope starts at declaration + scopeStart = findLocalDeclarationPosition(text, varName, method); + if (scopeStart < 0) scopeStart = method.startOffset; + } + + // Find all occurrences of varName within this method's shadowed range + Pattern pattern = Pattern.compile("\\b" + Pattern.quote(varName) + "\\b"); + Matcher m = pattern.matcher(text); + + while (m.find()) { + int pos = m.start(); + + // Skip if in string or comment + if (isInExcludedRange(pos, excluded)) + continue; + + // Check if within the shadowed scope of this method + if (pos >= scopeStart && pos < method.endOffset) { + localShadowedPositions.add(new int[]{m.start(), m.end()}); + } + } + } + } + } + + /** + * Check if a position is within a locally shadowed range + */ + private boolean isInLocalShadowedRange(int pos) { + for (int[] range : localShadowedPositions) { + if (pos >= range[0] && pos < range[1]) + return true; + } + return false; + } + + /** + * Check if varName is a parameter in the method + */ + private boolean isParameterInMethod(String text, String varName, MethodBlock method) { + String methodHeader = text.substring(method.startOffset, + Math.min(method.startOffset + 500, method.endOffset)); + int parenStart = methodHeader.indexOf('('); + int parenEnd = methodHeader.indexOf(')'); + if (parenStart >= 0 && parenEnd > parenStart) { + String params = methodHeader.substring(parenStart + 1, parenEnd); + Pattern paramPattern = Pattern.compile("\\b" + Pattern.quote(varName) + "\\b"); + return paramPattern.matcher(params).find(); + } + return false; + } /** * Check if position is in an excluded range @@ -738,14 +825,20 @@ public class RenameRefactorHandler { * Get cursor position within the rename word for rendering */ public int getCursorInWord() { - return cursorInWord; + if (callback == null) return 0; + int cursorPos = callback.getCursorPosition(); + return Math.max(0, Math.min(cursorPos - primaryOccurrenceStart, currentWord.length())); } /** * Check if word is fully selected (for visual rendering) */ public boolean isWordFullySelected() { - return wordFullySelected; + if (callback == null) return false; + SelectionState selection = callback.getSelectionState(); + return selection.hasSelection() && + selection.getStartSelection() == primaryOccurrenceStart && + selection.getEndSelection() == primaryOccurrenceEnd; } /** @@ -764,23 +857,17 @@ public class RenameRefactorHandler { * @return true if the click was within the primary occurrence box, false otherwise */ public boolean handleClick(int clickPosInText) { - if (!active) + if (!active || callback == null) return false; // Check if click is within the primary occurrence bounds if (clickPosInText >= primaryOccurrenceStart && clickPosInText <= primaryOccurrenceEnd) { - // Calculate position within the word - int relativePos = clickPosInText - primaryOccurrenceStart; - - // Clamp to valid range - cursorInWord = Math.max(0, Math.min(relativePos, currentWord.length())); - - // Clear full selection mode on click - wordFullySelected = false; - - markActivity(); + // Set cursor position in the text and clear selection + callback.setCursorPosition(clickPosInText); + callback.getSelectionState().setSelection(-1, -1); // Clear selection + callback.getSelectionState().markActivity(); return true; } @@ -802,17 +889,22 @@ public class RenameRefactorHandler { return active; } + /** + * @deprecated Use GuiScriptTextArea's cursor rendering instead + */ public void updateCursor() { - cursorCounter++; - } - - private void markActivity() { - lastInputTime = System.currentTimeMillis(); + // No-op - cursor is now handled by GuiScriptTextArea } + /** + * Check if cursor should be visible based on blink timing. + * Uses SelectionState's recent input check. + */ public boolean shouldShowCursor() { - boolean recentInput = System.currentTimeMillis() - lastInputTime < 500; - return recentInput || (cursorCounter / 10) % 2 == 0; + if (callback != null) { + return callback.getSelectionState().hadRecentInput(); + } + return true; } /** From 9de9e799d540a5306e7c97d1aa351a9feeae122c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 12:18:28 +0200 Subject: [PATCH 153/358] Expanded Refactor BG box and drawn its bottom layer red --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 7 +++++-- .../npcs/client/gui/util/script/RenameRefactorHandler.java | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 12fcef87e..61901d74d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -663,8 +663,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { int s = ClientProxy.Font.width(line.substring(0, occStart)); int e = isEmpty ? s + 2 : ClientProxy.Font.width( line.substring(0, occEnd)) + 1; // 2px wide for empty - int occX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + s; - int occEndX = x + LINE_NUMBER_GUTTER_WIDTH + 1 + e; + int occX = x + LINE_NUMBER_GUTTER_WIDTH + s; + int occEndX = x + LINE_NUMBER_GUTTER_WIDTH + 2 + e; boolean isPrimary = renameHandler.isPrimaryOccurrence(occ[0]); // Draw background highlight @@ -674,6 +674,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Draw white border for primary occurrence (IntelliJ-like) if (isPrimary) { int borderColor = 0xDDFFFFFF; + //RED BG + drawRect(occX, posY, occEndX, posY + container.lineHeight, 0x33ff0000); + // Top border drawRect(occX, posY, occEndX, posY + 1, borderColor); // Bottom border diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java index da6cbec43..e28d0217a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -158,7 +158,7 @@ public class RenameRefactorHandler { primaryOccurrenceStart = wordBounds[0]; primaryOccurrenceEnd = wordBounds[1]; callback.getSelectionState().setSelection(wordBounds[0], wordBounds[1]); - callback.setCursorPosition(wordBounds[1]); + // callback.setCursorPosition(wordBounds[1]); callback.getSelectionState().markActivity(); return true; From 6b40cbb367b2dd63fc9b1032060c70a166a61163 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 12:28:45 +0200 Subject: [PATCH 154/358] Removed RenameRefactor custom mouseClicked logic (redundant) --- .../gui/util/script/RenameRefactorHandler.java | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java index e28d0217a..65c70b973 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -859,18 +859,7 @@ public class RenameRefactorHandler { public boolean handleClick(int clickPosInText) { if (!active || callback == null) return false; - - // Check if click is within the primary occurrence bounds - if (clickPosInText >= primaryOccurrenceStart && - clickPosInText <= primaryOccurrenceEnd) { - - // Set cursor position in the text and clear selection - callback.setCursorPosition(clickPosInText); - callback.getSelectionState().setSelection(-1, -1); // Clear selection - callback.getSelectionState().markActivity(); - return true; - } - + return false; } From 79f092ae591330f803839543df2eefca0ce75ec0 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 12:29:10 +0200 Subject: [PATCH 155/358] RenameRefactor space bar now deletes selection --- .../util/script/RenameRefactorHandler.java | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java index 65c70b973..2495feb1e 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import org.lwjgl.input.Keyboard; import java.util.ArrayList; import java.util.List; @@ -236,19 +237,19 @@ public class RenameRefactorHandler { return false; // ESC - cancel - if (keyCode == org.lwjgl.input.Keyboard.KEY_ESCAPE) { + if (keyCode == Keyboard.KEY_ESCAPE) { cancel(); return true; } // Enter - confirm - if (keyCode == org.lwjgl.input.Keyboard.KEY_RETURN) { + if (keyCode == Keyboard.KEY_RETURN) { confirm(); return true; } // Tab - confirm and move on - if (keyCode == org.lwjgl.input.Keyboard.KEY_TAB) { + if (keyCode == Keyboard.KEY_TAB) { confirm(); return true; } @@ -262,7 +263,7 @@ public class RenameRefactorHandler { cursorInWord = Math.max(0, Math.min(cursorInWord, currentWord.length())); // Backspace - if (keyCode == org.lwjgl.input.Keyboard.KEY_BACK) { + if (keyCode == Keyboard.KEY_BACK) { if (hasSelection) { // Delete selected text (entire word if fully selected) currentWord = ""; @@ -282,7 +283,7 @@ public class RenameRefactorHandler { } // Delete - if (keyCode == org.lwjgl.input.Keyboard.KEY_DELETE) { + if (keyCode == Keyboard.KEY_DELETE) { if (hasSelection) { // Delete selected text (entire word if fully selected) currentWord = ""; @@ -299,8 +300,17 @@ public class RenameRefactorHandler { return true; } + // Backspace + if (keyCode == Keyboard.KEY_SPACE && hasSelection) { + // Delete selected text (entire word if fully selected) + currentWord = ""; + applyLiveRename(); + sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart); + callback.setCursorPosition(primaryOccurrenceStart); + } + // Left arrow - if (keyCode == org.lwjgl.input.Keyboard.KEY_LEFT) { + if (keyCode == Keyboard.KEY_LEFT) { if (cursorInWord > 0) { callback.setCursorPosition(primaryOccurrenceStart + cursorInWord - 1); sel.setSelection(0, 0); // Clear selection @@ -310,7 +320,7 @@ public class RenameRefactorHandler { } // Right arrow - if (keyCode == org.lwjgl.input.Keyboard.KEY_RIGHT) { + if (keyCode == Keyboard.KEY_RIGHT) { if (cursorInWord < currentWord.length()) { callback.setCursorPosition(primaryOccurrenceStart + cursorInWord + 1); sel.setSelection(0, 0); // Clear selection @@ -320,7 +330,7 @@ public class RenameRefactorHandler { } // Home - if (keyCode == org.lwjgl.input.Keyboard.KEY_HOME) { + if (keyCode == Keyboard.KEY_HOME) { callback.setCursorPosition(primaryOccurrenceStart); sel.setSelection(0, 0); // Clear selection sel.markActivity(); @@ -328,7 +338,7 @@ public class RenameRefactorHandler { } // End - if (keyCode == org.lwjgl.input.Keyboard.KEY_END) { + if (keyCode == Keyboard.KEY_END) { callback.setCursorPosition(primaryOccurrenceStart + currentWord.length()); sel.setSelection(0, 0); // Clear selection sel.markActivity(); @@ -336,8 +346,8 @@ public class RenameRefactorHandler { } // Ctrl+A - select all (select the whole word) - if (keyCode == org.lwjgl.input.Keyboard.KEY_A && - org.lwjgl.input.Keyboard.isKeyDown(org.lwjgl.input.Keyboard.KEY_LCONTROL)) { + if (keyCode == Keyboard.KEY_A && + Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) { sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart + currentWord.length()); callback.setCursorPosition(primaryOccurrenceStart + currentWord.length()); sel.markActivity(); From 49b8c2ef982012554a85c808c51d7032135d01fa Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 12:48:13 +0200 Subject: [PATCH 156/358] Fixed emptying refactoring string removing all occurences --- .../util/script/RenameRefactorHandler.java | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java index 2495feb1e..f025a8b23 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -416,21 +416,27 @@ public class RenameRefactorHandler { int lengthDiff = currentWord.length() - originalWord.length(); int newPrimaryStart = primaryOccurrenceStart + (occurrencesBefore * lengthDiff); - // Apply rename (even if currentWord is empty - we allow temporary empty state) + // If the current typed word is empty, do NOT modify the document's other occurrences. + // We treat empty as a temporary state: preserve the editor text and the previously + // discovered occurrences so they will re-appear when the user types again. + if (currentWord.isEmpty()) { + // Keep the text unchanged and preserve allOccurrences (do not clear). + callback.setTextWithoutUndo(text); + // Keep originalWord as the last non-empty word so length calculations remain correct + // and place the caret at the primary position for editing. + setSelection(newPrimaryStart, newPrimaryStart); + return; + } + + // Apply rename for non-empty currentWord String newText = applyRename(text, currentWord); callback.setTextWithoutUndo(newText); // Don't create undo entry for each keystroke - // Update tracking for current word + // Update tracking for current word (only when non-empty) originalWord = currentWord; // Recalculate occurrences based on the new word - // If empty, we still keep the positions tracked (just with empty strings) - if (!currentWord.isEmpty()) { - findOccurrences(newText, currentWord); - } else { - // For empty word, update positions based on length difference - updateOccurrencePositionsForEmpty(newPrimaryStart); - } + findOccurrences(newText, currentWord); // Update primary occurrence tracking if (!allOccurrences.isEmpty()) { @@ -457,9 +463,11 @@ public class RenameRefactorHandler { * Update occurrence positions when word is empty (for visual tracking) */ private void updateOccurrencePositionsForEmpty(int newPrimaryStart) { - // Just keep the primary occurrence position for rendering the empty box - allOccurrences.clear(); - allOccurrences.add(new int[]{newPrimaryStart, newPrimaryStart}); + // No-op: we preserve the previously discovered occurrences while the user + // temporarily clears the rename box. Clearing occurrences here caused them + // to be removed from the document during live-editing. Rendering will use + // primary occurrence position set by setSelection(). + // Intentionally left blank. } /** From f1d21496bdc31b6d3755ce15fe1ae3c9eb35a870 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 12:54:52 +0200 Subject: [PATCH 157/358] Fixed rename/refactor undo pushing the newest state, not the previous one --- .../npcs/client/gui/util/GuiScriptTextArea.java | 4 ++-- .../gui/util/script/RenameRefactorHandler.java | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 61901d74d..04854b7c7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -351,10 +351,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { } @Override - public void pushUndoState(String textState) { + public void pushUndoState(String textState, int cursor) { // Push a specific text state to the undo list if (!undoing) { - undoList.add(new UndoData(textState, selection.getCursorPosition())); + undoList.add(new UndoData(textState, cursor)); redoList.clear(); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java index f025a8b23..423de9821 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -55,7 +55,7 @@ public class RenameRefactorHandler { void setTextWithoutUndo(String text); // Set text without creating undo entry - void pushUndoState(String text); // Push an undo state + void pushUndoState(String text, int cursor); // Push an undo state List getLines(); @@ -198,10 +198,13 @@ public class RenameRefactorHandler { return; } - // Final text is already applied - push as single undo state - // The text was modified via setTextWithoutUndo, so push final state - String finalText = callback.getText(); - callback.pushUndoState(finalText); + // Push the original text (pre-rename) so the first undo will restore the + // document to its state before the refactor. + // During live editing we used setTextWithoutUndo to avoid creating intermediate + // undo entries; pushing the original snapshot here gives a single predictable + // undo step back to the pre-rename state. + if (originalText != null && !originalText.isEmpty()) + callback.pushUndoState(originalText, originalCursorPos); // Calculate cursor position relative to primary occurrence int cursorInWord = callback.getCursorPosition() - primaryOccurrenceStart; From 91a6a166bb213b9b349360c32b10036f7174503b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 13:12:54 +0200 Subject: [PATCH 158/358] CTRL+BACKSPACE deletes refactored word --- .../npcs/client/gui/util/script/RenameRefactorHandler.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java index 423de9821..83d6d90d4 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.util.script; +import net.minecraft.client.gui.GuiScreen; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; import org.lwjgl.input.Keyboard; @@ -44,6 +45,8 @@ public class RenameRefactorHandler { // Pattern for identifiers private static final Pattern IDENTIFIER = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*"); + // Placeholder to show when the rename box is empty + private static final String EMPTY_PLACEHOLDER = "|"; /** * Callback interface for rename operations @@ -267,7 +270,7 @@ public class RenameRefactorHandler { // Backspace if (keyCode == Keyboard.KEY_BACK) { - if (hasSelection) { + if (hasSelection || GuiScreen.isCtrlKeyDown()) { // Delete selected text (entire word if fully selected) currentWord = ""; applyLiveRename(); From b3a64f92d73542a03384a5315b2d05ef0c6c832d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 13:43:47 +0200 Subject: [PATCH 159/358] Heavily improved global/local/ this.field syntax highlight detection --- .../gui/util/script/JavaTextContainer.java | 56 ++++++++++++++++++- .../client/gui/util/script/MethodBlock.java | 45 ++++++++++++++- 2 files changed, 97 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 6ca5a4281..391afa5f7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -95,12 +95,20 @@ public class JavaTextContainer extends TextContainer { // Extract method blocks first methodBlocks = MethodBlock.collectMethodBlocks(text); + + // Get excluded ranges (strings and comments) for the entire text + List excludedRanges = MethodBlock.getExcludedRanges(text); - // Global fields (excluding those inside methods) + // Global fields (excluding those inside methods, strings, and comments) Matcher mGlobal = GLOBAL_FIELD_DECL.matcher(text); while (mGlobal.find()) { String varName = mGlobal.group(2); int varPosition = mGlobal.start(2); + + // Skip if inside a string or comment + if (isInExcludedRange(varPosition, excludedRanges)) { + continue; + } // Check if this variable is inside a method boolean isInsideMethod = false; @@ -157,6 +165,35 @@ public class JavaTextContainer extends TextContainer { "Boolean", "Long", "Byte", "Short", "Character", "List", "Map", "Set" )); + // First pass: handle field accesses (this.field and obj.field patterns) + Pattern thisFieldPattern = Pattern.compile("\\bthis\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); + Matcher thisFieldMatcher = thisFieldPattern.matcher(text); + while (thisFieldMatcher.find()) { + String fieldName = thisFieldMatcher.group(1); + int fieldPos = thisFieldMatcher.start(1); + // Highlight as GLOBAL_FIELD if it exists, UNDEFINED_VAR otherwise + if (globalFields.contains(fieldName)) { + marks.add(new Mark(fieldPos, thisFieldMatcher.end(1), TokenType.GLOBAL_FIELD)); + } else { + marks.add(new Mark(fieldPos, thisFieldMatcher.end(1), TokenType.UNDEFINED_VAR)); + } + } + + // Handle identifier.field patterns (e.g., obj.field, global.field) + // The field part should be highlighted as GLOBAL_FIELD (light blue) + Pattern objFieldPattern = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); + Matcher objFieldMatcher = objFieldPattern.matcher(text); + while (objFieldMatcher.find()) { + String fieldName = objFieldMatcher.group(2); + int fieldPos = objFieldMatcher.start(2); + // Skip if this is a "this.field" pattern (already handled above) + String objName = objFieldMatcher.group(1); + if (!objName.equals("this")) { + // Highlight the field part as GLOBAL_FIELD (represents another object's field) + marks.add(new Mark(fieldPos, objFieldMatcher.end(2), TokenType.GLOBAL_FIELD)); + } + } + Pattern identifier = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); Matcher m = identifier.matcher(text); while (m.find()) { @@ -177,10 +214,11 @@ public class JavaTextContainer extends TextContainer { MethodBlock methodBlock = findMethodBlockAtPosition(position); if (methodBlock != null) { - // Inside a method - check in order: parameter, local, global + // Inside a method - check in order: parameter, local (position-aware), global if (methodBlock.parameters.contains(name)) { marks.add(new Mark(m.start(1), m.end(1), TokenType.PARAMETER)); - } else if (methodBlock.localVariables.contains(name)) { + } else if (methodBlock.isLocalDeclaredAtPosition(name, position)) { + // Local variable is declared at or before this position marks.add(new Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD)); } else if (globalFields.contains(name)) { marks.add(new Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); @@ -249,6 +287,18 @@ public class JavaTextContainer extends TextContainer { return false; } + /** + * Check if position is in an excluded range (string or comment) + */ + private boolean isInExcludedRange(int pos, List ranges) { + for (int[] range : ranges) { + if (pos >= range[0] && pos < range[1]) { + return true; + } + } + return false; + } + public void formatCodeText() { // Step 1: Tokenize the full text List marks = new ArrayList<>(); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java index e16fdb764..ef9567779 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java @@ -1,7 +1,9 @@ package noppes.npcs.client.gui.util.script; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -26,6 +28,7 @@ public class MethodBlock { public String text; public List localVariables = new ArrayList<>(); public List parameters = new ArrayList<>(); // Method parameters + public Map localVarPositions = new HashMap<>(); // Track declaration positions public MethodBlock(int start, int end, String text) { this.startOffset = start; @@ -63,14 +66,54 @@ public class MethodBlock { // Extract local variables declared within this method private void extractLocalVariables() { localVariables.clear(); + localVarPositions.clear(); + + // Get excluded ranges (strings and comments) for this method's text + List excludedRanges = getExcludedRanges(text); + Matcher m = LOCAL_VAR_DECL.matcher(text); while (m.find()) { String varName = m.group(2); - if (!localVariables.contains(varName)) { + int declPosition = m.start(); + + // Skip if inside a string or comment + if (isInExcludedRange(declPosition, excludedRanges)) { + continue; + } + + // Check if this is a "this.field" assignment - if so, skip it + // Look backwards from the declaration position to see if preceded by "this." + boolean isThisFieldAssignment = false; + if (declPosition >= 5) { + int checkStart = Math.max(0, declPosition - 10); + String before = text.substring(checkStart, declPosition); + if (before.matches(".*\\bthis\\s*\\.\\s*$")) { + isThisFieldAssignment = true; + } + } + + if (!isThisFieldAssignment && !localVariables.contains(varName)) { localVariables.add(varName); + // Store the absolute position in the full text (startOffset + relative position) + localVarPositions.put(varName, startOffset + declPosition); } } } + + // Check if a reference position comes before the local variable declaration + // This handles cases like: global.field on line 7, then var global = "x" on line 9 + // At line 7, the local 'global' doesn't exist yet, so it should refer to the global 'global' + public boolean isLocalDeclaredAtPosition(String varName, int absolutePosition) { + if (!localVariables.contains(varName)) { + return false; + } + Integer declPos = localVarPositions.get(varName); + if (declPos == null) { + return false; + } + // The local variable is only "in scope" at or after its declaration position + return absolutePosition >= declPos; + } // Check if a position (in the full text) falls within this method public boolean containsPosition(int position) { From a05d411cbadc7a678b898dbe2dfc4413c8f2004b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 16:27:22 +0200 Subject: [PATCH 160/358] More formatting and syntax highlight fixes --- .../client/gui/util/script/FormatHelper.java | 152 ++++++++++++++++++ .../client/gui/util/script/IndentHelper.java | 69 +++++++- .../gui/util/script/JavaTextContainer.java | 44 ++++- 3 files changed, 261 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java b/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java index 675a6b097..5c534cc94 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java @@ -142,6 +142,131 @@ public class FormatHelper { return result.toString(); } + /** + * Split multiple statements on one line into separate lines with proper indentation. + * E.g., " this.a = 1; this.b = 2;" becomes two lines + * Also handles scope: "if(x){ int y = 20; }" becomes proper multi-line structure + */ + private String splitStatementsOntoSeparateLines(String text) { + String[] lines = text.split("\n", -1); + StringBuilder result = new StringBuilder(); + + for (int lineIdx = 0; lineIdx < lines.length; lineIdx++) { + String line = lines[lineIdx]; + + // Skip empty lines or comment-only lines + String trimmed = line.trim(); + if (trimmed.isEmpty() || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) { + result.append(line); + if (lineIdx < lines.length - 1) result.append("\n"); + continue; + } + + // Extract leading indentation + int indent = 0; + while (indent < line.length() && (line.charAt(indent) == ' ' || line.charAt(indent) == '\t')) { + indent++; + } + String indentation = line.substring(0, indent); + String content = line.substring(indent); + + // Split by semicolons (but preserve those in strings, for loops, etc.) + List statements = splitBySemicolons(content); + + for (int si = 0; si < statements.size(); si++) { + String stmt = statements.get(si).trim(); + if (stmt.isEmpty()) continue; + + // Check if statement opens a scope { and has code after it + int braceIdx = stmt.indexOf('{'); + if (braceIdx >= 0 && braceIdx < stmt.length() - 1) { + String afterBrace = stmt.substring(braceIdx + 1).trim(); + if (!afterBrace.isEmpty() && !afterBrace.equals("}")) { + // Split: keep up to and including {, put rest on new line with extra indent + result.append(indentation).append(stmt, 0, braceIdx + 1).append("\n"); + // Calculate new indent (add 4 spaces) + String newIndent = indentation + " "; + result.append(newIndent).append(afterBrace); + if (si < statements.size() - 1) result.append("\n"); + continue; + } + } + + result.append(indentation).append(stmt); + // Add semicolon back if it was removed + if (si < statements.size() - 1) { + result.append("\n"); + } + } + + if (lineIdx < lines.length - 1) result.append("\n"); + } + + return result.toString(); + } + + /** + * Split content by semicolons, preserving those in strings and for-loops + */ + private List splitBySemicolons(String content) { + List statements = new ArrayList<>(); + StringBuilder current = new StringBuilder(); + int parenDepth = 0; + boolean inString = false; + boolean inChar = false; + boolean escaped = false; + int forLoopDepth = 0; // Track for(...) depth to preserve semicolons + + for (int i = 0; i < content.length(); i++) { + char c = content.charAt(i); + char prev = i > 0 ? content.charAt(i - 1) : ' '; + + // Track strings + if (c == '\"' && !escaped && !inChar) { + inString = !inString; + } else if (c == '\'' && !escaped && !inString) { + inChar = !inChar; + } + + escaped = (c == '\\' && !escaped); + + if (inString || inChar) { + current.append(c); + continue; + } + + // Track for loops + if (c == '(' && i >= 3 && content.substring(Math.max(0, i - 3), i + 1).matches(".*\\bfor\\s*\\(")) { + forLoopDepth++; + parenDepth++; + } else if (c == '(') { + parenDepth++; + } else if (c == ')') { + parenDepth--; + if (forLoopDepth > 0 && parenDepth < forLoopDepth) { + forLoopDepth = 0; + } + } + + // Semicolon: split if not in for-loop header + if (c == ';' && forLoopDepth == 0) { + current.append(c); + statements.add(current.toString()); + current = new StringBuilder(); + continue; + } + + current.append(c); + } + + // Add remaining content + if (current.length() > 0) { + statements.add(current.toString()); + } + + return statements; + } + private int getLineStartOffset(String[] lines, int lineIndex) { int offset = 0; for (int i = 0; i < lineIndex; i++) { @@ -172,12 +297,39 @@ public class FormatHelper { return line; } + // Skip formatting for continuation lines (lines that start with operators, dots, etc.) + // These are wrapped lines from a previous format and should be preserved as-is + if (isContinuationLine(trimmed)) { + return line; + } + // Format the content String formatted = formatContent(content, fullText, lineOffset + indent); return indentation + formatted; } + /** + * Check if a line appears to be a continuation of a previous line + * (i.e., starts with an operator, dot, or other continuation character) + */ + private boolean isContinuationLine(String trimmedContent) { + if (trimmedContent.isEmpty()) return false; + char firstChar = trimmedContent.charAt(0); + // Lines starting with these are likely continuations + if (firstChar == '.' || firstChar == '+' || firstChar == '-' || + firstChar == '*' || firstChar == '/' || firstChar == '%' || + firstChar == '&' || firstChar == '|' || firstChar == '^' || + firstChar == '?' || firstChar == ':' || firstChar == ',') { + return true; + } + // Also check for && and || + if (trimmedContent.startsWith("&&") || trimmedContent.startsWith("||")) { + return true; + } + return false; + } + /** * Format line content, preserving strings and comments */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java b/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java index 88bc354b8..fd6da00f0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java @@ -229,6 +229,7 @@ public class IndentHelper { for (int li = 0; li < linesArr.length; li++) { String line = linesArr[li]; String trimmedLeading = line.replaceAll("^[ \\t]+", ""); + int originalIndent = getLineIndent(line); int opens = 0, closes = 0; boolean startsWithClose = false; @@ -268,7 +269,23 @@ public class IndentHelper { if (startsWithClose) indentLevel = Math.max(0, indentLevel - 1); int targetIndent = indentLevel * TAB_SIZE; - out.append(spaces(targetIndent)).append(trimmedLeading); + // Check if this is a continuation line: + // 1. Line starts with operator/dot (explicit continuation) + // 2. Previous line ends with operator (implicit continuation - this line continues it) + // If so, preserve extra indentation beyond the base level + boolean isContinuation = isContinuationLine(trimmedLeading); + if (!isContinuation && li > 0) { + // Check if previous line ends with an operator (indicating this line continues it) + String prevLine = linesArr[li - 1].trim(); + isContinuation = lineEndsWithContinuationOperator(prevLine); + } + + if (isContinuation && originalIndent > targetIndent) { + // This is a continuation line - preserve its extra indent + out.append(spaces(originalIndent)).append(trimmedLeading); + } else { + out.append(spaces(targetIndent)).append(trimmedLeading); + } if (li < linesArr.length - 1) out.append('\n'); depth = Math.max(0, depth + opens - closes); @@ -277,6 +294,56 @@ public class IndentHelper { return out.toString(); } + /** + * Check if a line appears to be a continuation of a previous line + * (i.e., starts with an operator, dot, or other continuation character) + */ + private static boolean isContinuationLine(String trimmedContent) { + if (trimmedContent.isEmpty()) return false; + char firstChar = trimmedContent.charAt(0); + // Lines starting with these are likely continuations + if (firstChar == '.' || firstChar == '+' || firstChar == '-' || + firstChar == '*' || firstChar == '/' || firstChar == '%' || + firstChar == '&' || firstChar == '|' || firstChar == '^' || + firstChar == '?' || firstChar == ':' || firstChar == ',') { + return true; + } + // Also check for && and || + if (trimmedContent.startsWith("&&") || trimmedContent.startsWith("||")) { + return true; + } + return false; + } + + /** + * Check if a line ends with an operator that indicates the next line is a continuation + */ + private static boolean lineEndsWithContinuationOperator(String trimmedLine) { + if (trimmedLine.isEmpty()) return false; + // Remove trailing comments + int commentIdx = trimmedLine.indexOf("//"); + if (commentIdx >= 0) { + trimmedLine = trimmedLine.substring(0, commentIdx).trim(); + } + if (trimmedLine.isEmpty()) return false; + + // Check if ends with operator + char lastChar = trimmedLine.charAt(trimmedLine.length() - 1); + if (lastChar == '+' || lastChar == '-' || lastChar == '*' || + lastChar == '/' || lastChar == '%' || lastChar == '=' || + lastChar == '&' || lastChar == '|' || lastChar == '^' || + lastChar == '?' || lastChar == ':' || lastChar == ',' || + lastChar == '(' || lastChar == '[') { + // But not if it's ; or } which end statements + return true; + } + // Check for && and || + if (trimmedLine.endsWith("&&") || trimmedLine.endsWith("||")) { + return true; + } + return false; + } + /** * Result of text formatting */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 391afa5f7..fa6320470 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -16,7 +16,7 @@ public class JavaTextContainer extends TextContainer { "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof)\\b"); public static final Pattern TYPE_DECL = Pattern.compile("\\b([A-Za-z_][a-zA-Z0-9_]*)" + // Group 1 → main type - "\\s*(<([^>]+)>)?" + // Group c2 → <…>, Group 3 → inner type + "\\s*(<([^>]+)>)?" + // Group 2 → <…>, Group 3 → inner type "\\s+[a-zA-Z_][a-zA-Z0-9_]*" // variable name ); @@ -275,12 +275,50 @@ public class JavaTextContainer extends TextContainer { /** * Check if this looks like a type reference (e.g., part of a declaration or generic) + * But NOT a comparison like "i < container" which uses < as less-than operator */ private boolean isTypeReference(String name, int position) { - // Check if preceded by 'new ', '<', or ',Type' + // Check if preceded by 'new ' if (position > 4) { String before = text.substring(Math.max(0, position - 5), position); - if (before.endsWith("new ") || before.endsWith("< ") || before.endsWith("<")) { + if (before.endsWith("new ")) { + return true; + } + } + + // Check if preceded by < but make sure it's a generic, not a comparison + // Generic: Type 1) { + int checkPos = position - 1; + // Skip whitespace + while (checkPos > 0 && Character.isWhitespace(text.charAt(checkPos))) { + checkPos--; + } + if (checkPos >= 0 && text.charAt(checkPos) == '<') { + // Check what's before the < + int beforeLt = checkPos - 1; + while (beforeLt >= 0 && Character.isWhitespace(text.charAt(beforeLt))) { + beforeLt--; + } + if (beforeLt >= 0) { + char beforeChar = text.charAt(beforeLt); + // If it's a letter/digit/underscore (identifier) or ) followed by space + <, it's a comparison + // If it's a type name directly followed by <, it's a generic + // Check if there was whitespace between the identifier and < + boolean hasSpaceBeforeLt = (checkPos > 0 && Character.isWhitespace(text.charAt(checkPos - 1))); + if (hasSpaceBeforeLt && (Character.isLetterOrDigit(beforeChar) || beforeChar == '_' || beforeChar == ')')) { + // This is a comparison like "i < container" or "(x + 1) < y" + return false; + } + // Otherwise it's likely a generic like List + if (Character.isLetter(beforeChar) || beforeChar == '>') { + return true; + } + } + } + // Check for comma in generics like Map + if (checkPos >= 0 && text.charAt(checkPos) == ',') { return true; } } From 95909b0fd14dbf64aaec78314cd2bb3c4888c286 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 17:03:06 +0200 Subject: [PATCH 161/358] Improved METHOD_CALL syntax highlight by removing color from ( --- .../noppes/npcs/client/gui/util/script/JavaTextContainer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index fa6320470..3816e13a8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -351,7 +351,7 @@ public class JavaTextContainer extends TextContainer { collectPatternMatches(marks, NEW_TYPE, TokenType.NEW_TYPE, 1); collectMethodDeclarations(marks); - collectPatternMatches(marks, METHOD_CALL, TokenType.METHOD_CALL); + collectPatternMatches(marks, METHOD_CALL, TokenType.METHOD_CALL, 1); collectPatternMatches(marks, NUMBER, TokenType.NUMBER); From 96e37341c7f5354df664d66abf5979ffa028d61d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 17:34:26 +0200 Subject: [PATCH 162/358] Fixed bug: //comment List str where List wasnt syntax highlighted --- .../gui/util/script/JavaTextContainer.java | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 3816e13a8..2cdf81041 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -414,21 +414,36 @@ public class JavaTextContainer extends TextContainer { } private void collectTypeDeclarations(List marks) { - Matcher m = TYPE_DECL.matcher(text); - while (m.find()) { - // main type - marks.add(new Mark(m.start(1), m.end(1), TokenType.TYPE_DECL)); + // Run TYPE_DECL on each line to avoid matches bleeding across newlines + List excluded = MethodBlock.getExcludedRanges(text); + for (LineData ld : lines) { + int lineStart = ld.start; + int lineEnd = ld.end; + if (lineStart >= lineEnd) continue; + String s = ld.text; + Matcher m = TYPE_DECL.matcher(s); + while (m.find()) { + int g1s = lineStart + m.start(1); + int g1e = lineStart + m.end(1); + // Skip if any part of the match overlaps an excluded range (string/comment) + boolean skip = false; + for (int[] r : excluded) { + if (g1s < r[1] && g1e > r[0]) { skip = true; break; } + } + if (skip) continue; + marks.add(new Mark(g1s, g1e, TokenType.TYPE_DECL)); - if (m.group(2) != null) { - int start = m.start(2); - int end = m.end(2); - // Color < and > as white - marks.add(new Mark(start, start + 1, TokenType.DEFAULT)); // < - marks.add(new Mark(end - 1, end, TokenType.DEFAULT)); // > + if (m.group(2) != null) { + int start = lineStart + m.start(2); + int end = lineStart + m.end(2); + // Color < and > as default + marks.add(new Mark(start, start + 1, TokenType.DEFAULT)); // < + marks.add(new Mark(end - 1, end, TokenType.DEFAULT)); // > - // Inner type (String) - if (m.group(3) != null) { - marks.add(new Mark(m.start(3), m.end(3), TokenType.NEW_TYPE)); + // Inner type: String in + if (m.group(3) != null) { + marks.add(new Mark(lineStart + m.start(3), lineStart + m.end(3), TokenType.NEW_TYPE)); + } } } } From 8506239d9333d7504b8aea2880ddc71072b79b5f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 18:38:04 +0200 Subject: [PATCH 163/358] Added syntax highlighting for: - Class/Enum/Interface type blocks - Imports - Imported class calls (i.e Math.min()) Stored all imported packages/classes --- .../gui/util/script/JavaTextContainer.java | 156 +++++++++++++++++- 1 file changed, 155 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 2cdf81041..8887d0ba8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -13,8 +13,10 @@ public class JavaTextContainer extends TextContainer { public static final Pattern MODIFIER = Pattern.compile( "\\b(public|protected|private|static|final|abstract|synchronized|native|default)\\b"); public static final Pattern KEYWORD = Pattern.compile( - "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof)\\b"); + "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof|import)\\b"); + public static final Pattern CLASS_DECL = Pattern.compile("\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)"); + public static final Pattern TYPE_DECL = Pattern.compile("\\b([A-Za-z_][a-zA-Z0-9_]*)" + // Group 1 → main type "\\s*(<([^>]+)>)?" + // Group 2 → <…>, Group 3 → inner type "\\s+[a-zA-Z_][a-zA-Z0-9_]*" // variable name @@ -40,6 +42,10 @@ public class JavaTextContainer extends TextContainer { public static final Pattern NUMBER = Pattern.compile( "\\b-?(?:0[xX][\\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?(?:[fFbBdDlLsS])?|NaN|null|Infinity|true|false)\\b"); + // Import statement: import [static] package.path.ClassName [.*]; + public static final Pattern IMPORT = Pattern.compile( + "\\bimport\\s+(?:static\\s+)?([a-zA-Z_][a-zA-Z0-9_.]*?)(?:\\s*\\.\\s*\\*)?\\s*;"); + public List lines = new ArrayList<>(); public List methodBlocks = new ArrayList<>(); @@ -87,7 +93,43 @@ public class JavaTextContainer extends TextContainer { private List globalFields = new ArrayList<>(); private List localFields = new ArrayList<>(); + private java.util.Map importedClasses = new java.util.HashMap<>(); // simpleName -> fullName + private java.util.Set importedPackages = new java.util.HashSet<>(); // wildcard imports + // Common java.lang classes (auto-imported) + private static final java.util.Set JAVA_LANG_CLASSES = new java.util.HashSet<>(java.util.Arrays.asList( + "Object", "String", "Class", "System", "Math", "Integer", "Double", "Float", "Long", "Short", "Byte", + "Character", "Boolean", + "Number", "Void", "Thread", "Runnable", "Exception", "RuntimeException", "Error", "Throwable", + "StringBuilder", "StringBuffer", "Enum", "Comparable", "Iterable", "CharSequence", "Cloneable", + "Process", "ProcessBuilder", "Runtime", "SecurityManager", "ClassLoader", "Package", + "ArithmeticException", "ArrayIndexOutOfBoundsException", "ClassCastException", "IllegalArgumentException", + "IllegalStateException", "IndexOutOfBoundsException", "NullPointerException", "NumberFormatException", + "UnsupportedOperationException", "AssertionError", "OutOfMemoryError", "StackOverflowError" + )); + + private void collectImports() { + importedClasses.clear(); + importedPackages.clear(); + + List excluded = MethodBlock.getExcludedRanges(text); + Matcher m = IMPORT.matcher(text); + while (m.find()) { + if (isInExcludedRange(m.start(), excluded)) + continue; + String fullPath = m.group(1).trim(); + // Check if wildcard import (import a.b.*;) + if (text.substring(m.start(), m.end()).contains("*")) { + importedPackages.add(fullPath); + } else { + // Specific class import: extract simple name + int lastDot = fullPath.lastIndexOf('.'); + String simpleName = lastDot >= 0 ? fullPath.substring(lastDot + 1) : fullPath; + importedClasses.put(simpleName, fullPath); + } + } + } + private void collectFields() { globalFields.clear(); localFields.clear(); @@ -336,14 +378,119 @@ public class JavaTextContainer extends TextContainer { } return false; } + + private void collectImportStatements(List marks) { + List excluded = MethodBlock.getExcludedRanges(text); + // Pattern to match: import [static] package.path.ClassName [.*]; + Matcher m = IMPORT.matcher(text); + while (m.find()) { + if (isInExcludedRange(m.start(), excluded)) + continue; + // "import" keyword already handled by KEYWORD pattern + // Highlight the package/class path + String fullPath = m.group(1); + int pathStart = m.start(1); + int pathEnd = m.end(1); + + // Highlight the 'import' keyword itself in modifier/orange color (override KEYWORD) + int importKwStart = m.start(); + int importKwEnd = Math.min(importKwStart + 6, text.length()); // "import" + marks.add(new Mark(importKwStart, importKwEnd, TokenType.IMPORT_KEYWORD)); + + // Split into package and class: last segment is class, rest is package + int lastDot = fullPath.lastIndexOf('.'); + if (lastDot > 0) { + // package part (e.g., "java.util") + int pkgEnd = pathStart + lastDot; + // +1 includes . + marks.add(new Mark(pathStart, pkgEnd + 1, TokenType.TYPE_DECL)); // dark blue + // class part (e.g., "ArrayList") + marks.add(new Mark(pkgEnd + 1, pathEnd, TokenType.IMPORTED_CLASS)); // aqua + } else { + // No package, just class name + marks.add(new Mark(pathStart, pathEnd, TokenType.IMPORTED_CLASS)); + } + + // Highlight .* if present (find '*' between match bounds) + String matchText = text.substring(m.start(), m.end()); + int starIndex = matchText.indexOf('*'); + if (starIndex != -1) { + int absStar = m.start() + starIndex; + marks.add(new Mark(absStar, absStar + 1, TokenType.DEFAULT)); + } + } + } + + private void collectImportedClassUsages(List marks) { + List excluded = MethodBlock.getExcludedRanges(text); + // Find identifiers followed by dot (e.g., Math.min, ArrayList.class) + Pattern classUsage = Pattern.compile("\\b([A-Z][a-zA-Z0-9_]*)\\s*\\."); + Matcher m = classUsage.matcher(text); + while (m.find()) { + String className = m.group(1); + int start = m.start(1); + int end = m.end(1); + if (isInExcludedRange(start, excluded)) + continue; + + // Check if this is an imported class, wildcard match, or java.lang class + boolean isImported = importedClasses.containsKey(className) || JAVA_LANG_CLASSES.contains(className); + if (!isImported) { + // Check wildcard imports (heuristic: if any imported package exists, mark it) + for (String pkg : importedPackages) { + isImported = true; + break; + } + } + + // Also skip if it's a local/global field or parameter (avoid false positives) + MethodBlock block = findMethodBlockAtPosition(start); + boolean isShadowed = globalFields.contains(className) || localFields.contains(className); + if (block != null) { + isShadowed = isShadowed || block.parameters.contains(className) || block.isLocalDeclaredAtPosition( + className, start); + } + + if (isImported && !isShadowed) { + marks.add(new Mark(start, end, TokenType.IMPORTED_CLASS)); + } + } + } + + private void collectClassDeclarations(List marks) { + Matcher m = CLASS_DECL.matcher(text); + List excluded = MethodBlock.getExcludedRanges(text); + while (m.find()) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.CLASS_KEYWORD)); + + int nameStart = m.start(2); + int nameEnd = m.end(2); + if (isInExcludedRange(nameStart, excluded)) + continue; + String kind = m.group(1); + if ("interface".equals(kind)) { + marks.add(new Mark(nameStart, nameEnd, TokenType.INTERFACE_DECL)); + } else if ("enum".equals(kind)) { + marks.add(new Mark(nameStart, nameEnd, TokenType.ENUM_DECL)); + } else { + marks.add(new Mark(nameStart, nameEnd, TokenType.CLASS_DECL)); + } + } + } public void formatCodeText() { // Step 1: Tokenize the full text List marks = new ArrayList<>(); + collectImports(); // Parse import statements first collectFields(); // Extract global and local fields with method scoping collectPatternMatches(marks, COMMENT, TokenType.COMMENT); collectPatternMatches(marks, STRING, TokenType.STRING); + + collectImportStatements(marks); // Highlight import statements + collectClassDeclarations(marks); // Highlight class/interface/enum declarations + + // Highlight general keywords collectPatternMatches(marks, KEYWORD, TokenType.KEYWORD); collectPatternMatches(marks, MODIFIER, TokenType.MODIFIER); @@ -356,6 +503,7 @@ public class JavaTextContainer extends TextContainer { collectPatternMatches(marks, NUMBER, TokenType.NUMBER); highlightVariableReferences(marks); + collectImportedClassUsages(marks); // Highlight imported class usages (e.g., Math.min) marks = resolveConflicts(marks); @@ -555,9 +703,15 @@ public class JavaTextContainer extends TextContainer { public enum TokenType { COMMENT('7', 130), STRING('5', 120), + CLASS_KEYWORD('c', 115), + IMPORT_KEYWORD('6', 110), KEYWORD('c', 100), MODIFIER('6', 90), NEW_TYPE('d', 80), + IMPORTED_CLASS('3', 75), //imported classes used in code + INTERFACE_DECL('b', 85), + ENUM_DECL('d', 85), + CLASS_DECL('3', 85), TYPE_DECL('3', 70), METHOD_DECARE('2', 60), METHOD_CALL('a', 50), From 5508ea26ea41d53fa4028ced83384f2fd0688cdd Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 19:24:26 +0200 Subject: [PATCH 164/358] Dynamically changed color of imported class/type declarations depending on its type (class/enum/interface) --- .../gui/util/script/JavaTextContainer.java | 60 +++++++++++++++++-- 1 file changed, 54 insertions(+), 6 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 8887d0ba8..d26f334d0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -95,6 +95,7 @@ public class JavaTextContainer extends TextContainer { private List localFields = new ArrayList<>(); private java.util.Map importedClasses = new java.util.HashMap<>(); // simpleName -> fullName private java.util.Set importedPackages = new java.util.HashSet<>(); // wildcard imports + private java.util.Map classTypeCache = new java.util.HashMap<>(); // fullClassName -> TokenType (cached reflection results) // Common java.lang classes (auto-imported) private static final java.util.Set JAVA_LANG_CLASSES = new java.util.HashSet<>(java.util.Arrays.asList( @@ -130,6 +131,38 @@ public class JavaTextContainer extends TextContainer { } } + /** + * Determine the token type for a class using reflection. + * Returns INTERFACE_DECL for interfaces, ENUM_DECL for enums, or the defaultType for classes/unknown. + */ + private TokenType getClassTokenType(String fullClassName, TokenType defaultType) { + if (fullClassName == null || fullClassName.isEmpty()) return defaultType; + + // Check cache first + if (classTypeCache.containsKey(fullClassName)) { + return classTypeCache.get(fullClassName); + } + + TokenType result = defaultType; + try { + Class clazz = Class.forName(fullClassName); + if (clazz.isInterface()) { + result = TokenType.INTERFACE_DECL; + } else if (clazz.isEnum()) { + result = TokenType.ENUM_DECL; + } else { + result = defaultType; // regular class + } + } catch (Exception e) { + // Class not found or can't be loaded - use default + result = defaultType; + } + + // Cache the result + classTypeCache.put(fullClassName, result); + return result; + } + private void collectFields() { globalFields.clear(); localFields.clear(); @@ -404,11 +437,13 @@ public class JavaTextContainer extends TextContainer { int pkgEnd = pathStart + lastDot; // +1 includes . marks.add(new Mark(pathStart, pkgEnd + 1, TokenType.TYPE_DECL)); // dark blue - // class part (e.g., "ArrayList") - marks.add(new Mark(pkgEnd + 1, pathEnd, TokenType.IMPORTED_CLASS)); // aqua + // class part (e.g., "ArrayList") - determine type dynamically + TokenType classTokenType = getClassTokenType(fullPath, TokenType.IMPORTED_CLASS); + marks.add(new Mark(pkgEnd + 1, pathEnd, classTokenType)); } else { // No package, just class name - marks.add(new Mark(pathStart, pathEnd, TokenType.IMPORTED_CLASS)); + TokenType classTokenType = getClassTokenType(fullPath, TokenType.IMPORTED_CLASS); + marks.add(new Mark(pathStart, pathEnd, classTokenType)); } // Highlight .* if present (find '*' between match bounds) @@ -452,7 +487,13 @@ public class JavaTextContainer extends TextContainer { } if (isImported && !isShadowed) { - marks.add(new Mark(start, end, TokenType.IMPORTED_CLASS)); + // Determine token type dynamically using reflection + String fullClassName = importedClasses.get(className); + if (fullClassName == null && JAVA_LANG_CLASSES.contains(className)) { + fullClassName = "java.lang." + className; + } + TokenType tokenType = getClassTokenType(fullClassName, TokenType.IMPORTED_CLASS); + marks.add(new Mark(start, end, tokenType)); } } } @@ -579,7 +620,14 @@ public class JavaTextContainer extends TextContainer { if (g1s < r[1] && g1e > r[0]) { skip = true; break; } } if (skip) continue; - marks.add(new Mark(g1s, g1e, TokenType.TYPE_DECL)); + // Check if this type is a known interface or enum using reflection + String typeName = m.group(1); + String fullClassName = importedClasses.get(typeName); + if (fullClassName == null && JAVA_LANG_CLASSES.contains(typeName)) { + fullClassName = "java.lang." + typeName; + } + TokenType tokenType = getClassTokenType(fullClassName, TokenType.TYPE_DECL); + marks.add(new Mark(g1s, g1e, tokenType)); if (m.group(2) != null) { int start = lineStart + m.start(2); @@ -590,7 +638,7 @@ public class JavaTextContainer extends TextContainer { // Inner type: String in if (m.group(3) != null) { - marks.add(new Mark(lineStart + m.start(3), lineStart + m.end(3), TokenType.NEW_TYPE)); + marks.add(new Mark(lineStart + m.start(3), lineStart + m.end(3), TokenType.IMPORTED_CLASS)); } } } From 1b0ce874bb6ce300dda74505efffe09e62430d85 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 20:38:42 +0200 Subject: [PATCH 165/358] Holy shit IMPORTS ACTUALLY RESOLVE/ERROR DEPENDING ON VALID CLASS PATH --- .../gui/util/script/JavaTextContainer.java | 358 ++++++++++++++++-- 1 file changed, 319 insertions(+), 39 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index d26f334d0..42077c5bc 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -17,9 +17,11 @@ public class JavaTextContainer extends TextContainer { public static final Pattern CLASS_DECL = Pattern.compile("\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)"); - public static final Pattern TYPE_DECL = Pattern.compile("\\b([A-Za-z_][a-zA-Z0-9_]*)" + // Group 1 → main type - "\\s*(<([^>]+)>)?" + // Group 2 → <…>, Group 3 → inner type - "\\s+[a-zA-Z_][a-zA-Z0-9_]*" // variable name + public static final Pattern TYPE_DECL = Pattern.compile( + "(?:(?:public|private|protected|static|final|transient|volatile)\\s+)*" + // optional modifiers + "([A-Za-z_][a-zA-Z0-9_]*)" + // Group 1 → main type + "\\s*(<([^<>]*(?:<[^<>]*>)*)>)?" + // Group 2 → <…>, Group 3 → inner type (handles one level of nesting) + "\\s+[a-zA-Z_][a-zA-Z0-9_]*" // variable name ); public static final Pattern NEW_TYPE = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)"); @@ -96,6 +98,8 @@ public class JavaTextContainer extends TextContainer { private java.util.Map importedClasses = new java.util.HashMap<>(); // simpleName -> fullName private java.util.Set importedPackages = new java.util.HashSet<>(); // wildcard imports private java.util.Map classTypeCache = new java.util.HashMap<>(); // fullClassName -> TokenType (cached reflection results) + private java.util.Set resolvedPackages = new java.util.HashSet<>(); + private java.util.Set knownPackages = new java.util.HashSet<>(); // Common java.lang classes (auto-imported) private static final java.util.Set JAVA_LANG_CLASSES = new java.util.HashSet<>(java.util.Arrays.asList( @@ -112,6 +116,9 @@ public class JavaTextContainer extends TextContainer { private void collectImports() { importedClasses.clear(); importedPackages.clear(); + // Clear cached import resolutions when re-parsing imports (keeps editor responsive during typing) + importResolveCache.clear(); + knownPackages.clear(); List excluded = MethodBlock.getExcludedRanges(text); Matcher m = IMPORT.matcher(text); @@ -126,23 +133,128 @@ public class JavaTextContainer extends TextContainer { // Specific class import: extract simple name int lastDot = fullPath.lastIndexOf('.'); String simpleName = lastDot >= 0 ? fullPath.substring(lastDot + 1) : fullPath; - importedClasses.put(simpleName, fullPath); + + // Remember the package prefix as a known package (even before resolution) + if (lastDot > 0) { + String pkg = fullPath.substring(0, lastDot); + knownPackages.add(pkg); + } + // Store with $ notation for inner classes (e.g., IOverlay.ColorType -> IOverlay$ColorType) + String storagePath = fullPath.replaceAll("\\.(\\p{Upper})", "\\$$1"); + importedClasses.put(simpleName, storagePath); } } } + + // Cache for resolved import attempts: original fullPath -> resolved class name (with $ for inner classes) or null if unresolved + private java.util.Map importResolveCache = new java.util.HashMap<>(); + + private static class ResolvedImport { + String resolvedClass; // full class name with $ notation if found + int classStartIndex; // index in segments where class starts + TokenType tokenType; + boolean found; + } + + /** + * Try to resolve an import path to an actual class using Class.forName by attempting + * different class-start splits and inner-class ($) combinations. Caches results. + */ + private ResolvedImport resolveImportPath(String fullPath) { + ResolvedImport ri = new ResolvedImport(); + if (fullPath == null || fullPath.isEmpty()) { + ri.found = false; return ri; + } + if (importResolveCache.containsKey(fullPath)) { + String cached = importResolveCache.get(fullPath); + if (cached == null) { ri.found = false; return ri; } + ri.resolvedClass = cached; + ri.found = true; + ri.tokenType = getClassTokenType(cached, TokenType.IMPORTED_CLASS); + // derive classStartIndex by comparing segments + String[] segs = fullPath.split("\\."); + String[] rcsegs = cached.replace('$', '.').split("\\."); + int si = 0; while (si < segs.length && si < rcsegs.length && segs[si].equals(rcsegs[si])) si++; + ri.classStartIndex = si; + return ri; + } + + String[] segments = fullPath.split("\\."); + int n = segments.length; + + // Try candidate class start positions. Start from left-most uppercase-looking segment for efficiency. + int firstUpper = 0; + for (int i = 0; i < n; i++) { + if (segments[i].length() > 0 && Character.isUpperCase(segments[i].charAt(0))) { firstUpper = i; break; } + } + + outer: + for (int k = firstUpper; k < n; k++) { + // number of inner parts available + int innerAvailable = n - (k + 1); + // Try the longest inner suffix first (deepest inner class) + for (int innerCount = innerAvailable; innerCount >= 0; innerCount--) { + // build top-level class and inner suffix + StringBuilder classBuilder = new StringBuilder(); + // package portion + if (k > 0) { + for (int i = 0; i < k; i++) { + if (i > 0) classBuilder.append('.'); + classBuilder.append(segments[i]); + } + classBuilder.append('.'); + } + // top class + classBuilder.append(segments[k]); + // inner parts + if (innerCount > 0) { + classBuilder.append('$'); + for (int j = 0; j < innerCount; j++) { + if (j > 0) classBuilder.append('$'); + classBuilder.append(segments[k + 1 + j]); + } + } + String candidate = classBuilder.toString(); + TokenType tt = getClassTokenType(candidate, TokenType.IMPORTED_CLASS); + if (tt != TokenType.UNDEFINED_VAR) { + ri.found = true; + ri.resolvedClass = candidate; + ri.classStartIndex = k; + ri.tokenType = tt; + importResolveCache.put(fullPath, candidate); + break outer; + } + // also try without package separator if k==0 (top-level without package) + if (k == 0 && innerCount == 0) { + // candidate is segments[0] only, already tried + } + } + } + + if (!ri.found) { + importResolveCache.put(fullPath, null); + ri.found = false; + // approximate classStartIndex as first uppercase or last segment + int classStart = 0; + for (int i = 0; i < segments.length; i++) { + if (segments[i].length() > 0 && Character.isUpperCase(segments[i].charAt(0))) { classStart = i; break; } + } + ri.classStartIndex = classStart; + } + return ri; + } /** * Determine the token type for a class using reflection. - * Returns INTERFACE_DECL for interfaces, ENUM_DECL for enums, or the defaultType for classes/unknown. + * Returns INTERFACE_DECL for interfaces, ENUM_DECL for enums, UNDEFINED for unfound classes, or the defaultType for classes. */ private TokenType getClassTokenType(String fullClassName, TokenType defaultType) { if (fullClassName == null || fullClassName.isEmpty()) return defaultType; // Check cache first - if (classTypeCache.containsKey(fullClassName)) { - return classTypeCache.get(fullClassName); - } - + // if (classTypeCache.containsKey(fullClassName)) { + // return classTypeCache.get(fullClassName); + //} TokenType result = defaultType; try { Class clazz = Class.forName(fullClassName); @@ -153,9 +265,31 @@ public class JavaTextContainer extends TextContainer { } else { result = defaultType; // regular class } - } catch (Exception e) { - // Class not found or can't be loaded - use default - result = defaultType; + } catch (ClassNotFoundException | NoClassDefFoundError e) { + // Try with $ notation for inner classes (e.g., IOverlay$ColorType) + String innerClassNotation = fullClassName.replaceAll("\\.(\\p{Upper})", "\\$$1"); + if (!innerClassNotation.equals(fullClassName)) { + try { + Class clazz = Class.forName(innerClassNotation); + if (clazz.isInterface()) { + result = TokenType.INTERFACE_DECL; + } else if (clazz.isEnum()) { + result = TokenType.ENUM_DECL; + } else { + result = defaultType; // regular class + } + // Cache with original notation + classTypeCache.put(fullClassName, result); + return result; + } catch (Exception e2) { + // Still not found + } + } + // Class not found - mark as undefined + result = TokenType.UNDEFINED_VAR; + } catch (LinkageError e) { + // Linkage errors (other than NoClassDefFoundError) - treat as undefined + result = TokenType.UNDEFINED_VAR; } // Cache the result @@ -163,6 +297,62 @@ public class JavaTextContainer extends TextContainer { return result; } + /** + * Parse and highlight generic type parameters recursively (e.g., "List" or "Map>>") + * Handles arbitrarily nested generics by tracking bracket depth. + */ + private void highlightGenericTypes(String genericContent, int contentStart, List marks, List excluded) { + if (genericContent == null || genericContent.isEmpty()) return; + + int i = 0; + while (i < genericContent.length()) { + char c = genericContent.charAt(i); + + // Check if we're starting a type name (uppercase letter) + if (Character.isJavaIdentifierStart(c) && Character.isUpperCase(c)) { + int start = i; + while (i < genericContent.length() && Character.isJavaIdentifierPart(genericContent.charAt(i))) { + i++; + } + String typeName = genericContent.substring(start, i); + int absStart = contentStart + start; + int absEnd = contentStart + i; + + if (!isInExcludedRange(absStart, excluded)) { + // Resolve full class name + String fullClassName = importedClasses.get(typeName); + if (fullClassName == null && JAVA_LANG_CLASSES.contains(typeName)) { + fullClassName = "java.lang." + typeName; + } + + TokenType tokenType = getClassTokenType(fullClassName, TokenType.IMPORTED_CLASS); + marks.add(new Mark(absStart, absEnd, tokenType)); + } + + // Check for nested generics after this type + while (i < genericContent.length() && Character.isWhitespace(genericContent.charAt(i))) i++; + if (i < genericContent.length() && genericContent.charAt(i) == '<') { + // Find matching > + int depth = 1; + int nestedStart = i + 1; + i++; + while (i < genericContent.length() && depth > 0) { + if (genericContent.charAt(i) == '<') depth++; + else if (genericContent.charAt(i) == '>') depth--; + i++; + } + // Recursively process nested content + String nestedContent = genericContent.substring(nestedStart, i - 1); + highlightGenericTypes(nestedContent, contentStart + nestedStart, marks, excluded); + // Continue after the closing > to find more types + } + // Continue to next character (handles commas, spaces, etc.) + } else { + i++; + } + } + } + private void collectFields() { globalFields.clear(); localFields.clear(); @@ -419,31 +609,107 @@ public class JavaTextContainer extends TextContainer { while (m.find()) { if (isInExcludedRange(m.start(), excluded)) continue; - // "import" keyword already handled by KEYWORD pattern - // Highlight the package/class path - String fullPath = m.group(1); + String fullPath = m.group(1).trim(); int pathStart = m.start(1); int pathEnd = m.end(1); - // Highlight the 'import' keyword itself in modifier/orange color (override KEYWORD) + // Highlight the 'import' keyword itself int importKwStart = m.start(); int importKwEnd = Math.min(importKwStart + 6, text.length()); // "import" marks.add(new Mark(importKwStart, importKwEnd, TokenType.IMPORT_KEYWORD)); - // Split into package and class: last segment is class, rest is package - int lastDot = fullPath.lastIndexOf('.'); - if (lastDot > 0) { - // package part (e.g., "java.util") - int pkgEnd = pathStart + lastDot; - // +1 includes . - marks.add(new Mark(pathStart, pkgEnd + 1, TokenType.TYPE_DECL)); // dark blue - // class part (e.g., "ArrayList") - determine type dynamically - TokenType classTokenType = getClassTokenType(fullPath, TokenType.IMPORTED_CLASS); - marks.add(new Mark(pkgEnd + 1, pathEnd, classTokenType)); + // Resolve import path to an actual class if possible + ResolvedImport ri = resolveImportPath(fullPath); + String[] segments = fullPath.split("\\."); + + // Compute package portion length + int pkgSegments = 0; + if (ri.found && ri.resolvedClass != null) { + pkgSegments = Math.max(0, ri.classStartIndex); + // register resolved package prefix + if (pkgSegments > 0) { + StringBuilder pb = new StringBuilder(); + for (int i = 0; i < pkgSegments; i++) { + if (i > 0) pb.append('.'); + pb.append(segments[i]); + } + resolvedPackages.add(pb.toString()); + } } else { - // No package, just class name - TokenType classTokenType = getClassTokenType(fullPath, TokenType.IMPORTED_CLASS); - marks.add(new Mark(pathStart, pathEnd, classTokenType)); + // Try to find the longest previously-resolved package prefix, or known package prefix + for (int p = segments.length - 1; p >= 1; p--) { + StringBuilder test = new StringBuilder(); + for (int i = 0; i < p; i++) { + if (i > 0) test.append('.'); + test.append(segments[i]); + } + String testStr = test.toString(); + if (resolvedPackages.contains(testStr)) { + pkgSegments = p; + break; + } + if (knownPackages.contains(testStr)) { + pkgSegments = p; + // don't break; prefer a resolved package if found later in loop + } + } + // fallback to ri.classStartIndex if nothing known + if (pkgSegments == 0) pkgSegments = Math.max(0, ri.classStartIndex); + } + StringBuilder pkgBuilder = new StringBuilder(); + for (int i = 0; i < pkgSegments; i++) { + if (i > 0) pkgBuilder.append('.'); + pkgBuilder.append(segments[i]); + } + int pkgLen = pkgBuilder.length(); + if (pkgLen > 0) { + marks.add(new Mark(pathStart, pathStart + pkgLen + 1, TokenType.TYPE_DECL)); // package + dot + } + + // Highlight class and inner-class segments + int classStart = pathStart + (pkgLen > 0 ? pkgLen + 1 : 0); + if (ri.found && ri.resolvedClass != null) { + // Use resolved class (with $). Break into segments for progressive checks. + for (int i = ri.classStartIndex; i < segments.length; i++) { + int segStart = classStart; + int segEnd = classStart + segments[i].length(); + + // Build candidate full name up to this segment using $ for inner separators + StringBuilder testBuilder = new StringBuilder(); + if (ri.classStartIndex > 0) { + for (int p = 0; p < ri.classStartIndex; p++) { + if (p > 0) testBuilder.append('.'); + testBuilder.append(segments[p]); + } + testBuilder.append('.'); + } + testBuilder.append(segments[ri.classStartIndex]); + for (int q = ri.classStartIndex + 1; q <= i; q++) { + testBuilder.append('$').append(segments[q]); + } + String testFull = testBuilder.toString(); + TokenType segType = getClassTokenType(testFull, TokenType.IMPORTED_CLASS); + marks.add(new Mark(segStart, segEnd, segType)); + + // Update importedClasses map for simple name resolution + if (i == segments.length - 1) { + importedClasses.put(segments[i], ri.resolvedClass); + } + + classStart = segEnd + 1; // +1 for dot + } + } else { + // Unresolved import: mark the entire tail (after known package prefix) as undefined + int undefStart = classStart; + int undefEnd = pathEnd; + if (undefStart < undefEnd) { + marks.add(new Mark(undefStart, undefEnd, TokenType.UNDEFINED_VAR)); + } + // Remove any previously-added simple-name mapping to avoid treating it as imported + String simpleName = segments[segments.length - 1]; + if (importedClasses.containsKey(simpleName)) { + importedClasses.remove(simpleName); + } } // Highlight .* if present (find '*' between match bounds) @@ -636,9 +902,11 @@ public class JavaTextContainer extends TextContainer { marks.add(new Mark(start, start + 1, TokenType.DEFAULT)); // < marks.add(new Mark(end - 1, end, TokenType.DEFAULT)); // > - // Inner type: String in + // Parse and highlight all types in generic content (handles nested generics) if (m.group(3) != null) { - marks.add(new Mark(lineStart + m.start(3), lineStart + m.end(3), TokenType.IMPORTED_CLASS)); + String genericContent = m.group(3); + int contentStart = lineStart + m.start(3); + highlightGenericTypes(genericContent, contentStart, marks, excluded); } } } @@ -657,7 +925,8 @@ public class JavaTextContainer extends TextContainer { } private List resolveConflicts(List marks) { - // Sort by start index first, then by descending priority + // Sort by start index first, then by descending priority so higher-priority marks + // at the same start are considered first. marks.sort((a, b) -> { if (a.start != b.start) return Integer.compare(a.start, b.start); @@ -666,16 +935,27 @@ public class JavaTextContainer extends TextContainer { List result = new ArrayList<>(); for (Mark m : marks) { - boolean overlap = false; - for (Mark r : result) { - if (m.start < r.end && m.end > r.start && r.type.priority >= m.type.priority) { - overlap = true; - break; + boolean skip = false; + // Remove any existing lower-priority marks that overlap with this higher-priority mark + for (int i = result.size() - 1; i >= 0; i--) { + Mark r = result.get(i); + if (m.start < r.end && m.end > r.start) { + if (r.type.priority < m.type.priority) { + // remove lower-priority overlapping mark + result.remove(i); + } else { + // an existing mark has equal or higher priority and overlaps -> skip adding m + skip = true; + break; + } } } - if (!overlap) + if (!skip) { result.add(m); + } } + // Optionally sort the result by start before returning + result.sort((a, b) -> Integer.compare(a.start, b.start)); return result; } @@ -768,7 +1048,7 @@ public class JavaTextContainer extends TextContainer { GLOBAL_FIELD('b', 35), // aqua - class-level fields LOCAL_FIELD('e', 25), // yellow - local variables PARAMETER('9', 36), // blue - method parameters (Bug 10) - UNDEFINED_VAR('4', 10), // dark red - undefined variables (Bug 11) + UNDEFINED_VAR('4', 105), // dark red - undefined variables (higher priority so it isn't overridden) DEFAULT('f', 0); public final char color; From 5714d89c417371891c34ae80545248d9fd706497 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 23:08:44 +0200 Subject: [PATCH 166/358] imports functional state? (not really) --- .../gui/util/script/ClassPathFinder.java | 561 +++++++++++++++++ .../gui/util/script/JavaTextContainer.java | 572 +++++++----------- 2 files changed, 777 insertions(+), 356 deletions(-) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java new file mode 100644 index 000000000..18f4252a5 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java @@ -0,0 +1,561 @@ +package noppes.npcs.client.gui.util.script; + +import java.util.*; + +/** + * Helper class for resolving Java classpath imports and determining class types. + * Handles: + * - Class resolution via Class.forName + * - Inner class notation (Outer.Inner -> Outer$Inner) + * - Package validation + * - Type detection (interface/enum/class) + * - Result caching for performance + */ +public class ClassPathFinder { + + // Cache: fully-qualified class name -> ClassInfo (null if not found) + private final Map classCache = new HashMap<>(); + + // Cache: package prefix -> whether it's a valid package (has classes under it) + private final Set validPackages = new HashSet<>(); + + // Common java.lang classes (auto-imported in Java) + public static final Set JAVA_LANG_CLASSES = new HashSet<>(Arrays.asList( + "Object", "String", "Class", "System", "Math", "Integer", "Double", "Float", "Long", "Short", "Byte", + "Character", "Boolean", "Number", "Void", "Thread", "Runnable", "Exception", "RuntimeException", + "Error", "Throwable", "StringBuilder", "StringBuffer", "Enum", "Comparable", "Iterable", + "CharSequence", "Cloneable", "Process", "ProcessBuilder", "Runtime", "SecurityManager", + "ClassLoader", "Package", "ArithmeticException", "ArrayIndexOutOfBoundsException", + "ClassCastException", "IllegalArgumentException", "IllegalStateException", + "IndexOutOfBoundsException", "NullPointerException", "NumberFormatException", + "UnsupportedOperationException", "AssertionError", "OutOfMemoryError", "StackOverflowError" + )); + + /** + * Information about a resolved class + */ + public static class ClassInfo { + public final String resolvedName; // Full class name with $ for inner classes + public final ClassType type; // INTERFACE, ENUM, or CLASS + public final int packageEndIndex; // Index in original path where package ends (for "java.util.List" -> 9) + + public ClassInfo(String resolvedName, ClassType type, int packageEndIndex) { + this.resolvedName = resolvedName; + this.type = type; + this.packageEndIndex = packageEndIndex; + } + } + + public enum ClassType { + INTERFACE, + ENUM, + CLASS + } + + /** + * Result of resolving an import path + */ + public static class ResolveResult { + public final boolean found; + public final ClassInfo classInfo; + public final String packagePortion; // The package part (e.g., "java.util") + public final String classPortion; // The class part (e.g., "List" or "IOverlay.ColorType") + public final List classSegments; // Each class/inner-class segment with its type + public final int invalidStartOffset; // Character offset where invalid portion starts (-1 if all valid or all invalid) + + private ResolveResult(boolean found, ClassInfo classInfo, String packagePortion, + String classPortion, List classSegments, int invalidStartOffset) { + this.found = found; + this.classInfo = classInfo; + this.packagePortion = packagePortion; + this.classPortion = classPortion; + this.classSegments = classSegments != null ? classSegments : Collections.emptyList(); + this.invalidStartOffset = invalidStartOffset; + } + + public static ResolveResult notFound(String packagePortion, String classPortion, int invalidStartOffset) { + return new ResolveResult(false, null, packagePortion, classPortion, null, invalidStartOffset); + } + + public static ResolveResult found(ClassInfo info, String packagePortion, + String classPortion, List segments) { + return new ResolveResult(true, info, packagePortion, classPortion, segments, -1); + } + } + + /** + * Represents a segment of the class portion (e.g., "IOverlay" or "ColorType") + */ + public static class ClassSegment { + public final String name; + public final ClassType type; + + public ClassSegment(String name, ClassType type) { + this.name = name; + this.type = type; + } + } + + /** + * Clear all caches (call when imports change) + */ + public void clearCache() { + classCache.clear(); + validPackages.clear(); + } + + /** + * Resolve an import path to determine if it's a valid class and get its type. + * Handles inner classes like "kamkeel.npcdbc.api.client.overlay.IOverlay.ColorType" + * + * @param importPath The import path (e.g., "java.util.List" or "kamkeel...IOverlay.ColorType") + * @return ResolveResult with resolution details + */ + public ResolveResult resolve(String importPath) { + if (importPath == null || importPath.isEmpty()) { + return ResolveResult.notFound("", "", -1); + } + + // Handle trailing dot (user is still typing a package path like "java.util.") + boolean trailingDot = importPath.endsWith("."); + String pathToResolve = trailingDot ? importPath.substring(0, importPath.length() - 1) : importPath; + + if (pathToResolve.isEmpty()) { + return ResolveResult.notFound("", "", -1); + } + + String[] segments = pathToResolve.split("\\."); + int n = segments.length; + + // Find the first uppercase segment (likely start of class name) + int firstUpperIndex = findFirstUppercaseSegment(segments); + + // If trailing dot, check if the entire path is a valid package (either cached or all-lowercase) + if (trailingDot) { + // If entire path is lowercase, treat as valid package being typed + if (firstUpperIndex == n) { + // Check if we have it cached OR if it could be a valid package + if (isValidPackage(pathToResolve)) { + return ResolveResult.notFound(pathToResolve, "", -1); + } + // Not cached, but all lowercase - use heuristic: assume valid package + return ResolveResult.notFound(pathToResolve, "", -1); + } + } + + // Try different splits: package vs class boundary + // Start from firstUpperIndex and try treating each uppercase segment as potential class start + for (int classStartIdx = firstUpperIndex; classStartIdx < n; classStartIdx++) { + // Try different inner class depths (deepest first) + for (int classEndIdx = n; classEndIdx > classStartIdx; classEndIdx--) { + String candidate = buildClassName(segments, classStartIdx, classEndIdx); + ClassInfo info = tryResolveClass(candidate); + + if (info != null) { + // Found a valid class! + String packagePortion = buildPackagePortion(segments, classStartIdx); + String validClassPortion = buildClassPortion(segments, classStartIdx, classEndIdx); + + // Register this package as valid for future lookups + if (!packagePortion.isEmpty()) { + registerValidPackage(packagePortion); + } + + // Check if there are MORE segments after what we resolved (invalid inner class) + if (classEndIdx < n) { + // e.g., IOverlay.ColorType resolved but there's more like ".idk" + // Mark the extra parts as invalid + List classSegments = buildClassSegments(segments, classStartIdx, classEndIdx); + String fullClassPortion = buildClassPortion(segments, classStartIdx, n); + // Calculate where the invalid portion starts (after valid class + dot) + int validEndOffset = packagePortion.length() + (packagePortion.isEmpty() ? 0 : 1) + validClassPortion.length() + 1; + return new ResolveResult(false, info, packagePortion, + fullClassPortion, classSegments, validEndOffset); + } + + // Full resolution successful + List classSegments = buildClassSegments(segments, classStartIdx, n); + return ResolveResult.found( + new ClassInfo(info.resolvedName, info.type, packagePortion.length()), + packagePortion, validClassPortion, classSegments + ); + } + } + } + + // Class resolution failed completely + // Strategy: Use heuristic - everything before first uppercase = package, rest = class + // This ensures consistent behavior regardless of cache state + + if (firstUpperIndex > 0 && firstUpperIndex < n) { + // There's a package portion (lowercase) and class portion (uppercase start) + String packagePortion = buildPackagePortion(segments, firstUpperIndex); + String classPortion = buildClassPortion(segments, firstUpperIndex, n); + + // Check if we can detect a typo in the package by comparing with cached packages + int typoIndex = findPackageTypoIndex(segments, firstUpperIndex); + if (typoIndex >= 0 && typoIndex < firstUpperIndex) { + // Found a divergence from a known valid package + String validPrefix = buildPackagePortion(segments, typoIndex); + int invalidOffset = validPrefix.isEmpty() ? 0 : validPrefix.length() + 1; + return ResolveResult.notFound(validPrefix, buildClassPortion(segments, typoIndex, n), invalidOffset); + } + + // No typo detected - assume package is valid, only class portion is invalid + int invalidOffset = packagePortion.length() + 1; // After the package dot + return ResolveResult.notFound(packagePortion, classPortion, invalidOffset); + + } else if (firstUpperIndex == 0) { + // Entire path starts with uppercase (e.g., "String" or "MyClass.Inner") + // Treat as class with no package + return ResolveResult.notFound("", pathToResolve, 0); + + } else { + // All lowercase, no uppercase found - entire path is package-like + // If trailing dot was handled above, this is just a package without class + return ResolveResult.notFound(pathToResolve, "", -1); + } + } + + /** + * Find index where the typed package diverges from a known cached package. + * Returns -1 if no typo detected, or the segment index where divergence starts. + */ + private int findPackageTypoIndex(String[] segments, int classStartIdx) { + // Build the typed package path segment by segment and compare with cache + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < classStartIdx; i++) { + if (i > 0) sb.append("."); + sb.append(segments[i]); + String currentPath = sb.toString(); + + // Check if this exact path is in cache + if (!validPackages.contains(currentPath)) { + // Not in cache - check if a DIFFERENT path at this level IS in cache + // This would indicate a typo (e.g., "guui" vs "gui") + if (i > 0) { + String parentPath = buildPackagePortion(segments, i); + // Look for any cached package that starts with parent but differs at segment i + for (String cached : validPackages) { + if (cached.startsWith(parentPath + ".") && !cached.startsWith(currentPath)) { + // Found a cached package with same parent but different segment + return i; // This segment is likely a typo + } + } + } + } + } + return -1; // No typo detected + } + + /** + * Check if a package path is valid (has been seen in successful resolutions or can be validated) + */ + public boolean isValidPackage(String packagePath) { + if (packagePath == null || packagePath.isEmpty()) { + return false; + } + + // Check cache first + if (validPackages.contains(packagePath)) { + return true; + } + + // Try to find any class under this package to validate it + // Common test classes for well-known packages + String[] testClasses = getTestClassesForPackage(packagePath); + for (String testClass : testClasses) { + try { + Class.forName(testClass); + registerValidPackage(packagePath); + return true; + } catch (ClassNotFoundException | LinkageError ignored) { + } + } + + return false; + } + + /** + * Get the ClassInfo for a simple class name, using provided imports map + */ + public ClassInfo resolveSimpleName(String simpleName, Map importedClasses) { + if (simpleName == null || simpleName.isEmpty()) { + return null; + } + + // Check imported classes first + String fullName = importedClasses.get(simpleName); + if (fullName != null) { + return tryResolveClass(fullName); + } + + // Check java.lang classes + if (JAVA_LANG_CLASSES.contains(simpleName)) { + return tryResolveClass("java.lang." + simpleName); + } + + return null; + } + + /** + * Try to resolve a fully-qualified class name (with $ for inner classes) + */ + private ClassInfo tryResolveClass(String className) { + if (className == null || className.isEmpty()) { + return null; + } + + // Check cache + if (classCache.containsKey(className)) { + return classCache.get(className); + } + + ClassInfo result = null; + + try { + Class clazz = Class.forName(className); + result = createClassInfo(className, clazz); + classCache.put(className, result); + } catch (ClassNotFoundException | NoClassDefFoundError e) { + // Not found with given name, will be cached as null + } catch (LinkageError e) { + // Linkage error - treat as not found + } + + return result; + } + + private ClassInfo createClassInfo(String resolvedName, Class clazz) { + ClassType type; + if (clazz.isInterface()) { + type = ClassType.INTERFACE; + } else if (clazz.isEnum()) { + type = ClassType.ENUM; + } else { + type = ClassType.CLASS; + } + + // Calculate package end index + int pkgEnd = resolvedName.lastIndexOf('.'); + if (pkgEnd < 0) + pkgEnd = 0; + + return new ClassInfo(resolvedName, type, pkgEnd); + } + + /** + * Build a class name candidate from segments, using $ for inner classes + */ + private String buildClassName(String[] segments, int classStartIdx, int classEndIdx) { + StringBuilder sb = new StringBuilder(); + + // Package portion (dot-separated) + for (int i = 0; i < classStartIdx; i++) { + if (i > 0) + sb.append('.'); + sb.append(segments[i]); + } + + // Class portion ($ for inner classes after the first class) + for (int i = classStartIdx; i < classEndIdx; i++) { + if (i == classStartIdx) { + if (sb.length() > 0) + sb.append('.'); + } else { + sb.append('$'); + } + sb.append(segments[i]); + } + + return sb.toString(); + } + + private String buildPackagePortion(String[] segments, int classStartIdx) { + if (classStartIdx <= 0) + return ""; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < classStartIdx; i++) { + if (i > 0) + sb.append('.'); + sb.append(segments[i]); + } + return sb.toString(); + } + + private String buildClassPortion(String[] segments, int classStartIdx, int endIdx) { + StringBuilder sb = new StringBuilder(); + for (int i = classStartIdx; i < endIdx; i++) { + if (i > classStartIdx) + sb.append('.'); + sb.append(segments[i]); + } + return sb.toString(); + } + + private List buildClassSegments(String[] segments, int classStartIdx, int endIdx) { + List result = new ArrayList<>(); + StringBuilder fullName = new StringBuilder(); + + // Build package portion + for (int i = 0; i < classStartIdx; i++) { + if (i > 0) + fullName.append('.'); + fullName.append(segments[i]); + } + + // Add each class segment + for (int i = classStartIdx; i < endIdx; i++) { + if (i == classStartIdx) { + if (fullName.length() > 0) + fullName.append('.'); + } else { + fullName.append('$'); + } + fullName.append(segments[i]); + + // Try to resolve this partial class name + ClassInfo info = tryResolveClass(fullName.toString()); + ClassType type = (info != null) ? info.type : ClassType.CLASS; + result.add(new ClassSegment(segments[i], type)); + } + + return result; + } + + private int findFirstUppercaseSegment(String[] segments) { + for (int i = 0; i < segments.length; i++) { + if (segments[i].length() > 0 && Character.isUpperCase(segments[i].charAt(0))) { + return i; + } + } + return segments.length - 1; // Default to last segment + } + + private void registerValidPackage(String packagePath) { + validPackages.add(packagePath); + + // Also register all parent packages + int lastDot; + String current = packagePath; + while ((lastDot = current.lastIndexOf('.')) > 0) { + current = current.substring(0, lastDot); + validPackages.add(current); + } + } + + private String[] getTestClassesForPackage(String packagePath) { + // Return common classes for well-known packages + switch (packagePath) { + case "java": + return new String[]{"java.lang.Object"}; + case "java.util": + return new String[]{"java.util.List", "java.util.Map", "java.util.Set"}; + case "java.io": + return new String[]{"java.io.File", "java.io.InputStream"}; + case "java.net": + return new String[]{"java.net.URL", "java.net.Socket"}; + case "java.lang": + return new String[]{"java.lang.Object", "java.lang.String"}; + default: + // For unknown packages, try common class suffixes + return new String[]{ + packagePath + ".package-info", // Won't work but harmless + }; + } + } + + // ==================== GENERIC TYPE PARSING ==================== + + /** + * Parse generic type content and return type names with their positions. + * Handles arbitrary nesting depth like "Map>>" + * + * @param genericContent The content inside <...> (without the outer angle brackets) + * @param importedClasses Map of simple name -> full class name for resolution + * @return List of TypeOccurrence with position and type info + */ + public List parseGenericTypes(String genericContent, Map importedClasses) { + List results = new ArrayList<>(); + parseGenericTypesRecursive(genericContent, 0, importedClasses, results); + return results; + } + + /** + * Represents a type occurrence within generic content + */ + public static class TypeOccurrence { + public final int startOffset; // Relative to the start of genericContent + public final int endOffset; + public final String typeName; + public final ClassType type; + + public TypeOccurrence(int startOffset, int endOffset, String typeName, ClassType type) { + this.startOffset = startOffset; + this.endOffset = endOffset; + this.typeName = typeName; + this.type = type; + } + } + + private void parseGenericTypesRecursive(String content, int baseOffset, + Map importedClasses, + List results) { + if (content == null || content.isEmpty()) + return; + + int i = 0; + while (i < content.length()) { + char c = content.charAt(i); + + // Skip whitespace and punctuation + if (!Character.isJavaIdentifierStart(c)) { + i++; + continue; + } + + // Found start of identifier + int start = i; + while (i < content.length() && Character.isJavaIdentifierPart(content.charAt(i))) { + i++; + } + String typeName = content.substring(start, i); + + // Only process if it looks like a type name (starts with uppercase) + if (Character.isUpperCase(typeName.charAt(0))) { + // Resolve the type + ClassInfo info = resolveSimpleName(typeName, importedClasses); + ClassType classType = (info != null) ? info.type : ClassType.CLASS; + + results.add(new TypeOccurrence(baseOffset + start, baseOffset + i, typeName, classType)); + } + + // Skip whitespace + while (i < content.length() && Character.isWhitespace(content.charAt(i))) { + i++; + } + + // Check for nested generic + if (i < content.length() && content.charAt(i) == '<') { + int nestedStart = i + 1; + int depth = 1; + i++; + + // Find matching > + while (i < content.length() && depth > 0) { + if (content.charAt(i) == '<') + depth++; + else if (content.charAt(i) == '>') + depth--; + i++; + } + + // Recursively parse nested content + if (nestedStart < i - 1) { + String nestedContent = content.substring(nestedStart, i - 1); + parseGenericTypesRecursive(nestedContent, baseOffset + nestedStart, importedClasses, results); + } + } + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 42077c5bc..95d446086 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -16,15 +16,8 @@ public class JavaTextContainer extends TextContainer { "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof|import)\\b"); public static final Pattern CLASS_DECL = Pattern.compile("\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)"); - - public static final Pattern TYPE_DECL = Pattern.compile( - "(?:(?:public|private|protected|static|final|transient|volatile)\\s+)*" + // optional modifiers - "([A-Za-z_][a-zA-Z0-9_]*)" + // Group 1 → main type - "\\s*(<([^<>]*(?:<[^<>]*>)*)>)?" + // Group 2 → <…>, Group 3 → inner type (handles one level of nesting) - "\\s+[a-zA-Z_][a-zA-Z0-9_]*" // variable name - ); - public static final Pattern NEW_TYPE = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)"); + public static final Pattern NEW_TYPE = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)");; public static final Pattern METHOD_DECL = Pattern.compile("\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(" // method name @@ -44,9 +37,12 @@ public class JavaTextContainer extends TextContainer { public static final Pattern NUMBER = Pattern.compile( "\\b-?(?:0[xX][\\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?(?:[fFbBdDlLsS])?|NaN|null|Infinity|true|false)\\b"); - // Import statement: import [static] package.path.ClassName [.*]; + // Import statement: import [static] package.path.ClassName [.*]; (semicolon optional for live typing) public static final Pattern IMPORT = Pattern.compile( - "\\bimport\\s+(?:static\\s+)?([a-zA-Z_][a-zA-Z0-9_.]*?)(?:\\s*\\.\\s*\\*)?\\s*;"); + "(?m)\\bimport\\s+(?:static\\s+)?([a-zA-Z_][a-zA-Z0-9_.]*?)(?:\\s*\\.\\s*\\*)?\\s*(?:;|$)"); + + // ClassPathFinder for resolving imports and determining class types + private final ClassPathFinder classPathFinder = new ClassPathFinder(); public List lines = new ArrayList<>(); public List methodBlocks = new ArrayList<>(); @@ -97,28 +93,11 @@ public class JavaTextContainer extends TextContainer { private List localFields = new ArrayList<>(); private java.util.Map importedClasses = new java.util.HashMap<>(); // simpleName -> fullName private java.util.Set importedPackages = new java.util.HashSet<>(); // wildcard imports - private java.util.Map classTypeCache = new java.util.HashMap<>(); // fullClassName -> TokenType (cached reflection results) - private java.util.Set resolvedPackages = new java.util.HashSet<>(); - private java.util.Set knownPackages = new java.util.HashSet<>(); - - // Common java.lang classes (auto-imported) - private static final java.util.Set JAVA_LANG_CLASSES = new java.util.HashSet<>(java.util.Arrays.asList( - "Object", "String", "Class", "System", "Math", "Integer", "Double", "Float", "Long", "Short", "Byte", - "Character", "Boolean", - "Number", "Void", "Thread", "Runnable", "Exception", "RuntimeException", "Error", "Throwable", - "StringBuilder", "StringBuffer", "Enum", "Comparable", "Iterable", "CharSequence", "Cloneable", - "Process", "ProcessBuilder", "Runtime", "SecurityManager", "ClassLoader", "Package", - "ArithmeticException", "ArrayIndexOutOfBoundsException", "ClassCastException", "IllegalArgumentException", - "IllegalStateException", "IndexOutOfBoundsException", "NullPointerException", "NumberFormatException", - "UnsupportedOperationException", "AssertionError", "OutOfMemoryError", "StackOverflowError" - )); private void collectImports() { importedClasses.clear(); importedPackages.clear(); - // Clear cached import resolutions when re-parsing imports (keeps editor responsive during typing) - importResolveCache.clear(); - knownPackages.clear(); + classPathFinder.clearCache(); List excluded = MethodBlock.getExcludedRanges(text); Matcher m = IMPORT.matcher(text); @@ -126,229 +105,53 @@ public class JavaTextContainer extends TextContainer { if (isInExcludedRange(m.start(), excluded)) continue; String fullPath = m.group(1).trim(); + String matchText = m.group(0); + // Check if wildcard import (import a.b.*;) - if (text.substring(m.start(), m.end()).contains("*")) { + if (matchText.contains("*")) { importedPackages.add(fullPath); } else { - // Specific class import: extract simple name + // Resolve the import to get the proper class name + ClassPathFinder.ResolveResult result = classPathFinder.resolve(fullPath); + + // Extract simple name (last segment) int lastDot = fullPath.lastIndexOf('.'); String simpleName = lastDot >= 0 ? fullPath.substring(lastDot + 1) : fullPath; - // Remember the package prefix as a known package (even before resolution) - if (lastDot > 0) { - String pkg = fullPath.substring(0, lastDot); - knownPackages.add(pkg); - } - // Store with $ notation for inner classes (e.g., IOverlay.ColorType -> IOverlay$ColorType) - String storagePath = fullPath.replaceAll("\\.(\\p{Upper})", "\\$$1"); - importedClasses.put(simpleName, storagePath); - } - } - } - - // Cache for resolved import attempts: original fullPath -> resolved class name (with $ for inner classes) or null if unresolved - private java.util.Map importResolveCache = new java.util.HashMap<>(); - - private static class ResolvedImport { - String resolvedClass; // full class name with $ notation if found - int classStartIndex; // index in segments where class starts - TokenType tokenType; - boolean found; - } - - /** - * Try to resolve an import path to an actual class using Class.forName by attempting - * different class-start splits and inner-class ($) combinations. Caches results. - */ - private ResolvedImport resolveImportPath(String fullPath) { - ResolvedImport ri = new ResolvedImport(); - if (fullPath == null || fullPath.isEmpty()) { - ri.found = false; return ri; - } - if (importResolveCache.containsKey(fullPath)) { - String cached = importResolveCache.get(fullPath); - if (cached == null) { ri.found = false; return ri; } - ri.resolvedClass = cached; - ri.found = true; - ri.tokenType = getClassTokenType(cached, TokenType.IMPORTED_CLASS); - // derive classStartIndex by comparing segments - String[] segs = fullPath.split("\\."); - String[] rcsegs = cached.replace('$', '.').split("\\."); - int si = 0; while (si < segs.length && si < rcsegs.length && segs[si].equals(rcsegs[si])) si++; - ri.classStartIndex = si; - return ri; - } - - String[] segments = fullPath.split("\\."); - int n = segments.length; - - // Try candidate class start positions. Start from left-most uppercase-looking segment for efficiency. - int firstUpper = 0; - for (int i = 0; i < n; i++) { - if (segments[i].length() > 0 && Character.isUpperCase(segments[i].charAt(0))) { firstUpper = i; break; } - } - - outer: - for (int k = firstUpper; k < n; k++) { - // number of inner parts available - int innerAvailable = n - (k + 1); - // Try the longest inner suffix first (deepest inner class) - for (int innerCount = innerAvailable; innerCount >= 0; innerCount--) { - // build top-level class and inner suffix - StringBuilder classBuilder = new StringBuilder(); - // package portion - if (k > 0) { - for (int i = 0; i < k; i++) { - if (i > 0) classBuilder.append('.'); - classBuilder.append(segments[i]); - } - classBuilder.append('.'); - } - // top class - classBuilder.append(segments[k]); - // inner parts - if (innerCount > 0) { - classBuilder.append('$'); - for (int j = 0; j < innerCount; j++) { - if (j > 0) classBuilder.append('$'); - classBuilder.append(segments[k + 1 + j]); - } - } - String candidate = classBuilder.toString(); - TokenType tt = getClassTokenType(candidate, TokenType.IMPORTED_CLASS); - if (tt != TokenType.UNDEFINED_VAR) { - ri.found = true; - ri.resolvedClass = candidate; - ri.classStartIndex = k; - ri.tokenType = tt; - importResolveCache.put(fullPath, candidate); - break outer; - } - // also try without package separator if k==0 (top-level without package) - if (k == 0 && innerCount == 0) { - // candidate is segments[0] only, already tried - } - } - } - - if (!ri.found) { - importResolveCache.put(fullPath, null); - ri.found = false; - // approximate classStartIndex as first uppercase or last segment - int classStart = 0; - for (int i = 0; i < segments.length; i++) { - if (segments[i].length() > 0 && Character.isUpperCase(segments[i].charAt(0))) { classStart = i; break; } - } - ri.classStartIndex = classStart; - } - return ri; - } - - /** - * Determine the token type for a class using reflection. - * Returns INTERFACE_DECL for interfaces, ENUM_DECL for enums, UNDEFINED for unfound classes, or the defaultType for classes. - */ - private TokenType getClassTokenType(String fullClassName, TokenType defaultType) { - if (fullClassName == null || fullClassName.isEmpty()) return defaultType; - - // Check cache first - // if (classTypeCache.containsKey(fullClassName)) { - // return classTypeCache.get(fullClassName); - //} - TokenType result = defaultType; - try { - Class clazz = Class.forName(fullClassName); - if (clazz.isInterface()) { - result = TokenType.INTERFACE_DECL; - } else if (clazz.isEnum()) { - result = TokenType.ENUM_DECL; - } else { - result = defaultType; // regular class - } - } catch (ClassNotFoundException | NoClassDefFoundError e) { - // Try with $ notation for inner classes (e.g., IOverlay$ColorType) - String innerClassNotation = fullClassName.replaceAll("\\.(\\p{Upper})", "\\$$1"); - if (!innerClassNotation.equals(fullClassName)) { - try { - Class clazz = Class.forName(innerClassNotation); - if (clazz.isInterface()) { - result = TokenType.INTERFACE_DECL; - } else if (clazz.isEnum()) { - result = TokenType.ENUM_DECL; + if (!simpleName.isEmpty()) { + if (result.found && result.classInfo != null) { + // Store with resolved name (has $ for inner classes) + importedClasses.put(simpleName, result.classInfo.resolvedName); } else { - result = defaultType; // regular class + // Store original path for unresolved imports + importedClasses.put(simpleName, fullPath); } - // Cache with original notation - classTypeCache.put(fullClassName, result); - return result; - } catch (Exception e2) { - // Still not found } } - // Class not found - mark as undefined - result = TokenType.UNDEFINED_VAR; - } catch (LinkageError e) { - // Linkage errors (other than NoClassDefFoundError) - treat as undefined - result = TokenType.UNDEFINED_VAR; } - - // Cache the result - classTypeCache.put(fullClassName, result); - return result; } /** - * Parse and highlight generic type parameters recursively (e.g., "List" or "Map>>") - * Handles arbitrarily nested generics by tracking bracket depth. + * Parse and highlight generic type parameters using ClassPathFinder. + * Handles arbitrarily nested generics like "Map>>". */ private void highlightGenericTypes(String genericContent, int contentStart, List marks, List excluded) { if (genericContent == null || genericContent.isEmpty()) return; - int i = 0; - while (i < genericContent.length()) { - char c = genericContent.charAt(i); + List occurrences = classPathFinder.parseGenericTypes(genericContent, importedClasses); + + for (ClassPathFinder.TypeOccurrence occ : occurrences) { + int absStart = contentStart + occ.startOffset; + int absEnd = contentStart + occ.endOffset; - // Check if we're starting a type name (uppercase letter) - if (Character.isJavaIdentifierStart(c) && Character.isUpperCase(c)) { - int start = i; - while (i < genericContent.length() && Character.isJavaIdentifierPart(genericContent.charAt(i))) { - i++; + if (!isInExcludedRange(absStart, excluded)) { + TokenType tokenType; + switch (occ.type) { + case INTERFACE: tokenType = TokenType.INTERFACE_DECL; break; + case ENUM: tokenType = TokenType.ENUM_DECL; break; + default: tokenType = TokenType.IMPORTED_CLASS; break; } - String typeName = genericContent.substring(start, i); - int absStart = contentStart + start; - int absEnd = contentStart + i; - - if (!isInExcludedRange(absStart, excluded)) { - // Resolve full class name - String fullClassName = importedClasses.get(typeName); - if (fullClassName == null && JAVA_LANG_CLASSES.contains(typeName)) { - fullClassName = "java.lang." + typeName; - } - - TokenType tokenType = getClassTokenType(fullClassName, TokenType.IMPORTED_CLASS); - marks.add(new Mark(absStart, absEnd, tokenType)); - } - - // Check for nested generics after this type - while (i < genericContent.length() && Character.isWhitespace(genericContent.charAt(i))) i++; - if (i < genericContent.length() && genericContent.charAt(i) == '<') { - // Find matching > - int depth = 1; - int nestedStart = i + 1; - i++; - while (i < genericContent.length() && depth > 0) { - if (genericContent.charAt(i) == '<') depth++; - else if (genericContent.charAt(i) == '>') depth--; - i++; - } - // Recursively process nested content - String nestedContent = genericContent.substring(nestedStart, i - 1); - highlightGenericTypes(nestedContent, contentStart + nestedStart, marks, excluded); - // Continue after the closing > to find more types - } - // Continue to next character (handles commas, spaces, etc.) - } else { - i++; + marks.add(new Mark(absStart, absEnd, tokenType)); } } } @@ -604,7 +407,6 @@ public class JavaTextContainer extends TextContainer { private void collectImportStatements(List marks) { List excluded = MethodBlock.getExcludedRanges(text); - // Pattern to match: import [static] package.path.ClassName [.*]; Matcher m = IMPORT.matcher(text); while (m.find()) { if (isInExcludedRange(m.start(), excluded)) @@ -612,108 +414,95 @@ public class JavaTextContainer extends TextContainer { String fullPath = m.group(1).trim(); int pathStart = m.start(1); int pathEnd = m.end(1); + String matchText = m.group(0); // Highlight the 'import' keyword itself int importKwStart = m.start(); - int importKwEnd = Math.min(importKwStart + 6, text.length()); // "import" + int importKwEnd = Math.min(importKwStart + 6, text.length()); marks.add(new Mark(importKwStart, importKwEnd, TokenType.IMPORT_KEYWORD)); - // Resolve import path to an actual class if possible - ResolvedImport ri = resolveImportPath(fullPath); - String[] segments = fullPath.split("\\."); + // Use ClassPathFinder to resolve the import + ClassPathFinder.ResolveResult result = classPathFinder.resolve(fullPath); - // Compute package portion length - int pkgSegments = 0; - if (ri.found && ri.resolvedClass != null) { - pkgSegments = Math.max(0, ri.classStartIndex); - // register resolved package prefix - if (pkgSegments > 0) { - StringBuilder pb = new StringBuilder(); - for (int i = 0; i < pkgSegments; i++) { - if (i > 0) pb.append('.'); - pb.append(segments[i]); - } - resolvedPackages.add(pb.toString()); + String packagePortion = result.packagePortion; + String classPortion = result.classPortion; + boolean hasPackage = packagePortion != null && !packagePortion.isEmpty(); + boolean hasClass = classPortion != null && !classPortion.isEmpty(); + + if (result.found && result.invalidStartOffset < 0) { + // Fully resolved - highlight package (blue) and class segments (typed) + if (hasPackage) { + int pkgEnd = pathStart + packagePortion.length(); + if (hasClass) pkgEnd++; // include dot + marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); } - } else { - // Try to find the longest previously-resolved package prefix, or known package prefix - for (int p = segments.length - 1; p >= 1; p--) { - StringBuilder test = new StringBuilder(); - for (int i = 0; i < p; i++) { - if (i > 0) test.append('.'); - test.append(segments[i]); - } - String testStr = test.toString(); - if (resolvedPackages.contains(testStr)) { - pkgSegments = p; - break; - } - if (knownPackages.contains(testStr)) { - pkgSegments = p; - // don't break; prefer a resolved package if found later in loop - } - } - // fallback to ri.classStartIndex if nothing known - if (pkgSegments == 0) pkgSegments = Math.max(0, ri.classStartIndex); - } - StringBuilder pkgBuilder = new StringBuilder(); - for (int i = 0; i < pkgSegments; i++) { - if (i > 0) pkgBuilder.append('.'); - pkgBuilder.append(segments[i]); - } - int pkgLen = pkgBuilder.length(); - if (pkgLen > 0) { - marks.add(new Mark(pathStart, pathStart + pkgLen + 1, TokenType.TYPE_DECL)); // package + dot - } - - // Highlight class and inner-class segments - int classStart = pathStart + (pkgLen > 0 ? pkgLen + 1 : 0); - if (ri.found && ri.resolvedClass != null) { - // Use resolved class (with $). Break into segments for progressive checks. - for (int i = ri.classStartIndex; i < segments.length; i++) { - int segStart = classStart; - int segEnd = classStart + segments[i].length(); - - // Build candidate full name up to this segment using $ for inner separators - StringBuilder testBuilder = new StringBuilder(); - if (ri.classStartIndex > 0) { - for (int p = 0; p < ri.classStartIndex; p++) { - if (p > 0) testBuilder.append('.'); - testBuilder.append(segments[p]); + + if (hasClass && !result.classSegments.isEmpty()) { + int classStart = pathStart + (hasPackage ? packagePortion.length() + 1 : 0); + for (ClassPathFinder.ClassSegment seg : result.classSegments) { + int segEnd = classStart + seg.name.length(); + TokenType segType; + switch (seg.type) { + case INTERFACE: segType = TokenType.INTERFACE_DECL; break; + case ENUM: segType = TokenType.ENUM_DECL; break; + default: segType = TokenType.IMPORTED_CLASS; break; } - testBuilder.append('.'); + marks.add(new Mark(classStart, segEnd, segType)); + classStart = segEnd + 1; } - testBuilder.append(segments[ri.classStartIndex]); - for (int q = ri.classStartIndex + 1; q <= i; q++) { - testBuilder.append('$').append(segments[q]); + } + } else if (result.classSegments != null && !result.classSegments.isEmpty()) { + // Partial resolution - some class segments are valid, rest is invalid + // (e.g., IOverlay.ColorType resolved but ".idk" is invalid) + if (hasPackage) { + int pkgEnd = pathStart + packagePortion.length() + 1; // include dot + marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); + } + + int classStart = pathStart + (hasPackage ? packagePortion.length() + 1 : 0); + for (ClassPathFinder.ClassSegment seg : result.classSegments) { + int segEnd = classStart + seg.name.length(); + TokenType segType; + switch (seg.type) { + case INTERFACE: segType = TokenType.INTERFACE_DECL; break; + case ENUM: segType = TokenType.ENUM_DECL; break; + default: segType = TokenType.IMPORTED_CLASS; break; } - String testFull = testBuilder.toString(); - TokenType segType = getClassTokenType(testFull, TokenType.IMPORTED_CLASS); - marks.add(new Mark(segStart, segEnd, segType)); - - // Update importedClasses map for simple name resolution - if (i == segments.length - 1) { - importedClasses.put(segments[i], ri.resolvedClass); + marks.add(new Mark(classStart, segEnd, segType)); + classStart = segEnd + 1; + } + + // Mark remaining invalid portion in red + if (result.invalidStartOffset >= 0) { + int invalidStart = pathStart + result.invalidStartOffset; + if (invalidStart < pathEnd) { + marks.add(new Mark(invalidStart, pathEnd, TokenType.UNDEFINED_VAR)); } - - classStart = segEnd + 1; // +1 for dot } } else { - // Unresolved import: mark the entire tail (after known package prefix) as undefined - int undefStart = classStart; - int undefEnd = pathEnd; - if (undefStart < undefEnd) { - marks.add(new Mark(undefStart, undefEnd, TokenType.UNDEFINED_VAR)); - } - // Remove any previously-added simple-name mapping to avoid treating it as imported - String simpleName = segments[segments.length - 1]; - if (importedClasses.containsKey(simpleName)) { - importedClasses.remove(simpleName); + // Not resolved at all - use invalidStartOffset to determine valid/invalid split + if (result.invalidStartOffset >= 0 && hasPackage) { + // Valid package prefix, invalid class + int pkgEnd = pathStart + packagePortion.length(); + marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); + + int invalidStart = pathStart + result.invalidStartOffset; + if (invalidStart < pathEnd) { + marks.add(new Mark(invalidStart, pathEnd, TokenType.UNDEFINED_VAR)); + } + } else if (fullPath.endsWith(".") && hasPackage) { + // Trailing dot on package - all blue + marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); + } else if (result.invalidStartOffset < 0 && hasPackage && !hasClass) { + // All-lowercase path treated as valid package + marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); + } else { + // Entire path is invalid + marks.add(new Mark(pathStart, pathEnd, TokenType.UNDEFINED_VAR)); } } - // Highlight .* if present (find '*' between match bounds) - String matchText = text.substring(m.start(), m.end()); + // Highlight .* if present int starIndex = matchText.indexOf('*'); if (starIndex != -1) { int absStar = m.start() + starIndex; @@ -735,13 +524,9 @@ public class JavaTextContainer extends TextContainer { continue; // Check if this is an imported class, wildcard match, or java.lang class - boolean isImported = importedClasses.containsKey(className) || JAVA_LANG_CLASSES.contains(className); - if (!isImported) { - // Check wildcard imports (heuristic: if any imported package exists, mark it) - for (String pkg : importedPackages) { - isImported = true; - break; - } + boolean isImported = importedClasses.containsKey(className) || ClassPathFinder.JAVA_LANG_CLASSES.contains(className); + if (!isImported && !importedPackages.isEmpty()) { + isImported = true; // Wildcard import present } // Also skip if it's a local/global field or parameter (avoid false positives) @@ -753,12 +538,15 @@ public class JavaTextContainer extends TextContainer { } if (isImported && !isShadowed) { - // Determine token type dynamically using reflection - String fullClassName = importedClasses.get(className); - if (fullClassName == null && JAVA_LANG_CLASSES.contains(className)) { - fullClassName = "java.lang." + className; + // Resolve the class to determine its type + ClassPathFinder.ClassInfo info = classPathFinder.resolveSimpleName(className, importedClasses); + TokenType tokenType = TokenType.IMPORTED_CLASS; + if (info != null) { + switch (info.type) { + case INTERFACE: tokenType = TokenType.INTERFACE_DECL; break; + case ENUM: tokenType = TokenType.ENUM_DECL; break; + } } - TokenType tokenType = getClassTokenType(fullClassName, TokenType.IMPORTED_CLASS); marks.add(new Mark(start, end, tokenType)); } } @@ -869,46 +657,118 @@ public class JavaTextContainer extends TextContainer { } private void collectTypeDeclarations(List marks) { - // Run TYPE_DECL on each line to avoid matches bleeding across newlines List excluded = MethodBlock.getExcludedRanges(text); + + // Pattern to find type declarations: Type<...> variableName or Type variableName + // We'll manually parse the generic portion to handle arbitrary nesting + Pattern typeStart = Pattern.compile( + "(?:(?:public|private|protected|static|final|transient|volatile)\\s+)*" + + "([A-Z][a-zA-Z0-9_]*)\\s*" // Group 1 → type name (must start with uppercase) + ); + for (LineData ld : lines) { int lineStart = ld.start; int lineEnd = ld.end; if (lineStart >= lineEnd) continue; String s = ld.text; - Matcher m = TYPE_DECL.matcher(s); - while (m.find()) { - int g1s = lineStart + m.start(1); - int g1e = lineStart + m.end(1); - // Skip if any part of the match overlaps an excluded range (string/comment) + + Matcher m = typeStart.matcher(s); + int searchFrom = 0; + + while (m.find(searchFrom)) { + int typeNameStart = lineStart + m.start(1); + int typeNameEnd = lineStart + m.end(1); + + // Skip if in excluded range boolean skip = false; for (int[] r : excluded) { - if (g1s < r[1] && g1e > r[0]) { skip = true; break; } + if (typeNameStart < r[1] && typeNameEnd > r[0]) { skip = true; break; } } - if (skip) continue; - // Check if this type is a known interface or enum using reflection + if (skip) { + searchFrom = m.end(); + continue; + } + String typeName = m.group(1); - String fullClassName = importedClasses.get(typeName); - if (fullClassName == null && JAVA_LANG_CLASSES.contains(typeName)) { - fullClassName = "java.lang." + typeName; + int posAfterType = m.end(1); + + // Skip whitespace after type name + while (posAfterType < s.length() && Character.isWhitespace(s.charAt(posAfterType))) { + posAfterType++; } - TokenType tokenType = getClassTokenType(fullClassName, TokenType.TYPE_DECL); - marks.add(new Mark(g1s, g1e, tokenType)); - - if (m.group(2) != null) { - int start = lineStart + m.start(2); - int end = lineStart + m.end(2); - // Color < and > as default - marks.add(new Mark(start, start + 1, TokenType.DEFAULT)); // < - marks.add(new Mark(end - 1, end, TokenType.DEFAULT)); // > - - // Parse and highlight all types in generic content (handles nested generics) - if (m.group(3) != null) { - String genericContent = m.group(3); - int contentStart = lineStart + m.start(3); + + // Check for generic parameters + String genericContent = null; + int genericStart = -1; + int genericEnd = -1; + + if (posAfterType < s.length() && s.charAt(posAfterType) == '<') { + genericStart = posAfterType; + int depth = 1; + int i = posAfterType + 1; + while (i < s.length() && depth > 0) { + char c = s.charAt(i); + if (c == '<') depth++; + else if (c == '>') depth--; + i++; + } + if (depth == 0) { + genericEnd = i; + genericContent = s.substring(genericStart + 1, genericEnd - 1); + posAfterType = genericEnd; + } + } + + // Skip whitespace after generics + while (posAfterType < s.length() && Character.isWhitespace(s.charAt(posAfterType))) { + posAfterType++; + } + + // Check if this looks like a type declaration: + // 1. Has generic content (e.g., List) - always a type + // 2. Followed by a variable name (lowercase start or underscore) + // 3. At end of line with generic content (line might be wrapped) + boolean hasGeneric = genericContent != null && !genericContent.isEmpty(); + boolean atEndOfLine = posAfterType >= s.length(); + boolean followedByVarName = false; + + if (!atEndOfLine) { + char nextChar = s.charAt(posAfterType); + followedByVarName = Character.isLetter(nextChar) || nextChar == '_'; + } + + // Accept as type if: has generics OR followed by variable name + // This handles both `List myList` and wrapped lines like `List` + if (hasGeneric || followedByVarName) { + // This looks like a type declaration + + // Resolve the main type + ClassPathFinder.ClassInfo info = classPathFinder.resolveSimpleName(typeName, importedClasses); + TokenType tokenType = TokenType.TYPE_DECL; + if (info != null) { + switch (info.type) { + case INTERFACE: tokenType = TokenType.INTERFACE_DECL; break; + case ENUM: tokenType = TokenType.ENUM_DECL; break; + } + } + marks.add(new Mark(typeNameStart, typeNameEnd, tokenType)); + + // Handle generic content + if (hasGeneric && genericStart >= 0) { + int absGenericStart = lineStart + genericStart; + int absGenericEnd = lineStart + genericEnd; + + // Mark < and > as default + marks.add(new Mark(absGenericStart, absGenericStart + 1, TokenType.DEFAULT)); + marks.add(new Mark(absGenericEnd - 1, absGenericEnd, TokenType.DEFAULT)); + + // Parse and highlight all types in generic content + int contentStart = lineStart + genericStart + 1; highlightGenericTypes(genericContent, contentStart, marks, excluded); } } + + searchFrom = m.end(); } } } From ee33c0f69a17cb71ec3fa36d39e29b0cf262556f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 23:33:56 +0200 Subject: [PATCH 167/358] More fixes? --- .../gui/util/script/ClassPathFinder.java | 46 +++++++++++++++---- .../gui/util/script/JavaTextContainer.java | 45 ++++++++++++++++-- 2 files changed, 79 insertions(+), 12 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java index 18f4252a5..eafa2aca9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java @@ -224,27 +224,55 @@ public class ClassPathFinder { private int findPackageTypoIndex(String[] segments, int classStartIdx) { // Build the typed package path segment by segment and compare with cache StringBuilder sb = new StringBuilder(); + int lastValidIndex = -1; + for (int i = 0; i < classStartIdx; i++) { if (i > 0) sb.append("."); sb.append(segments[i]); String currentPath = sb.toString(); // Check if this exact path is in cache - if (!validPackages.contains(currentPath)) { - // Not in cache - check if a DIFFERENT path at this level IS in cache - // This would indicate a typo (e.g., "guui" vs "gui") - if (i > 0) { - String parentPath = buildPackagePortion(segments, i); - // Look for any cached package that starts with parent but differs at segment i + if (validPackages.contains(currentPath)) { + lastValidIndex = i + 1; // This segment is valid + } else if (i > 0) { + // Not in cache - check if parent is valid but this segment is wrong + String parentPath = buildPackagePortion(segments, i); + if (validPackages.contains(parentPath)) { + // Parent is valid, but current segment isn't in any cached path + // Check if there ARE cached paths extending from parent + boolean hasAlternative = false; for (String cached : validPackages) { - if (cached.startsWith(parentPath + ".") && !cached.startsWith(currentPath)) { - // Found a cached package with same parent but different segment - return i; // This segment is likely a typo + if (cached.startsWith(parentPath + ".") && cached.length() > parentPath.length() + 1) { + hasAlternative = true; + break; } } + if (hasAlternative) { + // There are valid continuations from parent, but this segment doesn't match any + return i; // This segment is likely a typo + } } } } + + // If we found some valid prefix but not all, return where it stopped being valid + if (lastValidIndex > 0 && lastValidIndex < classStartIdx) { + // Check if segment at lastValidIndex diverges from any known path + String validPrefix = buildPackagePortion(segments, lastValidIndex); + for (String cached : validPackages) { + if (cached.startsWith(validPrefix + ".")) { + // There's a known continuation - check if typed segment matches + String nextTyped = segments[lastValidIndex]; + String cachedRemainder = cached.substring(validPrefix.length() + 1); + String nextCached = cachedRemainder.contains(".") ? + cachedRemainder.substring(0, cachedRemainder.indexOf('.')) : cachedRemainder; + if (!nextTyped.equals(nextCached)) { + return lastValidIndex; // Divergence point + } + } + } + } + return -1; // No typo detected } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 95d446086..7d7a93a77 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -93,10 +93,12 @@ public class JavaTextContainer extends TextContainer { private List localFields = new ArrayList<>(); private java.util.Map importedClasses = new java.util.HashMap<>(); // simpleName -> fullName private java.util.Set importedPackages = new java.util.HashSet<>(); // wildcard imports + private java.util.Set unresolvedClasses = new java.util.HashSet<>(); // classes that failed to resolve private void collectImports() { importedClasses.clear(); importedPackages.clear(); + unresolvedClasses.clear(); classPathFinder.clearCache(); List excluded = MethodBlock.getExcludedRanges(text); @@ -125,6 +127,8 @@ public class JavaTextContainer extends TextContainer { } else { // Store original path for unresolved imports importedClasses.put(simpleName, fullPath); + // Track this as an unresolved class + unresolvedClasses.add(simpleName); } } } @@ -277,6 +281,10 @@ public class JavaTextContainer extends TextContainer { // Skip field access (identifier preceded by a dot) - these should be gray/default // e.g., in "container.lines", the "lines" part should not be marked if (isFieldAccess(position)) continue; + + // Never treat parts of import/package statements as undefined variables + // (otherwise the first package segment like "noppes" gets marked red and overrides import highlighting) + if (isInImportOrPackageStatement(position)) continue; // Check if inside a method MethodBlock methodBlock = findMethodBlockAtPosition(position); @@ -301,11 +309,37 @@ public class JavaTextContainer extends TextContainer { // Outside any method - check global fields if (globalFields.contains(name)) { marks.add(new Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); + } else if (!isMethodCall(position) && !isTypeReference(name, position)) { + // Mark as undefined if it's not a method call or type reference + marks.add(new Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR)); } - // Don't mark as undefined outside methods - could be a type name or declaration } } } + + private boolean isInImportOrPackageStatement(int position) { + if (position < 0 || position >= text.length()) return false; + + int lineStart = text.lastIndexOf('\n', position); + lineStart = (lineStart < 0) ? 0 : lineStart + 1; + int lineEnd = text.indexOf('\n', position); + lineEnd = (lineEnd < 0) ? text.length() : lineEnd; + + // Quick scan of the line start for "import" or "package" + int i = lineStart; + while (i < lineEnd && Character.isWhitespace(text.charAt(i))) i++; + + if (i + 6 <= lineEnd && text.startsWith("import", i)) { + // Ensure word boundary + int after = i + 6; + return after == lineEnd || Character.isWhitespace(text.charAt(after)); + } + if (i + 7 <= lineEnd && text.startsWith("package", i)) { + int after = i + 7; + return after == lineEnd || Character.isWhitespace(text.charAt(after)); + } + return false; + } /** * Check if the identifier at this position is a field access (preceded by a dot) @@ -433,7 +467,6 @@ public class JavaTextContainer extends TextContainer { // Fully resolved - highlight package (blue) and class segments (typed) if (hasPackage) { int pkgEnd = pathStart + packagePortion.length(); - if (hasClass) pkgEnd++; // include dot marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); } @@ -455,7 +488,7 @@ public class JavaTextContainer extends TextContainer { // Partial resolution - some class segments are valid, rest is invalid // (e.g., IOverlay.ColorType resolved but ".idk" is invalid) if (hasPackage) { - int pkgEnd = pathStart + packagePortion.length() + 1; // include dot + int pkgEnd = pathStart + packagePortion.length(); marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); } @@ -523,6 +556,12 @@ public class JavaTextContainer extends TextContainer { if (isInExcludedRange(start, excluded)) continue; + // Check if this class was imported but failed to resolve + if (unresolvedClasses.contains(className)) { + marks.add(new Mark(start, end, TokenType.UNDEFINED_VAR)); + continue; + } + // Check if this is an imported class, wildcard match, or java.lang class boolean isImported = importedClasses.containsKey(className) || ClassPathFinder.JAVA_LANG_CLASSES.contains(className); if (!isImported && !importedPackages.isEmpty()) { From 05ef31e29ecc0b228df88833c7cf724f1cc2e86f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 21 Dec 2025 23:52:35 +0200 Subject: [PATCH 168/358] IMPORTS ARE FINALLY FULLY FUNCTIONAL --- .../gui/util/script/ClassPathFinder.java | 46 ++++++++++++++----- .../gui/util/script/JavaTextContainer.java | 4 +- 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java index eafa2aca9..326df3b79 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java @@ -130,19 +130,29 @@ public class ClassPathFinder { // Find the first uppercase segment (likely start of class name) int firstUpperIndex = findFirstUppercaseSegment(segments); - // If trailing dot, check if the entire path is a valid package (either cached or all-lowercase) - if (trailingDot) { - // If entire path is lowercase, treat as valid package being typed - if (firstUpperIndex == n) { - // Check if we have it cached OR if it could be a valid package - if (isValidPackage(pathToResolve)) { - return ResolveResult.notFound(pathToResolve, "", -1); - } - // Not cached, but all lowercase - use heuristic: assume valid package + // No uppercase segments at all: this is a package-like path. + // Validate as far as we can and mark invalid tail red. + if (firstUpperIndex == n) { + int longestValid = findLongestValidPackagePrefixIndex(segments); + if (longestValid == n) { + // Entire path is a valid package (or cached as valid) return ResolveResult.notFound(pathToResolve, "", -1); } + + if (longestValid > 0) { + String validPrefix = buildPackagePortion(segments, longestValid); + String invalidRemainder = buildClassPortion(segments, longestValid, n); + int invalidOffset = validPrefix.length() + 1; // after the dot + return ResolveResult.notFound(validPrefix, invalidRemainder, invalidOffset); + } + + // Nothing validated + return ResolveResult.notFound("", pathToResolve, 0); } + // Trailing dot is handled above for lowercase-only paths; for mixed-case paths + // we continue resolving as usual. + // Try different splits: package vs class boundary // Start from firstUpperIndex and try treating each uppercase segment as potential class start for (int classStartIdx = firstUpperIndex; classStartIdx < n; classStartIdx++) { @@ -288,7 +298,7 @@ public class ClassPathFinder { if (validPackages.contains(packagePath)) { return true; } - + // Try to find any class under this package to validate it // Common test classes for well-known packages String[] testClasses = getTestClassesForPackage(packagePath); @@ -457,7 +467,21 @@ public class ClassPathFinder { return i; } } - return segments.length - 1; // Default to last segment + return segments.length; // No uppercase segment + } + + private int findLongestValidPackagePrefixIndex(String[] segments) { + int longestValid = 0; + for (int i = 1; i <= segments.length; i++) { + String pkg = buildPackagePortion(segments, i); + if (isValidPackage(pkg)) { + longestValid = i; + } else { + // If a prefix isn't valid, deeper prefixes can't be valid packages either. + break; + } + } + return longestValid; } private void registerValidPackage(String packagePath) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 7d7a93a77..30f158a13 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -523,8 +523,8 @@ public class JavaTextContainer extends TextContainer { if (invalidStart < pathEnd) { marks.add(new Mark(invalidStart, pathEnd, TokenType.UNDEFINED_VAR)); } - } else if (fullPath.endsWith(".") && hasPackage) { - // Trailing dot on package - all blue + } else if (fullPath.endsWith(".") && hasPackage && result.invalidStartOffset < 0 && !hasClass) { + // Trailing dot on a fully-valid package - all blue marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); } else if (result.invalidStartOffset < 0 && hasPackage && !hasClass) { // All-lowercase path treated as valid package From bef83f9ddb7339d8249bbec91aff93916c129f3a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 00:20:21 +0200 Subject: [PATCH 169/358] String Capital <- Capital now treated as local field (previously uppercase were left as default, only lowercase) --- .../gui/util/script/JavaTextContainer.java | 85 +++++++++++++------ 1 file changed, 60 insertions(+), 25 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 30f158a13..039879043 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -272,45 +272,54 @@ public class JavaTextContainer extends TextContainer { String name = m.group(1); int position = m.start(1); - // Skip known keywords and types + // Skip known keywords if (knownIdentifiers.contains(name)) continue; - - // Skip type names (first letter uppercase) - if (Character.isUpperCase(name.charAt(0))) continue; - + // Skip field access (identifier preceded by a dot) - these should be gray/default - // e.g., in "container.lines", the "lines" part should not be marked if (isFieldAccess(position)) continue; // Never treat parts of import/package statements as undefined variables - // (otherwise the first package segment like "noppes" gets marked red and overrides import highlighting) if (isInImportOrPackageStatement(position)) continue; - + // Check if inside a method MethodBlock methodBlock = findMethodBlockAtPosition(position); - + if (methodBlock != null) { - // Inside a method - check in order: parameter, local (position-aware), global + // Inside a method - first check parameters, locals, and globals (respecting registrations) if (methodBlock.parameters.contains(name)) { marks.add(new Mark(m.start(1), m.end(1), TokenType.PARAMETER)); - } else if (methodBlock.isLocalDeclaredAtPosition(name, position)) { - // Local variable is declared at or before this position - marks.add(new Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD)); - } else if (globalFields.contains(name)) { - marks.add(new Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); - } else { - // Unknown variable - mark as undefined (Bug 11) - // But only if it looks like a variable reference (not a method call) - if (!isMethodCall(position) && !isTypeReference(name, position)) { - marks.add(new Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR)); - } + continue; } - } else { - // Outside any method - check global fields + + if (methodBlock.isLocalDeclaredAtPosition(name, position) || localFields.contains(name)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.LOCAL_FIELD)); + continue; + } + if (globalFields.contains(name)) { marks.add(new Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); - } else if (!isMethodCall(position) && !isTypeReference(name, position)) { - // Mark as undefined if it's not a method call or type reference + continue; + } + + // If it's an uppercase identifier that hasn't been registered as a local/param/global, + // treat it as a type reference (skip) rather than an undefined variable. + if (Character.isUpperCase(name.charAt(0))) continue; + + // Unknown variable - mark as undefined but only if it's not a method call or type reference + if (!isMethodCall(position) && !isTypeReference(name, position)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR)); + } + } else { + // Outside any method - check global fields first + if (globalFields.contains(name) || localFields.contains(name)) { + marks.add(new Mark(m.start(1), m.end(1), TokenType.GLOBAL_FIELD)); + continue; + } + + // Uppercase unregistered identifiers are likely type names; skip them + if (Character.isUpperCase(name.charAt(0))) continue; + + if (!isMethodCall(position) && !isTypeReference(name, position)) { marks.add(new Mark(m.start(1), m.end(1), TokenType.UNDEFINED_VAR)); } } @@ -805,6 +814,32 @@ public class JavaTextContainer extends TextContainer { int contentStart = lineStart + genericStart + 1; highlightGenericTypes(genericContent, contentStart, marks, excluded); } + + // If followed by a variable name, detect it and register uppercase-starting + // variable names as locals so subsequent identifier scanning treats them + // as variables (not types). This handles declarations like: + // MyType Capital = new MyType(); + if (followedByVarName) { + int v = posAfterType; + // skip whitespace + while (v < s.length() && Character.isWhitespace(s.charAt(v))) v++; + int varStart = v; + while (v < s.length() && (Character.isLetterOrDigit(s.charAt(v)) || s.charAt(v) == '_')) v++; + int varEnd = v; + if (varEnd > varStart) { + String varName = s.substring(varStart, varEnd); + int absVarStart = lineStart + varStart; + int absVarEnd = lineStart + varEnd; + if (Character.isUpperCase(varName.charAt(0))) { + MethodBlock mb = findMethodBlockAtPosition(absVarStart); + if (mb != null) { + if (!mb.localVariables.contains(varName)) mb.localVariables.add(varName); + if (!localFields.contains(varName)) localFields.add(varName); + marks.add(new Mark(absVarStart, absVarEnd, TokenType.LOCAL_FIELD)); + } + } + } + } } searchFrom = m.end(); From 292c695eb680fa2bfc5907c0b0b16bf4469b2882 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 00:29:24 +0200 Subject: [PATCH 170/358] Changed default TYPE_DECL to UNDEFINED --- .../npcs/client/gui/util/script/JavaTextContainer.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 039879043..b879db8a0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -373,7 +373,8 @@ public class JavaTextContainer extends TextContainer { private boolean isMethodCall(int position) { // Skip whitespace after identifier int i = position; - while (i < text.length() && Character.isLetterOrDigit(text.charAt(i)) || text.charAt(i) == '_') { + // Note: parentheses needed to avoid evaluating text.charAt(i) when i >= text.length() + while (i < text.length() && (Character.isLetterOrDigit(text.charAt(i)) || text.charAt(i) == '_')) { i++; } // Skip whitespace @@ -792,11 +793,12 @@ public class JavaTextContainer extends TextContainer { // Resolve the main type ClassPathFinder.ClassInfo info = classPathFinder.resolveSimpleName(typeName, importedClasses); - TokenType tokenType = TokenType.TYPE_DECL; + TokenType tokenType = TokenType.UNDEFINED_VAR; if (info != null) { switch (info.type) { case INTERFACE: tokenType = TokenType.INTERFACE_DECL; break; case ENUM: tokenType = TokenType.ENUM_DECL; break; + default: tokenType = TokenType.TYPE_DECL; } } marks.add(new Mark(typeNameStart, typeNameEnd, tokenType)); From 51aca955906caf0b49143eeabc0a8ee259f82fb3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 01:10:24 +0200 Subject: [PATCH 171/358] Fixed indent highlight being on the wrong braces block when braces are highlighted --- .../client/gui/util/GuiScriptTextArea.java | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 04854b7c7..870f50710 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -494,8 +494,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { int bracketLineIdx = -1; // Only consider curly braces for highlighting the indent guides boolean isCurlyBracket = false; + char bc = text.charAt(startBracket); if (text != null && startBracket >= 0 && startBracket < text.length()) { - char bc = text.charAt(startBracket); if (bc == '{' || bc == '}') isCurlyBracket = true; } for (int li = 0; li < list.size(); li++) { @@ -506,13 +506,35 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } if (bracketLineIdx >= 0 && isCurlyBracket) { + // Prefer a span that directly matches the bracket character's line: + // - if the bracket is an opening '{', prefer spans where openLine == bracketLineIdx + // - if the bracket is a closing '}', prefer spans where closeLine == bracketLineIdx + // If no exact match is found, fall back to the smallest enclosing span (innermost). + int bestSize = Integer.MAX_VALUE; + boolean foundExact = false; + char bracketChar = bc; // from earlier for (int[] span : braceSpans) { int openLine = span[1]; int closeLine = span[2]; if (bracketLineIdx >= openLine && bracketLineIdx <= closeLine) { - highlightedOpenLine = openLine; - highlightedCloseLine = closeLine; - break; + int size = closeLine - openLine; + boolean exactMatch = (bracketChar == '{' && openLine == bracketLineIdx) || (bracketChar == '}' && closeLine == bracketLineIdx); + if (exactMatch) { + // Prefer exact matches immediately (still choose smallest exact span) + if (!foundExact || size < bestSize) { + foundExact = true; + bestSize = size; + highlightedOpenLine = openLine; + highlightedCloseLine = closeLine; + } + } else if (!foundExact) { + // keep the smallest enclosing span as a fallback + if (size < bestSize) { + bestSize = size; + highlightedOpenLine = openLine; + highlightedCloseLine = closeLine; + } + } } } } From 0e3b0f047a8675c6f192a5183740ed0a0dd9866d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 02:22:01 +0200 Subject: [PATCH 172/358] Cached all GuiScriptTextArea tabs of GuiScriptInterface (fixes all tabs sharing the same undo/redo lists and other similar undesired behaviors) --- .../client/gui/script/GuiScriptInterface.java | 47 +++++++++++++++---- .../client/gui/util/GuiScriptTextArea.java | 2 +- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 99fbf7d3e..b7728e40d 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -44,7 +44,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb public List hookList = new ArrayList(); protected boolean loaded = false; - private GuiScriptTextArea textArea; + private List textAreas = new ArrayList<>(); public GuiScriptInterface() { this.drawDefaultBackground = true; this.closeOnEsc = true; @@ -109,18 +109,28 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb addLabel(hookLabel); ScriptContainer container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); - if (textArea == null) - textArea = new GuiScriptTextArea(this, 2, guiLeft + 1 + yoffset, guiTop + yoffset, + + // Ensure textAreas list has at least the same size as scripts, but only + // create/initialize the area for the active tab to avoid creating all tabs at once. + + int idx = this.activeTab - 1; + GuiScriptTextArea activeArea = getActiveScriptArea(); + if (activeArea == null) { + // Create only the active tab's editor + activeArea = new GuiScriptTextArea(this, 2, guiLeft + 1 + yoffset, guiTop + yoffset, xSize - 108 - yoffset, (int) (ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - else - textArea.init(guiLeft + 1 + yoffset, guiTop + yoffset, xSize - 108 - yoffset, + activeArea.setListener(this); + this.closeOnEsc(activeArea::closeOnEsc); + textAreas.add(idx, activeArea); + } else { + // Reinitialize existing active area (position/size/text may change) + activeArea.init(guiLeft + 1 + yoffset, guiTop + yoffset, xSize - 108 - yoffset, (int) (ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); + } - textArea.enableCodeHighlighting(); - textArea.setListener(this); - this.closeOnEsc(textArea::closeOnEsc); - this.addTextField(textArea); + activeArea.enableCodeHighlighting(); + this.addTextField(activeArea); int left1 = this.guiLeft + this.xSize - 104; this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); @@ -256,6 +266,25 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.displayGuiScreen(this); } + private GuiScriptTextArea getActiveScriptArea() { + if (this.activeTab > 0) { + int idx = this.activeTab - 1; + if (idx >= 0 && idx < textAreas.size()) + return textAreas.get(idx); + } + return null; + } + + @Override + public GuiNpcTextField getTextField(int id) { + if (id == 2) { + GuiScriptTextArea area = getActiveScriptArea(); + if (area != null) + return area; + } + return super.getTextField(id); + } + @Override protected void actionPerformed(GuiButton guibutton) { if (guibutton.id >= 0 && guibutton.id < scriptLimit) { diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 870f50710..b2e2db859 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -365,7 +365,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { } }); - initializeKeyBindings(); init(x, y, width, height, text); } @@ -378,6 +377,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.setText(text); this.undoing = false; initGui(); + initializeKeyBindings(); } public void initGui() { int endX = x + width, endY = y + height; From a1e7a7ee9f7151c2ee98591a44f45837eff35da3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 02:22:27 +0200 Subject: [PATCH 173/358] Improved undo/redo for new cached multi-tab script areas --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index b2e2db859..65052cd09 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -924,6 +924,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void initializeKeyBindings() { // Helper: execute action only if text area is active and enabled Supplier isActive = () -> active && isEnabled() && !KEYS_OVERLAY.showOverlay; + Supplier openBoxes = () -> !KEYS_OVERLAY.showOverlay; // CUT: Copy selection to clipboard and delete it. If no selection, cut the current sentence. KEYS.CUT.setTask(e -> { @@ -1006,7 +1007,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // UNDO: Restore last edit from undo list // Works in search bar. KEYS.UNDO.setTask(e -> { - if ((!e.isPress() && !e.isHold()) || !isActive.get() && !searchBar.isVisible()) + if ((!e.isPress() && !e.isHold()) || !openBoxes.get()) return; if (searchBar.hasFocus()) { @@ -1022,13 +1023,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { undoing = false; scrollToCursor(); searchBar.updateMatches(); + if (!active) + active = true; } }); // REDO: Restore last undone edit from redo list // Works in search bar. KEYS.REDO.setTask(e -> { - if ((!e.isPress() && !e.isHold()) || !isActive.get() && !searchBar.isVisible()) + if ((!e.isPress() && !e.isHold()) || !openBoxes.get()) return; if (searchBar.hasFocus()) { @@ -1044,6 +1047,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { undoing = false; scrollToCursor(); searchBar.updateMatches(); + if (!active) + active = true; } }); @@ -1108,7 +1113,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Check if can open just for SearchReplaceBar and GoToLine - Supplier openBoxes = () -> !KEYS_OVERLAY.showOverlay; // SEARCH: Open search bar (Ctrl+R) // Works in search bar. From b23bf333d98b953546c0f00263c90fe2b63fecb5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 02:39:09 +0200 Subject: [PATCH 174/358] Changed List to Map --- .../noppes/npcs/client/gui/script/GuiScriptInterface.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index b7728e40d..d4b7edca4 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -44,7 +44,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb public List hookList = new ArrayList(); protected boolean loaded = false; - private List textAreas = new ArrayList<>(); + private Map textAreas = new HashMap<>(); public GuiScriptInterface() { this.drawDefaultBackground = true; this.closeOnEsc = true; @@ -122,7 +122,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb container == null ? "" : container.script); activeArea.setListener(this); this.closeOnEsc(activeArea::closeOnEsc); - textAreas.add(idx, activeArea); + textAreas.put(idx, activeArea); } else { // Reinitialize existing active area (position/size/text may change) activeArea.init(guiLeft + 1 + yoffset, guiTop + yoffset, xSize - 108 - yoffset, From c30fb77eb6465a66ef3d4c0ed689b491f5e9d84b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 02:39:48 +0200 Subject: [PATCH 175/358] Made searchBar static and setCallbacks in init not constructor --- .../client/gui/util/GuiScriptTextArea.java | 107 +++++++++--------- .../gui/util/script/GoToLineDialog.java | 8 +- 2 files changed, 58 insertions(+), 57 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 65052cd09..f6ee5a2d7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -87,10 +87,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { public OverlayKeyPresetViewer KEYS_OVERLAY = new OverlayKeyPresetViewer(KEYS); // ==================== SEARCH/REPLACE ==================== - private final SearchReplaceBar searchBar = new SearchReplaceBar(); + private static final SearchReplaceBar searchBar = new SearchReplaceBar(); // ==================== GO TO LINE ==================== - private final GoToLineDialog goToLineDialog; + private final GoToLineDialog goToLineDialog = new GoToLineDialog(); // ==================== RENAME REFACTOR ==================== private final RenameRefactorHandler renameHandler = new RenameRefactorHandler(); @@ -99,7 +99,44 @@ public class GuiScriptTextArea extends GuiNpcTextField { public GuiScriptTextArea(GuiScreen guiScreen, int id, int x, int y, int width, int height, String text) { super(id, guiScreen, x, y, width, height, null); + init(x, y, width, height, text); + } + public void init(int x, int y, int width, int height, String text) { + this.x = xPosition = x; + this.y = yPosition = y; + this.width = width; + this.height = height; + this.undoing = true; + this.setText(text); + this.undoing = false; + setCallbacks(); + initGui(); + initializeKeyBindings(); + } + public void initGui() { + int endX = x + width, endY = y + height; + int xOffset = hasVerticalScrollbar() ? -8 : -2; + KEYS_OVERLAY.elementSpacing = KEYS_OVERLAY.yStartSpacing = 0; + KEYS_OVERLAY.initGui(endX - (width / 2)-3, y + (height / 4) - 10, endX + xOffset - 2, endY - 28); + + KEYS_OVERLAY.viewButton.scale = 0.45f; + KEYS_OVERLAY.viewButton.initGui(endX + xOffset, endY - 26); + + // Initialize search bar (preserves state across initGui calls) + searchBar.initGui(x, y, width); + if (searchBar.isVisible()) { // If open + // Shift viewport down again + searchBar.callback.resizeEditor(true); + if (!active) // Focus search if opening another script tab & bar is open + searchBar.focus(false); + } + + // Initialize Go To Line dialog + goToLineDialog.initGui(x, y, width); + } + + public void setCallbacks() { searchBar.setCallback(new SearchReplaceBar.SearchCallback() { @Override public String getText() { @@ -128,14 +165,16 @@ public class GuiScriptTextArea extends GuiNpcTextField { @Override public void scrollToPosition(int position) { // Find line containing position and scroll to it - if (container == null || container.lines == null) return; + if (container == null || container.lines == null) + return; // Calculate offset to account for search bar height int searchBarOffset = searchBar.getTotalHeight(); int effectiveHeight = GuiScriptTextArea.this.height - searchBarOffset; int visibleLines = effectiveHeight / container.lineHeight; // Calculate how many lines the search bar covers - int linesHiddenBySRB = searchBarOffset > 0 ? (int) Math.ceil((double) searchBarOffset / container.lineHeight) : 0; + int linesHiddenBySRB = searchBarOffset > 0 ? (int) Math.ceil( + (double) searchBarOffset / container.lineHeight) : 0; for (int i = 0; i < container.lines.size(); i++) { LineData ld = container.lines.get(i); @@ -198,12 +237,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { } }); // Initialize Go To Line dialog with callback - this.goToLineDialog = new GoToLineDialog(new GoToLineDialog.GoToLineCallback() { + goToLineDialog.setCallback(new GoToLineDialog.GoToLineCallback() { @Override public int getLineCount() { return container != null ? container.linesCount : 0; } - + @Override public int getColumnCount(int lineIndex) { if (container == null || container.lines == null || lineIndex < 0 || lineIndex >= container.lines.size()) { @@ -212,26 +251,28 @@ public class GuiScriptTextArea extends GuiNpcTextField { LineData ld = container.lines.get(lineIndex); return ld.end - ld.start; } - + @Override public void goToLineColumn(int line, int column) { - if (container == null || container.lines == null) return; - + if (container == null || container.lines == null) + return; + // Convert 1-indexed line to 0-indexed int lineIdx = line - 1; - if (lineIdx < 0 || lineIdx >= container.lines.size()) return; - + if (lineIdx < 0 || lineIdx >= container.lines.size()) + return; + LineData ld = container.lines.get(lineIdx); int lineLength = ld.end - ld.start; - + // Convert 1-indexed column to 0-indexed, clamp to line length // lineLength - 1 because the line's end is the start of the next line int col = Math.max(0, Math.min(column - 1, lineLength - 1)); int position = ld.start + col; - + // Set cursor position selection.reset(position); - + // Scroll to make the line visible int visible = GuiScriptTextArea.this.height / (container != null ? container.lineHeight : 12); int effectiveVisible = Math.max(1, visible - bottomPaddingLines); @@ -250,7 +291,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { active = true; selection.markActivity(); } - + @Override public void onDialogClose() { active = true; @@ -364,43 +405,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { return width - LINE_NUMBER_GUTTER_WIDTH - 8; // Account for gutter and scrollbar } }); - - init(x, y, width, height, text); } - - public void init(int x, int y, int width, int height, String text) { - this.x = xPosition = x; - this.y = yPosition = y; - this.width = width; - this.height = height; - this.undoing = true; - this.setText(text); - this.undoing = false; - initGui(); - initializeKeyBindings(); - } - public void initGui() { - int endX = x + width, endY = y + height; - int xOffset = hasVerticalScrollbar() ? -8 : -2; - KEYS_OVERLAY.elementSpacing = KEYS_OVERLAY.yStartSpacing = 0; - KEYS_OVERLAY.initGui(endX - (width / 2)-3, y + (height / 4) - 10, endX + xOffset - 2, endY - 28); - - KEYS_OVERLAY.viewButton.scale = 0.45f; - KEYS_OVERLAY.viewButton.initGui(endX + xOffset, endY - 26); - - // Initialize search bar (preserves state across initGui calls) - searchBar.initGui(x, y, width); - if (searchBar.isVisible()) { // If open - // Shift viewport down again - searchBar.callback.resizeEditor(true); - if (!active) // Focus search if opening another script tab & bar is open - searchBar.focus(false); - } - - // Initialize Go To Line dialog - goToLineDialog.initGui(x, y, width); - } - // ==================== RENDERING ==================== public void drawTextBox(int xMouse, int yMouse) { if (!visible) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java index 36fb52196..7a8217493 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java @@ -71,14 +71,10 @@ public class GoToLineDialog { void focusMainEditor(); } - - /** - * Initialize the dialog with callback - */ - public GoToLineDialog(GoToLineCallback callback) { + + public void setCallback(GoToLineCallback callback) { this.callback = callback; } - /** * Initialize/update position for the dialog */ From e7c7816e0c1b6c1bd4c01c3018ba5e8829f295d4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 02:45:51 +0200 Subject: [PATCH 176/358] Implemented GuiScriptInterface changes to GuiScript too --- .../noppes/npcs/client/gui/GuiScript.java | 51 +++++++++++-------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index 206526c65..e710eef23 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -12,17 +12,7 @@ import net.minecraft.util.StatCollector; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.NoppesUtil; import noppes.npcs.client.gui.script.GuiNPCEventScripts; -import noppes.npcs.client.gui.util.GuiCustomScroll; -import noppes.npcs.client.gui.util.GuiMenuTopButton; -import noppes.npcs.client.gui.util.GuiNPCInterface; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextArea; -import noppes.npcs.client.gui.util.GuiScriptTextArea; -import noppes.npcs.client.gui.util.ICustomScrollListener; -import noppes.npcs.client.gui.util.IGuiData; -import noppes.npcs.client.gui.util.IJTextAreaListener; -import noppes.npcs.client.gui.util.ITextChangeListener; +import noppes.npcs.client.gui.util.*; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.DataScript; @@ -44,7 +34,7 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall private static int activeConsole = 0; boolean loaded = false; - private GuiScriptTextArea textArea; + private Map textAreas = new HashMap<>(); public GuiScript(EntityNPCInterface npc) { super(npc); @@ -98,15 +88,17 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall ScriptContainer container = script.getNPCScript(activeTab); - if (textArea == null) - textArea = new GuiScriptTextArea(this, 2, guiLeft + 74, guiTop + 4, 239, 208, + int idx = this.activeTab; + GuiScriptTextArea activeArea = getActiveScriptArea(); + if (activeArea == null) { + activeArea = new GuiScriptTextArea(this, 2, guiLeft + 74, guiTop + 4, 239, 208, container == null ? "" : container.script); - else - textArea.init(guiLeft + 74, guiTop + 4, 239, 208, container == null ? "" : container.script); - textArea.enableCodeHighlighting(); - textArea.setListener(this); - this.closeOnEsc(textArea::closeOnEsc); - this.addTextField(textArea); + activeArea.setListener(this); + this.closeOnEsc(activeArea::closeOnEsc); + textAreas.put(idx, activeArea); + } else { + activeArea.init(guiLeft + 74, guiTop + 4, 239, 208, container == null ? "" : container.script); + } addButton(new GuiNpcButton(102, guiLeft + 315, guiTop + 4, 50, 20, "gui.clear")); addButton(new GuiNpcButton(101, guiLeft + 366, guiTop + 4, 50, 20, "gui.paste")); @@ -146,6 +138,25 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall } } + private GuiScriptTextArea getActiveScriptArea() { + if (this.activeTab > 0) { + int idx = this.activeTab; + if (idx >= 0 && idx < textAreas.size()) + return textAreas.get(idx); + } + return null; + } + + @Override + public GuiNpcTextField getTextField(int id) { + if (id == 2) { + GuiScriptTextArea area = getActiveScriptArea(); + if (area != null) + return area; + } + return super.getTextField(id); + } + private int getScriptIndex() { int i = 0; for (String language : languages.keySet()) { From 2e8b96fe3a661892ee72c0922c09ceb106fc8124 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 03:01:55 +0200 Subject: [PATCH 177/358] recursively changed object fields like player.world.field1.field2... so they all highlight as GLOBAL_FIELD --- .../gui/util/script/JavaTextContainer.java | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index b879db8a0..8c7489b2d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -253,15 +253,66 @@ public class JavaTextContainer extends TextContainer { // Handle identifier.field patterns (e.g., obj.field, global.field) // The field part should be highlighted as GLOBAL_FIELD (light blue) + // Also, detect chained accesses like `a.b.c` and treat subsequent segments + // as fields when the root `a` is a known variable (parameter/local/global/this). + // Collect function parameters from JS-style `function name(a, b)` declarations + // so script parameters (e.g., `function tick(e)`) are recognized as known roots. + java.util.Set scriptFunctionParams = new java.util.HashSet<>(); + Pattern funcPattern = Pattern.compile("\\bfunction\\s+[a-zA-Z_][a-zA-Z0-9_]*\\s*\\(([^)]*)\\)"); + Matcher funcM = funcPattern.matcher(text); + while (funcM.find()) { + String inside = funcM.group(1); + if (inside != null && !inside.trim().isEmpty()) { + for (String p : inside.split(",")) { + String pn = p.trim(); + if (!pn.isEmpty()) scriptFunctionParams.add(pn); + } + } + } + + Pattern chainPattern = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*(?:\\s*\\.\\s*[a-zA-Z_][a-zA-Z0-9_]*)+)\\b"); + Matcher chainMatcher = chainPattern.matcher(text); + while (chainMatcher.find()) { + if (isInExcludedRange(chainMatcher.start(), MethodBlock.getExcludedRanges(text))) continue; + String chain = chainMatcher.group(1); + int absStart = chainMatcher.start(1); + + java.util.List segs = new java.util.ArrayList<>(); + java.util.List segPos = new java.util.ArrayList<>(); + Matcher segM = Pattern.compile("[a-zA-Z_][a-zA-Z0-9_]*").matcher(chain); + while (segM.find()) { + segs.add(segM.group()); + segPos.add(absStart + segM.start()); + } + if (segs.size() < 2) continue; + + String root = segs.get(0); + if (Character.isUpperCase(root.charAt(0))) continue; + + boolean rootIsKnown = false; + if (globalFields.contains(root) || localFields.contains(root)) rootIsKnown = true; + MethodBlock mbForRoot = findMethodBlockAtPosition(segPos.get(0)); + if (mbForRoot != null && mbForRoot.parameters.contains(root)) rootIsKnown = true; + if (scriptFunctionParams.contains(root)) rootIsKnown = true; + if ("this".equals(root)) rootIsKnown = true; + + if (rootIsKnown) { + for (int si = 1; si < segs.size(); si++) { + int sAbs = segPos.get(si); + int sEnd = sAbs + segs.get(si).length(); + marks.add(new Mark(sAbs, sEnd, TokenType.GLOBAL_FIELD)); + } + } + } + + // Fallback: simple two-part identifier.field patterns Pattern objFieldPattern = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); Matcher objFieldMatcher = objFieldPattern.matcher(text); while (objFieldMatcher.find()) { String fieldName = objFieldMatcher.group(2); int fieldPos = objFieldMatcher.start(2); - // Skip if this is a "this.field" pattern (already handled above) String objName = objFieldMatcher.group(1); if (!objName.equals("this")) { - // Highlight the field part as GLOBAL_FIELD (represents another object's field) marks.add(new Mark(fieldPos, objFieldMatcher.end(2), TokenType.GLOBAL_FIELD)); } } From 3fb3b1dad3e82332ea8fd276e80c0a6f201bbc79 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 03:05:42 +0200 Subject: [PATCH 178/358] Commented out RenameRefactor status box (unneeded) --- .../client/gui/util/GuiScriptTextArea.java | 25 +++---------------- .../util/script/RenameRefactorHandler.java | 21 ++++++++++++++++ 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index f6ee5a2d7..69d1c48f7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -3,6 +3,7 @@ package noppes.npcs.client.gui.util; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.entity.player.EntityPlayer; import net.minecraft.util.ChatAllowedCharacters; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; @@ -113,6 +114,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { setCallbacks(); initGui(); initializeKeyBindings(); + Minecraft mc = Minecraft.getMinecraft(); + mc.thePlayer.worldObj.setRainStrength(0); } public void initGui() { int endX = x + width, endY = y + height; @@ -820,28 +823,6 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Draw go to line dialog (overlays everything) goToLineDialog.draw(xMouse, yMouse); - - // Draw rename status indicator - if (renameHandler.isActive()) { - String status = renameHandler.getScopeDescription(); - String hint = "Enter \u2713 | Esc \u2715"; // Enter to confirm, Esc to cancel - int statusWidth = Math.max(ClientProxy.Font.width(status), ClientProxy.Font.width(hint)) + 8; - int statusX = x + width - statusWidth - 8; - int statusY = y + height - 30; - // Semi-transparent background - drawRect(statusX - 2, statusY - 2, statusX + statusWidth + 2, statusY + 24, 0xDD1a1a2e); - // Border (purple/blue for rename mode) - int borderColor = 0xFF6677dd; - drawRect(statusX - 2, statusY - 2, statusX + statusWidth + 2, statusY - 1, borderColor); - drawRect(statusX - 2, statusY + 23, statusX + statusWidth + 2, statusY + 24, borderColor); - drawRect(statusX - 2, statusY - 2, statusX - 1, statusY + 24, borderColor); - drawRect(statusX + statusWidth + 1, statusY - 2, statusX + statusWidth + 2, statusY + 24, borderColor); - // Status text (top line) - ClientProxy.Font.drawString(status, statusX + 4, statusY + 2, 0xFFccddff); - // Hint text (bottom line - dimmer) - ClientProxy.Font.drawString(hint, statusX + 4, statusY + 13, 0xAA88aacc); - } - KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java index 83d6d90d4..4f36da54b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script; import net.minecraft.client.gui.GuiScreen; +import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; import org.lwjgl.input.Keyboard; @@ -948,4 +949,24 @@ public class RenameRefactorHandler { public int getOccurrenceCount() { return allOccurrences.size(); } + + public void drawStatusBox() { + // String status = renameHandler.getScopeDescription(); + // String hint = "Enter \u2713 | Esc \u2715"; // Enter to confirm, Esc to cancel + // int statusWidth = Math.max(ClientProxy.Font.width(status), ClientProxy.Font.width(hint)) + 8; + // int statusX = x + width - statusWidth - 8; + // int statusY = y + height - 30; + // // Semi-transparent background + // drawRect(statusX - 2, statusY - 2, statusX + statusWidth + 2, statusY + 24, 0xDD1a1a2e); + // // Border (purple/blue for rename mode) + // int borderColor = 0xFF6677dd; + // drawRect(statusX - 2, statusY - 2, statusX + statusWidth + 2, statusY - 1, borderColor); + // drawRect(statusX - 2, statusY + 23, statusX + statusWidth + 2, statusY + 24, borderColor); + // drawRect(statusX - 2, statusY - 2, statusX - 1, statusY + 24, borderColor); + // drawRect(statusX + statusWidth + 1, statusY - 2, statusX + statusWidth + 2, statusY + 24, borderColor); + // // Status text (top line) + // ClientProxy.Font.drawString(status, statusX + 4, statusY + 2, 0xFFccddff); + // // Hint text (bottom line - dimmer) + // ClientProxy.Font.drawString(hint, statusX + 4, statusY + 13, 0xAA88aacc); + } } From 6aebcfc63cce7df53c80107d465dcc21b06414e6 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 03:11:45 +0200 Subject: [PATCH 179/358] Fixed tab/backspace crash --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 69d1c48f7..ab785678f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1447,13 +1447,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { // intuitive on blank/indented lines outside any recognized scope. LineData currCheck = selection.findCurrentLine(container.lines); if (currCheck != null && currCheck.text.trim().length() == 0) { - int removeEnd = text.indexOf('\n', currCheck.start-1); + int removeEnd = text.indexOf('\n', currCheck.start); if (removeEnd == -1) { removeEnd = text.length(); } else { removeEnd = removeEnd + 1; // include the newline } - String before = text.substring(0, currCheck.start-1); + String before = text.substring(0, Math.max(0, currCheck.start)); String after = removeEnd <= text.length() ? text.substring(removeEnd) : ""; setText(before + after); int newCursor = Math.max(0, currCheck.start - 1); @@ -1493,7 +1493,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } else { // Merge current line content with the previous line preserving spacing int contentStart = curr.start + actualIndent; - String before = text.substring(0, newlinePos); + String before = newlinePos >= 0 ? text.substring(0, newlinePos) : ""; String content = contentStart <= text.length() ? text.substring(contentStart) : ""; // Decide whether a space is needed between concatenated fragments. From 8220bbd327f1ac61ba4dcf2cc715fb083582363f Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 03:12:17 +0200 Subject: [PATCH 180/358] Changed to just Rename --- src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java index 2e2fa46f4..8b5f66bd5 100644 --- a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java +++ b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java @@ -26,7 +26,7 @@ public class ScriptEditorKeys extends KeyPresetManager { public final KeyPreset GO_TO_LINE = add("Go to Line").setDefaultState(Keyboard.KEY_G, true, false, false); // Refactoring - public final KeyPreset RENAME = add("Rename/Refactor").setDefaultState(Keyboard.KEY_F6, false, true, false); + public final KeyPreset RENAME = add("Rename").setDefaultState(Keyboard.KEY_F6, false, true, false); public ScriptEditorKeys() { super("script_editor"); From d2bc98a4c10d39b0b6c96ca1291cc5b6431d6959 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 03:34:21 +0200 Subject: [PATCH 181/358] Fixed CTRL+D crash --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index ab785678f..036768bfc 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -1085,9 +1085,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Duplicate current line for (LineData line : container.lines) { if (selection.getCursorPosition() >= line.start && selection.getCursorPosition() <= line.end) { - String lineText = text.substring(line.start, line.end); + int safeStart = Math.max(0, Math.min(line.start, text.length())); + int safeEnd = Math.max(safeStart, Math.min(line.end, text.length())); + String lineText = text.substring(safeStart, safeEnd); String insertText = lineText.endsWith("\n") ? lineText : "\n" + lineText; - int insertionPoint = line.end; + int insertionPoint = Math.min(line.end, text.length()); setText(text.substring(0, insertionPoint) + insertText + text.substring(insertionPoint)); int newCursor = insertionPoint + insertText.length() - (insertText.endsWith("\n") ? 1 : 0); selection.reset(Math.max(0, Math.min(newCursor, this.text.length()))); From b9ebaa3292a13b7ca71413d7288d05b8d7431eeb Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 03:39:22 +0200 Subject: [PATCH 182/358] Fixed TAB + BACKSPACE bug again --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 036768bfc..18c90bb0b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -11,6 +11,7 @@ import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; import noppes.npcs.client.gui.util.script.*; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; import noppes.npcs.client.key.impl.ScriptEditorKeys; +import noppes.npcs.util.ValueUtil; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; @@ -1449,13 +1450,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { // intuitive on blank/indented lines outside any recognized scope. LineData currCheck = selection.findCurrentLine(container.lines); if (currCheck != null && currCheck.text.trim().length() == 0) { - int removeEnd = text.indexOf('\n', currCheck.start); + int removeEnd = text.indexOf('\n', currCheck.start - 1); if (removeEnd == -1) { removeEnd = text.length(); } else { removeEnd = removeEnd + 1; // include the newline } - String before = text.substring(0, Math.max(0, currCheck.start)); + String before = text.substring(0, ValueUtil.clamp(currCheck.start - 1, 0, text.length())); String after = removeEnd <= text.length() ? text.substring(removeEnd) : ""; setText(before + after); int newCursor = Math.max(0, currCheck.start - 1); From 85f8baeda562bc4c93f8ce4c433adc2720d4d9f5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 03:44:48 +0200 Subject: [PATCH 183/358] wtf --- .../java/noppes/npcs/client/gui/util/GuiScriptTextArea.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 18c90bb0b..7dde11774 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -3,7 +3,6 @@ package noppes.npcs.client.gui.util; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.ScaledResolution; -import net.minecraft.entity.player.EntityPlayer; import net.minecraft.util.ChatAllowedCharacters; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; @@ -115,8 +114,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setCallbacks(); initGui(); initializeKeyBindings(); - Minecraft mc = Minecraft.getMinecraft(); - mc.thePlayer.worldObj.setRainStrength(0); + } public void initGui() { int endX = x + width, endY = y + height; From 6012c103a6f4b4c7910ed294d5c1e116e6f83bcb Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 13:28:25 +0200 Subject: [PATCH 184/358] Fixed search bar window resize buggin with replace bar open --- .../client/gui/util/GuiScriptTextArea.java | 47 +++++++++++++++---- .../gui/util/script/SearchReplaceBar.java | 41 +++++++++------- 2 files changed, 63 insertions(+), 25 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 7dde11774..226291e4d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -58,6 +58,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Extra empty lines to allow padding at the bottom of the editor viewport private int bottomPaddingLines = 6; + // Search bar layout tracking to allow idempotent/resilient resizing + private int searchBaseY = 0; + private int searchBaseHeight = 0; + private int searchAppliedOffset = 0; + private boolean searchBaseInitialized = false; + private int getPaddedLineCount() { if (container == null) return 0; // Only add bottom padding when the content is already scrollable. This avoids @@ -112,9 +118,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.setText(text); this.undoing = false; setCallbacks(); + // Reset search-base tracking whenever the editor is (re)initialized + this.searchBaseY = 0; + this.searchBaseHeight = 0; + this.searchAppliedOffset = 0; + this.searchBaseInitialized = false; initGui(); initializeKeyBindings(); - } public void initGui() { int endX = x + width, endY = y + height; @@ -128,8 +138,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Initialize search bar (preserves state across initGui calls) searchBar.initGui(x, y, width); if (searchBar.isVisible()) { // If open - // Shift viewport down again - searchBar.callback.resizeEditor(true); + // Shift viewport down again using the bar's current height + searchBar.callback.resizeEditor(true, searchBar.getTotalHeight()); if (!active) // Focus search if opening another script tab & bar is open searchBar.focus(false); } @@ -230,12 +240,31 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Called when matches change - could be used for UI updates } - // Shift text editor viewport up or down bar - public void resizeEditor(boolean open) { - int scrollHeight = searchBar.getTotalHeight(); - GuiScriptTextArea.this.y += open ? scrollHeight : -scrollHeight; - GuiScriptTextArea.this.height += open ? -scrollHeight : scrollHeight; - container.visibleLines = Math.max(GuiScriptTextArea.this.height / container.lineHeight - 1, 1); + // Robust resize: use base editor bounds and apply the requested offset + public void resizeEditor(boolean open, int barHeight) { + int desiredOffset = Math.max(0, barHeight); + // Initialize base values if not set + if (!searchBaseInitialized) { + searchBaseY = GuiScriptTextArea.this.y; + searchBaseHeight = GuiScriptTextArea.this.height; + searchAppliedOffset = 0; + searchBaseInitialized = true; + } + + int targetOffset = open ? desiredOffset : 0; + if (targetOffset == searchAppliedOffset) + return; // already in desired state + + // Compute new bounds from base values (idempotent) + int newY = searchBaseY + targetOffset; + int newHeight = Math.max(12, searchBaseHeight - targetOffset); + + GuiScriptTextArea.this.y = newY; + GuiScriptTextArea.this.height = newHeight; + searchAppliedOffset = targetOffset; + + if (container != null) + container.visibleLines = Math.max(GuiScriptTextArea.this.height / container.lineHeight - 1, 1); } }); // Initialize Go To Line dialog with callback diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index 24de5f316..ef20e816a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -122,7 +122,8 @@ public class SearchReplaceBar { void unfocusMainEditor(); void focusMainEditor(); void onMatchesUpdated(); // Called when matches change (for undo/redo sync) - void resizeEditor(boolean open); + + void resizeEditor(boolean open, int barHeight); } public SearchReplaceBar() { @@ -142,28 +143,23 @@ public class SearchReplaceBar { this.width = textAreaWidth; // State is preserved - don't reset searchText, matches, visible, etc. } - - /** - * Open search bar (Ctrl+R) - */ - public void openSearch() { + + public void open() { boolean oldVisible = this.visible; - visible = true; - showReplace = false; - searchFieldFocused = true; - replaceFieldFocused = false; boolean matchHighlight = false; + visible = true; if (callback != null) { - if (!oldVisible) //if wasnt open before, do it once. - callback.resizeEditor(true); + // Always notify callback that the search UI is open and provide its current height. + callback.resizeEditor(true, getTotalHeight()); callback.unfocusMainEditor(); - + String highlight = callback.getHighlightedWord(); if (highlight != null) { searchText = highlight; matchHighlight = true; } } + searchSelectionStart = 0; searchSelectionEnd = searchText.length(); searchCursor = searchText.length(); @@ -173,15 +169,27 @@ public class SearchReplaceBar { setCurrentMatchToHighlight(); navigateToCurrentMatch(); } + } + /** + * Open search bar (Ctrl+R) + */ + public void openSearch() { + showReplace = false; + searchFieldFocused = true; + replaceFieldFocused = false; + open(); + } /** * Open search+replace bar (Ctrl+Shift+R) */ public void openSearchReplace() { - openSearch(); - showReplace = true; + showReplace = true; //both for window resize properly + searchFieldFocused = false; + replaceFieldFocused = true; + open(); } /** @@ -189,7 +197,8 @@ public class SearchReplaceBar { */ public void close() { if (callback != null) { - callback.resizeEditor(false); + // When closing, signal closed and height 0 + callback.resizeEditor(false, 0); callback.focusMainEditor(); } visible = false; From f0740c6d7a80487d61eea411b88a4fd30e9a3bd1 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 13:43:58 +0200 Subject: [PATCH 185/358] Added openOnClick for OverlayKeyPreset --- .../client/gui/util/GuiScriptTextArea.java | 12 +-- .../gui/util/key/OverlayKeyPresetViewer.java | 84 ++++++++++++++----- 2 files changed, 69 insertions(+), 27 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 226291e4d..a20f37da3 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -123,6 +123,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.searchBaseHeight = 0; this.searchAppliedOffset = 0; this.searchBaseInitialized = false; + + KEYS_OVERLAY.openOnClick = true; initGui(); initializeKeyBindings(); } @@ -1188,7 +1190,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { */ @Override public boolean textboxKeyTyped(char c, int i) { - KEYS_OVERLAY.keyTyped(c, i); + if (KEYS_OVERLAY.keyTyped(c, i)) + return true; // Handle rename refactor input first if active if (renameHandler.isActive() && renameHandler.keyTyped(c, i)) @@ -1793,7 +1796,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } public boolean closeOnEsc(){ - return !searchBar.isVisible() && !goToLineDialog.isVisible() && !renameHandler.isActive(); + return !KEYS_OVERLAY.showOverlay && !searchBar.isVisible() && !goToLineDialog.isVisible() && !renameHandler.isActive(); } // ==================== KEYBOARD MODIFIERS ==================== @@ -2004,10 +2007,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { searchBar.unfocus(); } - if (KEYS_OVERLAY.showOverlay) { - KEYS_OVERLAY.mouseClicked(xMouse, yMouse, mouseButton); + // Let the overlay consume clicks (it returns true when it handled the event) + if (KEYS_OVERLAY.mouseClicked(xMouse, yMouse, mouseButton)) return; - } // Determine whether click occurred inside the text area bounds this.active = xMouse >= this.x && xMouse < this.x + this.width && yMouse >= this.y && yMouse < this.y + this.height; diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java index 13518c079..d94242e21 100644 --- a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -8,6 +8,7 @@ import noppes.npcs.client.key.KeyPreset; import noppes.npcs.client.key.KeyPresetManager; import noppes.npcs.client.util.Color; import noppes.npcs.util.ValueUtil; +import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; @@ -33,7 +34,10 @@ public class OverlayKeyPresetViewer { public float RELATIVE_MAX_DESC_WIDTH = 0.5f; private final FontRenderer font = Minecraft.getMinecraft().fontRenderer; + public int bgCol1 = 0x88000000, bgCol2 = 0xcc303030; + public int borderCol1 = 0x22ffffff, borderCol2 = 0xffffffff; public boolean hasBorder = true; + public boolean openOnClick; public OverlayKeyPresetViewer(KeyPresetManager manager) { this.manager = manager; @@ -59,14 +63,15 @@ public class OverlayKeyPresetViewer { this.mouseY = mouseY; boolean aboveButton = viewButton.isMouseAbove(mouseX, mouseY); boolean aboveOverlay = isMouseAbove(mouseX, mouseY); - - if (aboveButton) + // If in hover mode, hovering the button opens the overlay. + if (aboveButton && !openOnClick) showOverlay = true; if (showOverlay) { drawOverlay(wheel); - if (!aboveOverlay && !aboveButton) + // In click-to-open mode we don't auto-close on mouse-out; closure is handled by clicks. + if (!openOnClick && !aboveOverlay && !aboveButton) showOverlay = false; } viewButton.drawButton(); @@ -76,20 +81,19 @@ public class OverlayKeyPresetViewer { scroll.update(wheel); if (hasBorder) { - int fromCol = 0x22ffffff, toCol = 0xffffffff; //BORDER GRADIENT - // int fromCol = 0xffff00ff,toCol = 0xff00ffff; NEON + // int borderCol2 = 0xffff00ff,borderCol2 = 0xff00ffff; NEON // Top - GuiUtil.drawGradientRectHorizontal(startX - 1, startY - 1, endX + 1, startY, toCol, fromCol); + GuiUtil.drawGradientRectHorizontal(startX - 1, startY - 1, endX + 1, startY, borderCol2, borderCol1); // Bottom - GuiUtil.drawGradientRectHorizontal(startX - 1, endY, endX + 1, endY + 1, fromCol, toCol); + GuiUtil.drawGradientRectHorizontal(startX - 1, endY, endX + 1, endY + 1, borderCol1, borderCol2); // Left - GuiUtil.drawGradientRect(startX - 1, startY - 1, startX, endY, toCol, fromCol); + GuiUtil.drawGradientRect(startX - 1, startY - 1, startX, endY, borderCol2, borderCol1); //Right - GuiUtil.drawGradientRect(endX, startY, endX + 1, endY, fromCol, toCol); + GuiUtil.drawGradientRect(endX, startY, endX + 1, endY, borderCol1, borderCol2); } - GuiUtil.drawGradientRect(startX + 1, startY + 1, endX - 1, endY - 1, 0x88000000, 0xcc303030); + GuiUtil.drawGradientRect(startX + 1, startY + 1, endX - 1, endY - 1, bgCol1, bgCol2); GL11.glEnable(GL11.GL_SCISSOR_TEST); GuiUtil.setScissorClip(startX, startY, scroll.maxScroll > 0 ? width - scroll.barWidth : width, height); GL11.glPushMatrix(); @@ -114,33 +118,69 @@ public class OverlayKeyPresetViewer { scroll.drawBar(); } - public void keyTyped(char c, int i) { + public boolean keyTyped(char c, int i) { if (!showOverlay) - return; + return false; - list.forEach((element) -> { - if (element.isEditing) + if (i == Keyboard.KEY_ESCAPE) { + showOverlay = false; + return true; + } + + for (PresetElement element : list) { + if (element.isEditing) { element.keyTyped(i); - }); + return true; + } + } + + // If overlay is visible but no element consumed the key, consider it handled + return true; } - public void mouseClicked(int mouseX, int mouseY, int button) { - if (!showOverlay) - return; + public boolean mouseClicked(int mouseX, int mouseY, int button) { + // Always allow clicking the view button to toggle when openOnClick is enabled + if (viewButton.isMouseAbove(mouseX, mouseY)) { + if (openOnClick) { + showOverlay = !showOverlay; + return true; + } else { + // If not click-to-open mode, clicking the button should still open the overlay + showOverlay = true; + return true; + } + } - list.forEach((element) -> { + // If overlay is not visible, no other clicks matter + if (!showOverlay) + return false; + + // In click-to-open mode, clicking outside the overlay (and not the button) should close it + if (openOnClick && !isMouseAbove(mouseX, mouseY) && !viewButton.isMouseAbove(mouseX, mouseY)) { + showOverlay = false; + return true; + } + + boolean consumed = false; + for (PresetElement element : list) { if (!element.isMouseAboveBox(mouseX, mouseY)) { - if (element.isEditing) + if (element.isEditing) { element.cancelEdit(); - } else + consumed = true; + } + } else { element.boxClicked(button); + consumed = true; + } if (element.isMouseAboveReset(mouseX, mouseY) && !element.key.isDefault()) { element.key.defaultState.writeTo(element.key.currentState); manager.save(); + consumed = true; } + } - }); + return consumed; } public boolean isMouseAbove(int mouseX, int mouseY) { From 0dcd02c5a0823dadc1ae2eb6b907fea832dc2009 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 13:51:57 +0200 Subject: [PATCH 186/358] Slight OverlayKeyPreset logic improvements --- .../npcs/client/gui/util/GuiScriptTextArea.java | 13 +++++++------ .../gui/util/key/OverlayKeyPresetViewer.java | 15 +++++++++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index a20f37da3..38284c219 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -477,7 +477,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); if (listener instanceof GuiNPCInterface) { ((GuiNPCInterface) listener).mouseScroll = wheelDelta; - if (wheelDelta != 0 && !KEYS_OVERLAY.showOverlay) + if (wheelDelta != 0 && !KEYS_OVERLAY.isVisible()) scroll.applyWheelScroll(wheelDelta, maxScroll); } @@ -939,8 +939,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { */ private void initializeKeyBindings() { // Helper: execute action only if text area is active and enabled - Supplier isActive = () -> active && isEnabled() && !KEYS_OVERLAY.showOverlay; - Supplier openBoxes = () -> !KEYS_OVERLAY.showOverlay; + Supplier isActive = () -> active && isEnabled() && !KEYS_OVERLAY.isVisible(); + Supplier openBoxes = () -> !KEYS_OVERLAY.isVisible(); // CUT: Copy selection to clipboard and delete it. If no selection, cut the current sentence. KEYS.CUT.setTask(e -> { @@ -1204,8 +1204,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Handle search bar input first if it has focus if (searchBar.isVisible() && searchBar.keyTyped(c, i)) return true; - - if (!active || KEYS_OVERLAY.showOverlay) return false; + + if (!active) + return false; if (this.isKeyComboCtrlA(i)) { selection.selectAll(text.length()); @@ -1796,7 +1797,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } public boolean closeOnEsc(){ - return !KEYS_OVERLAY.showOverlay && !searchBar.isVisible() && !goToLineDialog.isVisible() && !renameHandler.isActive(); + return !KEYS_OVERLAY.isVisible() && !searchBar.isVisible() && !goToLineDialog.isVisible() && !renameHandler.isActive(); } // ==================== KEYBOARD MODIFIERS ==================== diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java index d94242e21..5ae01dcf9 100644 --- a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -140,7 +140,8 @@ public class OverlayKeyPresetViewer { public boolean mouseClicked(int mouseX, int mouseY, int button) { // Always allow clicking the view button to toggle when openOnClick is enabled - if (viewButton.isMouseAbove(mouseX, mouseY)) { + boolean isAboveButton = viewButton.isMouseAbove(mouseX, mouseY); + if (isAboveButton) { if (openOnClick) { showOverlay = !showOverlay; return true; @@ -154,14 +155,15 @@ public class OverlayKeyPresetViewer { // If overlay is not visible, no other clicks matter if (!showOverlay) return false; - + + boolean isAboveOverlay = isMouseAbove(mouseX, mouseY); // In click-to-open mode, clicking outside the overlay (and not the button) should close it - if (openOnClick && !isMouseAbove(mouseX, mouseY) && !viewButton.isMouseAbove(mouseX, mouseY)) { + if (openOnClick && !isAboveOverlay && !isAboveButton) { showOverlay = false; return true; } - boolean consumed = false; + boolean consumed = isAboveOverlay; for (PresetElement element : list) { if (!element.isMouseAboveBox(mouseX, mouseY)) { if (element.isEditing) { @@ -194,6 +196,11 @@ public class OverlayKeyPresetViewer { return (int) (width / scale); } + public boolean isVisible() { + return showOverlay; + } + + public class Scrollable { private float scrollY, targetScrollY, maxScroll; private int totalHeight, scrollbarHeight, barWidth = 2; From 976bb9af4993c3ef2be877a5c26bd303ee8ea230 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 14:20:52 +0200 Subject: [PATCH 187/358] More OverlayKeyPreset improvements --- .../gui/util/key/OverlayKeyPresetViewer.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java index 5ae01dcf9..9c7b9a400 100644 --- a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -21,7 +21,7 @@ public class OverlayKeyPresetViewer { public int startX, startY, endX, endY, width, height; public int mouseX, mouseY; public boolean showOverlay; - + public boolean aboveButton, aboveOverlay; public int elementSpacing = 5, yStartSpacing = 5; public float scale = 0.75f; public Scrollable scroll = new Scrollable(); @@ -61,8 +61,8 @@ public class OverlayKeyPresetViewer { public void draw(int mouseX, int mouseY, int wheel) { this.mouseX = mouseX; this.mouseY = mouseY; - boolean aboveButton = viewButton.isMouseAbove(mouseX, mouseY); - boolean aboveOverlay = isMouseAbove(mouseX, mouseY); + aboveButton = viewButton.isMouseAbove(mouseX, mouseY); + aboveOverlay = isMouseAbove(mouseX, mouseY); // If in hover mode, hovering the button opens the overlay. if (aboveButton && !openOnClick) showOverlay = true; @@ -93,7 +93,7 @@ public class OverlayKeyPresetViewer { GuiUtil.drawGradientRect(endX, startY, endX + 1, endY, borderCol1, borderCol2); } - GuiUtil.drawGradientRect(startX + 1, startY + 1, endX - 1, endY - 1, bgCol1, bgCol2); + GuiUtil.drawGradientRect(startX, startY, endX, endY, bgCol1, bgCol2); GL11.glEnable(GL11.GL_SCISSOR_TEST); GuiUtil.setScissorClip(startX, startY, scroll.maxScroll > 0 ? width - scroll.barWidth : width, height); GL11.glPushMatrix(); @@ -122,11 +122,6 @@ public class OverlayKeyPresetViewer { if (!showOverlay) return false; - if (i == Keyboard.KEY_ESCAPE) { - showOverlay = false; - return true; - } - for (PresetElement element : list) { if (element.isEditing) { element.keyTyped(i); @@ -134,6 +129,11 @@ public class OverlayKeyPresetViewer { } } + if (i == Keyboard.KEY_ESCAPE) { + showOverlay = false; + return true; + } + // If overlay is visible but no element consumed the key, consider it handled return true; } @@ -426,7 +426,7 @@ public class OverlayKeyPresetViewer { mouseY -= startY + 5 - scroll.scrollY; float boxScaleX = 1.75f; - float screenY = boxScreenY + 10; + float screenY = boxScreenY + 5; return mouseX >= boxScreenX && mouseX < boxScreenX + 32 * boxScaleX && mouseY >= screenY && mouseY < screenY + 10; } @@ -482,7 +482,7 @@ public class OverlayKeyPresetViewer { GL11.glDisable(GL11.GL_BLEND); GL11.glPushMatrix(); - float color = 0.75f; + float color = aboveButton ? 1f : 0.4f; GL11.glColor4f(color, color, color, 1); GL11.glScalef(scale, scale, 1); GuiUtil.drawTexturedModalRect(screenX, screenY, textureWidth, textureHeight, 0, 0); From 1a1c2af54866f74efb2e159b31c21d68af08f6b8 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 14:37:08 +0200 Subject: [PATCH 188/358] Improved KeyPreset overlay dimensions and colorsfor GuiScriptTextArea --- .../noppes/npcs/client/gui/util/GuiScriptTextArea.java | 8 +++++--- .../npcs/client/gui/util/key/OverlayKeyPresetViewer.java | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 38284c219..7c151d5be 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -131,8 +131,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { public void initGui() { int endX = x + width, endY = y + height; int xOffset = hasVerticalScrollbar() ? -8 : -2; - KEYS_OVERLAY.elementSpacing = KEYS_OVERLAY.yStartSpacing = 0; - KEYS_OVERLAY.initGui(endX - (width / 2)-3, y + (height / 4) - 10, endX + xOffset - 2, endY - 28); + KEYS_OVERLAY.scale = 0.75f; + KEYS_OVERLAY.borderCol1 = KEYS_OVERLAY.borderCol2 = 0xFF3c3c3c; + KEYS_OVERLAY.initGui(x + (width / 4), y + 20, endX - (width / 4) + 5, endY - 28); KEYS_OVERLAY.viewButton.scale = 0.45f; KEYS_OVERLAY.viewButton.initGui(endX + xOffset, endY - 26); @@ -477,7 +478,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); if (listener instanceof GuiNPCInterface) { ((GuiNPCInterface) listener).mouseScroll = wheelDelta; - if (wheelDelta != 0 && !KEYS_OVERLAY.isVisible()) + boolean canScroll = !KEYS_OVERLAY.isVisible() || KEYS_OVERLAY.isVisible() && !KEYS_OVERLAY.aboveOverlay; + if (wheelDelta != 0 && canScroll) scroll.applyWheelScroll(wheelDelta, maxScroll); } diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java index 9c7b9a400..96d6ece84 100644 --- a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -34,7 +34,7 @@ public class OverlayKeyPresetViewer { public float RELATIVE_MAX_DESC_WIDTH = 0.5f; private final FontRenderer font = Minecraft.getMinecraft().fontRenderer; - public int bgCol1 = 0x88000000, bgCol2 = 0xcc303030; + public int bgCol1 = 0xdd101010, bgCol2 = 0xf8303030; public int borderCol1 = 0x22ffffff, borderCol2 = 0xffffffff; public boolean hasBorder = true; public boolean openOnClick; From b9691676dffd1d554ceadc07b62b73c4f8d5ac44 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 14:38:01 +0200 Subject: [PATCH 189/358] Changed default key presets --- .../noppes/npcs/client/key/impl/ScriptEditorKeys.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java index 8b5f66bd5..3891aa842 100644 --- a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java +++ b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java @@ -15,18 +15,18 @@ public class ScriptEditorKeys extends KeyPresetManager { public final KeyPreset UNDO = add("Undo").setDefaultState(Keyboard.KEY_Z, true, false, false); public final KeyPreset REDO = add("Redo").setDefaultState(Keyboard.KEY_Y, true, false, false); - public final KeyPreset FORMAT = add("Format Code").setDefaultState(Keyboard.KEY_F, true, false, false); + public final KeyPreset FORMAT = add("Format Code").setDefaultState(Keyboard.KEY_F, false, true, false); public final KeyPreset TOGGLE_COMMENT = add("Toggle Comment").setDefaultState(Keyboard.KEY_SLASH, true, false, false); // Search/Replace - public final KeyPreset SEARCH = add("Search").setDefaultState(Keyboard.KEY_R, true, false, false); - public final KeyPreset SEARCH_REPLACE = add("Replace").setDefaultState(Keyboard.KEY_R, true, false, true); + public final KeyPreset SEARCH = add("Search").setDefaultState(Keyboard.KEY_F, true, false, false); + public final KeyPreset SEARCH_REPLACE = add("Replace").setDefaultState(Keyboard.KEY_F, true, false, true); // Navigation public final KeyPreset GO_TO_LINE = add("Go to Line").setDefaultState(Keyboard.KEY_G, true, false, false); // Refactoring - public final KeyPreset RENAME = add("Rename").setDefaultState(Keyboard.KEY_F6, false, true, false); + public final KeyPreset RENAME = add("Rename").setDefaultState(Keyboard.KEY_R, true, false, false); public ScriptEditorKeys() { super("script_editor"); From 8e9087246c6925b45815b663defba0cb2c5face7 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 15:06:54 +0200 Subject: [PATCH 190/358] Full screen script editor lol --- .../client/gui/script/GuiScriptInterface.java | 309 +++++++++++++++--- .../client/key/impl/ScriptEditorKeys.java | 3 + 2 files changed, 267 insertions(+), 45 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index d4b7edca4..5eb92682b 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -6,6 +6,7 @@ import net.minecraft.client.gui.GuiYesNo; import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.ResourceLocation; import net.minecraft.util.StatCollector; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.NoppesUtil; @@ -17,6 +18,7 @@ import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextArea; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.GuiScriptTextArea; +import noppes.npcs.client.gui.util.GuiUtil; import noppes.npcs.client.gui.util.ICustomScrollListener; import noppes.npcs.client.gui.util.IGuiData; import noppes.npcs.client.gui.util.IJTextAreaListener; @@ -27,6 +29,7 @@ import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.scripted.item.ScriptCustomItem; +import org.lwjgl.opengl.Display; import java.util.ArrayList; import java.util.Date; @@ -45,6 +48,30 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected boolean loaded = false; private Map textAreas = new HashMap<>(); + + // ==================== FULLSCREEN MODE ==================== + /** Whether the editor viewport is currently in fullscreen mode */ + private static boolean isFullscreen = false; + + /** + * Fullscreen Layout Configuration + * Adjust these values to customize the fullscreen viewport appearance. + * All padding values are in scaled GUI pixels. + */ + public static class FullscreenConfig { + /** Padding from the top edge of the screen */ + public static int paddingTop = 8; + /** Padding from the bottom edge of the screen */ + public static int paddingBottom = 8; + /** Padding from the left edge of the screen */ + public static int paddingLeft = 12; + /** Padding from the right edge of the screen */ + public static int paddingRight = 12; + } + + /** Fullscreen toggle button drawn at top-right of viewport */ + private final FullscreenButton fullscreenButton = new FullscreenButton(); + public GuiScriptInterface() { this.drawDefaultBackground = true; this.closeOnEsc = true; @@ -53,7 +80,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } public void initGui() { - //this.xSize = (int)((double)this.width * 0.88D); + // ==================== BASE LAYOUT CALCULATION ==================== this.ySize = (int) ((double) this.xSize * 0.56D); if ((double) this.ySize > (double) this.height * 0.95D) { this.ySize = (int) ((double) this.height * 0.95D); @@ -64,6 +91,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb super.initGui(); this.guiTop += 10; int yoffset = (int) ((double) this.ySize * 0.02D); + + // ==================== TAB BUTTONS ==================== GuiMenuTopButton top; this.addTopButton(top = new GuiMenuTopButton(0, this.guiLeft + 4, this.guiTop - 17, "gui.settings")); @@ -88,9 +117,46 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.activeTab = 0; top = this.getTopButton(0); } - top.active = true; + + // ==================== SCRIPT EDITOR TAB (activeTab > 0) ==================== if (this.activeTab > 0) { + initScriptEditorTab(yoffset); + } else { + // ==================== SETTINGS TAB (activeTab == 0) ==================== + initSettingsTab(yoffset); + } + + this.xSize = 420; + this.ySize = 256; + } + + /** + * Initialize the script editor tab layout. + * Handles both normal and fullscreen modes. + */ + private void initScriptEditorTab(int yoffset) { + ScriptContainer container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); + + // ==================== CALCULATE VIEWPORT BOUNDS ==================== + int editorX, editorY, editorWidth, editorHeight; + + if (isFullscreen) { + // Fullscreen: viewport fills screen with configured padding + editorX = FullscreenConfig.paddingLeft; + editorY = FullscreenConfig.paddingTop; + editorWidth = this.width - FullscreenConfig.paddingLeft - FullscreenConfig.paddingRight; + editorHeight = this.height - FullscreenConfig.paddingTop - FullscreenConfig.paddingBottom; + } else { + // Normal: viewport within the GUI panel, leaving space for right panel + editorX = guiLeft + 1 + yoffset; + editorY = guiTop + yoffset; + editorWidth = xSize - 108 - yoffset; + editorHeight = (int) (ySize * 0.96) - yoffset * 2; + } + + // ==================== HOOKS SCROLL (hidden in fullscreen) ==================== + if (!isFullscreen) { GuiCustomScroll hooks = new GuiCustomScroll(this, 1); hooks.allowUserInput = false; hooks.setSize(108, 198); @@ -107,37 +173,45 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb GuiNpcLabel hookLabel = new GuiNpcLabel(0, "script.hooks", hooks.guiLeft, guiTop + 5); hookLabel.color = 0xaaaaaa; addLabel(hookLabel); + } - ScriptContainer container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); + // ==================== SCRIPT TEXT AREA ==================== + int idx = this.activeTab - 1; + GuiScriptTextArea activeArea = getActiveScriptArea(); + if (activeArea == null) { + activeArea = new GuiScriptTextArea(this, 2, editorX, editorY, editorWidth, editorHeight, + container == null ? "" : container.script); + activeArea.setListener(this); + this.closeOnEsc(activeArea::closeOnEsc); + textAreas.put(idx, activeArea); + } else { + activeArea.init(editorX, editorY, editorWidth, editorHeight, + container == null ? "" : container.script); + } - // Ensure textAreas list has at least the same size as scripts, but only - // create/initialize the area for the active tab to avoid creating all tabs at once. - - int idx = this.activeTab - 1; - GuiScriptTextArea activeArea = getActiveScriptArea(); - if (activeArea == null) { - // Create only the active tab's editor - activeArea = new GuiScriptTextArea(this, 2, guiLeft + 1 + yoffset, guiTop + yoffset, - xSize - 108 - yoffset, (int) (ySize * 0.96) - yoffset * 2, - container == null ? "" : container.script); - activeArea.setListener(this); - this.closeOnEsc(activeArea::closeOnEsc); - textAreas.put(idx, activeArea); - } else { - // Reinitialize existing active area (position/size/text may change) - activeArea.init(guiLeft + 1 + yoffset, guiTop + yoffset, xSize - 108 - yoffset, - (int) (ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); + // Setup fullscreen key binding + GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> { + if (e.isPress()) { + toggleFullscreen(); } + }); - activeArea.enableCodeHighlighting(); - this.addTextField(activeArea); + activeArea.enableCodeHighlighting(); + this.addTextField(activeArea); + // ==================== FULLSCREEN BUTTON ==================== + int scrollbarOffset = activeArea.hasVerticalScrollbar() ? -8 : -2; + fullscreenButton.initGui(editorX + editorWidth, editorY, scrollbarOffset); + + // ==================== RIGHT PANEL BUTTONS (hidden in fullscreen) ==================== + if (!isFullscreen) { int left1 = this.guiLeft + this.xSize - 104; this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); + GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); scroll.setSize(100, (int) ((double) this.ySize * 0.54D) - yoffset * 2); scroll.guiLeft = left1; @@ -145,33 +219,64 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb if (container != null) { scroll.setList(container.scripts); } - this.addScroll(scroll); - } else { - GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); - var8.canEdit = false; - var8.setFocused(true); - this.addTextField(var8); - int var9 = this.guiLeft + this.xSize - 150; - this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); - this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); - this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, (String[]) this.languages.keySet().toArray(new String[this.languages.keySet().size()]), this.getScriptIndex())); - this.getButton(103).enabled = this.languages.size() > 0; - this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); - this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, this.handler.getEnabled() ? 1 : 0)); - if (this.player.worldObj.isRemote) { - this.addButton(new GuiNpcButton(106, var9, this.guiTop + 55, 150, 20, "script.openfolder")); - } + } + } - this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); - this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); - this.addButton(new GuiNpcButton(110, var9, this.guiTop + 99, 80, 20, "script.apidoc")); - this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); + /** + * Initialize the settings tab layout (console view). + */ + private void initSettingsTab(int yoffset) { + GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, + this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); + var8.canEdit = false; + var8.setFocused(true); + this.addTextField(var8); + + int var9 = this.guiLeft + this.xSize - 150; + this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); + this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); + this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); + this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, + (String[]) this.languages.keySet().toArray(new String[this.languages.keySet().size()]), + this.getScriptIndex())); + this.getButton(103).enabled = this.languages.size() > 0; + this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); + this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, + new String[]{"gui.no", "gui.yes"}, this.handler.getEnabled() ? 1 : 0)); + + if (this.player.worldObj.isRemote) { + this.addButton(new GuiNpcButton(106, var9, this.guiTop + 55, 150, 20, "script.openfolder")); } - this.xSize = 420; - this.ySize = 256; + this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); + this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); + this.addButton(new GuiNpcButton(110, var9, this.guiTop + 99, 80, 20, "script.apidoc")); + this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); + } + + // ==================== RENDERING ==================== + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + super.drawScreen(mouseX, mouseY, partialTicks); + + // Draw fullscreen button on top of everything when on script editor tab + if (this.activeTab > 0) { + fullscreenButton.draw(mouseX, mouseY); + } + } + + // ==================== MOUSE HANDLING ==================== + + @Override + public void mouseClicked(int mouseX, int mouseY, int mouseButton) { + // Check fullscreen button first when on script editor tab + if (this.activeTab > 0 && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { + return; + } + + super.mouseClicked(mouseX, mouseY, mouseButton); } public String previousHookClicked = ""; @@ -414,4 +519,118 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb container.script = text; initGui(); } + + // ==================== FULLSCREEN METHODS ==================== + + /** + * Toggle fullscreen mode for the editor viewport. + * When entering fullscreen, maximizes the game window if it's not already. + */ + public void toggleFullscreen() { + isFullscreen = !isFullscreen; + + // When entering fullscreen, maximize the game window if not already fullscreen + if (isFullscreen && !Display.isFullscreen()) { + // Note: Display.setFullscreen(true) would make it truly fullscreen, + // but that's usually not desirable for an in-game editor. + // The viewport will expand to fill whatever window size exists. + } + + initGui(); + } + + /** + * Check if editor is currently in fullscreen mode + */ + public boolean isEditorFullscreen() { + return isFullscreen; + } + + // ==================== FULLSCREEN BUTTON ==================== + + /** + * Button to toggle fullscreen mode, displayed at top-right corner of viewport. + * Shows expand icon when minimized, collapse icon when fullscreen. + */ + + private static final ResourceLocation TEXTURE = new ResourceLocation( + "customnpcs:textures/gui/keypreset_highres.png"); + public class FullscreenButton { + public int x, y; + public int size = 12; + public boolean hovered = false; + + /** Initialize button position relative to viewport bounds */ + public void initGui(int viewportEndX, int viewportY, int scrollbarOffset) { + // Position at top-right, accounting for scrollbar + this.x = viewportEndX + scrollbarOffset - size - 4; + this.y = viewportY + 4; + } + + /** Draw the fullscreen toggle button */ + public void draw(int mouseX, int mouseY) { + hovered = isMouseOver(mouseX, mouseY); + + // Button background + int bgColor = hovered ? 0x80666666 : 0x60444444; + GuiUtil.drawRectD(x, y, x + size, y + size, bgColor); + + // Draw corner brackets to indicate expand/collapse + int iconColor = hovered ? 0xFFFFFFFF : 0xFFAAAAAA; + int inset = 2; + int cornerLen = 3; + + if (isFullscreen) { + // Draw collapse icon (corners pointing inward) ⌜⌝⌞⌟ + // Top-left corner pointing inward + drawRect(x + inset, y + inset, x + inset + cornerLen, y + inset + 1, iconColor); + drawRect(x + inset, y + inset, x + inset + 1, y + inset + cornerLen, iconColor); + // Top-right corner pointing inward + drawRect(x + size - inset - cornerLen, y + inset, x + size - inset, y + inset + 1, iconColor); + drawRect(x + size - inset - 1, y + inset, x + size - inset, y + inset + cornerLen, iconColor); + // Bottom-left corner pointing inward + drawRect(x + inset, y + size - inset - 1, x + inset + cornerLen, y + size - inset, iconColor); + drawRect(x + inset, y + size - inset - cornerLen, x + inset + 1, y + size - inset, iconColor); + // Bottom-right corner pointing inward + drawRect(x + size - inset - cornerLen, y + size - inset - 1, x + size - inset, y + size - inset, + iconColor); + drawRect(x + size - inset - 1, y + size - inset - cornerLen, x + size - inset, y + size - inset, + iconColor); + } else { + // Draw expand icon (corners pointing outward) ⌐¬⌙⌞ + // Top-left corner pointing outward + drawRect(x + inset, y + inset, x + inset + 1, y + inset + cornerLen, iconColor); + drawRect(x + inset, y + inset, x + inset + cornerLen, y + inset + 1, iconColor); + // Top-right corner pointing outward + drawRect(x + size - inset - 1, y + inset, x + size - inset, y + inset + cornerLen, iconColor); + drawRect(x + size - inset - cornerLen, y + inset, x + size - inset, y + inset + 1, iconColor); + // Bottom-left corner pointing outward + drawRect(x + inset, y + size - inset - cornerLen, x + inset + 1, y + size - inset, iconColor); + drawRect(x + inset, y + size - inset - 1, x + inset + cornerLen, y + size - inset, iconColor); + // Bottom-right corner pointing outward + drawRect(x + size - inset - 1, y + size - inset - cornerLen, x + size - inset, y + size - inset, + iconColor); + drawRect(x + size - inset - cornerLen, y + size - inset - 1, x + size - inset, y + size - inset, + iconColor); + } + } + + public void drawRect(int x1, int y1, int x2, int y2, int color) { + GuiUtil.drawRectD(x1, y1, x2, y2, color); + } + + /** Check if mouse is over the button */ + public boolean isMouseOver(int mouseX, int mouseY) { + return mouseX >= x && mouseX < x + size && mouseY >= y && mouseY < y + size; + } + + /** Handle click - returns true if click was consumed */ + public boolean mouseClicked(int mouseX, int mouseY, int button) { + if (button == 0 && isMouseOver(mouseX, mouseY)) { + toggleFullscreen(); + return true; + } + return false; + } + } } diff --git a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java index 3891aa842..f8f262893 100644 --- a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java +++ b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java @@ -27,6 +27,9 @@ public class ScriptEditorKeys extends KeyPresetManager { // Refactoring public final KeyPreset RENAME = add("Rename").setDefaultState(Keyboard.KEY_R, true, false, false); + + // View + public final KeyPreset FULLSCREEN = add("Toggle Fullscreen").setDefaultState(Keyboard.KEY_F11, false, false, false); public ScriptEditorKeys() { super("script_editor"); From cf4c97fdf06abd603cd44338fc8d3e72cef300e1 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 15:52:52 +0200 Subject: [PATCH 191/358] Alligned top buttons and full screen button properly --- .../client/gui/script/GuiScriptInterface.java | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 5eb92682b..bc9a6f4f0 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -30,6 +30,7 @@ import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.opengl.Display; +import org.lwjgl.opengl.GL11; import java.util.ArrayList; import java.util.Date; @@ -51,7 +52,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb // ==================== FULLSCREEN MODE ==================== /** Whether the editor viewport is currently in fullscreen mode */ - private static boolean isFullscreen = false; + public static boolean isFullscreen = false; /** * Fullscreen Layout Configuration @@ -60,13 +61,13 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb */ public static class FullscreenConfig { /** Padding from the top edge of the screen */ - public static int paddingTop = 8; + public static int paddingTop = 20; /** Padding from the bottom edge of the screen */ - public static int paddingBottom = 8; + public static int paddingBottom = 20; /** Padding from the left edge of the screen */ - public static int paddingLeft = 12; + public static int paddingLeft = 20; /** Padding from the right edge of the screen */ - public static int paddingRight = 12; + public static int paddingRight = 20; } /** Fullscreen toggle button drawn at top-right of viewport */ @@ -94,7 +95,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb // ==================== TAB BUTTONS ==================== GuiMenuTopButton top; - this.addTopButton(top = new GuiMenuTopButton(0, this.guiLeft + 4, this.guiTop - 17, "gui.settings")); + int topButtonsX = isFullscreen && activeTab != 0 ? FullscreenConfig.paddingLeft : this.guiLeft + 4; + int topButtonsY = isFullscreen && activeTab != 0 ? FullscreenConfig.paddingTop - 20 : this.guiTop - 17; + this.addTopButton(top = new GuiMenuTopButton(0, topButtonsX, topButtonsY, "gui.settings")); int topXoffset = 0; int topYoffset = 0; @@ -141,6 +144,10 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb // ==================== CALCULATE VIEWPORT BOUNDS ==================== int editorX, editorY, editorWidth, editorHeight; + FullscreenConfig.paddingTop = 30; + FullscreenConfig.paddingBottom = 20; + FullscreenConfig.paddingLeft = 20; + FullscreenConfig.paddingRight = 20; if (isFullscreen) { // Fullscreen: viewport fills screen with configured padding editorX = FullscreenConfig.paddingLeft; @@ -553,9 +560,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb * Shows expand icon when minimized, collapse icon when fullscreen. */ - private static final ResourceLocation TEXTURE = new ResourceLocation( - "customnpcs:textures/gui/keypreset_highres.png"); - public class FullscreenButton { + public class FullscreenButton { public int x, y; public int size = 12; public boolean hovered = false; @@ -570,6 +575,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb /** Draw the fullscreen toggle button */ public void draw(int mouseX, int mouseY) { hovered = isMouseOver(mouseX, mouseY); + int y = this.y + getYOffset(); // Button background int bgColor = hovered ? 0x80666666 : 0x60444444; @@ -621,6 +627,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb /** Check if mouse is over the button */ public boolean isMouseOver(int mouseX, int mouseY) { + int y = this.y + getYOffset(); return mouseX >= x && mouseX < x + size && mouseY >= y && mouseY < y + size; } @@ -632,5 +639,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } return false; } + + public int getYOffset() { + return GuiScriptTextArea.searchBar.getTotalHeight(); + } } } From 22ce3d0a1aa8e82c804d8bcbd55bf5bc2aa4f97a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 15:53:14 +0200 Subject: [PATCH 192/358] Text editor gray BG accounts for full screen --- .../npcs/client/gui/util/GuiScriptTextArea.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 7c151d5be..c7dee0f26 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -6,6 +6,7 @@ import net.minecraft.client.gui.ScaledResolution; import net.minecraft.util.ChatAllowedCharacters; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; +import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; import noppes.npcs.client.gui.util.script.*; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; @@ -94,7 +95,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { public OverlayKeyPresetViewer KEYS_OVERLAY = new OverlayKeyPresetViewer(KEYS); // ==================== SEARCH/REPLACE ==================== - private static final SearchReplaceBar searchBar = new SearchReplaceBar(); + public static final SearchReplaceBar searchBar = new SearchReplaceBar(); // ==================== GO TO LINE ==================== private final GoToLineDialog goToLineDialog = new GoToLineDialog(); @@ -440,6 +441,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { } }); } + + public boolean fullscreen() { + return GuiScriptInterface.isFullscreen; + } // ==================== RENDERING ==================== public void drawTextBox(int xMouse, int yMouse) { if (!visible) @@ -454,7 +459,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { LINE_NUMBER_GUTTER_WIDTH = digitWidth + 10; // 10px total padding (5px left + 5px right) } // Draw outer border around entire area - drawRect(x - 1, y - 1, x + width + 1, y + height + 1, 0xffa0a0a0); + int offset = fullscreen() ? 2 : 1; + drawRect(x - offset, y - offset - searchBar.getTotalHeight(), x + width + offset, y + height + offset, + 0xffa0a0a0); int searchHeight = searchBar.getTotalHeight(); From 26ed55ceb590fcf391a352cf5286f2bac30ebe6e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 16:06:20 +0200 Subject: [PATCH 193/358] Corrected minimise icon --- .../client/gui/script/GuiScriptInterface.java | 33 ++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index bc9a6f4f0..1a00de918 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -587,21 +587,24 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb int cornerLen = 3; if (isFullscreen) { - // Draw collapse icon (corners pointing inward) ⌜⌝⌞⌟ - // Top-left corner pointing inward - drawRect(x + inset, y + inset, x + inset + cornerLen, y + inset + 1, iconColor); - drawRect(x + inset, y + inset, x + inset + 1, y + inset + cornerLen, iconColor); - // Top-right corner pointing inward - drawRect(x + size - inset - cornerLen, y + inset, x + size - inset, y + inset + 1, iconColor); - drawRect(x + size - inset - 1, y + inset, x + size - inset, y + inset + cornerLen, iconColor); - // Bottom-left corner pointing inward - drawRect(x + inset, y + size - inset - 1, x + inset + cornerLen, y + size - inset, iconColor); - drawRect(x + inset, y + size - inset - cornerLen, x + inset + 1, y + size - inset, iconColor); - // Bottom-right corner pointing inward - drawRect(x + size - inset - cornerLen, y + size - inset - 1, x + size - inset, y + size - inset, - iconColor); - drawRect(x + size - inset - 1, y + size - inset - cornerLen, x + size - inset, y + size - inset, - iconColor); + // Draw collapse icon (corners pointing inward) — inward L-shapes + int top = y + inset + 2; + int left = x + inset + 2; + int right = x + size - inset - 3; + int bottom = y + size - inset - 3; + + drawRect(left, top - cornerLen + 1, left + 1, top + 1, iconColor); + drawRect(left - cornerLen + 1, top, left + 1, top + 1, iconColor); + + drawRect(right, top, right + cornerLen, top + 1, iconColor); + drawRect(right, top - cornerLen + 1, right + 1, top + 1, iconColor); + + drawRect(left - cornerLen + 1, bottom, left + 1, bottom + 1, iconColor); + drawRect(left, bottom, left + 1, bottom + cornerLen, iconColor); + + drawRect(right, bottom, right + cornerLen, bottom + 1, iconColor); + drawRect(right, bottom, right + 1, bottom + cornerLen, iconColor); + } else { // Draw expand icon (corners pointing outward) ⌐¬⌙⌞ // Top-left corner pointing outward From 01e2c354d598d230e59390ca2ce15586dbe262eb Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 16:31:12 +0200 Subject: [PATCH 194/358] Adjusted OverlayKeyPreset to fullscreen mode --- .../npcs/client/gui/util/GuiScriptTextArea.java | 4 +++- .../client/gui/util/key/OverlayKeyPresetViewer.java | 12 ++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index c7dee0f26..3426bcae8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -134,7 +134,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { int xOffset = hasVerticalScrollbar() ? -8 : -2; KEYS_OVERLAY.scale = 0.75f; KEYS_OVERLAY.borderCol1 = KEYS_OVERLAY.borderCol2 = 0xFF3c3c3c; - KEYS_OVERLAY.initGui(x + (width / 4), y + 20, endX - (width / 4) + 5, endY - 28); + int overlayWidth = 160; + KEYS_OVERLAY.initGui(x + (width - overlayWidth) / 2 + 5, y + height / 10, overlayWidth, + height - height / 5 - 10); KEYS_OVERLAY.viewButton.scale = 0.45f; KEYS_OVERLAY.viewButton.initGui(endX + xOffset, endY - 26); diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java index 96d6ece84..e8fefd475 100644 --- a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -48,13 +48,13 @@ public class OverlayKeyPresetViewer { }); } - public void initGui(int startX, int startY, int endX, int endY) { + public void initGui(int startX, int startY, int width, int height) { this.startX = startX; this.startY = startY; - this.endX = endX; - this.endY = endY; - this.width = endX - startX; - this.height = endY - startY; + this.width = width; + this.height = height; + this.endX = startX + width; + this.endY = startY + height; scroll.init(); } @@ -454,7 +454,7 @@ public class OverlayKeyPresetViewer { } public int getMaxStringWidth() { - return (int) (getWidth() * RELATIVE_MAX_DESC_WIDTH); + return (int) Math.max(getWidth() * RELATIVE_MAX_DESC_WIDTH, 0); } } From 83f45fe6b8077160abb3abaf5eda7af8cf3c970d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 17:39:56 +0200 Subject: [PATCH 195/358] Fixed imports not resolving if wrapped to new line --- .../gui/util/script/ClassPathFinder.java | 48 +++--- .../client/gui/util/script/IndentHelper.java | 2 +- .../gui/util/script/JavaTextContainer.java | 143 +++++++++++------- 3 files changed, 121 insertions(+), 72 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java index 326df3b79..4ceb33a90 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java @@ -62,24 +62,27 @@ public class ClassPathFinder { public final String classPortion; // The class part (e.g., "List" or "IOverlay.ColorType") public final List classSegments; // Each class/inner-class segment with its type public final int invalidStartOffset; // Character offset where invalid portion starts (-1 if all valid or all invalid) + public final int packageSegmentCount; // Number of dot-separated identifier tokens that belong to the package portion private ResolveResult(boolean found, ClassInfo classInfo, String packagePortion, - String classPortion, List classSegments, int invalidStartOffset) { + String classPortion, List classSegments, int invalidStartOffset, + int packageSegmentCount) { this.found = found; this.classInfo = classInfo; this.packagePortion = packagePortion; this.classPortion = classPortion; this.classSegments = classSegments != null ? classSegments : Collections.emptyList(); this.invalidStartOffset = invalidStartOffset; + this.packageSegmentCount = packageSegmentCount; } - public static ResolveResult notFound(String packagePortion, String classPortion, int invalidStartOffset) { - return new ResolveResult(false, null, packagePortion, classPortion, null, invalidStartOffset); + public static ResolveResult notFound(String packagePortion, String classPortion, int invalidStartOffset, int packageSegmentCount) { + return new ResolveResult(false, null, packagePortion, classPortion, null, invalidStartOffset, packageSegmentCount); } public static ResolveResult found(ClassInfo info, String packagePortion, - String classPortion, List segments) { - return new ResolveResult(true, info, packagePortion, classPortion, segments, -1); + String classPortion, List segments, int packageSegmentCount) { + return new ResolveResult(true, info, packagePortion, classPortion, segments, -1, packageSegmentCount); } } @@ -113,15 +116,20 @@ public class ClassPathFinder { */ public ResolveResult resolve(String importPath) { if (importPath == null || importPath.isEmpty()) { - return ResolveResult.notFound("", "", -1); + return ResolveResult.notFound("", "", -1, 0); } + // Normalize whitespace/newlines around dots so wrapped imports like + // "kamkeel.npcdbc.api.client.overlay.IOverlay.\n TextureFunction" become + // "kamkeel.npcdbc.api.client.overlay.IOverlay.TextureFunction" before resolving. + String normalizedPath = importPath.replaceAll("\\s*\\.\\s*", ".").trim(); + // Handle trailing dot (user is still typing a package path like "java.util.") - boolean trailingDot = importPath.endsWith("."); - String pathToResolve = trailingDot ? importPath.substring(0, importPath.length() - 1) : importPath; + boolean trailingDot = normalizedPath.endsWith("."); + String pathToResolve = trailingDot ? normalizedPath.substring(0, normalizedPath.length() - 1) : normalizedPath; if (pathToResolve.isEmpty()) { - return ResolveResult.notFound("", "", -1); + return ResolveResult.notFound("", "", -1, 0); } String[] segments = pathToResolve.split("\\."); @@ -136,18 +144,18 @@ public class ClassPathFinder { int longestValid = findLongestValidPackagePrefixIndex(segments); if (longestValid == n) { // Entire path is a valid package (or cached as valid) - return ResolveResult.notFound(pathToResolve, "", -1); + return ResolveResult.notFound(pathToResolve, "", -1, n); } if (longestValid > 0) { String validPrefix = buildPackagePortion(segments, longestValid); String invalidRemainder = buildClassPortion(segments, longestValid, n); int invalidOffset = validPrefix.length() + 1; // after the dot - return ResolveResult.notFound(validPrefix, invalidRemainder, invalidOffset); + return ResolveResult.notFound(validPrefix, invalidRemainder, invalidOffset, longestValid); } // Nothing validated - return ResolveResult.notFound("", pathToResolve, 0); + return ResolveResult.notFound("", pathToResolve, 0, 0); } // Trailing dot is handled above for lowercase-only paths; for mixed-case paths @@ -180,15 +188,15 @@ public class ClassPathFinder { // Calculate where the invalid portion starts (after valid class + dot) int validEndOffset = packagePortion.length() + (packagePortion.isEmpty() ? 0 : 1) + validClassPortion.length() + 1; return new ResolveResult(false, info, packagePortion, - fullClassPortion, classSegments, validEndOffset); + fullClassPortion, classSegments, validEndOffset, classStartIdx); } // Full resolution successful List classSegments = buildClassSegments(segments, classStartIdx, n); - return ResolveResult.found( + return ResolveResult.found( new ClassInfo(info.resolvedName, info.type, packagePortion.length()), - packagePortion, validClassPortion, classSegments - ); + packagePortion, validClassPortion, classSegments, classStartIdx + ); } } } @@ -208,22 +216,22 @@ public class ClassPathFinder { // Found a divergence from a known valid package String validPrefix = buildPackagePortion(segments, typoIndex); int invalidOffset = validPrefix.isEmpty() ? 0 : validPrefix.length() + 1; - return ResolveResult.notFound(validPrefix, buildClassPortion(segments, typoIndex, n), invalidOffset); + return ResolveResult.notFound(validPrefix, buildClassPortion(segments, typoIndex, n), invalidOffset, typoIndex); } // No typo detected - assume package is valid, only class portion is invalid int invalidOffset = packagePortion.length() + 1; // After the package dot - return ResolveResult.notFound(packagePortion, classPortion, invalidOffset); + return ResolveResult.notFound(packagePortion, classPortion, invalidOffset, firstUpperIndex); } else if (firstUpperIndex == 0) { // Entire path starts with uppercase (e.g., "String" or "MyClass.Inner") // Treat as class with no package - return ResolveResult.notFound("", pathToResolve, 0); + return ResolveResult.notFound("", pathToResolve, 0, 0); } else { // All lowercase, no uppercase found - entire path is package-like // If trailing dot was handled above, this is just a package without class - return ResolveResult.notFound(pathToResolve, "", -1); + return ResolveResult.notFound(pathToResolve, "", -1, n); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java b/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java index fd6da00f0..a978dc724 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java @@ -171,7 +171,7 @@ public class IndentHelper { settings.wrapComments = true; // Use 75% of viewport width as max line length for some margin int charWidth = 6; // Approximate character width - settings.maxLineLength = Math.max(60, (int)(viewportWidth * 0.75 / charWidth)); + settings.maxLineLength = Math.max(60, (int)(viewportWidth * 0.8f / charWidth)); } String formatted = helper.format(indented); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 8c7489b2d..d31743a5b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -37,9 +37,11 @@ public class JavaTextContainer extends TextContainer { public static final Pattern NUMBER = Pattern.compile( "\\b-?(?:0[xX][\\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?(?:[fFbBdDlLsS])?|NaN|null|Infinity|true|false)\\b"); - // Import statement: import [static] package.path.ClassName [.*]; (semicolon optional for live typing) - public static final Pattern IMPORT = Pattern.compile( - "(?m)\\bimport\\s+(?:static\\s+)?([a-zA-Z_][a-zA-Z0-9_.]*?)(?:\\s*\\.\\s*\\*)?\\s*(?:;|$)"); + // Import statement: import [static] package.path.ClassName [.*]; (semicolon optional for live typing) + // Allows optional whitespace (including newlines) around the dots so wrapped imports match. + // Match imports while allowing optional whitespace/newlines around dots. + public static final Pattern IMPORT = Pattern.compile( + "(?m)\\bimport\\s+(?:static\\s+)?([A-Za-z_][A-Za-z0-9_]*(?:\\s*\\.\\s*[A-Za-z_][A-Za-z0-9_]*)*)(?:\\s*\\.\\s*\\*)?\\s*(?:;|$)"); // ClassPathFinder for resolving imports and determining class types private final ClassPathFinder classPathFinder = new ClassPathFinder(); @@ -51,6 +53,26 @@ public class JavaTextContainer extends TextContainer { super(text); } + // Map an offset into the normalized path (tokens joined with '.') back to + // an absolute index in the original substring `orig` (absolute positions start at pathStart). + // The normalization removes whitespace/newlines around dots; this function + // advances a normalized counter when it encounters identifier chars or dots + // and returns the corresponding original index (absolute). + private int mapNormalizedOffsetToOriginal(String orig, int normalizedOffset, int pathStart) { + if (orig == null || orig.isEmpty()) return pathStart; + int norm = 0; + for (int i = 0; i < orig.length(); i++) { + char c = orig.charAt(i); + if (Character.isLetterOrDigit(c) || c == '_' || c == '.') { + norm++; + } + if (norm >= normalizedOffset) { + return pathStart + i; + } + } + return pathStart + orig.length(); + } + public void init(String text,int width, int height) { this.text = text == null ? "" : text.replaceAll("\\r?\\n|\\r", "\n"); lines.clear(); @@ -519,75 +541,94 @@ public class JavaTextContainer extends TextContainer { // Use ClassPathFinder to resolve the import ClassPathFinder.ResolveResult result = classPathFinder.resolve(fullPath); - String packagePortion = result.packagePortion; - String classPortion = result.classPortion; - boolean hasPackage = packagePortion != null && !packagePortion.isEmpty(); - boolean hasClass = classPortion != null && !classPortion.isEmpty(); + // Tokenize the original matched substring so we can map normalized segments + String orig = text.substring(pathStart, pathEnd); + java.util.List tokens = new java.util.ArrayList<>(); + java.util.List tokenStarts = new java.util.ArrayList<>(); + java.util.List tokenEnds = new java.util.ArrayList<>(); + Matcher idm = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*").matcher(orig); + while (idm.find()) { + tokens.add(idm.group()); + tokenStarts.add(idm.start()); + tokenEnds.add(idm.end()); + } + int pkgSegments = result.packageSegmentCount; + boolean hasPackage = pkgSegments > 0; + + // Highlight package tokens (if any) if (result.found && result.invalidStartOffset < 0) { - // Fully resolved - highlight package (blue) and class segments (typed) - if (hasPackage) { - int pkgEnd = pathStart + packagePortion.length(); + if (hasPackage && pkgSegments <= tokenEnds.size()) { + int pkgEnd = pathStart + tokenEnds.get(pkgSegments - 1); marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); } - - if (hasClass && !result.classSegments.isEmpty()) { - int classStart = pathStart + (hasPackage ? packagePortion.length() + 1 : 0); - for (ClassPathFinder.ClassSegment seg : result.classSegments) { - int segEnd = classStart + seg.name.length(); + + // Highlight class segments + if (result.classSegments != null && !result.classSegments.isEmpty()) { + int classTokenStart = hasPackage ? pkgSegments : 0; + for (int si = 0; si < result.classSegments.size(); si++) { + int tokIdx = classTokenStart + si; + if (tokIdx >= 0 && tokIdx < tokenStarts.size()) { + int s = pathStart + tokenStarts.get(tokIdx); + int e = pathStart + tokenEnds.get(tokIdx); + ClassPathFinder.ClassSegment seg = result.classSegments.get(si); + TokenType segType; + switch (seg.type) { + case INTERFACE: segType = TokenType.INTERFACE_DECL; break; + case ENUM: segType = TokenType.ENUM_DECL; break; + default: segType = TokenType.IMPORTED_CLASS; break; + } + marks.add(new Mark(s, e, segType)); + } + } + } + } else if (result.classSegments != null && !result.classSegments.isEmpty()) { + // Partial resolution - mark package and valid class segments + if (hasPackage && pkgSegments <= tokenEnds.size()) { + int pkgEnd = pathStart + tokenEnds.get(pkgSegments - 1); + marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); + } + + int classTokenStart = hasPackage ? pkgSegments : 0; + for (int si = 0; si < result.classSegments.size(); si++) { + int tokIdx = classTokenStart + si; + if (tokIdx >= 0 && tokIdx < tokenStarts.size()) { + int s = pathStart + tokenStarts.get(tokIdx); + int e = pathStart + tokenEnds.get(tokIdx); + ClassPathFinder.ClassSegment seg = result.classSegments.get(si); TokenType segType; switch (seg.type) { case INTERFACE: segType = TokenType.INTERFACE_DECL; break; case ENUM: segType = TokenType.ENUM_DECL; break; default: segType = TokenType.IMPORTED_CLASS; break; } - marks.add(new Mark(classStart, segEnd, segType)); - classStart = segEnd + 1; + marks.add(new Mark(s, e, segType)); } } - } else if (result.classSegments != null && !result.classSegments.isEmpty()) { - // Partial resolution - some class segments are valid, rest is invalid - // (e.g., IOverlay.ColorType resolved but ".idk" is invalid) - if (hasPackage) { - int pkgEnd = pathStart + packagePortion.length(); - marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); - } - - int classStart = pathStart + (hasPackage ? packagePortion.length() + 1 : 0); - for (ClassPathFinder.ClassSegment seg : result.classSegments) { - int segEnd = classStart + seg.name.length(); - TokenType segType; - switch (seg.type) { - case INTERFACE: segType = TokenType.INTERFACE_DECL; break; - case ENUM: segType = TokenType.ENUM_DECL; break; - default: segType = TokenType.IMPORTED_CLASS; break; - } - marks.add(new Mark(classStart, segEnd, segType)); - classStart = segEnd + 1; - } - - // Mark remaining invalid portion in red + + // Mark remaining invalid portion in red (map normalized offset to original) if (result.invalidStartOffset >= 0) { - int invalidStart = pathStart + result.invalidStartOffset; - if (invalidStart < pathEnd) { - marks.add(new Mark(invalidStart, pathEnd, TokenType.UNDEFINED_VAR)); + int invalidAbs = mapNormalizedOffsetToOriginal(orig, result.invalidStartOffset, pathStart); + if (invalidAbs < pathEnd) { + marks.add(new Mark(invalidAbs, pathEnd, TokenType.UNDEFINED_VAR)); } } } else { // Not resolved at all - use invalidStartOffset to determine valid/invalid split if (result.invalidStartOffset >= 0 && hasPackage) { - // Valid package prefix, invalid class - int pkgEnd = pathStart + packagePortion.length(); - marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); - - int invalidStart = pathStart + result.invalidStartOffset; - if (invalidStart < pathEnd) { - marks.add(new Mark(invalidStart, pathEnd, TokenType.UNDEFINED_VAR)); + if (pkgSegments <= tokenEnds.size()) { + int pkgEnd = pathStart + tokenEnds.get(pkgSegments - 1); + marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); } - } else if (fullPath.endsWith(".") && hasPackage && result.invalidStartOffset < 0 && !hasClass) { + + int invalidAbs = mapNormalizedOffsetToOriginal(orig, result.invalidStartOffset, pathStart); + if (invalidAbs < pathEnd) { + marks.add(new Mark(invalidAbs, pathEnd, TokenType.UNDEFINED_VAR)); + } + } else if (fullPath.endsWith(".") && hasPackage && result.invalidStartOffset < 0) { // Trailing dot on a fully-valid package - all blue marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); - } else if (result.invalidStartOffset < 0 && hasPackage && !hasClass) { + } else if (result.invalidStartOffset < 0 && hasPackage) { // All-lowercase path treated as valid package marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); } else { From 0d03871444c5f98ed70428e7672c1e9e9443fca4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 17:56:37 +0200 Subject: [PATCH 196/358] Fixed last dot in import "impot java." breaking syntax highlight --- .../noppes/npcs/client/gui/util/script/JavaTextContainer.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index d31743a5b..0fe40e117 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -41,7 +41,7 @@ public class JavaTextContainer extends TextContainer { // Allows optional whitespace (including newlines) around the dots so wrapped imports match. // Match imports while allowing optional whitespace/newlines around dots. public static final Pattern IMPORT = Pattern.compile( - "(?m)\\bimport\\s+(?:static\\s+)?([A-Za-z_][A-Za-z0-9_]*(?:\\s*\\.\\s*[A-Za-z_][A-Za-z0-9_]*)*)(?:\\s*\\.\\s*\\*)?\\s*(?:;|$)"); + "(?m)\\bimport\\s+(?:static\\s+)?([A-Za-z_][A-Za-z0-9_]*(?:\\s*\\.\\s*[A-Za-z_][A-Za-z0-9_]*)*)(?:\\s*\\.\\s*\\*?)?\\s*(?:;|$)"); // ClassPathFinder for resolving imports and determining class types private final ClassPathFinder classPathFinder = new ClassPathFinder(); From e0d322694583af2b30efaee0b006d0412ccd5e8d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 18:45:54 +0200 Subject: [PATCH 197/358] Fully functional syntax highlight of classes resolved from import wildcards i.e. import java.util.* List list --- .../gui/util/script/ClassPathFinder.java | 58 +++++++++++++++++-- .../gui/util/script/JavaTextContainer.java | 6 +- 2 files changed, 56 insertions(+), 8 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java index 4ceb33a90..f12c08a8b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java @@ -326,6 +326,16 @@ public class ClassPathFinder { * Get the ClassInfo for a simple class name, using provided imports map */ public ClassInfo resolveSimpleName(String simpleName, Map importedClasses) { + return resolveSimpleName(simpleName, importedClasses, Collections.emptySet()); + } + + /** + * Resolve a simple class name using explicit imports, java.lang, or wildcard imports. + * Supports package wildcard imports (e.g., java.util.*) and class-member wildcards + * that have been added to `importedPackages` (e.g., kamkeel..IOverlay.* treated as + * attempting to resolve inner types of IOverlay). + */ + public ClassInfo resolveSimpleName(String simpleName, Map importedClasses, Set importedPackages) { if (simpleName == null || simpleName.isEmpty()) { return null; } @@ -333,12 +343,45 @@ public class ClassPathFinder { // Check imported classes first String fullName = importedClasses.get(simpleName); if (fullName != null) { - return tryResolveClass(fullName); + ClassInfo info = tryResolveClass(fullName); + if (info != null) return info; } // Check java.lang classes if (JAVA_LANG_CLASSES.contains(simpleName)) { - return tryResolveClass("java.lang." + simpleName); + ClassInfo info = tryResolveClass("java.lang." + simpleName); + if (info != null) return info; + } + + // Check wildcard imports: these can be package wildcards (pkg.*) or + // class-member wildcards (Outer.*) where we want to allow resolving + // inner types of the Outer class. Heuristic: if the last segment of + // the imported path starts with uppercase, treat it as a class; otherwise + // treat it as a package. + if (importedPackages != null && !importedPackages.isEmpty()) { + for (String p : importedPackages) { + if (p == null || p.isEmpty()) continue; + String[] segs = p.split("\\."); + String last = segs[segs.length - 1]; + boolean lastIsClass = last.length() > 0 && Character.isUpperCase(last.charAt(0)); + + // Try package wildcard: pkg.SimpleName + if (!lastIsClass) { + String candidate = p + "." + simpleName; + ClassInfo info = tryResolveClass(candidate); + if (info != null) return info; + } else { + // Try inner type under the class: Outer$Inner + String candidateInner = p + "$" + simpleName; + ClassInfo info = tryResolveClass(candidateInner); + if (info != null) return info; + + // Also try package-like (in case someone imported a class-looking path but it is actually a package) + String candidatePkg = p + "." + simpleName; + info = tryResolveClass(candidatePkg); + if (info != null) return info; + } + } } return null; @@ -536,8 +579,12 @@ public class ClassPathFinder { * @return List of TypeOccurrence with position and type info */ public List parseGenericTypes(String genericContent, Map importedClasses) { + return parseGenericTypes(genericContent, importedClasses, Collections.emptySet()); + } + + public List parseGenericTypes(String genericContent, Map importedClasses, Set importedPackages) { List results = new ArrayList<>(); - parseGenericTypesRecursive(genericContent, 0, importedClasses, results); + parseGenericTypesRecursive(genericContent, 0, importedClasses, importedPackages, results); return results; } @@ -560,6 +607,7 @@ public class ClassPathFinder { private void parseGenericTypesRecursive(String content, int baseOffset, Map importedClasses, + Set importedPackages, List results) { if (content == null || content.isEmpty()) return; @@ -584,7 +632,7 @@ public class ClassPathFinder { // Only process if it looks like a type name (starts with uppercase) if (Character.isUpperCase(typeName.charAt(0))) { // Resolve the type - ClassInfo info = resolveSimpleName(typeName, importedClasses); + ClassInfo info = resolveSimpleName(typeName, importedClasses, importedPackages); ClassType classType = (info != null) ? info.type : ClassType.CLASS; results.add(new TypeOccurrence(baseOffset + start, baseOffset + i, typeName, classType)); @@ -613,7 +661,7 @@ public class ClassPathFinder { // Recursively parse nested content if (nestedStart < i - 1) { String nestedContent = content.substring(nestedStart, i - 1); - parseGenericTypesRecursive(nestedContent, baseOffset + nestedStart, importedClasses, results); + parseGenericTypesRecursive(nestedContent, baseOffset + nestedStart, importedClasses, importedPackages, results); } } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index 0fe40e117..c8e552356 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -164,7 +164,7 @@ public class JavaTextContainer extends TextContainer { private void highlightGenericTypes(String genericContent, int contentStart, List marks, List excluded) { if (genericContent == null || genericContent.isEmpty()) return; - List occurrences = classPathFinder.parseGenericTypes(genericContent, importedClasses); + List occurrences = classPathFinder.parseGenericTypes(genericContent, importedClasses, importedPackages); for (ClassPathFinder.TypeOccurrence occ : occurrences) { int absStart = contentStart + occ.startOffset; @@ -680,7 +680,7 @@ public class JavaTextContainer extends TextContainer { if (isImported && !isShadowed) { // Resolve the class to determine its type - ClassPathFinder.ClassInfo info = classPathFinder.resolveSimpleName(className, importedClasses); + ClassPathFinder.ClassInfo info = classPathFinder.resolveSimpleName(className, importedClasses, importedPackages); TokenType tokenType = TokenType.IMPORTED_CLASS; if (info != null) { switch (info.type) { @@ -884,7 +884,7 @@ public class JavaTextContainer extends TextContainer { // This looks like a type declaration // Resolve the main type - ClassPathFinder.ClassInfo info = classPathFinder.resolveSimpleName(typeName, importedClasses); + ClassPathFinder.ClassInfo info = classPathFinder.resolveSimpleName(typeName, importedClasses, importedPackages); TokenType tokenType = TokenType.UNDEFINED_VAR; if (info != null) { switch (info.type) { From c13faca8ae0a191d287c81c1e6c66d5e085cb4e2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 20:33:45 +0200 Subject: [PATCH 198/358] Huge import detection logic rewrite plus optimisations --- .../gui/util/script/ClassPathFinder.java | 12 + .../gui/util/script/JavaTextContainer.java | 293 ++++++++++++------ 2 files changed, 213 insertions(+), 92 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java index f12c08a8b..958b2c542 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java @@ -406,6 +406,10 @@ public class ClassPathFinder { Class clazz = Class.forName(className); result = createClassInfo(className, clazz); classCache.put(className, result); + + int lastDot = className.lastIndexOf("."); + if (lastDot > 0) + registerValidPackage(className.substring(0, lastDot)); } catch (ClassNotFoundException | NoClassDefFoundError e) { // Not found with given name, will be cached as null } catch (LinkageError e) { @@ -415,6 +419,14 @@ public class ClassPathFinder { return result; } + /** + * Public wrapper to attempt resolving a fully-qualified class name. + * Returns null if not found. + */ + public ClassInfo tryResolveClassName(String className) { + return tryResolveClass(className); + } + private ClassInfo createClassInfo(String resolvedName, Class clazz) { ClassType type; if (clazz.isInterface()) { diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index c8e552356..dbc1d3237 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -116,10 +116,36 @@ public class JavaTextContainer extends TextContainer { private java.util.Map importedClasses = new java.util.HashMap<>(); // simpleName -> fullName private java.util.Set importedPackages = new java.util.HashSet<>(); // wildcard imports private java.util.Set unresolvedClasses = new java.util.HashSet<>(); // classes that failed to resolve + private java.util.List importEntries = new java.util.ArrayList<>(); + + private static class ImportEntry { + public final String fullPath; // normalized path (no trailing . or *) + public final String matchText; // full matched import text + public final int pathStart; // start of the path group + public final int pathEnd; // end of the path group + public final int importKwStart; // start of the 'import' keyword + public final int importKwEnd; // end of the 'import' keyword + public final int starPos; // absolute pos of '*' in matchText, or -1 + public final boolean isWildcard; + // Cached resolve result (may be null for wildcard imports or if not resolved yet) + public ClassPathFinder.ResolveResult resolveResult; + + public ImportEntry(String fullPath, String matchText, int pathStart, int pathEnd, int importKwStart, int importKwEnd, int starPos, boolean isWildcard) { + this.fullPath = fullPath; + this.matchText = matchText; + this.pathStart = pathStart; + this.pathEnd = pathEnd; + this.importKwStart = importKwStart; + this.importKwEnd = importKwEnd; + this.starPos = starPos; + this.isWildcard = isWildcard; + } + } private void collectImports() { importedClasses.clear(); importedPackages.clear(); + importEntries.clear(); unresolvedClasses.clear(); classPathFinder.clearCache(); @@ -130,18 +156,29 @@ public class JavaTextContainer extends TextContainer { continue; String fullPath = m.group(1).trim(); String matchText = m.group(0); + int pathStart = m.start(1); + int pathEnd = m.end(1); + int importKwStart = m.start(); + int importKwEnd = Math.min(importKwStart + 6, text.length()); - // Check if wildcard import (import a.b.*;) - if (matchText.contains("*")) { + boolean isWildcard = matchText.contains("*"); + int starIndex = matchText.indexOf('*'); + int absStar = starIndex != -1 ? m.start() + starIndex : -1; + + // Record entry for finalization later + importEntries.add(new ImportEntry(fullPath, matchText, pathStart, pathEnd, importKwStart, importKwEnd, absStar, isWildcard)); + + // For wildcard imports we register the package for resolution; final marking deferred + if (isWildcard) { importedPackages.add(fullPath); } else { - // Resolve the import to get the proper class name + // Resolve the import to get the proper class name (so simple-name lookups can work now) ClassPathFinder.ResolveResult result = classPathFinder.resolve(fullPath); - + // Extract simple name (last segment) int lastDot = fullPath.lastIndexOf('.'); String simpleName = lastDot >= 0 ? fullPath.substring(lastDot + 1) : fullPath; - + if (!simpleName.isEmpty()) { if (result.found && result.classInfo != null) { // Store with resolved name (has $ for inner classes) @@ -153,6 +190,9 @@ public class JavaTextContainer extends TextContainer { unresolvedClasses.add(simpleName); } } + // Cache the full ResolveResult on the ImportEntry for reuse in finalizeImports + ImportEntry last = importEntries.get(importEntries.size() - 1); + last.resolveResult = result; } } } @@ -522,49 +562,66 @@ public class JavaTextContainer extends TextContainer { return false; } - private void collectImportStatements(List marks) { - List excluded = MethodBlock.getExcludedRanges(text); - Matcher m = IMPORT.matcher(text); - while (m.find()) { - if (isInExcludedRange(m.start(), excluded)) - continue; - String fullPath = m.group(1).trim(); - int pathStart = m.start(1); - int pathEnd = m.end(1); - String matchText = m.group(0); + private void finalizeImports(List marks) { + if (importEntries.isEmpty()) return; - // Highlight the 'import' keyword itself - int importKwStart = m.start(); - int importKwEnd = Math.min(importKwStart + 6, text.length()); - marks.add(new Mark(importKwStart, importKwEnd, TokenType.IMPORT_KEYWORD)); + for (ImportEntry ie : importEntries) { + // Highlight the 'import' keyword + marks.add(new Mark(ie.importKwStart, ie.importKwEnd, TokenType.IMPORT_KEYWORD)); - // Use ClassPathFinder to resolve the import - ClassPathFinder.ResolveResult result = classPathFinder.resolve(fullPath); + String fullPath = ie.fullPath; + int pathStart = ie.pathStart; + int pathEnd = ie.pathEnd; + String matchText = ie.matchText; - // Tokenize the original matched substring so we can map normalized segments - String orig = text.substring(pathStart, pathEnd); - java.util.List tokens = new java.util.ArrayList<>(); - java.util.List tokenStarts = new java.util.ArrayList<>(); - java.util.List tokenEnds = new java.util.ArrayList<>(); - Matcher idm = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*").matcher(orig); - while (idm.find()) { - tokens.add(idm.group()); - tokenStarts.add(idm.start()); - tokenEnds.add(idm.end()); - } + if (!ie.isWildcard) { + // Non-wildcard: reuse cached resolve result if available, otherwise resolve now + ClassPathFinder.ResolveResult result = classPathFinder.resolve(fullPath); - int pkgSegments = result.packageSegmentCount; - boolean hasPackage = pkgSegments > 0; - - // Highlight package tokens (if any) - if (result.found && result.invalidStartOffset < 0) { - if (hasPackage && pkgSegments <= tokenEnds.size()) { - int pkgEnd = pathStart + tokenEnds.get(pkgSegments - 1); - marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); + String orig = text.substring(pathStart, pathEnd); + List tokens = new ArrayList<>(); + List tokenStarts = new ArrayList<>(); + List tokenEnds = new ArrayList<>(); + Matcher idm = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*").matcher(orig); + while (idm.find()) { + tokens.add(idm.group()); + tokenStarts.add(idm.start()); + tokenEnds.add(idm.end()); } - // Highlight class segments - if (result.classSegments != null && !result.classSegments.isEmpty()) { + int pkgSegments = result.packageSegmentCount; + boolean hasPackage = pkgSegments > 0; + + if (result.found && result.invalidStartOffset < 0) { + if (hasPackage && pkgSegments <= tokenEnds.size()) { + int pkgEnd = pathStart + tokenEnds.get(pkgSegments - 1); + marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); + } + + if (result.classSegments != null && !result.classSegments.isEmpty()) { + int classTokenStart = hasPackage ? pkgSegments : 0; + for (int si = 0; si < result.classSegments.size(); si++) { + int tokIdx = classTokenStart + si; + if (tokIdx >= 0 && tokIdx < tokenStarts.size()) { + int s = pathStart + tokenStarts.get(tokIdx); + int e = pathStart + tokenEnds.get(tokIdx); + ClassPathFinder.ClassSegment seg = result.classSegments.get(si); + TokenType segType; + switch (seg.type) { + case INTERFACE: segType = TokenType.INTERFACE_DECL; break; + case ENUM: segType = TokenType.ENUM_DECL; break; + default: segType = TokenType.IMPORTED_CLASS; break; + } + marks.add(new Mark(s, e, segType)); + } + } + } + } else if (result.classSegments != null && !result.classSegments.isEmpty()) { + if (hasPackage && pkgSegments <= tokenEnds.size()) { + int pkgEnd = pathStart + tokenEnds.get(pkgSegments - 1); + marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); + } + int classTokenStart = hasPackage ? pkgSegments : 0; for (int si = 0; si < result.classSegments.size(); si++) { int tokIdx = classTokenStart + si; @@ -581,71 +638,118 @@ public class JavaTextContainer extends TextContainer { marks.add(new Mark(s, e, segType)); } } - } - } else if (result.classSegments != null && !result.classSegments.isEmpty()) { - // Partial resolution - mark package and valid class segments - if (hasPackage && pkgSegments <= tokenEnds.size()) { - int pkgEnd = pathStart + tokenEnds.get(pkgSegments - 1); - marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); - } - int classTokenStart = hasPackage ? pkgSegments : 0; - for (int si = 0; si < result.classSegments.size(); si++) { - int tokIdx = classTokenStart + si; - if (tokIdx >= 0 && tokIdx < tokenStarts.size()) { - int s = pathStart + tokenStarts.get(tokIdx); - int e = pathStart + tokenEnds.get(tokIdx); - ClassPathFinder.ClassSegment seg = result.classSegments.get(si); - TokenType segType; - switch (seg.type) { - case INTERFACE: segType = TokenType.INTERFACE_DECL; break; - case ENUM: segType = TokenType.ENUM_DECL; break; - default: segType = TokenType.IMPORTED_CLASS; break; + if (result.invalidStartOffset >= 0) { + int invalidAbs = mapNormalizedOffsetToOriginal(orig, result.invalidStartOffset, pathStart); + if (invalidAbs < pathEnd) { + marks.add(new Mark(invalidAbs, pathEnd, TokenType.UNDEFINED_VAR)); } - marks.add(new Mark(s, e, segType)); + } + } else { + if (result.invalidStartOffset >= 0 && hasPackage) { + if (pkgSegments <= tokenEnds.size()) { + int pkgEnd = pathStart + tokenEnds.get(pkgSegments - 1); + marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); + } + + int invalidAbs = mapNormalizedOffsetToOriginal(orig, result.invalidStartOffset, pathStart); + if (invalidAbs < pathEnd) { + marks.add(new Mark(invalidAbs, pathEnd, TokenType.UNDEFINED_VAR)); + } + } else if (fullPath.endsWith(".") && hasPackage && result.invalidStartOffset < 0) { + marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); + } else if (result.invalidStartOffset < 0 && hasPackage) { + marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); + } else { + marks.add(new Mark(pathStart, pathEnd, TokenType.UNDEFINED_VAR)); } } - // Mark remaining invalid portion in red (map normalized offset to original) - if (result.invalidStartOffset >= 0) { - int invalidAbs = mapNormalizedOffsetToOriginal(orig, result.invalidStartOffset, pathStart); - if (invalidAbs < pathEnd) { - marks.add(new Mark(invalidAbs, pathEnd, TokenType.UNDEFINED_VAR)); - } + // Highlight .* if present (shouldn't for non-wildcard, but keep behavior) + int starIndex = matchText.indexOf('*'); + if (starIndex != -1) { + int absStar = ie.importKwStart + (starIndex - 0); + marks.add(new Mark(absStar, absStar + 1, TokenType.DEFAULT)); } } else { - // Not resolved at all - use invalidStartOffset to determine valid/invalid split - if (result.invalidStartOffset >= 0 && hasPackage) { - if (pkgSegments <= tokenEnds.size()) { - int pkgEnd = pathStart + tokenEnds.get(pkgSegments - 1); - marks.add(new Mark(pathStart, pkgEnd, TokenType.TYPE_DECL)); - } + // Wildcard import: try to determine whether this is a valid package or + // a class whose inner types are being imported (Outer.*) + boolean marked = false; - int invalidAbs = mapNormalizedOffsetToOriginal(orig, result.invalidStartOffset, pathStart); - if (invalidAbs < pathEnd) { - marks.add(new Mark(invalidAbs, pathEnd, TokenType.UNDEFINED_VAR)); - } - } else if (fullPath.endsWith(".") && hasPackage && result.invalidStartOffset < 0) { - // Trailing dot on a fully-valid package - all blue - marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); - } else if (result.invalidStartOffset < 0 && hasPackage) { - // All-lowercase path treated as valid package + // If package is known valid, mark as package + if (classPathFinder.isValidPackage(fullPath)) { marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); + marked = true; } else { - // Entire path is invalid + // Probe uppercase identifiers in file to see if any inner types resolve + java.util.Set uppercaseIds = new java.util.HashSet<>(); + Matcher idm = Pattern.compile("\\b([A-Z][a-zA-Z0-9_]*)\\b").matcher(text); + while (idm.find()) uppercaseIds.add(idm.group(1)); + + for (String ident : uppercaseIds) { + String candInner = fullPath + "$" + ident; + ClassPathFinder.ClassInfo infoInner = classPathFinder.tryResolveClassName(candInner); + if (infoInner != null) { + // outer may be a class - try to get its info + ClassPathFinder.ClassInfo outerInfo = classPathFinder.tryResolveClassName(fullPath); + // mark package tokens (all but last) as TYPE_DECL and last as outer type + String orig = text.substring(pathStart, pathEnd); + List tokens = new ArrayList<>(); + List tokenStarts = new ArrayList<>(); + List tokenEnds = new ArrayList<>(); + Matcher tokm = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*").matcher(orig); + while (tokm.find()) { tokens.add(tokm.group()); tokenStarts.add(tokm.start()); tokenEnds.add(tokm.end()); } + + int tokenCount = tokenStarts.size(); + if (tokenCount > 0) { + int pkgEnd = pathStart + (tokenEnds.get(Math.max(0, tokenCount - 2))); + if (tokenCount >= 2) { + // mark package (all except last) + marks.add(new Mark(pathStart, pathStart + tokenEnds.get(tokenCount - 2), TokenType.TYPE_DECL)); + } + // mark outer class token (last) + int s = pathStart + tokenStarts.get(tokenCount - 1); + int e = pathStart + tokenEnds.get(tokenCount - 1); + TokenType segType = TokenType.IMPORTED_CLASS; + if (outerInfo != null) { + switch (outerInfo.type) { + case INTERFACE: segType = TokenType.INTERFACE_DECL; break; + case ENUM: segType = TokenType.ENUM_DECL; break; + default: segType = TokenType.IMPORTED_CLASS; break; + } + } + marks.add(new Mark(s, e, segType)); + } + marked = true; + // ensure package listed in importedPackages + importedPackages.add(fullPath); + break; + } + + // Try package-like resolution: fullPath + "." + ident + String candDot = fullPath + "." + ident; + if (classPathFinder.tryResolveClassName(candDot) != null) { + marks.add(new Mark(pathStart, pathEnd, TokenType.TYPE_DECL)); + marked = true; + importedPackages.add(fullPath); + break; + } + } + } + + if (!marked) { + // Unknown wildcard import - mark as undefined marks.add(new Mark(pathStart, pathEnd, TokenType.UNDEFINED_VAR)); } - } - // Highlight .* if present - int starIndex = matchText.indexOf('*'); - if (starIndex != -1) { - int absStar = m.start() + starIndex; - marks.add(new Mark(absStar, absStar + 1, TokenType.DEFAULT)); + // highlight star + if (ie.starPos >= 0) marks.add(new Mark(ie.starPos, ie.starPos + 1, TokenType.DEFAULT)); } } } + + private void collectImportedClassUsages(List marks) { List excluded = MethodBlock.getExcludedRanges(text); // Find identifiers followed by dot (e.g., Math.min, ArrayList.class) @@ -723,7 +827,8 @@ public class JavaTextContainer extends TextContainer { collectPatternMatches(marks, COMMENT, TokenType.COMMENT); collectPatternMatches(marks, STRING, TokenType.STRING); - collectImportStatements(marks); // Highlight import statements + // Import marking is deferred until after type/usages are discovered. + // finalizeImports(marks) will create import marks later. collectClassDeclarations(marks); // Highlight class/interface/enum declarations // Highlight general keywords @@ -741,6 +846,9 @@ public class JavaTextContainer extends TextContainer { highlightVariableReferences(marks); collectImportedClassUsages(marks); // Highlight imported class usages (e.g., Math.min) + // Finalize import markings after usages/types have been discovered + finalizeImports(marks); + marks = resolveConflicts(marks); // Compute indent guides based on matched braces, ignoring strings/comments @@ -754,7 +862,8 @@ public class JavaTextContainer extends TextContainer { // Step 3: Assign tokens to the correct lines for (LineData line : lines) { int cursor = line.start; - + // collectImportStatements used to create marks here; we now defer marking until types/usages are discovered + // so finalizeImports will create the visual marks from recorded import entries. for (Mark mark : marks) { if (mark.end <= line.start || mark.start >= line.end) continue; From 242d9fa1cb2db0cf90b8cbe11727b63327353513 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 20:40:12 +0200 Subject: [PATCH 199/358] crazy PackageFinder --- .../client/gui/util/script/PackageFinder.java | 161 ++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java diff --git a/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java new file mode 100644 index 000000000..b5c2fb7a4 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java @@ -0,0 +1,161 @@ +package noppes.npcs.client.gui.util.script; + +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.Set; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +/** + * PackageFinder builds an index of package names found on the provided ClassLoader + * (scanning jar files and directories). After construction queries via {@link #contains} + * are O(1) HashSet lookups. + */ +public class PackageFinder { + private static PackageFinder INSTANCE = null; + private final Set packages = new HashSet<>(); + + public static PackageFinder Instance() throws IOException { + if (INSTANCE == null) + INSTANCE = new PackageFinder(Thread.currentThread().getContextClassLoader()); + + return INSTANCE; + } + + private PackageFinder(ClassLoader cl) throws IOException { + if (cl == null) + cl = Thread.currentThread().getContextClassLoader(); + + // Try URLClassLoader first (common on most JVM versions used here) + if (cl instanceof URLClassLoader) { + URL[] urls = ((URLClassLoader) cl).getURLs(); + for (URL url : urls) { + try { + String protocol = url.getProtocol(); + if ("file".equals(protocol)) { + File f; + try { + URI u = url.toURI(); + f = new File(u); + } catch (Exception ex) { + f = new File(url.getPath()); + } + if (f.exists()) { + if (f.isDirectory()) + scanDirectory(f); + else if (f.isFile() && f.getName().endsWith(".jar")) + scanJar(f); + } + } else if ("jar".equals(protocol)) { + // jar:file:/path/to/jar.jar!/... + String s = url.getPath(); + int excl = s.indexOf('!'); + String jarPath = (excl >= 0) ? s.substring(0, excl) : s; + if (jarPath.startsWith("file:")) + jarPath = jarPath.substring(5); + File jf = new File(jarPath); + if (jf.exists()) + scanJar(jf); + } + } catch (IOException ignored) { + // ignore individual entries + } + } + } else { + // Fallback: scan system classpath as a best-effort + String cp = System.getProperty("java.class.path", ""); + String[] entries = cp.split(File.pathSeparator); + for (String e : entries) { + if (e == null || e.isEmpty()) + continue; + File f = new File(e); + if (!f.exists()) + continue; + if (f.isDirectory()) + scanDirectory(f); + else if (f.isFile() && f.getName().endsWith(".jar")) + scanJar(f); + } + } + } + + private void scanJar(File jarFile) throws IOException { + try (JarFile jf = new JarFile(jarFile)) { + Enumeration en = jf.entries(); + while (en.hasMoreElements()) { + JarEntry je = en.nextElement(); + String name = je.getName(); + if (name.endsWith(".class")) { + int idx = name.lastIndexOf('/'); + if (idx > 0) { + String pkg = name.substring(0, idx).replace('/', '.'); + addPackageHierarchy(pkg); + } + } + } + } + } + + private void scanDirectory(File dir) throws IOException { + final Path root = dir.toPath(); + if (!Files.exists(root)) + return; + Files.walk(root).forEach(p -> { + try { + if (Files.isRegularFile(p) && p.toString().endsWith(".class")) { + Path parent = p.getParent(); + if (parent == null) + return; + Path rel = root.relativize(parent); + String rp = rel.toString(); + if (rp.isEmpty()) + return; + // Normalize separators to dot + String pkg = rp.replace(File.separatorChar, '.'); + addPackageHierarchy(pkg); + } + } catch (Exception ignored) { + } + }); + } + + private void addPackageHierarchy(String pkg) { + if (pkg == null || pkg.isEmpty()) + return; + String cur = pkg; + while (true) { + if (!cur.isEmpty()) + packages.add(cur); + int idx = cur.lastIndexOf('.'); + if (idx < 0) + break; + cur = cur.substring(0, idx); + } + } + + public boolean contains(String pkg) { + if (pkg == null || pkg.isEmpty()) + return false; + return packages.contains(pkg); + } + + // Convenience factory using the context classloader + public static PackageFinder fromContext() throws IOException { + return new PackageFinder(Thread.currentThread().getContextClassLoader()); + } + + // Demo main that checks the given package (or 'kamkeel.npcdbc' by default) + public static void main(String[] args) throws Exception { + PackageFinder pf = PackageFinder.fromContext(); + String pkg = (args != null && args.length > 0) ? args[0] : "kamkeel.npcdbc"; + System.out.println(pkg + " exists? " + pf.contains(pkg)); + } +} From a77b550b21645bfeda89e38c2b5287f4acdc9859 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 21:04:59 +0200 Subject: [PATCH 200/358] Properly package index on FMLLoadCompleteEenet --- src/main/java/noppes/npcs/CommonProxy.java | 3 + src/main/java/noppes/npcs/CustomNpcs.java | 12 ++-- .../java/noppes/npcs/client/ClientProxy.java | 8 +++ .../client/gui/util/script/PackageFinder.java | 71 ++++++++++++++----- 4 files changed, 72 insertions(+), 22 deletions(-) diff --git a/src/main/java/noppes/npcs/CommonProxy.java b/src/main/java/noppes/npcs/CommonProxy.java index 58f931ab8..251272c1d 100644 --- a/src/main/java/noppes/npcs/CommonProxy.java +++ b/src/main/java/noppes/npcs/CommonProxy.java @@ -200,4 +200,7 @@ public class CommonProxy implements IGuiHandler { public boolean isGUIOpen() { return false; } + + public void buildPackageIndex() { + } } diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index ad42bc419..d20828922 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -4,12 +4,7 @@ import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.Mod; import cpw.mods.fml.common.Mod.EventHandler; import cpw.mods.fml.common.SidedProxy; -import cpw.mods.fml.common.event.FMLInitializationEvent; -import cpw.mods.fml.common.event.FMLPreInitializationEvent; -import cpw.mods.fml.common.event.FMLServerAboutToStartEvent; -import cpw.mods.fml.common.event.FMLServerStartedEvent; -import cpw.mods.fml.common.event.FMLServerStartingEvent; -import cpw.mods.fml.common.event.FMLServerStoppedEvent; +import cpw.mods.fml.common.event.*; import cpw.mods.fml.common.network.NetworkRegistry; import cpw.mods.fml.common.registry.EntityRegistry; import cpw.mods.fml.relauncher.Side; @@ -300,6 +295,11 @@ public class CustomNpcs { PacketHandler.Instance.registerChannels(); } + @EventHandler + public void loadComplete(FMLLoadCompleteEvent ev) { + proxy.buildPackageIndex(); + } + @EventHandler public void setAboutToStart(FMLServerAboutToStartEvent event) { globalJaninoCompiler = null; diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index f0346a5d3..da455f5a0 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -123,6 +123,7 @@ import noppes.npcs.client.gui.roles.GuiNpcTransporter; import noppes.npcs.client.gui.script.GuiScriptBlock; import noppes.npcs.client.gui.script.GuiScriptGlobal; import noppes.npcs.client.gui.script.GuiScriptItem; +import noppes.npcs.client.gui.util.script.PackageFinder; import noppes.npcs.client.model.ModelNPCGolem; import noppes.npcs.client.model.ModelNpcCrystal; import noppes.npcs.client.model.ModelNpcDragon; @@ -735,6 +736,13 @@ public class ClientProxy extends CommonProxy { return Minecraft.getMinecraft().currentScreen != null; } + public void buildPackageIndex() { + try { + PackageFinder.init(Thread.currentThread().getContextClassLoader()); + } catch (IOException e) { + } + } + public static class FontContainer { public StringCache textFont = null; public boolean useCustomFont = true; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java index b5c2fb7a4..391ea0e14 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java @@ -20,19 +20,19 @@ import java.util.jar.JarFile; * are O(1) HashSet lookups. */ public class PackageFinder { - private static PackageFinder INSTANCE = null; + private static volatile PackageFinder INSTANCE = null; private final Set packages = new HashSet<>(); - - public static PackageFinder Instance() throws IOException { - if (INSTANCE == null) - INSTANCE = new PackageFinder(Thread.currentThread().getContextClassLoader()); - - return INSTANCE; - } + private final ClassLoader classLoader; private PackageFinder(ClassLoader cl) throws IOException { - if (cl == null) - cl = Thread.currentThread().getContextClassLoader(); + this.classLoader = cl == null ? Thread.currentThread().getContextClassLoader() : cl; + buildIndex(); + } + + private void buildIndex() throws IOException { + packages.clear(); + + ClassLoader cl = this.classLoader; // Try URLClassLoader first (common on most JVM versions used here) if (cl instanceof URLClassLoader) { @@ -147,15 +147,54 @@ public class PackageFinder { return packages.contains(pkg); } - // Convenience factory using the context classloader + public static boolean find(String pkg) { + PackageFinder pf = getInstance(); + if (pf != null) + return pf.contains(pkg); + return false; + } + + /** + * Initialize the global singleton with the provided ClassLoader (or context CL if null). + * This is idempotent; calling again will re-create the index. + */ + public static synchronized void init(ClassLoader cl) throws IOException { + if (INSTANCE == null) { + INSTANCE = new PackageFinder(cl); + } else { + // rebuild index with new classloader if provided + if (cl != null) { + INSTANCE = new PackageFinder(cl); + } else { + INSTANCE.buildIndex(); + } + } + } + + /** + * Get the initialized singleton. Call {@link #init(ClassLoader)} first. + */ + public static PackageFinder getInstance() { + if (INSTANCE == null) { + try { + INSTANCE = fromContext(); + } catch (IOException e) { + } + } + return INSTANCE; + } + + /** + * Convenience factory using the context classloader and building a standalone finder (not touching singleton). + */ public static PackageFinder fromContext() throws IOException { return new PackageFinder(Thread.currentThread().getContextClassLoader()); } - // Demo main that checks the given package (or 'kamkeel.npcdbc' by default) - public static void main(String[] args) throws Exception { - PackageFinder pf = PackageFinder.fromContext(); - String pkg = (args != null && args.length > 0) ? args[0] : "kamkeel.npcdbc"; - System.out.println(pkg + " exists? " + pf.contains(pkg)); + /** + * Reload the index using the configured ClassLoader. + */ + public synchronized void reload() throws IOException { + buildIndex(); } } From f785b034ff539568e16fc1d75c672ec9618fb154 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 21:05:23 +0200 Subject: [PATCH 201/358] Implemented PackageFinder.find in ClassPathFinder --- .../npcs/client/gui/util/script/ClassPathFinder.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java index 958b2c542..e54f5d878 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java @@ -306,9 +306,14 @@ public class ClassPathFinder { if (validPackages.contains(packagePath)) { return true; } + + // Try to consult the global PackageFinder index if initialized (fast O(1) lookup) + if (PackageFinder.find(packagePath)) { + registerValidPackage(packagePath); + return true; + } - // Try to find any class under this package to validate it - // Common test classes for well-known packages + // Fallback: try to find any class under this package to validate it String[] testClasses = getTestClassesForPackage(packagePath); for (String testClass : testClasses) { try { From 358fd7e643ed34a694213f68c54940e19de7b562 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 22 Dec 2025 21:19:26 +0200 Subject: [PATCH 202/358] Unresolved object class types are now UNDEFINED --- .../noppes/npcs/client/gui/util/script/JavaTextContainer.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index dbc1d3237..ccdd9cc44 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -793,6 +793,8 @@ public class JavaTextContainer extends TextContainer { } } marks.add(new Mark(start, end, tokenType)); + } else if (!isImported && !isShadowed) { + marks.add(new Mark(start, end, TokenType.UNDEFINED_VAR)); } } } From f78fc30660cabd588c64a89191ca95b15b8d5798 Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Sat, 3 Jan 2026 02:54:08 +0100 Subject: [PATCH 203/358] Ban java.lang.reflect package --- src/main/java/noppes/npcs/CustomNpcs.java | 3 ++- src/main/java/somehussar/janino/AdvancedClassFilter.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index ad42bc419..3a6ac92d9 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -173,7 +173,8 @@ public class CustomNpcs { AdvancedClassFilter filter = new AdvancedClassFilter() .addRegexes("noppes\\.npcs\\.api\\..*") .banRegexes(".*ClassLoader.*", - ".*File*."); + ".*File*.", + "java\\.lang\\.reflect\\..*"); for (Consumer consumer : clientClassFilterConsumer) { consumer.accept(filter); diff --git a/src/main/java/somehussar/janino/AdvancedClassFilter.java b/src/main/java/somehussar/janino/AdvancedClassFilter.java index fb1fac6e8..b48de2c30 100644 --- a/src/main/java/somehussar/janino/AdvancedClassFilter.java +++ b/src/main/java/somehussar/janino/AdvancedClassFilter.java @@ -17,7 +17,7 @@ public class AdvancedClassFilter implements LoadClassCondition { public AdvancedClassFilter() { addClasses("java.io.Serializable", "java.util.Iterator"); - addRegexes("java\\.lang\\.."); + addRegexes("java\\.lang\\..*"); } public AdvancedClassFilter addRegexes(String... classRegexes) { From b5df6b17094216d838280b64942299587bde256b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 9 Jan 2026 23:44:07 +0200 Subject: [PATCH 204/358] Moved all janino classes to CNPC+ janino-scripts branch --- src/api | 2 +- .../client/gui/script/GuiJaninoScript.java | 406 ++++++++++++++++++ .../java/noppes/npcs/janino/JaninoScript.java | 276 ++++++++++++ .../noppes/npcs/janino/JaninoScriptable.java | 38 ++ 4 files changed, 721 insertions(+), 1 deletion(-) create mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java create mode 100644 src/main/java/noppes/npcs/janino/JaninoScript.java create mode 100644 src/main/java/noppes/npcs/janino/JaninoScriptable.java diff --git a/src/api b/src/api index 305a3778c..ed5640d23 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 305a3778c86fed2680b8cad173b7e5fb0f3c98e6 +Subproject commit ed5640d2385d5626a8dff2dd2d0bc59b6c455f4c diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java new file mode 100644 index 000000000..874beca4b --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java @@ -0,0 +1,406 @@ +package noppes.npcs.client.gui.script; + +import kamkeel.npcdbc.client.gui.component.SubGuiSelectList; +import noppes.npcs.janino.JaninoScript; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.*; +import net.minecraft.util.StatCollector; +import noppes.npcs.NoppesStringUtils; +import noppes.npcs.client.gui.util.*; +import noppes.npcs.controllers.ScriptController; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.stream.Collectors; + +public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback, ITextChangeListener, ICustomScrollListener, ISubGuiListener { + + private GuiScriptTextArea textArea; + private int activeTab = 0; + public List allExternalScripts = new ArrayList<>(); + private final int scriptLimit = 1; + public String previousHookClicked = ""; + + // GUI display list + List hooklist = new ArrayList<>(); + + // Map from display name (with parameters if overloaded) to Method + Map hookMap = new HashMap<>(); + + public final GuiScreen parent; + private final JaninoScript container; + + public Runnable saveFunction; + + public GuiJaninoScript(GuiScreen parent, JaninoScript handler) { + this.drawDefaultBackground = true; + this.closeOnEsc = true; + this.xSize = 420; + this.setBackground("menubg.png"); + + this.parent = parent; + this.container = handler; + + createHookList(); + + String lang = handler.getLanguage(); + allExternalScripts.addAll(ScriptController.Instance.getScripts(lang)); + } + + public GuiJaninoScript setSave(Runnable run) { + saveFunction = run; + return this; + } + + public void createHookList() { + // Group all declared methods of the handler's type by their method name + Map> grouped = Arrays.stream(container.type.getDeclaredMethods()) + .collect(Collectors.groupingBy(Method::getName)); + + for (Map.Entry> entry : grouped.entrySet()) { + List methods = entry.getValue(); + + // Sort overloads by number of parameters (ascending) + // This ensures the simplest overload comes first + methods.sort(Comparator.comparingInt(Method::getParameterCount)); + boolean first = true; + + + for (Method m : methods) { + if (Modifier.isFinal(m.getModifiers())) + continue; //skip finals + + String displayName = first ? m.getName() : + m.getName() + "(" + Arrays.stream(m.getParameterTypes()) + .map(Class::getSimpleName) + .collect(Collectors.joining(", ")) + ")"; + + + hooklist.add(displayName); + hookMap.put(displayName, m); + first = false; + } + } + } + public void initGui() { + this.ySize = (int) ((double) this.xSize * 0.56); + if ((double) this.ySize > (double) this.height * 0.95) { + this.ySize = (int) ((double) this.height * 0.95); + this.xSize = (int) ((double) this.ySize / 0.56); + } + + this.bgScale = (float) this.xSize / 400.0F; + super.initGui(); + this.guiTop += 10; + int yoffset = (int) ((double) this.ySize * 0.02); + GuiMenuTopButton top; + addTopButton(new GuiMenuTopButton(-1, guiLeft + xSize-2 , guiTop - 17, "X")); + this.addTopButton(top = new GuiMenuTopButton(0, this.guiLeft + 4, this.guiTop - 17, "gui.settings")); + int topXoffset = 0; + int topYoffset = 0; + + + if (this.container != null) { + this.addTopButton(top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, "Script")); + } else { + this.addTopButton(new GuiMenuTopButton(this.scriptLimit, top.xPosition + top.width, top.yPosition, "+")); + } + + top = this.getTopButton(this.activeTab); + if (top == null) { + this.activeTab = 0; + top = this.getTopButton(0); + } + + top.active = true; + if (this.activeTab == 1) { + GuiCustomScroll hooks = new GuiCustomScroll(this, 1); + hooks.allowUserInput = false; + hooks.setSize(108, 198); + hooks.guiLeft = this.guiLeft - 110; + hooks.guiTop = this.guiTop + 14; + + hooks.setUnsortedList(this.hooklist); + this.addScroll(hooks); + GuiNpcLabel hookLabel = new GuiNpcLabel(0, "script.hooks", hooks.guiLeft, this.guiTop + 5); + hookLabel.color = 11184810; + this.addLabel(hookLabel); + JaninoScript container = this.container; + if (textArea == null) + textArea = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); + else + textArea.init(this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); + textArea.enableCodeHighlighting(); + textArea.setListener(this); + this.closeOnEsc(textArea::closeOnEsc); + this.addTextField(textArea); + + int left1 = this.guiLeft + this.xSize - 104; + this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); + this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); + this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); + this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); + + this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); + // this.getButton(107).enabled = false; + + GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); + scroll.setSize(100, (int) ((double) this.ySize * 0.54) - yoffset * 2); + scroll.guiLeft = left1; + scroll.guiTop = this.guiTop + 88 + yoffset; + if (container != null) + scroll.setList(container.externalScripts); + + this.addScroll(scroll); + } else { + GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); + var8.enabled = false; + this.addTextField(var8); + int var9 = this.guiLeft + this.xSize - 150; + this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); + this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); + this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); + String containerLang = container.getLanguage(); + this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, new String[]{containerLang.equals("Java") ? "Janino (JVM 8)" : containerLang}, + this.getScriptIndex())); + // this.getButton(103).enabled = !this.languages.isEmpty(); + this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); + this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, this.container.getEnabled() ? 1 : 0)); + + this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); + this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); + this.addButton(new GuiNpcButton(110, var9, this.guiTop + 99, 80, 20, "script.apidoc")); + this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); + } + + this.xSize = 420; + this.ySize = 256; + } + + public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { + String hook = scroll.getSelected(); + if (this.previousHookClicked.equals(hook)) { + String addString = ""; + if (!this.getTextField(2) + .getText() + .isEmpty()) { + addString = addString + "\n"; + } + + addString = addString + generateMethodStub(hookMap.get(hook)); + this.getTextField(2) + .setText(this.getTextField(2) + .getText() + addString); + this.previousHookClicked = ""; + } else { + this.previousHookClicked = hook; + } + } + + public void confirmClicked(boolean flag, int i) { + if (flag) { + if (i == 0) + this.openLink("https://www.curseforge.com/minecraft/mc-mods/customnpc-plus"); + else if (i == 1) + this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); + else if (i == 2) + this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); + else if (i == 3) + this.openLink("http://www.minecraftforge.net/forum/index.php/board,122.0.html"); + + if (i == 10) { //Remove + this.container.setScript(""); + this.activeTab = 0; + } else if (i == 101) { //Paste + this.getTextField(2) + .setText(NoppesStringUtils.getClipboardContents()); + this.setScript(); + } else if (i == 102) { //Clear + if (this.activeTab == 1) + container.script = ""; + else + this.container.clearConsole(); + + this.initGui(); + } + } + + this.displayGuiScreen(this); + } + + protected void actionPerformed(GuiButton guibutton) { + if (guibutton.id == -1) { + close(); + return; + } + + if (guibutton.id == 0) { + this.setScript(); + this.activeTab = 0; + this.initGui(); + container.ensureCompiled(); + } + + if (guibutton.id == this.scriptLimit) { + // if (container.container == null) + //this.container.container = new ScriptContainer(this.container); + // else + // this.setScript(); + this.activeTab = 1; + this.initGui(); + } + + if (guibutton.id == 109) { + this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true)); + } else if (guibutton.id == 110) { + this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/KAMKEEL/CustomNPC-Plus-API", 1, true)); + } else if (guibutton.id == 111) { + this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/Noppes/CustomNPCsAPI", 2, true)); + } else if (guibutton.id == 112) { + this.displayGuiScreen(new GuiConfirmOpenLink(this, "http://www.minecraftforge.net/forum/index.php/board,122.0.html", 3, true)); + } + + GuiYesNo confirmScreen; + + //Copy + if (guibutton.id == 100) { + NoppesStringUtils.setClipboardContents(this.getTextField(2) + .getText()); + } + //Paste + else if (guibutton.id == 101) { + GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), StatCollector.translateToLocal("gui.sure"), 101); + this.displayGuiScreen(guiyesno); + } + //Clear + else if (guibutton.id == 102) { + confirmScreen = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); + this.displayGuiScreen(confirmScreen); + } + //Enable + else if (guibutton.id == 104) { + this.container.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); + } + //Remove + else if (guibutton.id == 105) { + confirmScreen = new GuiYesNo(this, "", guibutton.displayString, 10); + this.displayGuiScreen(confirmScreen); + } + //Load Script + else if (guibutton.id == 107) { + setSubGui(new SubGuiSelectList(allExternalScripts, container.externalScripts, "All Scripts", "Selected Scripts")); + } + } + + public void subGuiClosed(SubGuiInterface sub) { + if (sub instanceof SubGuiSelectList) + container.setExternalScripts(((SubGuiSelectList) sub).selected); + + } + + /** + * Generates a stub string for a given Method. + */ + public static String generateMethodStub(Method method) { + String mods = java.lang.reflect.Modifier.toString(method.getModifiers()); + if (!mods.isEmpty()) + mods += " "; + + String returnTypeStr = method.getReturnType() + .getSimpleName(); + String name = method.getName(); + + Map typeCount = new HashMap<>(); + String params = Arrays.stream(method.getParameters()) + .map(p -> { + String typeName = p.getType() + .getSimpleName(); + String baseName = Character.toLowerCase(typeName.charAt(0)) + typeName.substring(1); + + int count = typeCount.getOrDefault(baseName, 0) + 1; + typeCount.put(baseName, count); + + // append number if same type of arg exists + String paramName = count == 1 ? baseName : baseName + (count - 1); + + return typeName + " " + paramName; + }) + .collect(Collectors.joining(", ")); + + String body; + Class returnType = method.getReturnType(); + if (returnType == void.class) + body = ""; + else if (returnType.isPrimitive()) { + if (returnType == boolean.class) + body = " return false;"; + else if (returnType == char.class) + body = " return '\\0';"; + else + body = " return 0;"; + } else { + body = " return null;"; + } + + return String.format("%s%s %s(%s) {\n%s\n}\n", mods, returnTypeStr, name, params, body); + } + + private String getConsoleText() { + Map map = this.container.getConsoleText(); + StringBuilder builder = new StringBuilder(); + Iterator var3 = map.entrySet() + .iterator(); + + while (var3.hasNext()) { + Map.Entry entry = (Map.Entry) var3.next(); + builder.insert(0, new Date(entry.getKey()) + entry.getValue() + "\n"); + } + + return builder.toString(); + } + + private int getScriptIndex() { + return 0; + } + + private void setScript() { + if (this.activeTab == 1) { + String text = this.getTextField(2) + .getText(); + text = text.replace("\r\n", "\n"); + text = text.replace("\r", "\n"); + container.setScript(text); + } + } + + public void save() { + if (saveFunction != null) + saveFunction.run(); + else if (parent instanceof GuiNPCInterface) + ((GuiNPCInterface) parent).save(); + } + + public void textUpdate(String text) { +// container.setScript(text); + } + + public static GuiJaninoScript create(GuiScreen parent, JaninoScript script, int width, int height) { + GuiJaninoScript gui = new GuiJaninoScript(parent, script); + gui.setWorldAndResolution(Minecraft.getMinecraft(), width, height); + Minecraft.getMinecraft().currentScreen = gui; + gui.initGui(); + return gui; + } + + + @Override + public void close() { + this.save(); + this.setScript(); + container.ensureCompiled(); + parent.setWorldAndResolution(mc, width, height); + parent.initGui(); + mc.currentScreen = parent; + } +} diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java new file mode 100644 index 000000000..f836e97cf --- /dev/null +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -0,0 +1,276 @@ +package noppes.npcs.janino; + +import cpw.mods.fml.common.FMLCommonHandler; +import io.github.somehussar.janinoloader.api.script.IScriptBodyBuilder; +import io.github.somehussar.janinoloader.api.script.IScriptClassBody; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.CustomNpcs; +import noppes.npcs.NBTTags; +import noppes.npcs.config.ConfigScript; +import noppes.npcs.controllers.ScriptController; +import org.codehaus.commons.compiler.InternalCompilerException; +import org.codehaus.commons.compiler.Sandbox; + +import java.security.*; +import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; + +public abstract class JaninoScript { + public boolean enabled; + private String language = "Java"; + public TreeMap console = new TreeMap(); + + /** + * Text written in the script GUI + */ + public String script = ""; + /** + * This script's selected external files + * stored in "world/customnpcs/scripts/java" + */ + public List externalScripts = new ArrayList<>(); + /** + * To evaluate if script/externalScripts were changed or not, + * and compile the fullScript freshly if so. + */ + public boolean evaluated; + + public final Class type; + protected final Sandbox sandbox; + public final IScriptBodyBuilder builder; + protected IScriptClassBody scriptBody; + + protected JaninoScript(Class type, Consumer> buildSettings) { + this.type = type; + this.builder = IScriptBodyBuilder + .getBuilder(type, CustomNpcs.getClientCompiler()); + buildSettings.accept(builder); + + Permissions permissions = new Permissions(); + permissions.setReadOnly(); + sandbox = new Sandbox(permissions); + + this.scriptBody = builder.build(); + } + + protected T getUnsafe() { + return scriptBody.get(); + } + + public R call(Function fn) { + ensureCompiled(); + + T t = getUnsafe(); + if (t == null) + return null; + +// CodeSource cs = t.getClass().getProtectionDomain().getCodeSource(); +// ProtectionDomain pd = new ProtectionDomain(cs, new Permissions()); + + try { + return sandbox.confine((PrivilegedAction) () -> fn.apply(t)); +// return AccessController.doPrivileged((PrivilegedAction) () -> fn.apply(t), new AccessControlContext( +// new ProtectionDomain[] { +// pd +// } +// )); + } catch (Exception e) { + appendConsole("Runtime Error: " + e.getMessage()); + return null; + } + } + + public void run(Consumer fn) { + ensureCompiled(); + + T t = getUnsafe(); + if (t == null) + return; + + try { + sandbox.confine((PrivilegedAction) () -> { + fn.accept(t); + return null; + }); + } catch (Exception e) { + appendConsole("Runtime Error: " + e.getMessage()); + } + } + + public void unload() { + } + + /** + * Feed the code into the engine and compile it + */ + public void compileScript(String code) { + + try { + scriptBody.setScript(code); + } catch (InternalCompilerException e ) { + Throwable parentCause = null; + Throwable cause = e; + + appendConsole("Compilation error: " + e.getMessage()); + + while (cause.getCause() != null) { parentCause = cause; cause = cause.getCause(); } + Throwable rootCause = cause; + + if (e != cause) { + appendConsole("\n"); + + if (parentCause != null) + appendConsole(parentCause.getMessage()); + appendConsole(rootCause.getMessage()); + } + } catch (Exception e) { + appendConsole("Unknown error: " + e.getMessage()); + } + } + + public void compileScript() { + compileScript(getFullCode()); + } + + public void ensureCompiled() { + if (!isEnabled()) { + if (!evaluated) { + compileScript(""); + evaluated = true; + } + return; + } + + int global = ScriptController.Instance.globalRevision; + + if (!evaluated || global != lastSeenGlobalRevision) { + compileScript(); + lastSeenGlobalRevision = global; + evaluated = true; + } + } + + /** + * globalRevision is incremented on server + * each time externalScripts are updated + * then sent to client. + */ + private int lastSeenGlobalRevision; + + private String getFullCode() { + if (!isEnabled()) + return ""; + + StringBuilder sb = new StringBuilder(); + + if (ConfigScript.RunLoadedScriptsFirst) + this.appendExternalScripts(sb); + + // Main script + if (this.script != null && !this.script.isEmpty()) + sb.append(this.script).append("\n"); + + if (!ConfigScript.RunLoadedScriptsFirst) + this.appendExternalScripts(sb); + + return sb.toString(); + } + + private void appendExternalScripts(StringBuilder sb) { + for (String scriptName : externalScripts) { + String code = ScriptController.Instance.scripts.get(scriptName); + if (code != null && !code.isEmpty()) + sb.append(code).append("\n"); + } + } + + public void setScript(String script) { + this.script = script; + this.evaluated = false; + } + + public void setExternalScripts(List externalScripts) { + this.externalScripts = externalScripts; + this.evaluated = false; + } + + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setBoolean("enabled", enabled); + compound.setTag("console", NBTTags.NBTLongStringMap(this.console)); + compound.setTag("externalScripts", NBTTags.nbtStringList(this.externalScripts)); + compound.setString("script", script); + return compound; + } + + public JaninoScript readFromNBT(NBTTagCompound compound) { + this.enabled = compound.getBoolean("enabled"); + this.console = NBTTags.GetLongStringMap(compound.getTagList("console", 10)); + setExternalScripts(NBTTags.getStringList(compound.getTagList("externalScripts", 10))); + setScript(compound.getString("script")); //after everything else is read + return this; + } + + public boolean isEnabled() { + return this.enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled; + } + + public boolean isClient() { + return FMLCommonHandler.instance() + .getEffectiveSide() + .isClient(); + } + + public boolean getEnabled() { + return this.enabled; + } + + public void setEnabled(boolean b) { + if (this.enabled != b) { + this.enabled = b; + this.evaluated = false; + } + } + + public String getLanguage() { + return this.language; + } + + public void appendConsole(String message) { + if (message != null && !message.isEmpty()) { + long time = System.currentTimeMillis(); + if (this.console.containsKey(time)) { + message = (String) this.console.get(time) + "\n" + message; + } + + this.console.put(time, message); + + while (this.console.size() > 40) { + this.console.remove(this.console.firstKey()); + } + } + } + + public Map getConsoleText() { + TreeMap map = new TreeMap(); + int tab = 0; + + Iterator var5 = console.entrySet() + .iterator(); + + while (var5.hasNext()) { + Map.Entry longStringEntry = (Map.Entry) var5.next(); + map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + (String) longStringEntry.getValue()); + } + + + return map; + } + + public void clearConsole() { + console.clear(); + } +} + + diff --git a/src/main/java/noppes/npcs/janino/JaninoScriptable.java b/src/main/java/noppes/npcs/janino/JaninoScriptable.java new file mode 100644 index 000000000..9700bf893 --- /dev/null +++ b/src/main/java/noppes/npcs/janino/JaninoScriptable.java @@ -0,0 +1,38 @@ +package noppes.npcs.janino; + +import net.minecraft.nbt.NBTTagCompound; + +import java.util.function.Supplier; + +/** + + */ +public interface JaninoScriptable> { + + boolean hasScript(); + + JaninoScript getScript(); + + JaninoScript createScript(); + + void deleteScript(); + + void unloadScript(); + + static > S readFromNBT(NBTTagCompound compound, S script, Supplier factory) { + if (compound.hasKey("Script")) { + if (script == null) + script = factory.get(); + + script.readFromNBT(compound.getCompoundTag("Script")); + } + return script; + } + + static NBTTagCompound writeToNBT(NBTTagCompound compound, JaninoScript script) { + if (script != null) + compound.setTag("Script", script.writeToNBT(new NBTTagCompound())); + + return compound; + } +} From ca8285e23f960297cc4477d42153302de52ed0be Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 12 Jan 2026 03:28:18 +0200 Subject: [PATCH 205/358] Made these 3 detached GuiScripts depend on GuiScriptInterface --- .../client/gui/script/GuiScriptEffect.java | 415 +---------------- .../client/gui/script/GuiScriptInterface.java | 189 +++++++- .../gui/script/GuiScriptLinkedItem.java | 416 +---------------- .../client/gui/script/GuiScriptRecipe.java | 423 +----------------- 4 files changed, 234 insertions(+), 1209 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java index 796b81058..1f51339f1 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java @@ -1,423 +1,52 @@ package noppes.npcs.client.gui.script; import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.gui.GuiConfirmOpenLink; -import net.minecraft.client.gui.GuiYesNo; -import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.util.StatCollector; -import noppes.npcs.NBTTags; -import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.gui.global.GuiNPCManageEffects; -import noppes.npcs.client.gui.util.GuiCustomScroll; -import noppes.npcs.client.gui.util.GuiMenuTopButton; -import noppes.npcs.client.gui.util.GuiNPCInterface; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextArea; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.GuiScriptTextArea; -import noppes.npcs.client.gui.util.ICustomScrollListener; -import noppes.npcs.client.gui.util.IGuiData; -import noppes.npcs.client.gui.util.IJTextAreaListener; -import noppes.npcs.client.gui.util.ITextChangeListener; -import noppes.npcs.client.gui.util.ITextfieldListener; -import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.CustomEffect; import noppes.npcs.controllers.data.EffectScript; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +public class GuiScriptEffect extends GuiScriptInterface { -public class GuiScriptEffect extends GuiNPCInterface implements GuiYesNoCallback, IGuiData, ITextChangeListener, ICustomScrollListener, IJTextAreaListener, ITextfieldListener { - - private int activeTab = 0; - public Map> languages = new HashMap(); - private final int scriptLimit = 1; - public List hookList = new ArrayList(); - public String previousHookClicked = ""; - - public final GuiNPCManageEffects parent; public final CustomEffect effect; private final EffectScript scriptHandler; - boolean loaded = false; - - private GuiScriptTextArea textArea; - public GuiScriptEffect(GuiNPCManageEffects parent, CustomEffect effect) { - this.drawDefaultBackground = true; - this.closeOnEsc = true; - this.xSize = 420; - this.setBackground("menubg.png"); - + super(); this.parent = parent; this.effect = effect; this.scriptHandler = new EffectScript(); + this.handler = this.scriptHandler; + this.singleContainer = true; this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_EFFECT)); EffectScriptPacket.Get(effect.id); } - public void initGui() { - this.ySize = (int) ((double) this.xSize * 0.56); - if ((double) this.ySize > (double) this.height * 0.95) { - this.ySize = (int) ((double) this.height * 0.95); - this.xSize = (int) ((double) this.ySize / 0.56); - } - - this.bgScale = (float) this.xSize / 400.0F; - super.initGui(); - this.guiTop += 10; - int yoffset = (int) ((double) this.ySize * 0.02); - GuiMenuTopButton top; - this.addTopButton(top = new GuiMenuTopButton(0, this.guiLeft + 4, this.guiTop - 17, "gui.settings")); - int topXoffset = 0; - int topYoffset = 0; - - - if (this.scriptHandler.container != null) { - this.addTopButton(top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, "Script")); - - } else { - this.addTopButton(new GuiMenuTopButton(this.scriptLimit, top.xPosition + top.width, top.yPosition, "+")); - } - - top = this.getTopButton(this.activeTab); - if (top == null) { - this.activeTab = 0; - top = this.getTopButton(0); - } - - top.active = true; - if (this.activeTab == 1) { - GuiCustomScroll hooks = new GuiCustomScroll(this, 1); - hooks.allowUserInput = false; - hooks.setSize(108, 198); - hooks.guiLeft = this.guiLeft - 110; - hooks.guiTop = this.guiTop + 14; - - hooks.setUnsortedList(this.hookList); - this.addScroll(hooks); - GuiNpcLabel hookLabel = new GuiNpcLabel(0, "script.hooks", hooks.guiLeft, this.guiTop + 5); - hookLabel.color = 11184810; - this.addLabel(hookLabel); - ScriptContainer container = this.scriptHandler.container; - if (textArea == null) - textArea = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - else - textArea.init(this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - textArea.enableCodeHighlighting(); - textArea.setListener(this); - this.closeOnEsc(textArea::closeOnEsc); - this.addTextField(textArea); - - int left1 = this.guiLeft + this.xSize - 104; - this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); - this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); - this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); - - this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); - this.getButton(107).enabled = false; - - GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); - scroll.setSize(100, (int) ((double) this.ySize * 0.54) - yoffset * 2); - scroll.guiLeft = left1; - scroll.guiTop = this.guiTop + 88 + yoffset; - if (container != null) { - scroll.setList(container.scripts); - } - - this.addScroll(scroll); - } else { - GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); - var8.enabled = false; - this.addTextField(var8); - int var9 = this.guiLeft + this.xSize - 150; - this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); - this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); - this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, this.languages.keySet().toArray(new String[this.languages.keySet().size()]), this.getScriptIndex())); - this.getButton(103).enabled = !this.languages.isEmpty(); - this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); - this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, this.scriptHandler.getEnabled() ? 1 : 0)); - - this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); - this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); - this.addButton(new GuiNpcButton(110, var9, this.guiTop + 99, 80, 20, "script.apidoc")); - this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); - } - - this.xSize = 420; - this.ySize = 256; + @Override + protected ScriptContext getScriptContext() { + return ScriptContext.GLOBAL; } - public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { - String hook = scroll.getSelected(); - if (this.previousHookClicked.equals(hook)) { - String addString = ""; - if (!this.getTextField(2).getText().isEmpty()) { - addString = addString + "\n"; - } - - addString = addString + "function " + hook + "(event) {\n \n}\n"; - this.getTextField(2).setText(this.getTextField(2).getText() + addString); - this.previousHookClicked = ""; - } else { - this.previousHookClicked = hook; - } - - } - - public void unFocused(GuiNpcTextField textfield) { - } - - private String getConsoleText() { - Map map = this.scriptHandler.getConsoleText(); - StringBuilder builder = new StringBuilder(); - Iterator var3 = map.entrySet().iterator(); - - while (var3.hasNext()) { - Map.Entry entry = (Map.Entry) var3.next(); - builder.insert(0, new Date((Long) entry.getKey()) + (String) entry.getValue() + "\n"); - } - - return builder.toString(); - } - - private int getScriptIndex() { - int i = 0; - - for (Iterator var2 = this.languages.keySet().iterator(); var2.hasNext(); ++i) { - String language = (String) var2.next(); - if (language.equalsIgnoreCase(this.scriptHandler.getLanguage())) { - return i; - } - } - - return 0; - } - - public void confirmClicked(boolean flag, int i) { - if (flag) { - if (i == 0) { - this.openLink("https://www.curseforge.com/minecraft/mc-mods/customnpc-plus"); - } - - if (i == 1) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 2) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 3) { - this.openLink("http://www.minecraftforge.net/forum/index.php/board,122.0.html"); - } - - if (i == 10) { - this.scriptHandler.container = null; - this.activeTab = 0; - } - - if (i == 101) { - this.getTextField(2).setText(NoppesStringUtils.getClipboardContents()); - this.setScript(); - } - - if (i == 102) { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - container.script = ""; - } else { - this.scriptHandler.clearConsole(); - } - - this.initGui(); - } - } - - this.displayGuiScreen(this); - } - - protected void actionPerformed(GuiButton guibutton) { - if (guibutton.id == 0) { - this.setScript(); - this.activeTab = 0; - this.initGui(); - } - - if (guibutton.id == this.scriptLimit) { - if (scriptHandler.container == null) - this.scriptHandler.container = new ScriptContainer(this.scriptHandler); - else - this.setScript(); - this.activeTab = 1; - this.initGui(); - } - - if (guibutton.id == 109) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true)); - } - - if (guibutton.id == 110) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/KAMKEEL/CustomNPC-Plus-API", 1, true)); - } - - if (guibutton.id == 111) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/Noppes/CustomNPCsAPI", 2, true)); - } - - if (guibutton.id == 112) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "http://www.minecraftforge.net/forum/index.php/board,122.0.html", 3, true)); - } - - if (guibutton.id == 100) { - NoppesStringUtils.setClipboardContents(this.getTextField(2).getText()); - } - - if (guibutton.id == 101) { - GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), StatCollector.translateToLocal("gui.sure"), 101); - this.displayGuiScreen(guiyesno); - } - - GuiYesNo container1; - if (guibutton.id == 102) { - container1 = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 103) { - this.scriptHandler.setLanguage(guibutton.displayString); - } - - if (guibutton.id == 104) { - this.scriptHandler.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); - } - - if (guibutton.id == 105) { - container1 = new GuiYesNo(this, "", ((GuiNpcButton) guibutton).displayString, 10); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 107) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - -// this.setSubGui(new EventGuiScriptList((List)this.languages.get(this.script.getLanguage()), container)); - } - - } - - private void setScript() { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - - String text = this.getTextField(2).getText(); - text = text.replace("\r\n", "\n"); - text = text.replace("\r", "\n"); - container.script = text; - } - - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - this.scriptHandler.setLanguage(compound.getString("ScriptLanguage")); - this.scriptHandler.setEnabled(compound.getBoolean("ScriptEnabled")); - this.copiedSetGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(this.scriptHandler); - container.readFromNBT(compound.getCompoundTag("Script")); - this.scriptHandler.container = container; - this.initGui(); - } - } - - private void copiedSetGuiData(NBTTagCompound compound) { - NBTTagList data = compound.getTagList("Languages", 10); - HashMap languages = new HashMap(); - - for (int i = 0; i < data.tagCount(); ++i) { - NBTTagCompound comp = data.getCompoundTagAt(i); - ArrayList scripts = new ArrayList(); - NBTTagList list = comp.getTagList("Scripts", 8); - - for (int j = 0; j < list.tagCount(); ++j) { - scripts.add(list.getStringTagAt(j)); - } - - languages.put(comp.getString("Language"), scripts); - } - - this.languages = languages; - this.initGui(); - } - - public void save() { - if (loaded) { - this.setScript(); - - List containers = this.scriptHandler.getScripts(); - for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); - EffectScriptPacket.Save(effect.id, i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.scriptHandler.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.scriptHandler.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.scriptHandler.getConsoleText())); - - EffectScriptPacket.Save(effect.id, -1, containers.size(), scriptData); - } - } - - public void textUpdate(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - } - - public void saveText(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - this.initGui(); + protected void setHandlerContainer(ScriptContainer container) { + this.scriptHandler.container = container; } @Override - public void close() { - this.save(); - parent.setWorldAndResolution(mc, width, height); - parent.initGui(); - mc.currentScreen = parent; + public void setGuiData(NBTTagCompound compound) { + setGuiDataWithOldContainer(compound); } -} + + protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { + EffectScriptPacket.Save(effect.id, index, totalCount, scriptNBT); + } + + @Override + public void save() { + saveWithPackets(); + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 1a00de918..765f29f60 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -1,9 +1,6 @@ package noppes.npcs.client.gui.script; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.gui.GuiConfirmOpenLink; -import net.minecraft.client.gui.GuiYesNo; -import net.minecraft.client.gui.GuiYesNoCallback; +import net.minecraft.client.gui.*; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.ResourceLocation; @@ -49,6 +46,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected boolean loaded = false; private Map textAreas = new HashMap<>(); + protected GuiScreen parent; + + public boolean singleContainer = false; // ==================== FULLSCREEN MODE ==================== /** Whether the editor viewport is currently in fullscreen mode */ @@ -101,20 +101,28 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb int topXoffset = 0; int topYoffset = 0; - for (int ta = 0; ta < this.handler.getScripts().size(); ++ta) { - if (ta % 20 == 0 && ta > 0) { - topYoffset -= 20; - topXoffset -= top.width + 20 * 22; + if(!singleContainer) { + for (int ta = 0; ta < this.handler.getScripts().size(); ++ta) { + if (ta % 20 == 0 && ta > 0) { + topYoffset -= 20; + topXoffset -= top.width + 20 * 22; + } + this.addTopButton(top = new GuiMenuTopButton(ta + 1, top.xPosition + top.width + topXoffset, + top.yPosition + topYoffset, ta + 1 + "")); + topXoffset = 0; + topYoffset = 0; + scriptLimit = ta + 2; } - this.addTopButton(top = new GuiMenuTopButton(ta + 1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, ta + 1 + "")); - topXoffset = 0; - topYoffset = 0; - scriptLimit = ta + 2; } - if (this.handler.getScripts().size() < 100) + if (singleContainer && getFirst() != null) + this.addTopButton( + top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, + "Script")); + else if (this.handler.getScripts().size() < 100) this.addTopButton(new GuiMenuTopButton(scriptLimit, top.xPosition + top.width, top.yPosition, "+")); + top = this.getTopButton(this.activeTab); if (top == null) { this.activeTab = 0; @@ -322,7 +330,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return builder.toString(); } - private int getScriptIndex() { + public int getScriptIndex() { int i = 0; for (Iterator var2 = this.languages.keySet().iterator(); var2.hasNext(); ++i) { @@ -353,7 +361,10 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.openLink("http://www.minecraftforge.net/forum/index.php/board,122.0.html"); } if (i == 10) { - this.handler.getScripts().remove(this.activeTab - 1); + if (singleContainer) + setHandlerContainer(null); + else + this.handler.getScripts().remove(this.activeTab - 1); this.activeTab = 0; } if (i == 101) { @@ -378,7 +389,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.displayGuiScreen(this); } - private GuiScriptTextArea getActiveScriptArea() { + public GuiScriptTextArea getActiveScriptArea() { if (this.activeTab > 0) { int idx = this.activeTab - 1; if (idx >= 0 && idx < textAreas.size()) @@ -397,6 +408,11 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return super.getTextField(id); } + public ScriptContainer getFirst() { + if (this.handler.getScripts().isEmpty()) + return null; + return this.handler.getScripts().get(0); + } @Override protected void actionPerformed(GuiButton guibutton) { if (guibutton.id >= 0 && guibutton.id < scriptLimit) { @@ -406,8 +422,16 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } if (guibutton.id == scriptLimit) { - this.handler.getScripts().add(new ScriptContainer(this.handler)); - this.activeTab = this.handler.getScripts().size(); + if (singleContainer) { + if (getFirst() == null) + setHandlerContainer(new ScriptContainer(handler)); + else + this.setScript(); + this.activeTab = 1; + } else { + this.handler.getScripts().add(new ScriptContainer(this.handler)); + this.activeTab = this.handler.getScripts().size(); + } this.initGui(); } @@ -464,18 +488,26 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb if (guibutton.id == 107) { container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); if (container == null) { - this.handler.getScripts().add(container = new ScriptContainer(this.handler)); + container = new ScriptContainer(this.handler); + if (singleContainer) + setHandlerContainer(container); + else + this.handler.getScripts().add(container); } this.setSubGui(new EventGuiScriptList((List) this.languages.get(this.handler.getLanguage()), container)); } } - private void setScript() { + protected void setScript() { if (this.activeTab > 0) { ScriptContainer container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); if (container == null) { - this.handler.getScripts().add(container = new ScriptContainer(this.handler)); + container = new ScriptContainer(this.handler); + if (singleContainer) + setHandlerContainer(container); + else + this.handler.getScripts().add(container); } String text = (this.getTextField(2)).getText(); @@ -506,6 +538,106 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.initGui(); } + /** + * Get the script context for this GUI. + * Override in subclasses to return the appropriate context. + * + * @return The script context (default: GLOBAL) + */ + protected ScriptContext getScriptContext() { + return ScriptContext.GLOBAL; + } + + // ==================== UNIFIED SCRIPT DATA HANDLING ==================== + + /** + * Unified setGuiData for script GUIs with old container system. + * Handles both language data and tab-specific script loading. + */ + protected void setGuiDataWithOldContainer(NBTTagCompound compound) { + if (compound.hasKey("LoadComplete")) { + loaded = true; + return; + } + + if (!compound.hasKey("Tab")) { + this.handler.setLanguage(compound.getString("ScriptLanguage")); + this.handler.setEnabled(compound.getBoolean("ScriptEnabled")); + this.loadLanguagesData(compound); + } else { + int tab = compound.getInteger("Tab"); + ScriptContainer container = new ScriptContainer(this.handler); + container.readFromNBT(compound.getCompoundTag("Script")); + this.setHandlerContainer(container); + this.initGui(); + } + } + + /** + * Load languages data from NBT. + * Separated for potential override in subclasses if needed. + */ + protected void loadLanguagesData(NBTTagCompound compound) { + NBTTagList data = compound.getTagList("Languages", 10); + HashMap languages = new HashMap(); + + for (int i = 0; i < data.tagCount(); ++i) { + NBTTagCompound comp = data.getCompoundTagAt(i); + java.util.ArrayList scripts = new java.util.ArrayList(); + NBTTagList list = comp.getTagList("Scripts", 8); + + for (int j = 0; j < list.tagCount(); ++j) { + scripts.add(list.getStringTagAt(j)); + } + + languages.put(comp.getString("Language"), scripts); + } + + this.languages = languages; + this.initGui(); + } + + /** + * Set the handler's container. Override if handler is not IScriptHandler. + */ + protected void setHandlerContainer(ScriptContainer container) { + // Default implementation - subclasses may need to cast and set differently + // e.g., ((LinkedItemScript) handler).container = container; + } + + /** + * Unified save method for script GUIs with packet-based saving. + * Subclasses only need to override sendScriptPackets() and sendMetadataPacket(). + */ + protected void saveWithPackets() { + if (loaded) { + this.setScript(); + + List containers = this.handler.getScripts(); + for (int i = 0; i < containers.size(); i++) { + ScriptContainer container = containers.get(i); + sendSavePacket(i, containers.size(), container.writeToNBT(new NBTTagCompound())); + } + + NBTTagCompound scriptData = new NBTTagCompound(); + scriptData.setString("ScriptLanguage", this.handler.getLanguage()); + scriptData.setBoolean("ScriptEnabled", this.handler.getEnabled()); + scriptData.setTag("ScriptConsole", noppes.npcs.NBTTags.NBTLongStringMap(this.handler.getConsoleText())); + + sendSavePacket(-1, containers.size(), scriptData); + } + } + + /** + * Send a script container packet. Override in subclasses. + * @param index The index of this script + * @param totalCount Total number of scripts + * @param scriptNBT The script container NBT data + */ + protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { + // Default: do nothing (for non-packet-based GUIs like GuiScriptItem) + } + public void save() { if (loaded) this.setScript(); @@ -519,14 +651,27 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } + @Deprecated @Override + //NEVER USED public void saveText(String text) { - ScriptContainer container = handler.getScripts().get(activeTab); + ScriptContainer container = handler.getScripts().get(activeTab ); //activeTab - 1? if (container != null) container.script = text; initGui(); } + @Override + public void close() { + if (parent != null) { + this.save(); + parent.setWorldAndResolution(mc, width, height); + parent.initGui(); + mc.currentScreen = parent; + } else + super.close(); + } + // ==================== FULLSCREEN METHODS ==================== /** diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java index 9091a6ceb..306b62a91 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java @@ -1,423 +1,51 @@ package noppes.npcs.client.gui.script; import kamkeel.npcs.network.packets.request.script.item.LinkedItemScriptPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.gui.GuiConfirmOpenLink; -import net.minecraft.client.gui.GuiYesNo; -import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.util.StatCollector; -import noppes.npcs.NBTTags; -import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.gui.global.GuiNPCManageLinked; -import noppes.npcs.client.gui.util.GuiCustomScroll; -import noppes.npcs.client.gui.util.GuiMenuTopButton; -import noppes.npcs.client.gui.util.GuiNPCInterface; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextArea; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.GuiScriptTextArea; -import noppes.npcs.client.gui.util.ICustomScrollListener; -import noppes.npcs.client.gui.util.IGuiData; -import noppes.npcs.client.gui.util.IJTextAreaListener; -import noppes.npcs.client.gui.util.ITextChangeListener; -import noppes.npcs.client.gui.util.ITextfieldListener; -import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.LinkedItem; import noppes.npcs.controllers.data.LinkedItemScript; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +public class GuiScriptLinkedItem extends GuiScriptInterface { -public class GuiScriptLinkedItem extends GuiNPCInterface implements GuiYesNoCallback, IGuiData, ITextChangeListener, ICustomScrollListener, IJTextAreaListener, ITextfieldListener { - - private int activeTab = 0; - public Map> languages = new HashMap(); - private final int scriptLimit = 1; - public List hookList = new ArrayList(); - public String previousHookClicked = ""; - - public final GuiNPCManageLinked parent; public final LinkedItem linkedItem; - private final LinkedItemScript scriptHandler; - boolean loaded = false; - - private GuiScriptTextArea textArea; public GuiScriptLinkedItem(GuiNPCManageLinked parent, LinkedItem linkedItem) { - this.drawDefaultBackground = true; - this.closeOnEsc = true; - this.xSize = 420; - this.setBackground("menubg.png"); - + super(); this.parent = parent; this.linkedItem = linkedItem; - this.scriptHandler = new LinkedItemScript(); + this.handler = new LinkedItemScript(); + this.singleContainer = true; this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_LINKED_ITEM)); LinkedItemScriptPacket.Get(linkedItem.id); } - public void initGui() { - this.ySize = (int) ((double) this.xSize * 0.56); - if ((double) this.ySize > (double) this.height * 0.95) { - this.ySize = (int) ((double) this.height * 0.95); - this.xSize = (int) ((double) this.ySize / 0.56); - } - - this.bgScale = (float) this.xSize / 400.0F; - super.initGui(); - this.guiTop += 10; - int yoffset = (int) ((double) this.ySize * 0.02); - GuiMenuTopButton top; - this.addTopButton(top = new GuiMenuTopButton(0, this.guiLeft + 4, this.guiTop - 17, "gui.settings")); - int topXoffset = 0; - int topYoffset = 0; - - - if (this.scriptHandler.container != null) { - this.addTopButton(top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, "Script")); - - } else { - this.addTopButton(new GuiMenuTopButton(this.scriptLimit, top.xPosition + top.width, top.yPosition, "+")); - } - - top = this.getTopButton(this.activeTab); - if (top == null) { - this.activeTab = 0; - top = this.getTopButton(0); - } - - top.active = true; - if (this.activeTab == 1) { - GuiCustomScroll hooks = new GuiCustomScroll(this, 1); - hooks.allowUserInput = false; - hooks.setSize(108, 198); - hooks.guiLeft = this.guiLeft - 110; - hooks.guiTop = this.guiTop + 14; - - hooks.setUnsortedList(this.hookList); - this.addScroll(hooks); - GuiNpcLabel hookLabel = new GuiNpcLabel(0, "script.hooks", hooks.guiLeft, this.guiTop + 5); - hookLabel.color = 11184810; - this.addLabel(hookLabel); - ScriptContainer container = this.scriptHandler.container; - if (textArea == null) - textArea = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - else - textArea.init(this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - textArea.enableCodeHighlighting(); - textArea.setListener(this); - this.closeOnEsc(textArea::closeOnEsc); - this.addTextField(textArea); - - int left1 = this.guiLeft + this.xSize - 104; - this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); - this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); - this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); - - this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); - this.getButton(107).enabled = false; - - GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); - scroll.setSize(100, (int) ((double) this.ySize * 0.54) - yoffset * 2); - scroll.guiLeft = left1; - scroll.guiTop = this.guiTop + 88 + yoffset; - if (container != null) { - scroll.setList(container.scripts); - } - - this.addScroll(scroll); - } else { - GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); - var8.enabled = false; - this.addTextField(var8); - int var9 = this.guiLeft + this.xSize - 150; - this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); - this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); - this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, this.languages.keySet().toArray(new String[this.languages.keySet().size()]), this.getScriptIndex())); - this.getButton(103).enabled = !this.languages.isEmpty(); - this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); - this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, this.scriptHandler.getEnabled() ? 1 : 0)); - - this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); - this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); - this.addButton(new GuiNpcButton(110, var9, this.guiTop + 99, 80, 20, "script.apidoc")); - this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); - } - - this.xSize = 420; - this.ySize = 256; + @Override + protected ScriptContext getScriptContext() { + return ScriptContext.ITEM; } - public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { - String hook = scroll.getSelected(); - if (this.previousHookClicked.equals(hook)) { - String addString = ""; - if (!this.getTextField(2).getText().isEmpty()) { - addString = addString + "\n"; - } - - addString = addString + "function " + hook + "(event) {\n \n}\n"; - this.getTextField(2).setText(this.getTextField(2).getText() + addString); - this.previousHookClicked = ""; - } else { - this.previousHookClicked = hook; - } - - } - - public void unFocused(GuiNpcTextField textfield) { - } - - private String getConsoleText() { - Map map = this.scriptHandler.getConsoleText(); - StringBuilder builder = new StringBuilder(); - Iterator var3 = map.entrySet().iterator(); - - while (var3.hasNext()) { - Map.Entry entry = (Map.Entry) var3.next(); - builder.insert(0, new Date((Long) entry.getKey()) + (String) entry.getValue() + "\n"); - } - - return builder.toString(); - } - - private int getScriptIndex() { - int i = 0; - - for (Iterator var2 = this.languages.keySet().iterator(); var2.hasNext(); ++i) { - String language = (String) var2.next(); - if (language.equalsIgnoreCase(this.scriptHandler.getLanguage())) { - return i; - } - } - - return 0; - } - - public void confirmClicked(boolean flag, int i) { - if (flag) { - if (i == 0) { - this.openLink("https://www.curseforge.com/minecraft/mc-mods/customnpc-plus"); - } - - if (i == 1) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 2) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 3) { - this.openLink("http://www.minecraftforge.net/forum/index.php/board,122.0.html"); - } - - if (i == 10) { - this.scriptHandler.container = null; - this.activeTab = 0; - } - - if (i == 101) { - this.getTextField(2).setText(NoppesStringUtils.getClipboardContents()); - this.setScript(); - } - - if (i == 102) { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - container.script = ""; - } else { - this.scriptHandler.clearConsole(); - } - - this.initGui(); - } - } - - this.displayGuiScreen(this); - } - - protected void actionPerformed(GuiButton guibutton) { - if (guibutton.id == 0) { - this.setScript(); - this.activeTab = 0; - this.initGui(); - } - - if (guibutton.id == this.scriptLimit) { - if (scriptHandler.container == null) - this.scriptHandler.container = new ScriptContainer(this.scriptHandler); - else - this.setScript(); - this.activeTab = 1; - this.initGui(); - } - - if (guibutton.id == 109) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true)); - } - - if (guibutton.id == 110) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/KAMKEEL/CustomNPC-Plus-API", 1, true)); - } - - if (guibutton.id == 111) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/Noppes/CustomNPCsAPI", 2, true)); - } - - if (guibutton.id == 112) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "http://www.minecraftforge.net/forum/index.php/board,122.0.html", 3, true)); - } - - if (guibutton.id == 100) { - NoppesStringUtils.setClipboardContents(this.getTextField(2).getText()); - } - - if (guibutton.id == 101) { - GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), StatCollector.translateToLocal("gui.sure"), 101); - this.displayGuiScreen(guiyesno); - } - - GuiYesNo container1; - if (guibutton.id == 102) { - container1 = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 103) { - this.scriptHandler.setLanguage(guibutton.displayString); - } - - if (guibutton.id == 104) { - this.scriptHandler.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); - } - - if (guibutton.id == 105) { - container1 = new GuiYesNo(this, "", ((GuiNpcButton) guibutton).displayString, 10); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 107) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - -// this.setSubGui(new EventGuiScriptList((List)this.languages.get(this.script.getLanguage()), container)); - } - - } - - private void setScript() { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - - String text = this.getTextField(2).getText(); - text = text.replace("\r\n", "\n"); - text = text.replace("\r", "\n"); - container.script = text; - } - - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - this.scriptHandler.setLanguage(compound.getString("ScriptLanguage")); - this.scriptHandler.setEnabled(compound.getBoolean("ScriptEnabled")); - this.copiedSetGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(this.scriptHandler); - container.readFromNBT(compound.getCompoundTag("Script")); - this.scriptHandler.container = container; - this.initGui(); - } - - } - - private void copiedSetGuiData(NBTTagCompound compound) { - NBTTagList data = compound.getTagList("Languages", 10); - HashMap languages = new HashMap(); - - for (int i = 0; i < data.tagCount(); ++i) { - NBTTagCompound comp = data.getCompoundTagAt(i); - ArrayList scripts = new ArrayList(); - NBTTagList list = comp.getTagList("Scripts", 8); - - for (int j = 0; j < list.tagCount(); ++j) { - scripts.add(list.getStringTagAt(j)); - } - - languages.put(comp.getString("Language"), scripts); - } - - this.languages = languages; - this.initGui(); - } - - public void save() { - if (loaded) { - this.setScript(); - - List containers = this.scriptHandler.getScripts(); - for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); - LinkedItemScriptPacket.Save(linkedItem.id, i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.scriptHandler.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.scriptHandler.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.scriptHandler.getConsoleText())); - - LinkedItemScriptPacket.Save(linkedItem.id, -1, containers.size(), scriptData); - } - } - - public void textUpdate(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - } - - public void saveText(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - this.initGui(); + protected void setHandlerContainer(ScriptContainer container) { + ((LinkedItemScript) handler).container = container; } @Override - public void close() { - this.save(); - parent.setWorldAndResolution(mc, width, height); - parent.initGui(); - mc.currentScreen = parent; + public void setGuiData(NBTTagCompound compound) { + setGuiDataWithOldContainer(compound); + } + + protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { + LinkedItemScriptPacket.Save(linkedItem.id, index, totalCount, scriptNBT); + } + + @Override + public void save() { + saveWithPackets(); } } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java index 198e7af16..d401bc274 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java @@ -1,70 +1,27 @@ package noppes.npcs.client.gui.script; import kamkeel.npcs.network.packets.request.script.RecipeScriptPacket; -import net.minecraft.client.gui.GuiButton; -import net.minecraft.client.gui.GuiConfirmOpenLink; -import net.minecraft.client.gui.GuiYesNo; -import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; -import net.minecraft.util.StatCollector; -import noppes.npcs.NBTTags; -import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.gui.global.GuiNpcManageRecipes; -import noppes.npcs.client.gui.util.GuiCustomScroll; -import noppes.npcs.client.gui.util.GuiMenuTopButton; -import noppes.npcs.client.gui.util.GuiNPCInterface; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextArea; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.GuiScriptTextArea; -import noppes.npcs.client.gui.util.ICustomScrollListener; -import noppes.npcs.client.gui.util.IGuiData; -import noppes.npcs.client.gui.util.IJTextAreaListener; -import noppes.npcs.client.gui.util.ITextChangeListener; -import noppes.npcs.client.gui.util.ITextfieldListener; -import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.RecipeAnvil; import noppes.npcs.controllers.data.RecipeCarpentry; import noppes.npcs.controllers.data.RecipeScript; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +public class GuiScriptRecipe extends GuiScriptInterface { -public class GuiScriptRecipe extends GuiNPCInterface implements GuiYesNoCallback, IGuiData, ITextChangeListener, ICustomScrollListener, IJTextAreaListener, ITextfieldListener { - private int activeTab = 0; - public Map> languages = new HashMap(); - private final int scriptLimit = 1; - public List hookList = new ArrayList(); - public String previousHookClicked = ""; - - public final GuiNpcManageRecipes parent; public final int recipeId; public final boolean anvil; - private final RecipeScript scriptHandler; - boolean loaded = false; - - private GuiScriptTextArea textArea; - public GuiScriptRecipe(GuiNpcManageRecipes parent, RecipeCarpentry recipe) { - this.drawDefaultBackground = true; - this.closeOnEsc = true; - this.xSize = 420; - this.setBackground("menubg.png"); - + super(); this.parent = parent; - - this.scriptHandler = new RecipeScript(); + this.handler = new RecipeScript(); this.anvil = false; this.recipeId = recipe.id; + this.singleContainer = true; this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_RECIPE)); @@ -72,14 +29,9 @@ public class GuiScriptRecipe extends GuiNPCInterface implements GuiYesNoCallback } public GuiScriptRecipe(GuiNpcManageRecipes parent, RecipeAnvil recipe) { - this.drawDefaultBackground = true; - this.closeOnEsc = true; - this.xSize = 420; - this.setBackground("menubg.png"); - + super(); this.parent = parent; - - this.scriptHandler = new RecipeScript(); + this.handler = new RecipeScript(); this.anvil = true; this.recipeId = recipe.id; @@ -88,355 +40,26 @@ public class GuiScriptRecipe extends GuiNPCInterface implements GuiYesNoCallback RecipeScriptPacket.Get(true, recipeId); } - public void initGui() { - this.ySize = (int) ((double) this.xSize * 0.56); - if ((double) this.ySize > (double) this.height * 0.95) { - this.ySize = (int) ((double) this.height * 0.95); - this.xSize = (int) ((double) this.ySize / 0.56); - } - - this.bgScale = (float) this.xSize / 400.0F; - super.initGui(); - this.guiTop += 10; - int yoffset = (int) ((double) this.ySize * 0.02); - GuiMenuTopButton top; - this.addTopButton(top = new GuiMenuTopButton(0, this.guiLeft + 4, this.guiTop - 17, "gui.settings")); - int topXoffset = 0; - int topYoffset = 0; - - if (this.scriptHandler.container != null) { - this.addTopButton(top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, "Script")); - - } else { - this.addTopButton(new GuiMenuTopButton(this.scriptLimit, top.xPosition + top.width, top.yPosition, "+")); - } - - top = this.getTopButton(this.activeTab); - if (top == null) { - this.activeTab = 0; - top = this.getTopButton(0); - } - - top.active = true; - if (this.activeTab == 1) { - GuiCustomScroll hooks = new GuiCustomScroll(this, 1); - hooks.allowUserInput = false; - hooks.setSize(108, 198); - hooks.guiLeft = this.guiLeft - 110; - hooks.guiTop = this.guiTop + 14; - - hooks.setUnsortedList(this.hookList); - this.addScroll(hooks); - GuiNpcLabel hookLabel = new GuiNpcLabel(0, "script.hooks", hooks.guiLeft, this.guiTop + 5); - hookLabel.color = 11184810; - this.addLabel(hookLabel); - ScriptContainer container = this.scriptHandler.container; - if (textArea == null) - textArea = new GuiScriptTextArea(this, 2, this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - else - textArea.init(this.guiLeft + 1 + yoffset, this.guiTop + yoffset, this.xSize - 108 - yoffset, (int) ((double) this.ySize * 0.96) - yoffset * 2, container == null ? "" : container.script); - textArea.enableCodeHighlighting(); - textArea.setListener(this); - this.closeOnEsc(textArea::closeOnEsc); - this.addTextField(textArea); - - int left1 = this.guiLeft + this.xSize - 104; - this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); - this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); - this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); - - this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); - this.getButton(107).enabled = false; - - GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); - scroll.setSize(100, (int) ((double) this.ySize * 0.54) - yoffset * 2); - scroll.guiLeft = left1; - scroll.guiTop = this.guiTop + 88 + yoffset; - if (container != null) { - scroll.setList(container.scripts); - } - - this.addScroll(scroll); - } else { - GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); - var8.enabled = false; - this.addTextField(var8); - int var9 = this.guiLeft + this.xSize - 150; - this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); - this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); - this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); - this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, this.languages.keySet().toArray(new String[this.languages.keySet().size()]), this.getScriptIndex())); - this.getButton(103).enabled = !this.languages.isEmpty(); - this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); - this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, this.scriptHandler.getEnabled() ? 1 : 0)); - - this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); - this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); - this.addButton(new GuiNpcButton(110, var9, this.guiTop + 99, 80, 20, "script.apidoc")); - this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); - } - - this.xSize = 420; - this.ySize = 256; + @Override + protected ScriptContext getScriptContext() { + return ScriptContext.GLOBAL; } - public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { - String hook = scroll.getSelected(); - if (this.previousHookClicked.equals(hook)) { - String addString = ""; - if (!this.getTextField(2).getText().isEmpty()) { - addString = addString + "\n"; - } - - addString = addString + "function " + hook + "(event) {\n \n}\n"; - this.getTextField(2).setText(this.getTextField(2).getText() + addString); - this.previousHookClicked = ""; - } else { - this.previousHookClicked = hook; - } - - } - - public void unFocused(GuiNpcTextField textfield) { - } - - private String getConsoleText() { - Map map = this.scriptHandler.getConsoleText(); - StringBuilder builder = new StringBuilder(); - Iterator var3 = map.entrySet().iterator(); - - while (var3.hasNext()) { - Map.Entry entry = (Map.Entry) var3.next(); - builder.insert(0, new Date((Long) entry.getKey()) + (String) entry.getValue() + "\n"); - } - - return builder.toString(); - } - - private int getScriptIndex() { - int i = 0; - - for (Iterator var2 = this.languages.keySet().iterator(); var2.hasNext(); ++i) { - String language = (String) var2.next(); - if (language.equalsIgnoreCase(this.scriptHandler.getLanguage())) { - return i; - } - } - - return 0; - } - - public void confirmClicked(boolean flag, int i) { - if (flag) { - if (i == 0) { - this.openLink("https://www.curseforge.com/minecraft/mc-mods/customnpc-plus"); - } - - if (i == 1) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 2) { - this.openLink("https://kamkeel.github.io/CustomNPC-Plus/"); - } - - if (i == 3) { - this.openLink("http://www.minecraftforge.net/forum/index.php/board,122.0.html"); - } - - if (i == 10) { - this.scriptHandler.container = null; - this.activeTab = 0; - } - - if (i == 101) { - this.getTextField(2).setText(NoppesStringUtils.getClipboardContents()); - this.setScript(); - } - - if (i == 102) { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - container.script = ""; - } else { - this.scriptHandler.clearConsole(); - } - - this.initGui(); - } - } - - this.displayGuiScreen(this); - } - - protected void actionPerformed(GuiButton guibutton) { - if (guibutton.id == 0) { - this.setScript(); - this.activeTab = 0; - this.initGui(); - } - - if (guibutton.id == this.scriptLimit) { - if (scriptHandler.container == null) - this.scriptHandler.container = new ScriptContainer(this.scriptHandler); - else - this.setScript(); - this.activeTab = 1; - this.initGui(); - } - - if (guibutton.id == 109) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true)); - } - - if (guibutton.id == 110) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/KAMKEEL/CustomNPC-Plus-API", 1, true)); - } - - if (guibutton.id == 111) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "https://github.com/Noppes/CustomNPCsAPI", 2, true)); - } - - if (guibutton.id == 112) { - this.displayGuiScreen(new GuiConfirmOpenLink(this, "http://www.minecraftforge.net/forum/index.php/board,122.0.html", 3, true)); - } - - if (guibutton.id == 100) { - NoppesStringUtils.setClipboardContents(this.getTextField(2).getText()); - } - - if (guibutton.id == 101) { - GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), StatCollector.translateToLocal("gui.sure"), 101); - this.displayGuiScreen(guiyesno); - } - - GuiYesNo container1; - if (guibutton.id == 102) { - container1 = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 103) { - this.scriptHandler.setLanguage(guibutton.displayString); - } - - if (guibutton.id == 104) { - this.scriptHandler.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); - } - - if (guibutton.id == 105) { - container1 = new GuiYesNo(this, "", ((GuiNpcButton) guibutton).displayString, 10); - this.displayGuiScreen(container1); - } - - if (guibutton.id == 107) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - -// this.setSubGui(new EventGuiScriptList((List)this.languages.get(this.script.getLanguage()), container)); - } - - } - - private void setScript() { - if (this.activeTab == 1) { - ScriptContainer container = this.scriptHandler.container; - if (container == null) { - container = new ScriptContainer(this.scriptHandler); - this.scriptHandler.container = container; - } - - String text = this.getTextField(2).getText(); - text = text.replace("\r\n", "\n"); - text = text.replace("\r", "\n"); - container.script = text; - } - - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - this.scriptHandler.setLanguage(compound.getString("ScriptLanguage")); - this.scriptHandler.setEnabled(compound.getBoolean("ScriptEnabled")); - this.copiedSetGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(this.scriptHandler); - container.readFromNBT(compound.getCompoundTag("Script")); - this.scriptHandler.container = container; - this.initGui(); - } - } - - private void copiedSetGuiData(NBTTagCompound compound) { - NBTTagList data = compound.getTagList("Languages", 10); - HashMap languages = new HashMap(); - - for (int i = 0; i < data.tagCount(); ++i) { - NBTTagCompound comp = data.getCompoundTagAt(i); - ArrayList scripts = new ArrayList(); - NBTTagList list = comp.getTagList("Scripts", 8); - - for (int j = 0; j < list.tagCount(); ++j) { - scripts.add(list.getStringTagAt(j)); - } - - languages.put(comp.getString("Language"), scripts); - } - - this.languages = languages; - this.initGui(); - } - - public void save() { - if (loaded) { - this.setScript(); - - List containers = this.scriptHandler.getScripts(); - for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); - RecipeScriptPacket.Save(anvil, recipeId, i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.scriptHandler.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.scriptHandler.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.scriptHandler.getConsoleText())); - - RecipeScriptPacket.Save(anvil, recipeId, -1, containers.size(), scriptData); - } - } - - public void textUpdate(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - } - - public void saveText(String text) { - ScriptContainer container = this.scriptHandler.container; - if (container != null) { - container.script = text; - } - - this.initGui(); + protected void setHandlerContainer(ScriptContainer container) { + ((RecipeScript) handler).container = container; } @Override - public void close() { - this.save(); - parent.setWorldAndResolution(mc, width, height); - parent.initGui(); - mc.currentScreen = parent; + public void setGuiData(NBTTagCompound compound) { + setGuiDataWithOldContainer(compound); + } + + protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { + RecipeScriptPacket.Save(anvil, recipeId, index, totalCount, scriptNBT); + } + + @Override + public void save() { + saveWithPackets(); } } From 1372127266e2e9d3757fa8e083748cf99d9de45a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 12 Jan 2026 03:56:22 +0200 Subject: [PATCH 206/358] GuiScript now extends GuiScriptInterface + Full screen support --- .../noppes/npcs/client/gui/GuiScript.java | 329 ++++++++++-------- .../client/gui/script/GuiScriptInterface.java | 129 ++++--- 2 files changed, 259 insertions(+), 199 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index e710eef23..a8017222d 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -4,7 +4,6 @@ import kamkeel.npcs.network.packets.request.script.NPCScriptPacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiConfirmOpenLink; import net.minecraft.client.gui.GuiYesNo; -import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.server.MinecraftServer; @@ -12,44 +11,77 @@ import net.minecraft.util.StatCollector; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.NoppesUtil; import noppes.npcs.client.gui.script.GuiNPCEventScripts; +import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.*; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.DataScript; import noppes.npcs.entity.EntityNPCInterface; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; +import java.util.*; -public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCallback, ICustomScrollListener, IJTextAreaListener, ITextChangeListener { +/** + * NPC Script GUI - extends GuiScriptInterface but with unique behavior: + * - Uses scroll-based tab selection instead of top buttons + * - Shows settings/script toggle + * - Uses DataScript with getNPCScript(index) access pattern + * - Has console filtering by hook type + */ +public class GuiScript extends GuiScriptInterface { public boolean showScript = false; - private int activeTab = 0; public DataScript script; - public Map> languages = new HashMap>(); - + private static int activeConsole = 0; - boolean loaded = false; - - private Map textAreas = new HashMap<>(); public GuiScript(EntityNPCInterface npc) { - super(npc); - script = npc.script; - drawDefaultBackground = true; - closeOnEsc = true; - xSize = 420; - - setBackground("menubg.png"); + super(); + this.npc = npc; + this.script = npc.script; + this.handler = script; + this.useScrollTabs = true; + this.useSettingsToggle = true; + + // Initialize hook list for NPC scripts + hookList.add("script.init"); + hookList.add("script.update"); + hookList.add("script.interact"); + hookList.add("dialog.dialog"); + hookList.add("script.damaged"); + hookList.add("script.killed"); + hookList.add("script.attack"); + hookList.add("script.target"); + hookList.add("script.collide"); + hookList.add("script.kills"); + hookList.add("script.dialog_closed"); + hookList.add("script.timer"); + hookList.add("script.targetLost"); + hookList.add("script.projectileTick"); + hookList.add("script.projectileImpact"); + NPCScriptPacket.Get(); } + + @Override + protected ScriptContext getScriptContext() { + return ScriptContext.NPC; + } + + @Override + protected int getActiveScriptIndex() { + return this.activeTab; + } + + @Override + protected ScriptContainer getCurrentContainer() { + return script.getNPCScript(activeTab); + } + @Override public void initGui() { super.initGui(); - guiTop += 10; + this.guiTop += 10; + + // ==================== TOP BUTTONS ==================== GuiMenuTopButton top; addTopButton(top = new GuiMenuTopButton(13, guiLeft + 4, guiTop - 17, "script.scripts")); addTopButton(new GuiMenuTopButton(16, guiLeft + (xSize - 102), guiTop - 17, "eventscript.eventScripts")); @@ -59,51 +91,86 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall top.active = !showScript; addTopButton(new GuiMenuTopButton(15, top, "gui.website")); - List list = new ArrayList(); - list.add("script.init"); - list.add("script.update"); - list.add("script.interact"); - list.add("dialog.dialog"); - list.add("script.damaged"); - list.add("script.killed"); - list.add("script.attack"); - list.add("script.target"); - list.add("script.collide"); - list.add("script.kills"); - list.add("script.dialog_closed"); - list.add("script.timer"); - list.add("script.targetLost"); - list.add("script.projectileTick"); - list.add("script.projectileImpact"); - if (showScript) { + initScriptView(); + } else { + initSettingsView(); + } + } + + private void initScriptView() { + ScriptContainer container = getCurrentContainer(); + + // ==================== CALCULATE VIEWPORT BOUNDS ==================== + int editorX, editorY, editorWidth, editorHeight; + + if (isFullscreen) { + // Fullscreen: viewport fills screen with configured padding + FullscreenConfig.paddingTop = 30; + FullscreenConfig.paddingBottom = 20; + FullscreenConfig.paddingLeft = 20; + FullscreenConfig.paddingRight = 20; + editorX = FullscreenConfig.paddingLeft; + editorY = FullscreenConfig.paddingTop; + editorWidth = this.width - FullscreenConfig.paddingLeft - FullscreenConfig.paddingRight; + editorHeight = this.height - FullscreenConfig.paddingTop - FullscreenConfig.paddingBottom; + } else { + // Normal: fixed layout with hooks on left + editorX = guiLeft + 74; + editorY = guiTop + 4; + editorWidth = 239; + editorHeight = 208; + } + + // ==================== HOOKS SCROLL (hidden in fullscreen) ==================== + if (!isFullscreen) { addLabel(new GuiNpcLabel(0, "script.hooks", guiLeft + 4, guiTop + 5)); + GuiCustomScroll hooks = new GuiCustomScroll(this, 1); hooks.setSize(68, 198); hooks.guiLeft = guiLeft + 4; hooks.guiTop = guiTop + 14; - hooks.setUnsortedList(list); + hooks.setUnsortedList(hookList); hooks.selected = activeTab; addScroll(hooks); + } - ScriptContainer container = script.getNPCScript(activeTab); + // ==================== SCRIPT TEXT AREA ==================== + int idx = getActiveScriptIndex(); + GuiScriptTextArea activeArea = getActiveScriptArea(); + if (activeArea == null) { + activeArea = new GuiScriptTextArea(this, 2, editorX, editorY, editorWidth, editorHeight, + container == null ? "" : container.script); + activeArea.setListener(this); + this.closeOnEsc(activeArea::closeOnEsc); + textAreas.put(idx, activeArea); + } else { + activeArea.init(editorX, editorY, editorWidth, editorHeight, + container == null ? "" : container.script); + } - int idx = this.activeTab; - GuiScriptTextArea activeArea = getActiveScriptArea(); - if (activeArea == null) { - activeArea = new GuiScriptTextArea(this, 2, guiLeft + 74, guiTop + 4, 239, 208, - container == null ? "" : container.script); - activeArea.setListener(this); - this.closeOnEsc(activeArea::closeOnEsc); - textAreas.put(idx, activeArea); - } else { - activeArea.init(guiLeft + 74, guiTop + 4, 239, 208, container == null ? "" : container.script); + activeArea.setLanguage(script.getLanguage()); + activeArea.setScriptContext(getScriptContext()); + + // Setup fullscreen key binding + GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> { + if (e.isPress()) { + toggleFullscreen(); } + }); + + activeArea.enableCodeHighlighting(); + addTextField(activeArea); + + // Initialize fullscreen button + int scrollbarOffset = activeArea.hasVerticalScrollbar() ? -8 : -2; + fullscreenButton.initGui(editorX + editorWidth, editorY, scrollbarOffset); + // ==================== RIGHT PANEL BUTTONS (hidden in fullscreen) ==================== + if (!isFullscreen) { addButton(new GuiNpcButton(102, guiLeft + 315, guiTop + 4, 50, 20, "gui.clear")); addButton(new GuiNpcButton(101, guiLeft + 366, guiTop + 4, 50, 20, "gui.paste")); addButton(new GuiNpcButton(100, guiLeft + 315, guiTop + 25, 50, 20, "gui.copy")); - addButton(new GuiNpcButton(107, guiLeft + 315, guiTop + 70, 80, 20, "script.loadscript")); GuiCustomScroll scroll = new GuiCustomScroll(this, 0).setUnselectable(); @@ -113,74 +180,49 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall if (container != null) scroll.setList(container.scripts); addScroll(scroll); - } else { - addLabel(new GuiNpcLabel(0, "script.console", guiLeft + 4, guiTop + 16)); - getTopButton(14).active = true; - addTextField(new GuiNpcTextArea(2, this, guiLeft + 4, guiTop + 26, 226, 186, getConsoleText())); - getTextField(2).canEdit = false; - addButton(new GuiNpcButton(100, guiLeft + 232, guiTop + 170, 56, 20, "gui.copy")); - addButton(new GuiNpcButton(102, guiLeft + 232, guiTop + 192, 56, 20, "gui.clear")); - - List l = new ArrayList(); - l.add("All"); - l.addAll(list); - addButton(new GuiNpcButton(105, guiLeft + 60, guiTop + 4, 80, 20, l.toArray(new String[l.size()]), activeConsole)); - - addLabel(new GuiNpcLabel(1, "script.language", guiLeft + 232, guiTop + 30)); - addButton(new GuiNpcButton(103, guiLeft + 294, guiTop + 25, 80, 20, languages.keySet().toArray(new String[languages.keySet().size()]), getScriptIndex())); - getButton(103).enabled = languages.size() > 0; - - addLabel(new GuiNpcLabel(2, "gui.enabled", guiLeft + 232, guiTop + 53)); - addButton(new GuiNpcButton(104, guiLeft + 294, guiTop + 48, 50, 20, new String[]{"gui.no", "gui.yes"}, script.enabled ? 1 : 0)); - - if (MinecraftServer.getServer() != null) - addButton(new GuiNpcButton(106, guiLeft + 232, guiTop + 71, 150, 20, "script.openfolder")); } } + + private void initSettingsView() { + addLabel(new GuiNpcLabel(0, "script.console", guiLeft + 4, guiTop + 16)); + getTopButton(14).active = true; + + GuiNpcTextArea consoleArea = new GuiNpcTextArea(2, this, guiLeft + 4, guiTop + 26, 226, 186, getConsoleText()); + consoleArea.canEdit = false; + addTextField(consoleArea); + + addButton(new GuiNpcButton(100, guiLeft + 232, guiTop + 170, 56, 20, "gui.copy")); + addButton(new GuiNpcButton(102, guiLeft + 232, guiTop + 192, 56, 20, "gui.clear")); - private GuiScriptTextArea getActiveScriptArea() { - if (this.activeTab > 0) { - int idx = this.activeTab; - if (idx >= 0 && idx < textAreas.size()) - return textAreas.get(idx); - } - return null; + List consoleOptions = new ArrayList<>(); + consoleOptions.add("All"); + consoleOptions.addAll(hookList); + addButton(new GuiNpcButton(105, guiLeft + 60, guiTop + 4, 80, 20, + consoleOptions.toArray(new String[0]), activeConsole)); + + addLabel(new GuiNpcLabel(1, "script.language", guiLeft + 232, guiTop + 30)); + addButton(new GuiNpcButton(103, guiLeft + 294, guiTop + 25, 80, 20, + languages.keySet().toArray(new String[0]), getScriptIndex())); + getButton(103).enabled = languages.size() > 0; + + addLabel(new GuiNpcLabel(2, "gui.enabled", guiLeft + 232, guiTop + 53)); + addButton(new GuiNpcButton(104, guiLeft + 294, guiTop + 48, 50, 20, + new String[]{"gui.no", "gui.yes"}, script.enabled ? 1 : 0)); + + if (MinecraftServer.getServer() != null) + addButton(new GuiNpcButton(106, guiLeft + 232, guiTop + 71, 150, 20, "script.openfolder")); } @Override - public GuiNpcTextField getTextField(int id) { - if (id == 2) { - GuiScriptTextArea area = getActiveScriptArea(); - if (area != null) - return area; - } - return super.getTextField(id); - } - - private int getScriptIndex() { - int i = 0; - for (String language : languages.keySet()) { - if (language.equalsIgnoreCase(script.scriptLanguage)) - return i; - i++; - } - return 0; - } - - private String getConsoleText() { + protected String getConsoleText() { Map map = this.script.getOldConsoleText(); StringBuilder builder = new StringBuilder(); - Iterator var3 = map.entrySet().iterator(); - - while (var3.hasNext()) { - Map.Entry entry = (Map.Entry) var3.next(); - builder.insert(0, new Date((Long) entry.getKey()) + (String) entry.getValue() + "\n"); + for (Map.Entry entry : map.entrySet()) { + builder.insert(0, new Date(entry.getKey()) + entry.getValue() + "\n"); } - return builder.toString(); } - @Override public void confirmClicked(boolean result, int id) { NoppesUtil.openGUI(player, this); @@ -205,8 +247,6 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall } } } - - displayGuiScreen(this); } @@ -222,8 +262,7 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall initGui(); } if (guibutton.id == 15) { - GuiConfirmOpenLink guiyesno = new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true); - mc.displayGuiScreen(guiyesno); + displayGuiScreen(new GuiConfirmOpenLink(this, "https://kamkeel.github.io/CustomNPC-Plus/", 0, true)); } if (guibutton.id == 16) { close(); @@ -236,12 +275,12 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall NoppesStringUtils.setClipboardContents(getTextField(2).getText()); } if (guibutton.id == 101) { - GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), StatCollector.translateToLocal("gui.sure"), 101); - displayGuiScreen(guiyesno); + displayGuiScreen(new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), + StatCollector.translateToLocal("gui.sure"), 101)); } if (guibutton.id == 102) { - GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); - displayGuiScreen(guiyesno); + displayGuiScreen(new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), + StatCollector.translateToLocal("gui.sure"), 102)); } if (guibutton.id == 103) { script.scriptLanguage = ((GuiNpcButton) guibutton).displayString; @@ -254,21 +293,20 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall initGui(); } if (guibutton.id == 106) { - // TODO: Uses ScriptController.Instance.dir (shared script repository); this GUI is gated behind the scripter tool - // and CustomNpcsPermissions.TOOL_SCRIPTER so only authorized editors touch the controller-backed scripts. NoppesUtil.openFolder(ScriptController.Instance.dir); } if (guibutton.id == 107) { - ScriptContainer container = script.getNPCScript(activeTab); + ScriptContainer container = getCurrentContainer(); if (container == null) script.setNPCScript(activeTab, container = new ScriptContainer(this.script)); setSubGui(new GuiScriptList(languages.get(script.scriptLanguage), container)); } } - private void setScript() { + @Override + protected void setScript() { if (showScript) { - ScriptContainer container = script.getNPCScript(activeTab); + ScriptContainer container = getCurrentContainer(); if (container == null) script.setNPCScript(activeTab, container = new ScriptContainer(this.script)); String text = getTextField(2).getText(); @@ -277,23 +315,31 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall container.script = text; } } + + @Override + public void mouseClicked(int mouseX, int mouseY, int mouseButton) { + // Check if click is within autocomplete menu bounds and consume it if so + GuiScriptTextArea activeArea = getActiveScriptArea(); + boolean isOverAutocomplete = activeArea != null + && activeArea.isPointOnAutocompleteMenu(mouseX, mouseY); + if (isOverAutocomplete) { + activeArea.mouseClicked(mouseX, mouseY, mouseButton); + return; + } + + // Check fullscreen button when in script view + if (showScript && !isOverAutocomplete + && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { + return; + } + + super.mouseClicked(mouseX, mouseY, mouseButton); + } @Override public void setGuiData(NBTTagCompound compound) { script.readFromNBT(compound); - NBTTagList data = compound.getTagList("Languages", 10); - Map> languages = new HashMap>(); - for (int i = 0; i < data.tagCount(); i++) { - NBTTagCompound comp = data.getCompoundTagAt(i); - List scripts = new ArrayList(); - NBTTagList list = comp.getTagList("Scripts", 8); - for (int j = 0; j < list.tagCount(); j++) { - scripts.add(list.getStringTagAt(j)); - } - languages.put(comp.getString("Language"), scripts); - } - this.languages = languages; - initGui(); + loadLanguagesData(compound); loaded = true; } @@ -313,20 +359,5 @@ public class GuiScript extends GuiNPCInterface implements IGuiData, GuiYesNoCall initGui(); } } - - public void textUpdate(String text) { - ScriptContainer container = script.getNPCScript(activeTab); - if (container != null) - container.script = text; - } - - @Override - public void saveText(String text) { - ScriptContainer container = script.getNPCScript(activeTab); - if (container != null) - container.script = text; - initGui(); - } - } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 765f29f60..1df897ecc 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -41,14 +41,20 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected int activeTab = 0; public IScriptHandler handler; public Map> languages = new HashMap(); - private int scriptLimit = 1; + protected int scriptLimit = 1; public List hookList = new ArrayList(); protected boolean loaded = false; - private Map textAreas = new HashMap<>(); + protected Map textAreas = new HashMap<>(); protected GuiScreen parent; - public boolean singleContainer = false; + public boolean singleContainer = false; + + /** If true, uses scroll-based tab selection instead of top buttons */ + protected boolean useScrollTabs = false; + + /** If true, shows settings/script toggle instead of numbered tabs */ + protected boolean useSettingsToggle = false; // ==================== FULLSCREEN MODE ==================== /** Whether the editor viewport is currently in fullscreen mode */ @@ -94,48 +100,54 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb int yoffset = (int) ((double) this.ySize * 0.02D); // ==================== TAB BUTTONS ==================== - GuiMenuTopButton top; - int topButtonsX = isFullscreen && activeTab != 0 ? FullscreenConfig.paddingLeft : this.guiLeft + 4; - int topButtonsY = isFullscreen && activeTab != 0 ? FullscreenConfig.paddingTop - 20 : this.guiTop - 17; - this.addTopButton(top = new GuiMenuTopButton(0, topButtonsX, topButtonsY, "gui.settings")); + // Skip standard tab creation for GUIs that use custom toggle/scroll tabs + if (!useSettingsToggle) { + GuiMenuTopButton top; + int topButtonsX = isFullscreen && activeTab != 0 ? FullscreenConfig.paddingLeft : this.guiLeft + 4; + int topButtonsY = isFullscreen && activeTab != 0 ? FullscreenConfig.paddingTop - 20 : this.guiTop - 17; + this.addTopButton(top = new GuiMenuTopButton(0, topButtonsX, topButtonsY, "gui.settings")); - int topXoffset = 0; - int topYoffset = 0; - if(!singleContainer) { - for (int ta = 0; ta < this.handler.getScripts().size(); ++ta) { - if (ta % 20 == 0 && ta > 0) { - topYoffset -= 20; - topXoffset -= top.width + 20 * 22; + int topXoffset = 0; + int topYoffset = 0; + if(!singleContainer) { + for (int ta = 0; ta < this.handler.getScripts().size(); ++ta) { + if (ta % 20 == 0 && ta > 0) { + topYoffset -= 20; + topXoffset -= top.width + 20 * 22; + } + this.addTopButton(top = new GuiMenuTopButton(ta + 1, top.xPosition + top.width + topXoffset, + top.yPosition + topYoffset, ta + 1 + "")); + topXoffset = 0; + topYoffset = 0; + scriptLimit = ta + 2; } - this.addTopButton(top = new GuiMenuTopButton(ta + 1, top.xPosition + top.width + topXoffset, - top.yPosition + topYoffset, ta + 1 + "")); - topXoffset = 0; - topYoffset = 0; - scriptLimit = ta + 2; } + + if (singleContainer && getFirst() != null) + this.addTopButton( + top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, + "Script")); + else if (this.handler.getScripts().size() < 100) + this.addTopButton(new GuiMenuTopButton(scriptLimit, top.xPosition + top.width, top.yPosition, "+")); + + + top = this.getTopButton(this.activeTab); + if (top == null) { + this.activeTab = 0; + top = this.getTopButton(0); + } + top.active = true; } - if (singleContainer && getFirst() != null) - this.addTopButton( - top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, - "Script")); - else if (this.handler.getScripts().size() < 100) - this.addTopButton(new GuiMenuTopButton(scriptLimit, top.xPosition + top.width, top.yPosition, "+")); - - - top = this.getTopButton(this.activeTab); - if (top == null) { - this.activeTab = 0; - top = this.getTopButton(0); - } - top.active = true; - // ==================== SCRIPT EDITOR TAB (activeTab > 0) ==================== - if (this.activeTab > 0) { - initScriptEditorTab(yoffset); - } else { - // ==================== SETTINGS TAB (activeTab == 0) ==================== - initSettingsTab(yoffset); + // Skip for GUIs that handle their own view initialization (useSettingsToggle) + if (!useSettingsToggle) { + if (this.activeTab > 0) { + initScriptEditorTab(yoffset); + } else { + // ==================== SETTINGS TAB (activeTab == 0) ==================== + initSettingsTab(yoffset); + } } this.xSize = 420; @@ -147,7 +159,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb * Handles both normal and fullscreen modes. */ private void initScriptEditorTab(int yoffset) { - ScriptContainer container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); + ScriptContainer container = getCurrentContainer(); // ==================== CALCULATE VIEWPORT BOUNDS ==================== int editorX, editorY, editorWidth, editorHeight; @@ -191,7 +203,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } // ==================== SCRIPT TEXT AREA ==================== - int idx = this.activeTab - 1; + int idx = getActiveScriptIndex(); GuiScriptTextArea activeArea = getActiveScriptArea(); if (activeArea == null) { activeArea = new GuiScriptTextArea(this, 2, editorX, editorY, editorWidth, editorHeight, @@ -390,11 +402,28 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } public GuiScriptTextArea getActiveScriptArea() { - if (this.activeTab > 0) { - int idx = this.activeTab - 1; - if (idx >= 0 && idx < textAreas.size()) - return textAreas.get(idx); - } + int idx = getActiveScriptIndex(); + if (idx >= 0 && textAreas.containsKey(idx)) + return textAreas.get(idx); + return null; + } + + /** + * Get the current script index for text area storage. + * Override in subclasses with different tab logic. + */ + protected int getActiveScriptIndex() { + return this.activeTab - 1; + } + + /** + * Get the current script container. + * Override in subclasses with different container access patterns. + */ + protected ScriptContainer getCurrentContainer() { + int idx = getActiveScriptIndex(); + if (idx >= 0 && idx < this.handler.getScripts().size()) + return this.handler.getScripts().get(idx); return null; } @@ -500,8 +529,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } protected void setScript() { - if (this.activeTab > 0) { - ScriptContainer container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); + if (this.activeTab > 0 || useScrollTabs) { + ScriptContainer container = getCurrentContainer(); if (container == null) { container = new ScriptContainer(this.handler); if (singleContainer) @@ -644,7 +673,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } public void textUpdate(String text) { - ScriptContainer container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); + ScriptContainer container = getCurrentContainer(); if (container != null) { container.script = text; } @@ -655,7 +684,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb @Override //NEVER USED public void saveText(String text) { - ScriptContainer container = handler.getScripts().get(activeTab ); //activeTab - 1? + ScriptContainer container = getCurrentContainer(); if (container != null) container.script = text; initGui(); From c3e032dcf2355b4cf129aae33aeece0013e40ff2 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 13 Jan 2026 17:34:14 +0200 Subject: [PATCH 207/358] Abstracted ScriptContainer & JaninoScript to IScriptUnit + global implementation --- .../java/kamkeel/npcs/network/PacketUtil.java | 5 +- .../script/item/ItemScriptErrorPacket.java | 10 +- src/main/java/noppes/npcs/NBTTags.java | 16 +- .../npcs/blocks/tiles/TileScripted.java | 21 ++- .../client/gui/script/GuiNPCEventScripts.java | 5 +- .../client/gui/script/GuiScriptAllNPCs.java | 5 +- .../client/gui/script/GuiScriptForge.java | 5 +- .../client/gui/script/GuiScriptInterface.java | 31 ++-- .../client/gui/script/GuiScriptPlayers.java | 7 +- .../npcs/controllers/ScriptContainer.java | 73 +++++++- .../npcs/controllers/data/DataScript.java | 24 +-- .../npcs/controllers/data/EffectScript.java | 34 ++-- .../controllers/data/ForgeDataScript.java | 40 +++-- .../controllers/data/GlobalNPCDataScript.java | 40 +++-- .../npcs/controllers/data/IScriptHandler.java | 5 +- .../npcs/controllers/data/IScriptUnit.java | 117 +++++++++++++ .../controllers/data/LinkedItemScript.java | 32 ++-- .../controllers/data/PlayerDataScript.java | 69 ++++---- .../npcs/controllers/data/RecipeScript.java | 19 +- .../java/noppes/npcs/janino/JaninoScript.java | 163 ++++++++++++++---- .../npcs/scripted/item/ScriptCustomItem.java | 17 +- 21 files changed, 518 insertions(+), 220 deletions(-) create mode 100644 src/main/java/noppes/npcs/controllers/data/IScriptUnit.java diff --git a/src/main/java/kamkeel/npcs/network/PacketUtil.java b/src/main/java/kamkeel/npcs/network/PacketUtil.java index f262bce45..cb2881eb8 100644 --- a/src/main/java/kamkeel/npcs/network/PacketUtil.java +++ b/src/main/java/kamkeel/npcs/network/PacketUtil.java @@ -15,6 +15,7 @@ import noppes.npcs.NBTTags; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.items.ItemNpcTool; import java.io.IOException; @@ -221,9 +222,9 @@ public class PacketUtil { compound.setTag("Languages", ScriptController.Instance.nbtLanguages()); compound.setTag("ScriptConsole", NBTTags.NBTLongStringMap(data.getConsoleText())); GuiDataPacket.sendGuiData(player, compound); - List containers = data.getScripts(); + List containers = data.getScripts(); for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); + IScriptUnit container = containers.get(i); NBTTagCompound tabCompound = new NBTTagCompound(); tabCompound.setInteger("Tab", i); tabCompound.setTag("Script", container.writeToNBT(new NBTTagCompound())); diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptErrorPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptErrorPacket.java index b4a8c619e..c1a7c74c5 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptErrorPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptErrorPacket.java @@ -16,7 +16,7 @@ import noppes.npcs.CustomNpcsPermissions; import noppes.npcs.NBTTags; import noppes.npcs.api.item.IItemStack; import noppes.npcs.config.ConfigScript; -import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.item.ScriptCustomItem; @@ -72,10 +72,10 @@ public final class ItemScriptErrorPacket extends AbstractPacket { if (requestedAction == Action.GET) { TreeMap map = new TreeMap<>(); int tab = 0; - for (ScriptContainer script : ((ScriptCustomItem) iw).scripts) { + for (IScriptUnit script : ((ScriptCustomItem) iw).scripts) { ++tab; - for (Map.Entry longStringEntry : script.console.entrySet()) { + for (Map.Entry longStringEntry : script.getConsole().entrySet()) { map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); } } @@ -84,8 +84,8 @@ public final class ItemScriptErrorPacket extends AbstractPacket { compound.setTag("ItemScriptConsole", NBTTags.NBTLongStringMap(map)); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); } else { - for (ScriptContainer script : ((ScriptCustomItem) iw).scripts) { - script.console.clear(); + for (IScriptUnit script : ((ScriptCustomItem) iw).scripts) { + script.clearConsole(); } } } diff --git a/src/main/java/noppes/npcs/NBTTags.java b/src/main/java/noppes/npcs/NBTTags.java index ebf30ab94..21e751f11 100644 --- a/src/main/java/noppes/npcs/NBTTags.java +++ b/src/main/java/noppes/npcs/NBTTags.java @@ -8,6 +8,7 @@ import net.minecraft.nbt.NBTTagInt; import net.minecraft.nbt.NBTTagList; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.IScriptUnit; import java.util.ArrayList; import java.util.HashMap; @@ -540,8 +541,8 @@ public class NBTTags { } } - public static List GetScriptOld(NBTTagList list, IScriptHandler handler) { - ArrayList scripts = new ArrayList(); + public static List GetScriptOld(NBTTagList list, IScriptHandler handler) { + ArrayList scripts = new ArrayList<>(); for (int i = 0; i < list.tagCount(); ++i) { NBTTagCompound compoundd = list.getCompoundTagAt(i); @@ -553,8 +554,8 @@ public class NBTTags { return scripts; } - public static List GetScript(NBTTagCompound compound, IScriptHandler handler) { - ArrayList scripts = new ArrayList<>(); + public static List GetScript(NBTTagCompound compound, IScriptHandler handler) { + ArrayList scripts = new ArrayList<>(); for (int i = 0; i < compound.getInteger("TotalScripts"); ++i) { NBTTagCompound containerCompound = compound.getCompoundTag("Tab" + i); @@ -566,12 +567,9 @@ public class NBTTags { return scripts; } - public static NBTTagList NBTScript(List scripts) { + public static NBTTagList NBTScript(List scripts) { NBTTagList list = new NBTTagList(); - Iterator var2 = scripts.iterator(); - - while (var2.hasNext()) { - ScriptContainer script = (ScriptContainer) var2.next(); + for (IScriptUnit script : scripts) { NBTTagCompound compound = new NBTTagCompound(); script.writeToNBT(compound); list.appendTag(compound); diff --git a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java index 8babf8936..2ecdb518e 100644 --- a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java +++ b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java @@ -28,6 +28,7 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.IScriptBlockHandler; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.entity.data.DataTimers; import noppes.npcs.scripted.BlockScriptedWrapper; import noppes.npcs.util.ValueUtil; @@ -40,7 +41,7 @@ import java.util.Map.Entry; import java.util.TreeMap; public class TileScripted extends TileEntity implements IScriptBlockHandler { - public List scripts = new ArrayList<>(); + public List scripts = new ArrayList<>(); public Map tempData = new HashMap<>(); @@ -367,8 +368,10 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler { EventHooks.onScriptBlockInit(this); } - for (ScriptContainer script : scripts) { - script.run(type, event); + for (IScriptUnit script : scripts) { + if (script instanceof ScriptContainer) { + ((ScriptContainer) script).run(type, event); + } } } @@ -408,12 +411,12 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler { } @Override - public void setScripts(List list) { + public void setScripts(List list) { scripts = list; } @Override - public List getScripts() { + public List getScripts() { return scripts; } @@ -421,9 +424,9 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler { public Map getConsoleText() { Map map = new TreeMap<>(); int tab = 0; - for (ScriptContainer script : getScripts()) { + for (IScriptUnit script : getScripts()) { tab++; - for (Entry entry : script.console.entrySet()) { + for (Entry entry : script.getConsole().entrySet()) { map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); } } @@ -432,8 +435,8 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler { @Override public void clearConsole() { - for (ScriptContainer script : getScripts()) { - script.console.clear(); + for (IScriptUnit script : getScripts()) { + script.clearConsole(); } } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java index 027f51a60..8b72bb145 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java @@ -7,6 +7,7 @@ import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.DataScript; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.entity.EntityNPCInterface; import java.util.ArrayList; @@ -49,9 +50,9 @@ public class GuiNPCEventScripts extends GuiScriptInterface { public void save() { if (loaded) { super.save(); - List containers = this.script.getScripts(); + List containers = this.script.getScripts(); for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); + IScriptUnit container = containers.get(i); EventScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); } NBTTagCompound scriptData = new NBTTagCompound(); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java index cef9096bd..27e2f8433 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java @@ -7,6 +7,7 @@ import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.GlobalNPCDataScript; +import noppes.npcs.controllers.data.IScriptUnit; import java.util.ArrayList; import java.util.List; @@ -47,9 +48,9 @@ public class GuiScriptAllNPCs extends GuiScriptInterface { public void save() { if (loaded) { super.save(); - List containers = this.script.getScripts(); + List containers = this.script.getScripts(); for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); + IScriptUnit container = containers.get(i); GlobalNPCScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); } NBTTagCompound scriptData = new NBTTagCompound(); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java index 01a179d54..d4aa8ade7 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java @@ -17,6 +17,7 @@ import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.ForgeDataScript; +import noppes.npcs.controllers.data.IScriptUnit; import org.apache.commons.lang3.StringUtils; import java.io.IOException; @@ -102,9 +103,9 @@ public class GuiScriptForge extends GuiScriptInterface { public void save() { if (loaded) { super.save(); - List containers = this.script.getScripts(); + List containers = this.script.getScripts(); for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); + IScriptUnit container = containers.get(i); ForgeScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); } NBTTagCompound scriptData = new NBTTagCompound(); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 1df897ecc..2e0173a87 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -25,6 +25,7 @@ import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.opengl.Display; import org.lwjgl.opengl.GL11; @@ -159,7 +160,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb * Handles both normal and fullscreen modes. */ private void initScriptEditorTab(int yoffset) { - ScriptContainer container = getCurrentContainer(); + IScriptUnit container = getCurrentContainer(); // ==================== CALCULATE VIEWPORT BOUNDS ==================== int editorX, editorY, editorWidth, editorHeight; @@ -207,13 +208,13 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb GuiScriptTextArea activeArea = getActiveScriptArea(); if (activeArea == null) { activeArea = new GuiScriptTextArea(this, 2, editorX, editorY, editorWidth, editorHeight, - container == null ? "" : container.script); + container == null ? "" : container.getScript()); activeArea.setListener(this); this.closeOnEsc(activeArea::closeOnEsc); textAreas.put(idx, activeArea); } else { activeArea.init(editorX, editorY, editorWidth, editorHeight, - container == null ? "" : container.script); + container == null ? "" : container.getScript()); } // Setup fullscreen key binding @@ -244,7 +245,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb scroll.guiLeft = left1; scroll.guiTop = this.guiTop + 88 + yoffset; if (container != null) { - scroll.setList(container.scripts); + scroll.setList(container.getExternalScripts()); } this.addScroll(scroll); } @@ -420,7 +421,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb * Get the current script container. * Override in subclasses with different container access patterns. */ - protected ScriptContainer getCurrentContainer() { + protected IScriptUnit getCurrentContainer() { int idx = getActiveScriptIndex(); if (idx >= 0 && idx < this.handler.getScripts().size()) return this.handler.getScripts().get(idx); @@ -437,7 +438,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return super.getTextField(id); } - public ScriptContainer getFirst() { + public IScriptUnit getFirst() { if (this.handler.getScripts().isEmpty()) return null; return this.handler.getScripts().get(0); @@ -530,7 +531,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected void setScript() { if (this.activeTab > 0 || useScrollTabs) { - ScriptContainer container = getCurrentContainer(); + IScriptUnit container = getCurrentContainer(); if (container == null) { container = new ScriptContainer(this.handler); if (singleContainer) @@ -542,7 +543,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb String text = (this.getTextField(2)).getText(); text = text.replace("\r\n", "\n"); text = text.replace("\r", "\n"); - container.script = text; + container.setScript(text); } } @@ -629,7 +630,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb /** * Set the handler's container. Override if handler is not IScriptHandler. */ - protected void setHandlerContainer(ScriptContainer container) { + protected void setHandlerContainer(IScriptUnit container) { // Default implementation - subclasses may need to cast and set differently // e.g., ((LinkedItemScript) handler).container = container; } @@ -642,9 +643,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb if (loaded) { this.setScript(); - List containers = this.handler.getScripts(); + List containers = this.handler.getScripts(); for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); + IScriptUnit container = containers.get(i); sendSavePacket(i, containers.size(), container.writeToNBT(new NBTTagCompound())); } @@ -673,9 +674,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } public void textUpdate(String text) { - ScriptContainer container = getCurrentContainer(); + IScriptUnit container = getCurrentContainer(); if (container != null) { - container.script = text; + container.setScript(text); } } @@ -684,9 +685,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb @Override //NEVER USED public void saveText(String text) { - ScriptContainer container = getCurrentContainer(); + IScriptUnit container = getCurrentContainer(); if (container != null) - container.script = text; + container.setScript(text); initGui(); } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java index e0dfc8366..2befb8d74 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java @@ -6,7 +6,8 @@ import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.NBTTags; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; +import noppes.npcs.controllers.data.EffectScript; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.controllers.data.PlayerDataScript; import java.util.ArrayList; @@ -49,9 +50,9 @@ public class GuiScriptPlayers extends GuiScriptInterface { public void save() { if (loaded) { super.save(); - List containers = this.script.getScripts(); + List containers = this.script.getScripts(); for (int i = 0; i < containers.size(); i++) { - ScriptContainer container = containers.get(i); + IScriptUnit container = containers.get(i); PlayerScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); } NBTTagCompound scriptData = new NBTTagCompound(); diff --git a/src/main/java/noppes/npcs/controllers/ScriptContainer.java b/src/main/java/noppes/npcs/controllers/ScriptContainer.java index 3ea46b6a7..70e29df25 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptContainer.java +++ b/src/main/java/noppes/npcs/controllers/ScriptContainer.java @@ -7,6 +7,7 @@ import noppes.npcs.NBTTags; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.NpcAPI; import javax.script.Compilable; @@ -24,7 +25,7 @@ import java.util.Map; import java.util.Objects; import java.util.TreeMap; -public class ScriptContainer { +public class ScriptContainer implements IScriptUnit { private static final String lock = "lock"; public static ScriptContainer Current; private static String CurrentType; @@ -251,6 +252,7 @@ public class ScriptContainer { return evaluated && !errored; } + @Override public boolean hasCode() { if (!scripts.isEmpty()) return true; @@ -267,4 +269,73 @@ public class ScriptContainer { this.evaluated = false; } } + + // ==================== IScriptUnit IMPLEMENTATION ==================== + + @Override + public String getScript() { + return this.script; + } + + @Override + public void setScript(String script) { + this.script = script; + this.evaluated = false; + } + + @Override + public List getExternalScripts() { + return this.scripts; + } + + @Override + public void setExternalScripts(List scripts) { + this.scripts = scripts; + this.evaluated = false; + } + + @Override + public TreeMap getConsole() { + return this.console; + } + + @Override + public void clearConsole() { + this.console.clear(); + } + + @Override + public String getLanguage() { + // If a specific language is set for this container, use it + // Otherwise fall back to the handler's language + if (currentScriptLanguage != null) { + return currentScriptLanguage; + } + return handler != null ? handler.getLanguage() : "ECMAScript"; + } + + @Override + public void setLanguage(String language) { + if (!Objects.equals(this.currentScriptLanguage, language)) { + this.currentScriptLanguage = language; + this.evaluated = false; + } + } + + @Override + public String generateHookStub(String hookName, Object hookData) { + // ECMAScript function template + return "function " + hookName + "(event) {\n \n}\n"; + } + + @Override + public boolean hasErrored() { + return this.errored; + } + + @Override + public void setErrored(boolean errored) { + this.errored = errored; + } } + diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index bf4327d38..45ba9a966 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -36,7 +36,7 @@ import java.util.Map; import java.util.TreeMap; public class DataScript implements INpcScriptHandler { - public List eventScripts = new ArrayList<>(); + public List eventScripts = new ArrayList<>(); private HashMap scripts = new HashMap<>(); private final static EntityType entities = new EntityType(); @@ -146,14 +146,16 @@ public class DataScript implements INpcScriptHandler { if (!hasInited && !npc.isRemote() && type != EnumScriptType.INIT) { hasInited = true; - for (ScriptContainer scriptContainer : this.eventScripts) { - scriptContainer.errored = false; + for (IScriptUnit scriptUnit : this.eventScripts) { + scriptUnit.setErrored(false); } EventHooks.onNPCInit(this.npc); } - for (ScriptContainer script : this.eventScripts) { - script.run(type, event); + for (IScriptUnit script : this.eventScripts) { + if (script instanceof ScriptContainer) { + ((ScriptContainer) script).run(type, event); + } } ScriptContainer script = scripts.get(type); @@ -224,10 +226,10 @@ public class DataScript implements INpcScriptHandler { public Map getConsoleText() { TreeMap map = new TreeMap<>(); int tab = 0; - for (ScriptContainer script : this.getScripts()) { + for (IScriptUnit script : this.getScripts()) { ++tab; - for (Map.Entry longStringEntry : script.console.entrySet()) { + for (Map.Entry longStringEntry : script.getConsole().entrySet()) { map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); } } @@ -235,8 +237,8 @@ public class DataScript implements INpcScriptHandler { } public void clearConsole() { - for (ScriptContainer script : this.getScripts()) { - script.console.clear(); + for (IScriptUnit script : this.getScripts()) { + script.clearConsole(); } } @@ -274,11 +276,11 @@ public class DataScript implements INpcScriptHandler { this.scriptLanguage = lang; } - public void setScripts(List list) { + public void setScripts(List list) { this.eventScripts = list; } - public List getScripts() { + public List getScripts() { return this.eventScripts; } diff --git a/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/src/main/java/noppes/npcs/controllers/data/EffectScript.java index bd2b75dae..497b46421 100644 --- a/src/main/java/noppes/npcs/controllers/data/EffectScript.java +++ b/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -15,13 +15,12 @@ import noppes.npcs.scripted.event.player.PlayerEvent; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; public class EffectScript implements INpcScriptHandler { - public ScriptContainer container; + public IScriptUnit container; public String scriptLanguage = "ECMAScript"; public boolean enabled = false; @@ -64,7 +63,8 @@ public class EffectScript implements INpcScriptHandler { if (!this.isEnabled()) { return; } - container.run(s, event); + if (container instanceof ScriptContainer) + ((ScriptContainer) container).run(s, event); } @Override @@ -93,7 +93,7 @@ public class EffectScript implements INpcScriptHandler { } @Override - public void setScripts(List list) { + public void setScripts(List list) { if (list == null || list.isEmpty()) { container = null; return; @@ -102,7 +102,7 @@ public class EffectScript implements INpcScriptHandler { } @Override - public List getScripts() { + public List getScripts() { if (container == null) return new ArrayList<>(); return Collections.singletonList(container); @@ -115,31 +115,21 @@ public class EffectScript implements INpcScriptHandler { @Override public Map getConsoleText() { - TreeMap map = new TreeMap(); + TreeMap map = new TreeMap<>(); int tab = 0; - Iterator var3 = this.getScripts().iterator(); - - while (var3.hasNext()) { - ScriptContainer script = (ScriptContainer) var3.next(); + for (IScriptUnit script : this.getScripts()) { ++tab; - Iterator var5 = script.console.entrySet().iterator(); - - while (var5.hasNext()) { - Map.Entry longStringEntry = (Map.Entry) var5.next(); - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + (String) longStringEntry.getValue()); + for (Map.Entry entry : script.getConsole().entrySet()) { + map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); } } - return map; } @Override public void clearConsole() { - Iterator var1 = this.getScripts().iterator(); - - while (var1.hasNext()) { - ScriptContainer script = (ScriptContainer) var1.next(); - script.console.clear(); + for (IScriptUnit script : this.getScripts()) { + script.clearConsole(); } } @@ -180,4 +170,4 @@ public class EffectScript implements INpcScriptHandler { this.function = functionName; } } -} +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java index dde123aae..d38342f0e 100644 --- a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java @@ -16,7 +16,7 @@ import java.util.Map.Entry; import java.util.TreeMap; public class ForgeDataScript implements INpcScriptHandler { - private List scripts = new ArrayList(); + private List scripts = new ArrayList<>(); private String scriptLanguage = "ECMAScript"; public long lastInited = -1L; private long lastForgeUpdate = -1L; @@ -26,14 +26,14 @@ public class ForgeDataScript implements INpcScriptHandler { } public void clear() { - this.scripts = new ArrayList(); + this.scripts = new ArrayList<>(); } public void readFromNBT(NBTTagCompound compound) { if (compound.hasKey("Scripts")) { - this.scripts = NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this); + this.scripts = new ArrayList<>(NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this)); } else { - this.scripts = NBTTags.GetScript(compound, this); + this.scripts = new ArrayList<>(NBTTags.GetScript(compound, this)); } this.scriptLanguage = compound.getString("ScriptLanguage"); if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) { @@ -67,8 +67,9 @@ public class ForgeDataScript implements INpcScriptHandler { this.lastInited = ScriptController.Instance.lastLoaded; this.lastForgeUpdate = ScriptController.Instance.lastForgeUpdate; - for (ScriptContainer script : this.scripts) { - script.errored = false; + for (IScriptUnit script : this.scripts) { + if (script instanceof ScriptContainer) + ((ScriptContainer) script).errored = false; } if (!type.equals("init")) { @@ -76,11 +77,15 @@ public class ForgeDataScript implements INpcScriptHandler { } } - for (ScriptContainer script : this.scripts) { - if (script == null || script.errored || !script.hasCode()) + for (IScriptUnit script : this.scripts) { + if (script == null || !script.hasCode()) continue; - - script.run(type, event); + if (script instanceof ScriptContainer) { + ScriptContainer container = (ScriptContainer) script; + if (container.errored) + continue; + container.run(type, event); + } } } } @@ -109,11 +114,11 @@ public class ForgeDataScript implements INpcScriptHandler { this.scriptLanguage = lang; } - public void setScripts(List list) { + public void setScripts(List list) { this.scripts = list; } - public List getScripts() { + public List getScripts() { return this.scripts; } @@ -124,19 +129,18 @@ public class ForgeDataScript implements INpcScriptHandler { public Map getConsoleText() { TreeMap map = new TreeMap<>(); int tab = 0; - for (ScriptContainer script : this.getScripts()) { + for (IScriptUnit script : this.getScripts()) { ++tab; - - for (Entry longStringEntry : script.console.entrySet()) { - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); + for (Entry entry : script.getConsole().entrySet()) { + map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); } } return map; } public void clearConsole() { - for (ScriptContainer script : this.getScripts()) { - script.console.clear(); + for (IScriptUnit script : this.getScripts()) { + script.clearConsole(); } } } diff --git a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java index 335256e0f..f21818136 100644 --- a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java @@ -27,7 +27,7 @@ import java.util.OptionalLong; import java.util.TreeMap; public class GlobalNPCDataScript implements INpcScriptHandler { - public List scripts = new ArrayList(); + public List scripts = new ArrayList<>(); public String scriptLanguage = "ECMAScript"; private EntityNPCInterface npc; private ICustomNpc npcAPI; @@ -42,14 +42,14 @@ public class GlobalNPCDataScript implements INpcScriptHandler { } public void clear() { - this.scripts = new ArrayList(); + this.scripts = new ArrayList<>(); } public void readFromNBT(NBTTagCompound compound) { if (compound.hasKey("Scripts")) { - this.scripts = NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this); + this.scripts = new ArrayList<>(NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this)); } else { - this.scripts = NBTTags.GetScript(compound, this); + this.scripts = new ArrayList<>(NBTTags.GetScript(compound, this)); } this.scriptLanguage = compound.getString("ScriptLanguage"); if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) { @@ -87,16 +87,21 @@ public class GlobalNPCDataScript implements INpcScriptHandler { this.lastInited = ScriptController.Instance.lastLoaded; this.lastNpcUpdate = ScriptController.Instance.lastGlobalNpcUpdate; - for (ScriptContainer script : this.scripts) { - script.errored = false; + for (IScriptUnit script : this.scripts) { + if (script instanceof ScriptContainer) + ((ScriptContainer) script).errored = false; } } - for (ScriptContainer script : this.scripts) { - if (script == null || script.errored || !script.hasCode()) + for (IScriptUnit script : this.scripts) { + if (script == null || !script.hasCode()) continue; - - script.run(hookName, event); + if (script instanceof ScriptContainer) { + ScriptContainer container = (ScriptContainer) script; + if (container.errored) + continue; + container.run(hookName, event); + } } } } @@ -121,11 +126,11 @@ public class GlobalNPCDataScript implements INpcScriptHandler { this.scriptLanguage = lang; } - public void setScripts(List list) { + public void setScripts(List list) { this.scripts = list; } - public List getScripts() { + public List getScripts() { return this.scripts; } @@ -148,19 +153,18 @@ public class GlobalNPCDataScript implements INpcScriptHandler { public Map getConsoleText() { TreeMap map = new TreeMap<>(); int tab = 0; - for (ScriptContainer script : this.getScripts()) { + for (IScriptUnit script : this.getScripts()) { ++tab; - - for (Map.Entry longStringEntry : script.console.entrySet()) { - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); + for (Map.Entry entry : script.getConsole().entrySet()) { + map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); } } return map; } public void clearConsole() { - for (ScriptContainer script : this.getScripts()) { - script.console.clear(); + for (IScriptUnit script : this.getScripts()) { + script.clearConsole(); } } diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java index be37c86f1..d89fbc7a2 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -1,7 +1,6 @@ package noppes.npcs.controllers.data; import cpw.mods.fml.common.eventhandler.Event; -import noppes.npcs.controllers.ScriptContainer; import java.util.List; import java.util.Map; @@ -19,9 +18,9 @@ public interface IScriptHandler { void setLanguage(String language); - void setScripts(List list); + void setScripts(List list); - List getScripts(); + List getScripts(); String noticeString(); diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java new file mode 100644 index 000000000..d8d280be0 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java @@ -0,0 +1,117 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; + +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Common interface for script units that can be edited in a script GUI. + * Implemented by both ScriptContainer (ECMAScript) and JaninoScript (Java/Janino). + * This allows GuiScriptInterface to work with both script types uniformly. + */ +public interface IScriptUnit { + + // ==================== SCRIPT CONTENT ==================== + + /** + * Get the main script text content. + */ + String getScript(); + + /** + * Set the main script text content. + */ + void setScript(String script); + + /** + * Get the list of external script file names loaded for this unit. + */ + List getExternalScripts(); + + /** + * Set the list of external script file names to load. + */ + void setExternalScripts(List scripts); + + // ==================== CONSOLE ==================== + + /** + * Get the console output map (timestamp -> message). + */ + TreeMap getConsole(); + + /** + * Clear all console output. + */ + void clearConsole(); + + /** + * Append a message to the console. + */ + void appendConsole(String message); + + // ==================== LANGUAGE ==================== + + /** + * Get the scripting language for this unit. + * Returns "ECMAScript", "Java", etc. + */ + String getLanguage(); + + /** + * Set the scripting language for this unit. + * @param language The language name (e.g., "ECMAScript", "Java") + */ + void setLanguage(String language); + + /** + * Check if this script unit uses Janino (Java) compilation. + */ + default boolean isJanino() { + return "Java".equals(getLanguage()); + } + + // ==================== HOOK GENERATION ==================== + + /** + * Generate a stub/template for the given hook name. + * ECMAScript: function hookName(event) { } + * Janino: full Java method signature with return type + * + * @param hookName The hook name to generate a stub for + * @param hookData Optional data (e.g., Method object for Janino) + * @return The generated stub string + */ + String generateHookStub(String hookName, Object hookData); + + // ==================== NBT ==================== + + /** + * Write this script unit to NBT. + */ + NBTTagCompound writeToNBT(NBTTagCompound compound); + + /** + * Read this script unit from NBT. + */ + void readFromNBT(NBTTagCompound compound); + + // ==================== STATE ==================== + + /** + * Check if this script unit has any code (script or external scripts). + */ + boolean hasCode(); + + /** + * Check if this script unit has encountered an error during execution. + */ + boolean hasErrored(); + + /** + * Set the error state of this script unit. + */ + void setErrored(boolean errored); +} diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java index cce8df255..18df07229 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -14,13 +14,12 @@ import noppes.npcs.controllers.ScriptController; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; public class LinkedItemScript implements INpcScriptHandler { - public ScriptContainer container; + public IScriptUnit container; public String scriptLanguage = "ECMAScript"; public boolean enabled = false; @@ -58,7 +57,8 @@ public class LinkedItemScript implements INpcScriptHandler { if (!this.isEnabled()) { return; } - container.run(s, event); + if (container instanceof ScriptContainer) + ((ScriptContainer) container).run(s, event); } @Override @@ -87,7 +87,7 @@ public class LinkedItemScript implements INpcScriptHandler { } @Override - public void setScripts(List list) { + public void setScripts(List list) { if (list == null || list.isEmpty()) { container = null; return; @@ -96,7 +96,7 @@ public class LinkedItemScript implements INpcScriptHandler { } @Override - public List getScripts() { + public List getScripts() { if (container == null) return new ArrayList<>(); return Collections.singletonList(container); @@ -109,31 +109,21 @@ public class LinkedItemScript implements INpcScriptHandler { @Override public Map getConsoleText() { - TreeMap map = new TreeMap(); + TreeMap map = new TreeMap<>(); int tab = 0; - Iterator var3 = this.getScripts().iterator(); - - while (var3.hasNext()) { - ScriptContainer script = (ScriptContainer) var3.next(); + for (IScriptUnit script : this.getScripts()) { ++tab; - Iterator var5 = script.console.entrySet().iterator(); - - while (var5.hasNext()) { - Map.Entry longStringEntry = (Map.Entry) var5.next(); - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + (String) longStringEntry.getValue()); + for (Map.Entry entry : script.getConsole().entrySet()) { + map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); } } - return map; } @Override public void clearConsole() { - Iterator var1 = this.getScripts().iterator(); - - while (var1.hasNext()) { - ScriptContainer script = (ScriptContainer) var1.next(); - script.console.clear(); + for (IScriptUnit script : this.getScripts()) { + script.clearConsole(); } } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index a425bdcd4..5d2eee9f9 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -31,7 +31,7 @@ import java.util.OptionalLong; import java.util.TreeMap; public class PlayerDataScript implements INpcScriptHandler { - public List scripts = new ArrayList(); + public List scripts = new ArrayList<>(); public String scriptLanguage = "ECMAScript"; private EntityPlayer player; private IPlayer playerAPI; @@ -53,14 +53,14 @@ public class PlayerDataScript implements INpcScriptHandler { public void clear() { console = new TreeMap(); errored = new ArrayList(); - scripts = new ArrayList(); + scripts = new ArrayList<>(); } public void readFromNBT(NBTTagCompound compound) { if (compound.hasKey("Scripts")) { - this.scripts = NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this); + this.scripts = new ArrayList<>(NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this)); } else { - this.scripts = NBTTags.GetScript(compound, this); + this.scripts = new ArrayList<>(NBTTags.GetScript(compound, this)); } this.scriptLanguage = compound.getString("ScriptLanguage"); if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) { @@ -104,10 +104,12 @@ public class PlayerDataScript implements INpcScriptHandler { errored.clear(); if (player != null) { scripts.clear(); - for (ScriptContainer script : ScriptController.Instance.playerScripts.scripts) { - ScriptContainer s = new ScriptContainer(this); - s.readFromNBT(script.writeToNBT(new NBTTagCompound())); - scripts.add(s); + for (IScriptUnit script : ScriptController.Instance.playerScripts.scripts) { + if (script instanceof ScriptContainer) { + ScriptContainer s = new ScriptContainer(this); + s.readFromNBT(((ScriptContainer) script).writeToNBT(new NBTTagCompound())); + scripts.add(s); + } } } lastPlayerUpdate = ScriptController.Instance.lastPlayerUpdate; @@ -117,26 +119,30 @@ public class PlayerDataScript implements INpcScriptHandler { } } for (int i = 0; i < scripts.size(); i++) { - ScriptContainer script = scripts.get(i); + IScriptUnit script = scripts.get(i); if (errored.contains(i)) continue; - script.run(hookName, event); - if (script.errored) { - errored.add(i); + if (script instanceof ScriptContainer) { + ScriptContainer container = (ScriptContainer) script; + container.run(hookName, event); + if (container.errored) { + errored.add(i); + } + for (Entry entry : container.console.entrySet()) { + if (!console.containsKey(entry.getKey())) + console.put(entry.getKey(), " tab " + (i + 1) + ":\n" + entry.getValue()); + } + container.console.clear(); } - for (Entry entry : script.console.entrySet()) { - if (!console.containsKey(entry.getKey())) - console.put(entry.getKey(), " tab " + (i + 1) + ":\n" + entry.getValue()); - } - script.console.clear(); } } else { if (ScriptController.Instance.lastLoaded > this.lastInited || ScriptController.Instance.lastPlayerUpdate > this.lastPlayerUpdate) { this.lastInited = ScriptController.Instance.lastLoaded; this.lastPlayerUpdate = ScriptController.Instance.lastPlayerUpdate; - for (ScriptContainer scriptContainer : this.scripts) { - scriptContainer.errored = false; + for (IScriptUnit script : this.scripts) { + if (script instanceof ScriptContainer) + ((ScriptContainer) script).errored = false; } if (!Objects.equals(hookName, EnumScriptType.INIT.function) && event instanceof PlayerEvent) { @@ -145,11 +151,13 @@ public class PlayerDataScript implements INpcScriptHandler { } } - for (ScriptContainer script : this.scripts) { - if (script == null || script.errored || !script.hasCode()) + for (IScriptUnit script : this.scripts) { + if (script == null || script.hasErrored()|| !script.hasCode()) continue; - - script.run(hookName, event); + if (script instanceof ScriptContainer) { + ScriptContainer container = (ScriptContainer) script; + container.run(hookName, event); + } } } } @@ -175,11 +183,11 @@ public class PlayerDataScript implements INpcScriptHandler { this.scriptLanguage = lang; } - public void setScripts(List list) { + public void setScripts(List list) { this.scripts = list; } - public List getScripts() { + public List getScripts() { return this.scripts; } @@ -205,11 +213,10 @@ public class PlayerDataScript implements INpcScriptHandler { TreeMap map = new TreeMap<>(); int tab = 0; - for (ScriptContainer script : this.getScripts()) { + for (IScriptUnit script : this.getScripts()) { ++tab; - - for (Entry longStringEntry : script.console.entrySet()) { - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); + for (Entry entry : script.getConsole().entrySet()) { + map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); } } return map; @@ -219,8 +226,8 @@ public class PlayerDataScript implements INpcScriptHandler { if (ConfigScript.IndividualPlayerScripts) console.clear(); else { - for (ScriptContainer script : this.getScripts()) { - script.console.clear(); + for (IScriptUnit script : this.getScripts()) { + script.clearConsole(); } } } diff --git a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java index b94acfbc6..b42595cf5 100644 --- a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java +++ b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java @@ -19,7 +19,7 @@ import java.util.Map; import java.util.TreeMap; public class RecipeScript implements INpcScriptHandler { - public ScriptContainer container; + public IScriptUnit container; public String scriptLanguage = "ECMAScript"; public boolean enabled = false; @@ -54,7 +54,8 @@ public class RecipeScript implements INpcScriptHandler { public void callScript(String s, Event event) { if (!this.isEnabled()) return; - container.run(s, event); + if (container instanceof ScriptContainer) + ((ScriptContainer) container).run(s, event); } @Override @@ -83,7 +84,7 @@ public class RecipeScript implements INpcScriptHandler { } @Override - public void setScripts(List list) { + public void setScripts(List list) { if (list == null || list.isEmpty()) { container = null; return; @@ -92,7 +93,7 @@ public class RecipeScript implements INpcScriptHandler { } @Override - public List getScripts() { + public List getScripts() { if (container == null) return new ArrayList<>(); return Collections.singletonList(container); @@ -107,9 +108,9 @@ public class RecipeScript implements INpcScriptHandler { public Map getConsoleText() { TreeMap map = new TreeMap<>(); int tab = 0; - for (ScriptContainer script : this.getScripts()) { + for (IScriptUnit script : this.getScripts()) { ++tab; - for (Map.Entry entry : script.console.entrySet()) { + for (Map.Entry entry : script.getConsole().entrySet()) { map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); } } @@ -118,8 +119,8 @@ public class RecipeScript implements INpcScriptHandler { @Override public void clearConsole() { - for (ScriptContainer script : this.getScripts()) { - script.console.clear(); + for (IScriptUnit script : this.getScripts()) { + script.clearConsole(); } } @@ -156,4 +157,4 @@ public class RecipeScript implements INpcScriptHandler { this.function = functionName; } } -} +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index f836e97cf..5fb02a46e 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -8,16 +8,21 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.NBTTags; import noppes.npcs.config.ConfigScript; import noppes.npcs.controllers.ScriptController; +import noppes.npcs.controllers.data.IScriptUnit; import org.codehaus.commons.compiler.InternalCompilerException; import org.codehaus.commons.compiler.Sandbox; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.security.*; import java.util.*; import java.util.function.Consumer; import java.util.function.Function; +import java.util.stream.Collectors; -public abstract class JaninoScript { +public abstract class JaninoScript implements IScriptUnit { public boolean enabled; + public boolean errored = false; private String language = "Java"; public TreeMap console = new TreeMap(); @@ -184,18 +189,132 @@ public abstract class JaninoScript { sb.append(code).append("\n"); } } + + // ==================== IScriptUnit IMPLEMENTATION ==================== + @Override + public String getScript() { + return this.script; + } + + @Override public void setScript(String script) { this.script = script; this.evaluated = false; } + + @Override + public List getExternalScripts() { + return this.externalScripts; + } + @Override public void setExternalScripts(List externalScripts) { this.externalScripts = externalScripts; this.evaluated = false; } + + @Override + public TreeMap getConsole() { + return this.console; + } + + @Override + public void clearConsole() { + console.clear(); + } + + @Override + public void appendConsole(String message) { + if (message != null && !message.isEmpty()) { + long time = System.currentTimeMillis(); + if (this.console.containsKey(time)) { + message = (String) this.console.get(time) + "\n" + message; + } + + this.console.put(time, message); + + while (this.console.size() > 40) { + this.console.remove(this.console.firstKey()); + } + } + } + + @Override + public String getLanguage() { + return this.language; + } + + @Override + public void setLanguage(String language) { + // JaninoScript is always Java, ignore attempts to change + // This method exists for IScriptUnit interface compatibility + } + + @Override + public boolean hasCode() { + if (!externalScripts.isEmpty()) + return true; + return script != null && !script.isEmpty(); + } + + @Override + public String generateHookStub(String hookName, Object hookData) { + // If hookData is a Method, generate full Java method stub + if (hookData instanceof Method) { + return generateMethodStub((Method) hookData); + } + // Fallback for simple hook names - generate basic override + return String.format("@Override\npublic void %s() {\n \n}\n", hookName); + } + + /** + * Generates a stub string for a given Method. + */ + public static String generateMethodStub(Method method) { + String mods = Modifier.toString(method.getModifiers()); + // Remove 'abstract' from modifiers for implementation + mods = mods.replace("abstract ", "").replace("abstract", "").trim(); + if (!mods.isEmpty()) + mods += " "; + + String returnTypeStr = method.getReturnType().getSimpleName(); + String name = method.getName(); + + Map typeCount = new HashMap<>(); + String params = Arrays.stream(method.getParameters()) + .map(p -> { + String typeName = p.getType().getSimpleName(); + String baseName = Character.toLowerCase(typeName.charAt(0)) + typeName.substring(1); + + int count = typeCount.getOrDefault(baseName, 0) + 1; + typeCount.put(baseName, count); + + String paramName = count == 1 ? baseName : baseName + (count - 1); + return typeName + " " + paramName; + }) + .collect(Collectors.joining(", ")); + + String body; + Class returnType = method.getReturnType(); + if (returnType == void.class) + body = ""; + else if (returnType.isPrimitive()) { + if (returnType == boolean.class) + body = " return false;"; + else if (returnType == char.class) + body = " return '\\0';"; + else + body = " return 0;"; + } else { + body = " return null;"; + } + + return String.format("@Override\n%s%s %s(%s) {\n%s\n}\n", mods, returnTypeStr, name, params, body); + } + @Override public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setBoolean("enabled", enabled); compound.setTag("console", NBTTags.NBTLongStringMap(this.console)); @@ -204,12 +323,12 @@ public abstract class JaninoScript { return compound; } - public JaninoScript readFromNBT(NBTTagCompound compound) { + @Override + public void readFromNBT(NBTTagCompound compound) { this.enabled = compound.getBoolean("enabled"); this.console = NBTTags.GetLongStringMap(compound.getTagList("console", 10)); setExternalScripts(NBTTags.getStringList(compound.getTagList("externalScripts", 10))); - setScript(compound.getString("script")); //after everything else is read - return this; + setScript(compound.getString("script")); } public boolean isEnabled() { @@ -233,25 +352,6 @@ public abstract class JaninoScript { } } - public String getLanguage() { - return this.language; - } - - public void appendConsole(String message) { - if (message != null && !message.isEmpty()) { - long time = System.currentTimeMillis(); - if (this.console.containsKey(time)) { - message = (String) this.console.get(time) + "\n" + message; - } - - this.console.put(time, message); - - while (this.console.size() > 40) { - this.console.remove(this.console.firstKey()); - } - } - } - public Map getConsoleText() { TreeMap map = new TreeMap(); int tab = 0; @@ -264,13 +364,16 @@ public abstract class JaninoScript { map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + (String) longStringEntry.getValue()); } - return map; } - - public void clearConsole() { - console.clear(); + + @Override + public boolean hasErrored() { + return this.errored; } -} - - + + @Override + public void setErrored(boolean errored) { + this.errored = errored; + } +} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java index 6e85e1555..e03995e16 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java @@ -7,6 +7,7 @@ import noppes.npcs.EventHooks; import noppes.npcs.NBTTags; import noppes.npcs.api.item.IItemCustom; import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.INpcScriptHandler; @@ -19,7 +20,7 @@ import java.util.Objects; import java.util.TreeMap; public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCustom, INpcScriptHandler { - public List scripts = new ArrayList(); + public List scripts = new ArrayList(); public List errored = new ArrayList(); public String scriptLanguage = "ECMAScript"; public boolean enabled = false; @@ -95,14 +96,16 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus } for (int i = 0; i < this.scripts.size(); i++) { - ScriptContainer script = this.scripts.get(i); + IScriptUnit script = this.scripts.get(i); if (!this.errored.contains(i)) { - if (script == null || script.errored || !script.hasCode()) + if (script == null || script.hasErrored() || !script.hasCode()) continue; - script.run(hookName, event); + if (script instanceof ScriptContainer) { + ((ScriptContainer) script).run(hookName, event); + } - if (script.errored) { + if (script.hasErrored()) { this.errored.add(i); } } @@ -129,11 +132,11 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus this.scriptLanguage = lang; } - public void setScripts(List list) { + public void setScripts(List list) { this.scripts = list; } - public List getScripts() { + public List getScripts() { return this.scripts; } From cd92c340c72cc8a8097ecf0e00172ab7e3e57c12 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 13 Jan 2026 18:14:38 +0200 Subject: [PATCH 208/358] resolved merge conflicts with dev --- .../client/gui/script/GuiScriptBlock.java | 2 +- .../client/gui/script/GuiScriptEffect.java | 3 ++ .../client/gui/script/GuiScriptInterface.java | 49 +++++++++++++++++++ .../npcs/client/gui/script/GuiScriptItem.java | 7 +-- .../gui/script/GuiScriptLinkedItem.java | 3 ++ .../client/gui/script/GuiScriptRecipe.java | 3 ++ .../npcs/controllers/data/IScriptHandler.java | 19 +++++++ .../java/noppes/npcs/scripted/NpcAPI.java | 17 +------ 8 files changed, 81 insertions(+), 22 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java index a97efa90f..c95c2c443 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java @@ -4,11 +4,11 @@ import kamkeel.npcs.network.packets.request.script.BlockScriptPacket; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.blocks.tiles.TileScripted; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptHookController; import java.util.ArrayList; - public class GuiScriptBlock extends GuiScriptInterface { private final TileScripted tileScripted; diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java index 1f51339f1..bd5f59b1e 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.script; import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.client.gui.global.GuiNPCManageEffects; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; @@ -9,6 +10,8 @@ import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.CustomEffect; import noppes.npcs.controllers.data.EffectScript; +import java.util.ArrayList; + public class GuiScriptEffect extends GuiScriptInterface { public final CustomEffect effect; diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 2e0173a87..5f180e578 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -216,6 +216,14 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb activeArea.init(editorX, editorY, editorWidth, editorHeight, container == null ? "" : container.getScript()); } + + // Set the scripting language for proper syntax highlighting + // Use the container's language if available, otherwise fall back to handler's language + String language = (container != null) ? container.getLanguage() : this.handler.getLanguage(); + activeArea.setLanguage(language); + + // Set the script context for context-aware hook autocomplete + activeArea.setScriptContext(getScriptContext()); // Setup fullscreen key binding GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> { @@ -238,6 +246,13 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); + + // Language toggle button (only if handler supports Janino) + if (handler.supportsJanino() && container != null) { + String langLabel = container.isJanino() ? "Java" : "ECMAScript"; + this.addButton(new GuiNpcButton(113, left1, this.guiTop + 42 + yoffset, 121, 20, langLabel)); + } + this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); @@ -527,6 +542,40 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.setSubGui(new EventGuiScriptList((List) this.languages.get(this.handler.getLanguage()), container)); } + + // Language toggle button - switch between ECMAScript and Java + if (guibutton.id == 113) { + int idx = getActiveScriptIndex(); + if (idx >= 0 && idx < handler.getScripts().size()) { + IScriptUnit currentUnit = handler.getScripts().get(idx); + IScriptUnit newUnit; + + if (currentUnit.isJanino()) { + // Switch from Java to ECMAScript + newUnit = new ScriptContainer(handler); + newUnit.setScript(currentUnit.getScript()); + newUnit.setExternalScripts(new ArrayList<>(currentUnit.getExternalScripts())); + } else { + // Switch from ECMAScript to Java + newUnit = handler.createJaninoScriptUnit(); + if (newUnit != null) { + newUnit.setScript(currentUnit.getScript()); + newUnit.setExternalScripts(new ArrayList<>(currentUnit.getExternalScripts())); + } else { + return; // Handler doesn't support Janino + } + } + + // Replace the script unit in the list + handler.getScripts().set(idx, newUnit); + + // Clear the cached text area so it recreates with new language + textAreas.remove(idx); + + // Reinitialize the GUI + initGui(); + } + } } protected void setScript() { diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java index 8b2a423cf..cb5b807f2 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java @@ -7,14 +7,11 @@ import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomItems; import noppes.npcs.NBTTags; import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.scripted.item.ScriptCustomItem; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Map; +import java.util.*; public class GuiScriptItem extends GuiScriptInterface { private ScriptCustomItem item; diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java index 306b62a91..d00c7b2c3 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.script; import kamkeel.npcs.network.packets.request.script.item.LinkedItemScriptPacket; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.client.gui.global.GuiNPCManageLinked; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; @@ -10,6 +11,8 @@ import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.LinkedItem; import noppes.npcs.controllers.data.LinkedItemScript; +import java.util.ArrayList; + public class GuiScriptLinkedItem extends GuiScriptInterface { public final LinkedItem linkedItem; diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java index d401bc274..262151d5c 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.script; import kamkeel.npcs.network.packets.request.script.RecipeScriptPacket; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.client.gui.global.GuiNpcManageRecipes; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; @@ -10,6 +11,8 @@ import noppes.npcs.controllers.data.RecipeAnvil; import noppes.npcs.controllers.data.RecipeCarpentry; import noppes.npcs.controllers.data.RecipeScript; +import java.util.ArrayList; + public class GuiScriptRecipe extends GuiScriptInterface { public final int recipeId; diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java index d89fbc7a2..2aa9c1db5 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -27,4 +27,23 @@ public interface IScriptHandler { Map getConsoleText(); void clearConsole(); + + /** + * Create a new Janino (Java) script unit for this handler. + * Handlers that don't support Janino scripts should return null. + * + * @return A new JaninoScript instance, or null if not supported + */ + default IScriptUnit createJaninoScriptUnit() { + return null; // Default: Janino not supported + } + + /** + * Check if this handler supports Janino (Java) scripts. + * + * @return true if createJaninoScriptUnit() can create valid Janino scripts + */ + default boolean supportsJanino() { + return createJaninoScriptUnit() != null; + } } diff --git a/src/main/java/noppes/npcs/scripted/NpcAPI.java b/src/main/java/noppes/npcs/scripted/NpcAPI.java index 26a0eb89a..3d71e0023 100644 --- a/src/main/java/noppes/npcs/scripted/NpcAPI.java +++ b/src/main/java/noppes/npcs/scripted/NpcAPI.java @@ -59,22 +59,7 @@ import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.entity.IEntity; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.api.gui.ICustomGui; -import noppes.npcs.api.handler.IActionManager; -import noppes.npcs.api.handler.IAbilityHandler; -import noppes.npcs.api.handler.IAnimationHandler; -import noppes.npcs.api.handler.IAttributeHandler; -import noppes.npcs.api.handler.ICloneHandler; -import noppes.npcs.api.handler.ICustomEffectHandler; -import noppes.npcs.api.handler.IDialogHandler; -import noppes.npcs.api.handler.IFactionHandler; -import noppes.npcs.api.handler.IMagicHandler; -import noppes.npcs.api.handler.INaturalSpawnsHandler; -import noppes.npcs.api.handler.IPartyHandler; -import noppes.npcs.api.handler.IProfileHandler; -import noppes.npcs.api.handler.IQuestHandler; -import noppes.npcs.api.handler.IRecipeHandler; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.api.handler.ITransportHandler; +import noppes.npcs.api.handler.*; import noppes.npcs.api.handler.data.IAnimation; import noppes.npcs.api.handler.data.IFrame; import noppes.npcs.api.handler.data.IFramePart; From 71dba152704128fc6d55484801f7c0eadcdd4297 Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 9 Jan 2026 22:59:13 -0500 Subject: [PATCH 209/358] Add Script Context and better parsing for d.ts handling --- .gitignore | 4 + .../noppes/npcs/client/gui/GuiScript.java | 7 +- .../client/gui/script/GuiScriptBlock.java | 6 +- .../client/gui/script/GuiScriptInterface.java | 94 +------- .../npcs/client/gui/script/GuiScriptItem.java | 7 +- .../client/gui/script/GuiScriptPlayers.java | 8 +- .../client/gui/util/GuiScriptTextArea.java | 22 ++ .../noppes/npcs/constants/ScriptContext.java | 201 ++++++++++++++++++ 8 files changed, 253 insertions(+), 96 deletions(-) create mode 100644 src/main/java/noppes/npcs/constants/ScriptContext.java diff --git a/.gitignore b/.gitignore index 23d9047f1..ed907b3d2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,10 +6,13 @@ /out/ /build/ /eclipse/ +/gradle-plugins/ .classpath .project /bin/ /config/ +/copilot/ +/buildSrc/build/ /crash-reports/ /logs/ options.txt @@ -39,3 +42,4 @@ addon.late.local.gradle.kts layout.json out run +src/main/resources/assets/customnpcs/api \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index a8017222d..4349b1ef6 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -13,6 +13,7 @@ import noppes.npcs.client.NoppesUtil; import noppes.npcs.client.gui.script.GuiNPCEventScripts; import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.*; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.DataScript; @@ -166,8 +167,10 @@ public class GuiScript extends GuiScriptInterface { int scrollbarOffset = activeArea.hasVerticalScrollbar() ? -8 : -2; fullscreenButton.initGui(editorX + editorWidth, editorY, scrollbarOffset); - // ==================== RIGHT PANEL BUTTONS (hidden in fullscreen) ==================== - if (!isFullscreen) { + // Set language and script context for proper syntax highlighting and autocomplete + activeArea.setLanguage(script.getLanguage()); + activeArea.setScriptContext(ScriptContext.NPC); + addButton(new GuiNpcButton(102, guiLeft + 315, guiTop + 4, 50, 20, "gui.clear")); addButton(new GuiNpcButton(101, guiLeft + 366, guiTop + 4, 50, 20, "gui.paste")); addButton(new GuiNpcButton(100, guiLeft + 315, guiTop + 25, 50, 20, "gui.copy")); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java index c95c2c443..5b9762a96 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java @@ -5,7 +5,6 @@ import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.blocks.tiles.TileScripted; import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.ScriptHookController; import java.util.ArrayList; @@ -25,6 +24,11 @@ public class GuiScriptBlock extends GuiScriptInterface { loaded = true; } + @Override + protected ScriptContext getScriptContext() { + return ScriptContext.BLOCK; + } + public void save() { if (loaded) { super.save(); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 5f180e578..11c326880 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -23,6 +23,7 @@ import noppes.npcs.client.gui.util.ITextChangeListener; import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.IScriptUnit; @@ -225,6 +226,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb // Set the script context for context-aware hook autocomplete activeArea.setScriptContext(getScriptContext()); + // Set the script context for context-aware hook autocomplete + activeArea.setScriptContext(getScriptContext()); + // Setup fullscreen key binding GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> { if (e.isPress()) { @@ -627,96 +631,6 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return ScriptContext.GLOBAL; } - // ==================== UNIFIED SCRIPT DATA HANDLING ==================== - - /** - * Unified setGuiData for script GUIs with old container system. - * Handles both language data and tab-specific script loading. - */ - protected void setGuiDataWithOldContainer(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - this.handler.setLanguage(compound.getString("ScriptLanguage")); - this.handler.setEnabled(compound.getBoolean("ScriptEnabled")); - this.loadLanguagesData(compound); - } else { - int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(this.handler); - container.readFromNBT(compound.getCompoundTag("Script")); - this.setHandlerContainer(container); - this.initGui(); - } - } - - /** - * Load languages data from NBT. - * Separated for potential override in subclasses if needed. - */ - protected void loadLanguagesData(NBTTagCompound compound) { - NBTTagList data = compound.getTagList("Languages", 10); - HashMap languages = new HashMap(); - - for (int i = 0; i < data.tagCount(); ++i) { - NBTTagCompound comp = data.getCompoundTagAt(i); - java.util.ArrayList scripts = new java.util.ArrayList(); - NBTTagList list = comp.getTagList("Scripts", 8); - - for (int j = 0; j < list.tagCount(); ++j) { - scripts.add(list.getStringTagAt(j)); - } - - languages.put(comp.getString("Language"), scripts); - } - - this.languages = languages; - this.initGui(); - } - - /** - * Set the handler's container. Override if handler is not IScriptHandler. - */ - protected void setHandlerContainer(IScriptUnit container) { - // Default implementation - subclasses may need to cast and set differently - // e.g., ((LinkedItemScript) handler).container = container; - } - - /** - * Unified save method for script GUIs with packet-based saving. - * Subclasses only need to override sendScriptPackets() and sendMetadataPacket(). - */ - protected void saveWithPackets() { - if (loaded) { - this.setScript(); - - List containers = this.handler.getScripts(); - for (int i = 0; i < containers.size(); i++) { - IScriptUnit container = containers.get(i); - sendSavePacket(i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.handler.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.handler.getEnabled()); - scriptData.setTag("ScriptConsole", noppes.npcs.NBTTags.NBTLongStringMap(this.handler.getConsoleText())); - - sendSavePacket(-1, containers.size(), scriptData); - } - } - - /** - * Send a script container packet. Override in subclasses. - * @param index The index of this script - * @param totalCount Total number of scripts - * @param scriptNBT The script container NBT data - */ - protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { - // Default: do nothing (for non-packet-based GUIs like GuiScriptItem) - } - public void save() { if (loaded) this.setScript(); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java index cb5b807f2..5d23aca89 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java @@ -6,9 +6,7 @@ import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomItems; import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.scripted.item.ScriptCustomItem; import java.util.*; @@ -37,6 +35,11 @@ public class GuiScriptItem extends GuiScriptInterface { } } + @Override + protected ScriptContext getScriptContext() { + return ScriptContext.ITEM; + } + public void save() { if (loaded) { super.save(); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java index 2befb8d74..755f56f56 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java @@ -4,7 +4,8 @@ import kamkeel.npcs.network.packets.request.script.PlayerScriptPacket; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.constants.ScriptContext; +import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.data.EffectScript; import noppes.npcs.controllers.data.IScriptUnit; @@ -47,6 +48,11 @@ public class GuiScriptPlayers extends GuiScriptInterface { } } + @Override + protected ScriptContext getScriptContext() { + return ScriptContext.PLAYER; + } + public void save() { if (loaded) { super.save(); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 3426bcae8..5f3e590f0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -11,6 +11,7 @@ import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; import noppes.npcs.client.gui.util.script.*; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; import noppes.npcs.client.key.impl.ScriptEditorKeys; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.util.ValueUtil; import org.lwjgl.input.Keyboard; import org.lwjgl.input.Mouse; @@ -2159,6 +2160,27 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.container.formatCodeText(); } + /** + * Set the script context (NPC, PLAYER, BLOCK, ITEM, etc.). + * This determines which hooks and event types are available for autocomplete. + * + * @param context The script context + */ + public void setScriptContext(ScriptContext context) { + if (this.container != null) { + this.container.setScriptContext(context); + } + } + + /** + * Get the current script context. + * + * @return The script context (NPC, PLAYER, BLOCK, ITEM, etc.) + */ + public ScriptContext getScriptContext() { + return this.container != null ? this.container.getScriptContext() : ScriptContext.GLOBAL; + } + public void setListener(ITextChangeListener listener) { this.listener = listener; } diff --git a/src/main/java/noppes/npcs/constants/ScriptContext.java b/src/main/java/noppes/npcs/constants/ScriptContext.java new file mode 100644 index 000000000..a4567955b --- /dev/null +++ b/src/main/java/noppes/npcs/constants/ScriptContext.java @@ -0,0 +1,201 @@ +package noppes.npcs.constants; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Represents a script context that maps to one or more event interface namespaces. + * + * A script context can have multiple namespaces because script editors often + * support hooks from multiple event types. For example, Player scripts support: + * - IPlayerEvent (core player hooks) + * - IAnimationEvent (animation hooks) + * - IPartyEvent (party hooks) + * - ICustomGuiEvent (custom GUI hooks) + * - etc. + * + * This is a registerable system - any mod can register their own contexts: + * + * // Register a new context with multiple namespaces: + * ScriptContext.register("DBC", "IDBCEvent", "IDBCPlayerEvent", "IDBCFormEvent"); + * + * // Or add namespaces to an existing context: + * ScriptContext.PLAYER.addNamespace("IDBCPlayerEvent"); + */ +public class ScriptContext { + + // Registry of all script contexts + private static final Map REGISTRY = new ConcurrentHashMap<>(); + + // ==================== BUILT-IN CONTEXTS ==================== + + public static final ScriptContext NPC = register("NPC", + "INpcEvent", + "IProjectileEvent" + ); + + public static final ScriptContext PLAYER = register("PLAYER", + "IPlayerEvent", + "IAnimationEvent", + "IPartyEvent", + "IDialogEvent", + "IQuestEvent", + "IFactionEvent", + "ICustomGuiEvent", + "IEffectEvent" + ); + + public static final ScriptContext BLOCK = register("BLOCK", + "IBlockEvent" + ); + + public static final ScriptContext ITEM = register("ITEM", + "IItemEvent" + ); + + public static final ScriptContext FORGE = register("FORGE", + "IForgeEvent" + ); + + public static final ScriptContext GLOBAL = register("GLOBAL", + "Global" + ); + + // ==================== INSTANCE FIELDS ==================== + + /** Unique identifier for this context (e.g., "NPC", "PLAYER", "DBC") */ + public final String id; + + /** The event interface namespaces this context supports */ + private final List namespaces; + + // ==================== CONSTRUCTOR ==================== + + private ScriptContext(String id, String... namespaces) { + this.id = id; + this.namespaces = new ArrayList<>(Arrays.asList(namespaces)); + } + + // ==================== NAMESPACE ACCESS ==================== + + /** + * Get all namespaces this context supports. + * + * @return Unmodifiable list of namespace strings + */ + public List getNamespaces() { + return Collections.unmodifiableList(namespaces); + } + + /** + * Check if this context includes a specific namespace. + * + * @param namespace The namespace to check (e.g., "IPlayerEvent") + * @return true if this context supports hooks from that namespace + */ + public boolean hasNamespace(String namespace) { + return namespaces.contains(namespace); + } + + /** + * Add a namespace to this context. + * Mods can use this to extend built-in contexts with their own event types. + * + * @param namespace The namespace to add (e.g., "IDBCPlayerEvent") + */ + public void addNamespace(String namespace) { + if (!namespaces.contains(namespace)) { + namespaces.add(namespace); + } + } + + /** + * Get the primary namespace (first one registered). + * This is mainly for backward compatibility. + * + * @return The first namespace, or "Global" if none + */ + public String getPrimaryNamespace() { + return namespaces.isEmpty() ? "Global" : namespaces.get(0); + } + + // ==================== REGISTRATION API ==================== + + /** + * Register a new script context with one or more namespaces. + * + * @param id Unique identifier (e.g., "DBC", "CUSTOM") + * @param namespaces The event interface names (e.g., "IDBCEvent", "IDBCPlayerEvent") + * @return The registered ScriptContext + */ + public static ScriptContext register(String id, String... namespaces) { + ScriptContext context = new ScriptContext(id, namespaces); + REGISTRY.put(id, context); + return context; + } + + /** + * Get a script context by its ID. + * + * @param id The context ID (e.g., "NPC", "PLAYER", "DBC") + * @return The ScriptContext, or GLOBAL if not found + */ + public static ScriptContext byId(String id) { + return REGISTRY.getOrDefault(id, GLOBAL); + } + + /** + * Find a script context that contains a specific namespace. + * + * @param namespace The event interface namespace (e.g., "INpcEvent") + * @return The first ScriptContext that contains this namespace, or GLOBAL if not found + */ + public static ScriptContext byNamespace(String namespace) { + if (namespace == null) return GLOBAL; + for (ScriptContext ctx : REGISTRY.values()) { + if (ctx.hasNamespace(namespace)) { + return ctx; + } + } + return GLOBAL; + } + + /** + * Get all registered script contexts. + * + * @return Unmodifiable collection of all registered contexts + */ + public static Collection values() { + return Collections.unmodifiableCollection(REGISTRY.values()); + } + + /** + * Check if a context with the given ID exists. + * + * @param id The context ID to check + * @return true if registered + */ + public static boolean exists(String id) { + return REGISTRY.containsKey(id); + } + + // ==================== OBJECT METHODS ==================== + + @Override + public String toString() { + return id + " -> " + namespaces; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof ScriptContext)) return false; + ScriptContext other = (ScriptContext) obj; + return id.equals(other.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} From fd0e20f3647e699d07199ac48a6db4d57032a9b5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sat, 10 Jan 2026 06:53:48 +0200 Subject: [PATCH 210/358] Made ScriptContext class based instead --- .../noppes/npcs/constants/ScriptContext.java | 48 +++++++++++++------ 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/src/main/java/noppes/npcs/constants/ScriptContext.java b/src/main/java/noppes/npcs/constants/ScriptContext.java index a4567955b..942c406e6 100644 --- a/src/main/java/noppes/npcs/constants/ScriptContext.java +++ b/src/main/java/noppes/npcs/constants/ScriptContext.java @@ -1,5 +1,7 @@ package noppes.npcs.constants; +import noppes.npcs.api.event.*; + import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -30,35 +32,34 @@ public class ScriptContext { // ==================== BUILT-IN CONTEXTS ==================== public static final ScriptContext NPC = register("NPC", - "INpcEvent", - "IProjectileEvent" + INpcEvent.class, + IProjectileEvent.class ); public static final ScriptContext PLAYER = register("PLAYER", - "IPlayerEvent", - "IAnimationEvent", - "IPartyEvent", - "IDialogEvent", - "IQuestEvent", - "IFactionEvent", - "ICustomGuiEvent", - "IEffectEvent" + IPlayerEvent.class, + IAnimationEvent.class, + IPartyEvent.class, + IDialogEvent.class, + IQuestEvent.class, + IFactionEvent.class, + ICustomGuiEvent.class ); public static final ScriptContext BLOCK = register("BLOCK", - "IBlockEvent" + IBlockEvent.class ); public static final ScriptContext ITEM = register("ITEM", - "IItemEvent" + IItemEvent.class ); public static final ScriptContext FORGE = register("FORGE", - "IForgeEvent" + IForgeEvent.class ); public static final ScriptContext GLOBAL = register("GLOBAL", - "Global" + "Global" // Special case: Global namespace doesn't have a corresponding event class ); // ==================== INSTANCE FIELDS ==================== @@ -134,6 +135,25 @@ public class ScriptContext { return context; } + /** + * Register a new script context using event interface classes. + * Automatically extracts the simple name from each class. + * + * Example: + * ScriptContext.register("NPC", INpcEvent.class, IProjectileEvent.class); + * + * @param id Unique identifier (e.g., "DBC", "CUSTOM") + * @param eventClasses The event interface classes (simple names will be extracted) + * @return The registered ScriptContext + */ + public static ScriptContext register(String id, Class... eventClasses) { + String[] namespaces = new String[eventClasses.length]; + for (int i = 0; i < eventClasses.length; i++) { + namespaces[i] = eventClasses[i].getSimpleName(); + } + return register(id, namespaces); + } + /** * Get a script context by its ID. * From 6173e989981be7c9d00b3df2066803df78ccbca0 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 13 Jan 2026 19:05:52 +0200 Subject: [PATCH 211/358] merge conflict resolutioon --- .../client/gui/script/GuiScriptInterface.java | 92 ++++++++++++++++++- .../client/gui/script/GuiScriptPlayers.java | 4 +- 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 11c326880..d91f63c5a 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -221,7 +221,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb // Set the scripting language for proper syntax highlighting // Use the container's language if available, otherwise fall back to handler's language String language = (container != null) ? container.getLanguage() : this.handler.getLanguage(); - activeArea.setLanguage(language); + // activeArea.setLanguage(language); // Set the script context for context-aware hook autocomplete activeArea.setScriptContext(getScriptContext()); @@ -631,6 +631,96 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return ScriptContext.GLOBAL; } + // ==================== UNIFIED SCRIPT DATA HANDLING ==================== + + /** + * Unified setGuiData for script GUIs with old container system. + * Handles both language data and tab-specific script loading. + */ + protected void setGuiDataWithOldContainer(NBTTagCompound compound) { + if (compound.hasKey("LoadComplete")) { + loaded = true; + return; + } + + if (!compound.hasKey("Tab")) { + this.handler.setLanguage(compound.getString("ScriptLanguage")); + this.handler.setEnabled(compound.getBoolean("ScriptEnabled")); + this.loadLanguagesData(compound); + } else { + int tab = compound.getInteger("Tab"); + ScriptContainer container = new ScriptContainer(this.handler); + container.readFromNBT(compound.getCompoundTag("Script")); + this.setHandlerContainer(container); + this.initGui(); + } + } + + /** + * Load languages data from NBT. + * Separated for potential override in subclasses if needed. + */ + protected void loadLanguagesData(NBTTagCompound compound) { + NBTTagList data = compound.getTagList("Languages", 10); + HashMap languages = new HashMap(); + + for (int i = 0; i < data.tagCount(); ++i) { + NBTTagCompound comp = data.getCompoundTagAt(i); + java.util.ArrayList scripts = new java.util.ArrayList(); + NBTTagList list = comp.getTagList("Scripts", 8); + + for (int j = 0; j < list.tagCount(); ++j) { + scripts.add(list.getStringTagAt(j)); + } + + languages.put(comp.getString("Language"), scripts); + } + + this.languages = languages; + this.initGui(); + } + + /** + * Set the handler's container. Override if handler is not IScriptHandler. + */ + protected void setHandlerContainer(IScriptUnit container) { + // Default implementation - subclasses may need to cast and set differently + // e.g., ((LinkedItemScript) handler).container = container; + } + + /** + * Unified save method for script GUIs with packet-based saving. + * Subclasses only need to override sendScriptPackets() and sendMetadataPacket(). + */ + protected void saveWithPackets() { + if (loaded) { + this.setScript(); + + List containers = this.handler.getScripts(); + for (int i = 0; i < containers.size(); i++) { + IScriptUnit container = containers.get(i); + sendSavePacket(i, containers.size(), container.writeToNBT(new NBTTagCompound())); + } + + NBTTagCompound scriptData = new NBTTagCompound(); + scriptData.setString("ScriptLanguage", this.handler.getLanguage()); + scriptData.setBoolean("ScriptEnabled", this.handler.getEnabled()); + scriptData.setTag("ScriptConsole", noppes.npcs.NBTTags.NBTLongStringMap(this.handler.getConsoleText())); + + sendSavePacket(-1, containers.size(), scriptData); + } + } + + /** + * Send a script container packet. Override in subclasses. + * @param index The index of this script + * @param totalCount Total number of scripts + * @param scriptNBT The script container NBT data + */ + protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { + // Default: do nothing (for non-packet-based GUIs like GuiScriptItem) + } + public void save() { if (loaded) this.setScript(); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java index 755f56f56..3965c4e33 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java @@ -4,10 +4,10 @@ import kamkeel.npcs.network.packets.request.script.PlayerScriptPacket; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.NBTTags; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.ScriptContext; -import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.data.EffectScript; +import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.controllers.data.PlayerDataScript; From 1df05406c8bb2f4057f8a8377266159130abb908 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 13 Jan 2026 19:08:29 +0200 Subject: [PATCH 212/358] Imported temp place holders from syntax-highlighter-unified --- .../npcs/client/gui/script/GuiScriptInterface.java | 2 +- .../npcs/client/gui/util/GuiScriptTextArea.java | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index d91f63c5a..b2da09a40 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -221,7 +221,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb // Set the scripting language for proper syntax highlighting // Use the container's language if available, otherwise fall back to handler's language String language = (container != null) ? container.getLanguage() : this.handler.getLanguage(); - // activeArea.setLanguage(language); + activeArea.setLanguage(language); // Set the script context for context-aware hook autocomplete activeArea.setScriptContext(getScriptContext()); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 5f3e590f0..99e6ce480 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -448,6 +448,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { public boolean fullscreen() { return GuiScriptInterface.isFullscreen; } + + public void setLanguage(String language) { + if (this.container != null) { + // this.container.setLanguage(language); + if (this.enableCodeHighlighting) { + this.container.formatCodeText(); + } + } + } // ==================== RENDERING ==================== public void drawTextBox(int xMouse, int yMouse) { if (!visible) @@ -2168,7 +2177,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { */ public void setScriptContext(ScriptContext context) { if (this.container != null) { - this.container.setScriptContext(context); + // this.container.setScriptContext(context); } } @@ -2178,7 +2187,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { * @return The script context (NPC, PLAYER, BLOCK, ITEM, etc.) */ public ScriptContext getScriptContext() { - return this.container != null ? this.container.getScriptContext() : ScriptContext.GLOBAL; + // return this.container != null ? this.container.getScriptContext() : ScriptContext.GLOBAL; + return ScriptContext.GLOBAL; } public void setListener(ITextChangeListener listener) { From 8c8f37e75d24c45dfdfee9751715a008c272387e Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 13 Jan 2026 19:13:31 +0200 Subject: [PATCH 213/358] more merge conflict resolution --- src/main/java/noppes/npcs/client/gui/GuiScript.java | 6 +++--- .../java/noppes/npcs/client/gui/script/GuiScriptBlock.java | 1 + .../noppes/npcs/client/gui/script/GuiScriptInterface.java | 2 +- .../java/noppes/npcs/client/gui/script/GuiScriptItem.java | 2 ++ 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index 4349b1ef6..83f38b011 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -184,7 +184,7 @@ public class GuiScript extends GuiScriptInterface { scroll.setList(container.scripts); addScroll(scroll); } - } + private void initSettingsView() { addLabel(new GuiNpcLabel(0, "script.console", guiLeft + 4, guiTop + 16)); @@ -323,8 +323,8 @@ public class GuiScript extends GuiScriptInterface { public void mouseClicked(int mouseX, int mouseY, int mouseButton) { // Check if click is within autocomplete menu bounds and consume it if so GuiScriptTextArea activeArea = getActiveScriptArea(); - boolean isOverAutocomplete = activeArea != null - && activeArea.isPointOnAutocompleteMenu(mouseX, mouseY); + boolean isOverAutocomplete = activeArea != null; + // && activeArea.isPointOnAutocompleteMenu(mouseX, mouseY); if (isOverAutocomplete) { activeArea.mouseClicked(mouseX, mouseY, mouseButton); return; diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java index 5b9762a96..b11947ee7 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java @@ -5,6 +5,7 @@ import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.blocks.tiles.TileScripted; import noppes.npcs.constants.ScriptContext; +import noppes.npcs.controllers.ScriptHookController; import java.util.ArrayList; diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index b2da09a40..90e7e7d06 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -79,7 +79,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } /** Fullscreen toggle button drawn at top-right of viewport */ - private final FullscreenButton fullscreenButton = new FullscreenButton(); + public final FullscreenButton fullscreenButton = new FullscreenButton(); public GuiScriptInterface() { this.drawDefaultBackground = true; diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java index 5d23aca89..fceaeb6ed 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java @@ -6,7 +6,9 @@ import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomItems; import noppes.npcs.NBTTags; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.ScriptContext; +import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.scripted.item.ScriptCustomItem; import java.util.*; From 7b811bdea1b8144cdf4aead489c95091b0e4fa02 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 13 Jan 2026 19:16:58 +0200 Subject: [PATCH 214/358] more bruh --- .../client/gui/script/GuiJaninoScript.java | 2 +- .../client/gui/script/SubGuiSelectList.java | 123 ++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java index 874beca4b..2f609b205 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java @@ -1,6 +1,6 @@ package noppes.npcs.client.gui.script; -import kamkeel.npcdbc.client.gui.component.SubGuiSelectList; + import noppes.npcs.janino.JaninoScript; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.*; diff --git a/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java b/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java new file mode 100644 index 000000000..e2812ecb2 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java @@ -0,0 +1,123 @@ +package noppes.npcs.client.gui.script; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiMenuTopButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.ArrayList; +import java.util.List; + +public class SubGuiSelectList extends SubGuiInterface { + private GuiCustomScroll scroll1; + private GuiCustomScroll scroll2; + + public String allName, selectedName; + public List all; + public List selected; + + public SubGuiSelectList(List all, List selected, String allName, String selectedName) { + setBackground("menubg.png"); + xSize = 346; + ySize = 216; + this.allName = allName; + this.selectedName = selectedName; + + if (all == null) + all = new ArrayList<>(); + this.all = all; + + if (selected == null) + selected = new ArrayList<>(); + this.selected = selected; + } + + @Override + public void initGui() { + super.initGui(); + if (scroll1 == null) { + scroll1 = new GuiCustomScroll(this, 0); + scroll1.setSize(140, 180); + } + + addTopButton(new GuiMenuTopButton(-1, guiLeft + xSize-22 , guiTop - 17, "X")); + + scroll1.guiLeft = guiLeft + 4; + scroll1.guiTop = guiTop + 14; + this.addScroll(scroll1); + addLabel(new GuiNpcLabel(1, allName, guiLeft + 4, guiTop + 4)); + + if (scroll2 == null) { + scroll2 = new GuiCustomScroll(this, 1); + scroll2.setSize(140, 180); + } + scroll2.guiLeft = guiLeft + 200; + scroll2.guiTop = guiTop + 14; + this.addScroll(scroll2); + addLabel(new GuiNpcLabel(2, selectedName, guiLeft + 200, guiTop + 4)); + + List tempAll = new ArrayList<>(all); + tempAll.removeAll(selected); + scroll1.setList(tempAll); + scroll2.setList(selected); + + addButton(new GuiNpcButton(1, guiLeft + 145, guiTop + 40, 55, 20, ">")); + addButton(new GuiNpcButton(2, guiLeft + 145, guiTop + 62, 55, 20, "<")); + + addButton(new GuiNpcButton(3, guiLeft + 145, guiTop + 90, 55, 20, ">>")); + addButton(new GuiNpcButton(4, guiLeft + 145, guiTop + 112, 55, 20, "<<")); + + addButton(new GuiNpcButton(66, guiLeft + 260, guiTop + 194, 60, 20, "gui.done")); + } + + protected void actionPerformed(GuiButton guibutton) { + GuiNpcButton button = (GuiNpcButton) guibutton; + + if (button.id == -1) { + close(); + return; + } + + if (button.id == 1) { + if (scroll1.hasSelected()) { + selected.add(scroll1.getSelected()); + + scroll1.selected = -1; + scroll2.selected = -1; + initGui(); + } + } + if (button.id == 2) { + if (scroll2.hasSelected()) { + selected.remove(scroll2.getSelected()); + scroll2.selected = -1; + initGui(); + } + } + if (button.id == 3) { + selected.clear(); + for (String type : all) + selected.add(type); + + scroll1.selected = -1; + scroll2.selected = -1; + initGui(); + } + if (button.id == 4) { + selected.clear(); + scroll1.selected = -1; + scroll2.selected = -1; + initGui(); + } + if (button.id == 66) { + close(); + } + } + + @Override + public void save() { + + } +} From ba6c3e607c5dfd41434a26e3cd6788ead7cc5985 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 13 Jan 2026 20:30:01 +0200 Subject: [PATCH 215/358] more bruh 2 --- .../java/noppes/npcs/client/gui/script/GuiScriptEffect.java | 3 ++- .../noppes/npcs/client/gui/script/GuiScriptLinkedItem.java | 3 ++- .../java/noppes/npcs/client/gui/script/GuiScriptRecipe.java | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java index bd5f59b1e..e50b31ade 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java @@ -9,6 +9,7 @@ import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.CustomEffect; import noppes.npcs.controllers.data.EffectScript; +import noppes.npcs.controllers.data.IScriptUnit; import java.util.ArrayList; @@ -35,7 +36,7 @@ public class GuiScriptEffect extends GuiScriptInterface { return ScriptContext.GLOBAL; } - protected void setHandlerContainer(ScriptContainer container) { + protected void setHandlerContainer(IScriptUnit container) { this.scriptHandler.container = container; } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java index d00c7b2c3..2a001b732 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java @@ -8,6 +8,7 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptHookController; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.controllers.data.LinkedItem; import noppes.npcs.controllers.data.LinkedItemScript; @@ -34,7 +35,7 @@ public class GuiScriptLinkedItem extends GuiScriptInterface { return ScriptContext.ITEM; } - protected void setHandlerContainer(ScriptContainer container) { + protected void setHandlerContainer(IScriptUnit container) { ((LinkedItemScript) handler).container = container; } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java index 262151d5c..176294f7e 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java @@ -7,6 +7,7 @@ import noppes.npcs.client.gui.global.GuiNpcManageRecipes; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptHookController; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.controllers.data.RecipeAnvil; import noppes.npcs.controllers.data.RecipeCarpentry; import noppes.npcs.controllers.data.RecipeScript; @@ -48,7 +49,7 @@ public class GuiScriptRecipe extends GuiScriptInterface { return ScriptContext.GLOBAL; } - protected void setHandlerContainer(ScriptContainer container) { + protected void setHandlerContainer(IScriptUnit container) { ((RecipeScript) handler).container = container; } From 275cca70c23a810bfbc8199192f6cb707ede4b61 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 13 Jan 2026 20:59:54 +0200 Subject: [PATCH 216/358] Redundant mouseClicked in GuiScript --- .../noppes/npcs/client/gui/GuiScript.java | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index 83f38b011..916a0ecd3 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -318,26 +318,6 @@ public class GuiScript extends GuiScriptInterface { container.script = text; } } - - @Override - public void mouseClicked(int mouseX, int mouseY, int mouseButton) { - // Check if click is within autocomplete menu bounds and consume it if so - GuiScriptTextArea activeArea = getActiveScriptArea(); - boolean isOverAutocomplete = activeArea != null; - // && activeArea.isPointOnAutocompleteMenu(mouseX, mouseY); - if (isOverAutocomplete) { - activeArea.mouseClicked(mouseX, mouseY, mouseButton); - return; - } - - // Check fullscreen button when in script view - if (showScript && !isOverAutocomplete - && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { - return; - } - - super.mouseClicked(mouseX, mouseY, mouseButton); - } @Override public void setGuiData(NBTTagCompound compound) { From 92cd894f39187df9c5965d12f76a2facc56abf62 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 13 Jan 2026 21:39:04 +0200 Subject: [PATCH 217/358] Improved JaninoScript hook generation --- .../java/noppes/npcs/janino/JaninoScript.java | 87 ++++++++++++++++--- 1 file changed, 76 insertions(+), 11 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 5fb02a46e..bac0b1cf8 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -46,6 +46,12 @@ public abstract class JaninoScript implements IScriptUnit { public final IScriptBodyBuilder builder; protected IScriptClassBody scriptBody; + private boolean hooksInitialized = false; + // Hook list for GUI display + private List hookList = new ArrayList<>(); + // Map from display name (with parameters if overloaded) to Method + private Map hookMap = new HashMap<>(); + protected JaninoScript(Class type, Consumer> buildSettings) { this.type = type; this.builder = IScriptBodyBuilder @@ -257,15 +263,22 @@ public abstract class JaninoScript implements IScriptUnit { return true; return script != null && !script.isEmpty(); } - + @Override public String generateHookStub(String hookName, Object hookData) { - // If hookData is a Method, generate full Java method stub - if (hookData instanceof Method) { - return generateMethodStub((Method) hookData); + // Build hook list and map + if (!hooksInitialized) { + createHookList(); + hooksInitialized = true; } - // Fallback for simple hook names - generate basic override - return String.format("@Override\npublic void %s() {\n \n}\n", hookName); + + // Look up in hookMap (handles both simple names and overloaded display names) + Method method = hookMap.get(hookName); + if (method != null) + return generateMethodStub(method); + + // Fallback for unknown hook names - generate basic override + return String.format("public void %s() {\n \n}\n", hookName); } /** @@ -285,12 +298,22 @@ public abstract class JaninoScript implements IScriptUnit { String params = Arrays.stream(method.getParameters()) .map(p -> { String typeName = p.getType().getSimpleName(); - String baseName = Character.toLowerCase(typeName.charAt(0)) + typeName.substring(1); - int count = typeCount.getOrDefault(baseName, 0) + 1; - typeCount.put(baseName, count); + // Check for @ParamName annotation first + ParamName paramNameAnnotation = p.getAnnotation(ParamName.class); + String paramName; - String paramName = count == 1 ? baseName : baseName + (count - 1); + if (paramNameAnnotation != null) { + // Use annotated name + paramName = paramNameAnnotation.value(); + } else { + // Fall back to generated name + String baseName = Character.toLowerCase(typeName.charAt(0)) + typeName.substring(1); + int count = typeCount.getOrDefault(baseName, 0) + 1; + typeCount.put(baseName, count); + paramName = count == 1 ? baseName : baseName + (count - 1); + } + return typeName + " " + paramName; }) .collect(Collectors.joining(", ")); @@ -310,7 +333,49 @@ public abstract class JaninoScript implements IScriptUnit { body = " return null;"; } - return String.format("@Override\n%s%s %s(%s) {\n%s\n}\n", mods, returnTypeStr, name, params, body); + return String.format("%s%s %s(%s) {\n%s\n}\n", mods, returnTypeStr, name, params, body); + } + + + /** + * Create the hook list and hook map for GUI display and generation. + * Groups methods by name and creates display names with parameters for overloaded methods. + */ + private void createHookList() { + // Group all declared methods by their name + Map> grouped = Arrays.stream(type.getDeclaredMethods()) + .collect(Collectors.groupingBy(Method::getName)); + + for (Map.Entry> entry : grouped.entrySet()) { + List methods = entry.getValue(); + + // Sort overloads by parameter count (ascending) + methods.sort(Comparator.comparingInt(Method::getParameterCount)); + boolean first = true; + + for (Method m : methods) { + if (Modifier.isFinal(m.getModifiers())) + continue; // Skip final methods + + // For first (simplest) overload, use just the method name + // For additional overloads, include parameter types in display name + String displayName = first ? m.getName() : + m.getName() + "(" + Arrays.stream(m.getParameterTypes()) + .map(Class::getSimpleName) + .collect(Collectors.joining(", ")) + ")"; + + hookList.add(displayName); + hookMap.put(displayName, m); + first = false; + } + } + } + + /** + * Get the list of hook names for GUI display. + */ + public List getHookList() { + return new ArrayList<>(hookList); } From fbcda09fe24623fd7371b70a41db0a30d2cbb2af Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 13 Jan 2026 21:39:39 +0200 Subject: [PATCH 218/358] Used IScriptUnit hook stub generation --- .../npcs/client/gui/script/GuiScriptInterface.java | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 90e7e7d06..12f0a21f2 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -332,10 +332,17 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { String hook = scroll.getSelected(); if (previousHookClicked.equals(hook)) { + IScriptUnit container = getCurrentContainer(); + if (container == null) + return; + String addString = ""; if (!this.getTextField(2).getText().isEmpty()) addString += "\n"; - addString += "function " + hook + "(event) {\n \n}\n"; + + // Generate the appropriate stub based on language + // hook is already the proper method name from EnumScriptType.function + addString += container.generateHookStub(hook, null); this.getTextField(2).setText(this.getTextField(2).getText() + addString); previousHookClicked = ""; From 73db727c471cf6dc289a844bae473fecf8d4827b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 13 Jan 2026 22:16:51 +0200 Subject: [PATCH 219/358] Deleagted all ScriptContainer NBT based creation to IScriptUnit.createFromNBT --- .../java/kamkeel/npcs/network/PacketUtil.java | 4 +-- src/main/java/noppes/npcs/NBTTags.java | 6 ++-- .../client/gui/script/GuiNPCEventScripts.java | 4 +-- .../client/gui/script/GuiScriptAllNPCs.java | 4 +-- .../client/gui/script/GuiScriptForge.java | 4 +-- .../client/gui/script/GuiScriptInterface.java | 4 +-- .../client/gui/script/GuiScriptPlayers.java | 4 +-- .../npcs/controllers/ScriptContainer.java | 1 + .../npcs/controllers/data/EffectScript.java | 7 ++-- .../npcs/controllers/data/IScriptUnit.java | 35 +++++++++++++++++++ .../controllers/data/LinkedItemScript.java | 7 ++-- .../controllers/data/PlayerDataScript.java | 9 +++-- .../npcs/controllers/data/RecipeScript.java | 7 ++-- .../java/noppes/npcs/janino/JaninoScript.java | 1 + 14 files changed, 61 insertions(+), 36 deletions(-) diff --git a/src/main/java/kamkeel/npcs/network/PacketUtil.java b/src/main/java/kamkeel/npcs/network/PacketUtil.java index cb2881eb8..9235dc1f7 100644 --- a/src/main/java/kamkeel/npcs/network/PacketUtil.java +++ b/src/main/java/kamkeel/npcs/network/PacketUtil.java @@ -251,8 +251,8 @@ public class PacketUtil { data.getScripts().add(new ScriptContainer(data)); } NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); - ScriptContainer script = new ScriptContainer(data); - script.readFromNBT(tabCompound); + // Use factory method to create correct script unit type based on NBT + IScriptUnit script = IScriptUnit.createFromNBT(tabCompound, data); data.getScripts().set(tab, script); } else { NBTTagCompound compound = ByteBufUtils.readNBT(buffer); diff --git a/src/main/java/noppes/npcs/NBTTags.java b/src/main/java/noppes/npcs/NBTTags.java index 21e751f11..fff8d1c5d 100644 --- a/src/main/java/noppes/npcs/NBTTags.java +++ b/src/main/java/noppes/npcs/NBTTags.java @@ -546,8 +546,7 @@ public class NBTTags { for (int i = 0; i < list.tagCount(); ++i) { NBTTagCompound compoundd = list.getCompoundTagAt(i); - ScriptContainer script = new ScriptContainer(handler); - script.readFromNBT(compoundd); + IScriptUnit script = IScriptUnit.createFromNBT(compoundd, handler); scripts.add(script); } @@ -559,8 +558,7 @@ public class NBTTags { for (int i = 0; i < compound.getInteger("TotalScripts"); ++i) { NBTTagCompound containerCompound = compound.getCompoundTag("Tab" + i); - ScriptContainer script = new ScriptContainer(handler); - script.readFromNBT(containerCompound); + IScriptUnit script = IScriptUnit.createFromNBT(containerCompound, handler); scripts.add(script); } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java index 8b72bb145..e0ad70596 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java @@ -35,8 +35,8 @@ public class GuiNPCEventScripts extends GuiScriptInterface { super.setGuiData(compound); } else { int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(script); - container.readFromNBT(compound.getCompoundTag("Script")); + NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); + IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, script); if (script.getScripts().isEmpty()) { for (int i = 0; i < compound.getInteger("TotalScripts"); i++) { script.getScripts().add(new ScriptContainer(script)); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java index 27e2f8433..71bc73d7d 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java @@ -33,8 +33,8 @@ public class GuiScriptAllNPCs extends GuiScriptInterface { super.setGuiData(compound); } else { int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(script); - container.readFromNBT(compound.getCompoundTag("Script")); + NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); + IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, script); if (script.getScripts().isEmpty()) { for (int i = 0; i < compound.getInteger("TotalScripts"); i++) { script.getScripts().add(new ScriptContainer(script)); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java index d4aa8ade7..59333eee4 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java @@ -88,8 +88,8 @@ public class GuiScriptForge extends GuiScriptInterface { super.setGuiData(compound); } else { int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(script); - container.readFromNBT(compound.getCompoundTag("Script")); + NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); + IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, script); if (script.getScripts().isEmpty()) { for (int i = 0; i < compound.getInteger("TotalScripts"); i++) { script.getScripts().add(new ScriptContainer(script)); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 12f0a21f2..3e16377f4 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -656,8 +656,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.loadLanguagesData(compound); } else { int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(this.handler); - container.readFromNBT(compound.getCompoundTag("Script")); + NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); + IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, this.handler); this.setHandlerContainer(container); this.initGui(); } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java index 3965c4e33..de884b356 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java @@ -36,8 +36,8 @@ public class GuiScriptPlayers extends GuiScriptInterface { super.setGuiData(compound); } else { int tab = compound.getInteger("Tab"); - ScriptContainer container = new ScriptContainer(script); - container.readFromNBT(compound.getCompoundTag("Script")); + NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); + IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, script); if (script.getScripts().isEmpty()) { for (int i = 0; i < compound.getInteger("TotalScripts"); i++) { script.getScripts().add(new ScriptContainer(script)); diff --git a/src/main/java/noppes/npcs/controllers/ScriptContainer.java b/src/main/java/noppes/npcs/controllers/ScriptContainer.java index 70e29df25..b85bb1407 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptContainer.java +++ b/src/main/java/noppes/npcs/controllers/ScriptContainer.java @@ -70,6 +70,7 @@ public class ScriptContainer implements IScriptUnit { } public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString(IScriptUnit.NBT_TYPE_KEY, IScriptUnit.TYPE_ECMASCRIPT); if (this.script.length() < 65535) { compound.setString("Script", this.script); } else { diff --git a/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/src/main/java/noppes/npcs/controllers/data/EffectScript.java index 497b46421..d15e88878 100644 --- a/src/main/java/noppes/npcs/controllers/data/EffectScript.java +++ b/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -39,8 +39,7 @@ public class EffectScript implements INpcScriptHandler { enabled = compound.getBoolean("ScriptEnabled"); if (compound.hasKey("ScriptContent", Constants.NBT.TAG_COMPOUND)) { - container = new ScriptContainer(this); - container.readFromNBT(compound.getCompoundTag("ScriptContent")); + container = IScriptUnit.createFromNBT(compound.getCompoundTag("ScriptContent"), this); } return this; } @@ -142,9 +141,7 @@ public class EffectScript implements INpcScriptHandler { if (tab == 0) { NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); - ScriptContainer script = new ScriptContainer(this); - script.readFromNBT(tabCompound); - this.container = script; + this.container = IScriptUnit.createFromNBT(tabCompound, this); } else { NBTTagCompound compound = ByteBufUtils.readNBT(buffer); this.setLanguage(compound.getString("ScriptLanguage")); diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java index d8d280be0..fcd3fc6f8 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java @@ -1,6 +1,8 @@ package noppes.npcs.controllers.data; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.janino.JaninoScript; import java.util.List; import java.util.Map; @@ -13,6 +15,39 @@ import java.util.TreeMap; */ public interface IScriptUnit { + // NBT type identifiers + String NBT_TYPE_KEY = "ScriptUnitType"; + String TYPE_ECMASCRIPT = "ECMAScript"; + String TYPE_JANINO = "Janino"; + + /** + * Create an IScriptUnit from NBT data. + * Uses the "ScriptUnitType" tag to determine which implementation to create. + * + * @param compound The NBT data + * @param handler The script handler (used for ScriptContainer) + * @return A ScriptContainer or JaninoScript based on the NBT type + */ + static IScriptUnit createFromNBT(NBTTagCompound compound, IScriptHandler handler) { + String type = compound.getString(NBT_TYPE_KEY); + IScriptUnit unit; + + if (TYPE_JANINO.equals(type)) { + // Create appropriate JaninoScript based on handler + unit = handler.createJaninoScriptUnit(); + if (unit == null) { + // Fallback to ScriptContainer if handler doesn't support Janino + unit = new ScriptContainer(handler); + } + } else { + // Default to ScriptContainer for ECMAScript or missing type + unit = new ScriptContainer(handler); + } + + unit.readFromNBT(compound); + return unit; + } + // ==================== SCRIPT CONTENT ==================== /** diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java index 18df07229..1ef66c44a 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -37,8 +37,7 @@ public class LinkedItemScript implements INpcScriptHandler { enabled = compound.getBoolean("ScriptEnabled"); if (compound.hasKey("ScriptContent", Constants.NBT.TAG_COMPOUND)) { - container = new ScriptContainer(this); - container.readFromNBT(compound.getCompoundTag("ScriptContent")); + container = IScriptUnit.createFromNBT(compound.getCompoundTag("ScriptContent"), this); } return this; } @@ -136,9 +135,7 @@ public class LinkedItemScript implements INpcScriptHandler { if (tab == 0) { NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); - ScriptContainer script = new ScriptContainer(this); - script.readFromNBT(tabCompound); - this.container = script; + this.container = IScriptUnit.createFromNBT(tabCompound, this); } else { NBTTagCompound compound = ByteBufUtils.readNBT(buffer); this.setLanguage(compound.getString("ScriptLanguage")); diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index 5d2eee9f9..6aa09cd76 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -105,11 +105,10 @@ public class PlayerDataScript implements INpcScriptHandler { if (player != null) { scripts.clear(); for (IScriptUnit script : ScriptController.Instance.playerScripts.scripts) { - if (script instanceof ScriptContainer) { - ScriptContainer s = new ScriptContainer(this); - s.readFromNBT(((ScriptContainer) script).writeToNBT(new NBTTagCompound())); - scripts.add(s); - } + // Clone the script unit by writing to NBT and reading back with factory + NBTTagCompound nbt = script.writeToNBT(new NBTTagCompound()); + IScriptUnit cloned = IScriptUnit.createFromNBT(nbt, this); + scripts.add(cloned); } } lastPlayerUpdate = ScriptController.Instance.lastPlayerUpdate; diff --git a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java index b42595cf5..a021bf35f 100644 --- a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java +++ b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java @@ -35,8 +35,7 @@ public class RecipeScript implements INpcScriptHandler { scriptLanguage = compound.getString("ScriptLanguage"); enabled = compound.getBoolean("ScriptEnabled"); if (compound.hasKey("ScriptContent", Constants.NBT.TAG_COMPOUND)) { - container = new ScriptContainer(this); - container.readFromNBT(compound.getCompoundTag("ScriptContent")); + container = IScriptUnit.createFromNBT(compound.getCompoundTag("ScriptContent"), this); } return this; } @@ -131,9 +130,7 @@ public class RecipeScript implements INpcScriptHandler { this.container = null; if (tab == 0) { NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); - ScriptContainer script = new ScriptContainer(this); - script.readFromNBT(tabCompound); - this.container = script; + this.container = IScriptUnit.createFromNBT(tabCompound, this); } else { NBTTagCompound compound = ByteBufUtils.readNBT(buffer); this.setLanguage(compound.getString("ScriptLanguage")); diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index bac0b1cf8..13a7a33c9 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -381,6 +381,7 @@ public abstract class JaninoScript implements IScriptUnit { @Override public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString(IScriptUnit.NBT_TYPE_KEY, IScriptUnit.TYPE_JANINO); compound.setBoolean("enabled", enabled); compound.setTag("console", NBTTags.NBTLongStringMap(this.console)); compound.setTag("externalScripts", NBTTags.nbtStringList(this.externalScripts)); From 10b6ccf427f9a2c0969a505f54904f37af00d182 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 13 Jan 2026 22:35:54 +0200 Subject: [PATCH 220/358] Converted to using IScriptUnit --- .../noppes/npcs/client/gui/GuiScriptList.java | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiScriptList.java b/src/main/java/noppes/npcs/client/gui/GuiScriptList.java index 0d03d13cd..547f75f83 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScriptList.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScriptList.java @@ -6,6 +6,7 @@ import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.data.IScriptUnit; import java.util.ArrayList; import java.util.List; @@ -13,16 +14,20 @@ import java.util.List; public class GuiScriptList extends SubGuiInterface { private GuiCustomScroll scroll1; private GuiCustomScroll scroll2; - private ScriptContainer container; + private IScriptUnit scriptUnit; private List scripts; public GuiScriptList(List scripts, ScriptContainer container) { - this.container = container; + this(scripts, (IScriptUnit) container); + } + + public GuiScriptList(List scripts, IScriptUnit scriptUnit) { + this.scriptUnit = scriptUnit; setBackground("menubg.png"); xSize = 346; ySize = 216; if (scripts == null) - scripts = new ArrayList(); + scripts = new ArrayList<>(); this.scripts = scripts; } @@ -46,10 +51,10 @@ public class GuiScriptList extends SubGuiInterface { scroll2.guiTop = guiTop + 14; this.addScroll(scroll2); addLabel(new GuiNpcLabel(2, "script.loadedScripts", guiLeft + 200, guiTop + 4)); - List temp = new ArrayList(scripts); - temp.removeAll(container.scripts); + List temp = new ArrayList<>(scripts); + temp.removeAll(scriptUnit.getExternalScripts()); scroll1.setList(temp); - scroll2.setList(container.scripts); + scroll2.setList(scriptUnit.getExternalScripts()); addButton(new GuiNpcButton(1, guiLeft + 145, guiTop + 40, 55, 20, ">")); addButton(new GuiNpcButton(2, guiLeft + 145, guiTop + 62, 55, 20, "<")); @@ -64,7 +69,7 @@ public class GuiScriptList extends SubGuiInterface { GuiNpcButton button = (GuiNpcButton) guibutton; if (button.id == 1) { if (scroll1.hasSelected()) { - container.scripts.add(scroll1.getSelected()); + scriptUnit.getExternalScripts().add(scroll1.getSelected()); scroll1.selected = -1; scroll2.selected = -1; initGui(); @@ -72,22 +77,22 @@ public class GuiScriptList extends SubGuiInterface { } if (button.id == 2) { if (scroll2.hasSelected()) { - container.scripts.remove(scroll2.getSelected()); + scriptUnit.getExternalScripts().remove(scroll2.getSelected()); scroll2.selected = -1; initGui(); } } if (button.id == 3) { - container.scripts.clear(); + scriptUnit.getExternalScripts().clear(); for (String script : scripts) { - container.scripts.add(script); + scriptUnit.getExternalScripts().add(script); } scroll1.selected = -1; scroll2.selected = -1; initGui(); } if (button.id == 4) { - container.scripts.clear(); + scriptUnit.getExternalScripts().clear(); scroll1.selected = -1; scroll2.selected = -1; initGui(); From cd60a03544d9f565541c29de7b61c906bb91b74a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Tue, 13 Jan 2026 23:45:14 +0200 Subject: [PATCH 221/358] Moved all INpcScriptHandler implementations to IScriptHandler as its redundant --- src/main/java/noppes/npcs/EventHooks.java | 33 +++++++++---------- .../controllers/LinkedItemController.java | 4 +-- .../npcs/controllers/data/DataScript.java | 2 +- .../npcs/controllers/data/EffectScript.java | 2 +- .../controllers/data/ForgeDataScript.java | 2 +- .../controllers/data/GlobalNPCDataScript.java | 2 +- .../controllers/data/INpcScriptHandler.java | 7 ++-- .../controllers/data/IScriptBlockHandler.java | 2 +- .../npcs/controllers/data/IScriptHandler.java | 3 ++ .../controllers/data/LinkedItemScript.java | 2 +- .../controllers/data/PlayerDataScript.java | 2 +- .../npcs/controllers/data/RecipeScript.java | 2 +- .../npcs/scripted/item/ScriptCustomItem.java | 6 ++-- .../npcs/scripted/item/ScriptLinkedItem.java | 4 +-- 14 files changed, 37 insertions(+), 36 deletions(-) diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index 0dfac34ff..a0c3d2b57 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -40,7 +40,6 @@ import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.Dialog; import noppes.npcs.controllers.data.EffectScript; import noppes.npcs.controllers.data.ForgeDataScript; -import noppes.npcs.controllers.data.INpcScriptHandler; import noppes.npcs.controllers.data.IScriptBlockHandler; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.Party; @@ -85,7 +84,7 @@ public class EventHooks { } public static void onScriptItemInit(IItemCustomizable item) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); if (handler != null && !handler.isClient()) { ItemEvent.InitEvent event = new ItemEvent.InitEvent(item); handler.callScript(EnumScriptType.INIT, event); @@ -94,7 +93,7 @@ public class EventHooks { } public static void onScriptItemUpdate(IItemCustomizable item, EntityLivingBase player) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); if (handler != null && !handler.isClient()) { ItemEvent.UpdateEvent event = new ItemEvent.UpdateEvent(item, NpcAPI.Instance().getIEntity(player)); handler.callScript(EnumScriptType.TICK, event); @@ -103,7 +102,7 @@ public class EventHooks { } public static boolean onScriptItemTossed(IItemCustomizable item, EntityPlayer player, EntityItem entity) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.TossedEvent event = new ItemEvent.TossedEvent(item, (IPlayer) NpcAPI.Instance().getIEntity(player), NpcAPI.Instance().getIEntity(entity)); if (handler != null) { handler.callScript(EnumScriptType.TOSSED, event); @@ -112,7 +111,7 @@ public class EventHooks { } public static boolean onScriptItemPickedUp(IItemCustomizable item, EntityPlayer player) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.PickedUpEvent event = new ItemEvent.PickedUpEvent(item, (IPlayer) NpcAPI.Instance().getIEntity(player)); if (handler != null) { handler.callScript(EnumScriptType.PICKEDUP, event); @@ -121,7 +120,7 @@ public class EventHooks { } public static boolean onScriptItemSpawn(IItemCustomizable item, EntityItem entity) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.SpawnEvent event = new ItemEvent.SpawnEvent(item, NpcAPI.Instance().getIEntity(entity)); if (handler != null) { handler.callScript(EnumScriptType.SPAWN, event); @@ -130,7 +129,7 @@ public class EventHooks { } public static boolean onScriptItemInteract(IItemCustomizable item, noppes.npcs.scripted.event.ItemEvent.InteractEvent event) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); if (handler != null) { handler.callScript(EnumScriptType.INTERACT, event); } @@ -138,7 +137,7 @@ public class EventHooks { } public static boolean onScriptItemRightClick(IItemCustomizable item, noppes.npcs.scripted.event.ItemEvent.RightClickEvent event) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); if (handler != null) { handler.callScript(EnumScriptType.RIGHT_CLICK, event); } @@ -146,7 +145,7 @@ public class EventHooks { } public static boolean onScriptItemAttack(IItemCustomizable item, noppes.npcs.scripted.event.ItemEvent.AttackEvent event) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); if (handler != null) { handler.callScript(EnumScriptType.ATTACK, event); } @@ -154,7 +153,7 @@ public class EventHooks { } public static boolean onStartUsingCustomItem(IItemCustomizable item, IPlayer player, int duration) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.StartUsingItem event = new ItemEvent.StartUsingItem(item, player, duration); if (handler != null) { handler.callScript(EnumScriptType.START_USING_ITEM, event); @@ -163,7 +162,7 @@ public class EventHooks { } public static boolean onUsingCustomItem(IItemCustomizable item, IPlayer player, int duration) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.UsingItem event = new ItemEvent.UsingItem(item, player, duration); if (handler != null) { handler.callScript(EnumScriptType.USING_ITEM, event); @@ -172,7 +171,7 @@ public class EventHooks { } public static boolean onStopUsingCustomItem(IItemCustomizable item, IPlayer player, int duration) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.StopUsingItem event = new ItemEvent.StopUsingItem(item, player, duration); if (handler != null) { handler.callScript(EnumScriptType.STOP_USING_ITEM, event); @@ -181,7 +180,7 @@ public class EventHooks { } public static boolean onFinishUsingCustomItem(IItemCustomizable item, IPlayer player, int duration) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.FinishUsingItem event = new ItemEvent.FinishUsingItem(item, player, duration); if (handler != null) { handler.callScript(EnumScriptType.FINISH_USING_ITEM, event); @@ -190,7 +189,7 @@ public class EventHooks { } public static void onRepairCustomItem(IItemCustomizable item, IPlayer player, IItemStack left, IItemStack right, float anvilBreakChance) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.RepairItem event = new ItemEvent.RepairItem(item, player, left, right, anvilBreakChance); if (handler != null) { handler.callScript(EnumScriptType.REPAIR_ITEM, event); @@ -199,7 +198,7 @@ public class EventHooks { } public static void onBreakCustomItem(IItemCustomizable item, IPlayer player) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); ItemEvent.BreakItem event = new ItemEvent.BreakItem(item, player); if (handler != null) { handler.callScript(EnumScriptType.BREAK_ITEM, event); @@ -242,7 +241,7 @@ public class EventHooks { public static void onLinkedItemVersionChange(IItemLinked item, int version, int prevVersion) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); LinkedItemEvent.VersionChangeEvent event = new LinkedItemEvent.VersionChangeEvent(item, version, prevVersion); if (handler != null) { handler.callScript(EnumScriptType.LINKED_ITEM_VERSION, event); @@ -251,7 +250,7 @@ public class EventHooks { } public static void onLinkedItemBuild(IItemLinked item) { - INpcScriptHandler handler = (INpcScriptHandler) item.getScriptHandler(); + IScriptHandler handler = (IScriptHandler) item.getScriptHandler(); LinkedItemEvent.BuildEvent event = new LinkedItemEvent.BuildEvent(item); if (handler != null) { handler.callScript(EnumScriptType.LINKED_ITEM_BUILD, event); diff --git a/src/main/java/noppes/npcs/controllers/LinkedItemController.java b/src/main/java/noppes/npcs/controllers/LinkedItemController.java index c2bc19044..48f19958b 100644 --- a/src/main/java/noppes/npcs/controllers/LinkedItemController.java +++ b/src/main/java/noppes/npcs/controllers/LinkedItemController.java @@ -7,7 +7,7 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; import noppes.npcs.api.handler.data.ILinkedItem; import noppes.npcs.api.item.IItemStack; -import noppes.npcs.controllers.data.INpcScriptHandler; +import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.LinkedItem; import noppes.npcs.controllers.data.LinkedItemScript; import noppes.npcs.util.NBTJsonUtil; @@ -90,7 +90,7 @@ public class LinkedItemController { return this.linkedItems.containsValue(linkedItem); } - public INpcScriptHandler getScriptHandler(int id) { + public IScriptHandler getScriptHandler(int id) { return this.linkedItemsScripts.get(id); } diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index 45ba9a966..71921e60b 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -35,7 +35,7 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; -public class DataScript implements INpcScriptHandler { +public class DataScript implements IScriptHandler { public List eventScripts = new ArrayList<>(); private HashMap scripts = new HashMap<>(); diff --git a/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/src/main/java/noppes/npcs/controllers/data/EffectScript.java index d15e88878..5ca67697d 100644 --- a/src/main/java/noppes/npcs/controllers/data/EffectScript.java +++ b/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -19,7 +19,7 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; -public class EffectScript implements INpcScriptHandler { +public class EffectScript implements IScriptHandler { public IScriptUnit container; public String scriptLanguage = "ECMAScript"; public boolean enabled = false; diff --git a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java index d38342f0e..66563bf54 100644 --- a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java @@ -15,7 +15,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; -public class ForgeDataScript implements INpcScriptHandler { +public class ForgeDataScript implements IScriptHandler { private List scripts = new ArrayList<>(); private String scriptLanguage = "ECMAScript"; public long lastInited = -1L; diff --git a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java index f21818136..4bbb742ce 100644 --- a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java @@ -26,7 +26,7 @@ import java.util.OptionalInt; import java.util.OptionalLong; import java.util.TreeMap; -public class GlobalNPCDataScript implements INpcScriptHandler { +public class GlobalNPCDataScript implements IScriptHandler { public List scripts = new ArrayList<>(); public String scriptLanguage = "ECMAScript"; private EntityNPCInterface npc; diff --git a/src/main/java/noppes/npcs/controllers/data/INpcScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/INpcScriptHandler.java index 508836d1c..82119a6d4 100644 --- a/src/main/java/noppes/npcs/controllers/data/INpcScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/INpcScriptHandler.java @@ -1,8 +1,7 @@ package noppes.npcs.controllers.data; -import cpw.mods.fml.common.eventhandler.Event; -import noppes.npcs.constants.EnumScriptType; - public interface INpcScriptHandler extends IScriptHandler { - void callScript(EnumScriptType var1, Event var2); + // This interface is now empty - callScript methods moved to IScriptHandler + // Kept for backward compatibility + } diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptBlockHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptBlockHandler.java index c0445594e..f39b87112 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptBlockHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptBlockHandler.java @@ -2,7 +2,7 @@ package noppes.npcs.controllers.data; import noppes.npcs.api.IBlock; -public interface IScriptBlockHandler extends INpcScriptHandler { +public interface IScriptBlockHandler extends IScriptHandler { IBlock getBlock(); } diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java index 2aa9c1db5..6b551feb9 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -1,11 +1,14 @@ package noppes.npcs.controllers.data; import cpw.mods.fml.common.eventhandler.Event; +import noppes.npcs.constants.EnumScriptType; import java.util.List; import java.util.Map; public interface IScriptHandler { + void callScript(EnumScriptType type, Event event); + void callScript(String hookName, Event event); boolean isClient(); diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java index 1ef66c44a..d13e64c50 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -18,7 +18,7 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; -public class LinkedItemScript implements INpcScriptHandler { +public class LinkedItemScript implements IScriptHandler { public IScriptUnit container; public String scriptLanguage = "ECMAScript"; public boolean enabled = false; diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index 6aa09cd76..7fd2eb644 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -30,7 +30,7 @@ import java.util.OptionalInt; import java.util.OptionalLong; import java.util.TreeMap; -public class PlayerDataScript implements INpcScriptHandler { +public class PlayerDataScript implements IScriptHandler { public List scripts = new ArrayList<>(); public String scriptLanguage = "ECMAScript"; private EntityPlayer player; diff --git a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java index a021bf35f..ff9f681ad 100644 --- a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java +++ b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java @@ -18,7 +18,7 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; -public class RecipeScript implements INpcScriptHandler { +public class RecipeScript implements IScriptHandler { public IScriptUnit container; public String scriptLanguage = "ECMAScript"; public boolean enabled = false; diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java index e03995e16..87d233540 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java @@ -10,7 +10,7 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; -import noppes.npcs.controllers.data.INpcScriptHandler; +import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.scripted.CustomNPCsException; import java.util.ArrayList; @@ -19,7 +19,7 @@ import java.util.Map; import java.util.Objects; import java.util.TreeMap; -public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCustom, INpcScriptHandler { +public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCustom, IScriptHandler { public List scripts = new ArrayList(); public List errored = new ArrayList(); public String scriptLanguage = "ECMAScript"; @@ -70,7 +70,7 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus } @Override - public INpcScriptHandler getScriptHandler() { + public IScriptHandler getScriptHandler() { return this; } diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptLinkedItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptLinkedItem.java index 3a777134c..1c4a2ac91 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptLinkedItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptLinkedItem.java @@ -4,7 +4,7 @@ import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.api.item.IItemLinked; import noppes.npcs.controllers.LinkedItemController; -import noppes.npcs.controllers.data.INpcScriptHandler; +import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.LinkedItem; public class ScriptLinkedItem extends ScriptCustomizableItem implements IItemLinked { @@ -51,7 +51,7 @@ public class ScriptLinkedItem extends ScriptCustomizableItem implements IItemLin } @Override - public INpcScriptHandler getScriptHandler() { + public IScriptHandler getScriptHandler() { if (this.getLinkedItem() == null) return null; return this.getLinkedItem().getScriptHandler(); From cc0959dcfe8db467c4880dcc4640ee8fe75aa366 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 14 Jan 2026 00:16:58 +0200 Subject: [PATCH 222/358] Added run(EnumScriptType, Event) and run(String, Event) methods to IScriptUnit interface, with JaninoScript delegating to callByHookType(). This provides a cleaner abstraction that supports both ScriptContainer and JaninoScript without type checking. Affected files: - IScriptUnit: Added run() methods to interface - JaninoScript: Implemented run() delegation methods - DataScript, PlayerDataScript, GlobalNPCDataScript, ForgeDataScript - EffectScript, LinkedItemScript, RecipeScript - ScriptCustomItem, TileScripted: Refactored to use polymorphic calls --- .../npcs/blocks/tiles/TileScripted.java | 5 +- .../npcs/controllers/data/DataScript.java | 4 +- .../npcs/controllers/data/EffectScript.java | 6 +- .../controllers/data/ForgeDataScript.java | 9 +- .../controllers/data/GlobalNPCDataScript.java | 9 +- .../npcs/controllers/data/IScriptUnit.java | 25 +++++- .../controllers/data/LinkedItemScript.java | 6 +- .../controllers/data/PlayerDataScript.java | 26 +++--- .../npcs/controllers/data/RecipeScript.java | 6 +- .../java/noppes/npcs/janino/JaninoScript.java | 85 ++++++++++++++++++- .../java/noppes/npcs/janino/ScriptHook.java | 22 +++++ .../npcs/scripted/item/ScriptCustomItem.java | 5 +- 12 files changed, 156 insertions(+), 52 deletions(-) create mode 100644 src/main/java/noppes/npcs/janino/ScriptHook.java diff --git a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java index 2ecdb518e..7174bac6e 100644 --- a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java +++ b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java @@ -25,7 +25,6 @@ import noppes.npcs.api.IBlock; import noppes.npcs.api.block.ITextPlane; import noppes.npcs.client.renderer.blocks.BlockScriptedRenderer; import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.IScriptBlockHandler; import noppes.npcs.controllers.data.IScriptUnit; @@ -369,9 +368,7 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler { } for (IScriptUnit script : scripts) { - if (script instanceof ScriptContainer) { - ((ScriptContainer) script).run(type, event); - } + script.run(type, event); } } diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index 71921e60b..014959919 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -153,9 +153,7 @@ public class DataScript implements IScriptHandler { } for (IScriptUnit script : this.eventScripts) { - if (script instanceof ScriptContainer) { - ((ScriptContainer) script).run(type, event); - } + script.run(type, event); } ScriptContainer script = scripts.get(type); diff --git a/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/src/main/java/noppes/npcs/controllers/data/EffectScript.java index 5ca67697d..dcd497fc8 100644 --- a/src/main/java/noppes/npcs/controllers/data/EffectScript.java +++ b/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -8,7 +8,6 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraftforge.common.util.Constants; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.scripted.event.player.PlayerEvent; @@ -62,8 +61,9 @@ public class EffectScript implements IScriptHandler { if (!this.isEnabled()) { return; } - if (container instanceof ScriptContainer) - ((ScriptContainer) container).run(s, event); + if (container != null) { + container.run(s, event); + } } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java index 66563bf54..043e56efe 100644 --- a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java @@ -78,14 +78,9 @@ public class ForgeDataScript implements IScriptHandler { } for (IScriptUnit script : this.scripts) { - if (script == null || !script.hasCode()) + if (script == null || script.hasErrored() || !script.hasCode()) continue; - if (script instanceof ScriptContainer) { - ScriptContainer container = (ScriptContainer) script; - if (container.errored) - continue; - container.run(type, event); - } + script.run(type, event); } } } diff --git a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java index 4bbb742ce..c4dc15913 100644 --- a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java @@ -94,14 +94,9 @@ public class GlobalNPCDataScript implements IScriptHandler { } for (IScriptUnit script : this.scripts) { - if (script == null || !script.hasCode()) + if (script == null || script.hasErrored() || !script.hasCode()) continue; - if (script instanceof ScriptContainer) { - ScriptContainer container = (ScriptContainer) script; - if (container.errored) - continue; - container.run(hookName, event); - } + script.run(hookName, event); } } } diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java index fcd3fc6f8..392901ede 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java @@ -1,8 +1,9 @@ package noppes.npcs.controllers.data; +import cpw.mods.fml.common.eventhandler.Event; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.janino.JaninoScript; import java.util.List; import java.util.Map; @@ -149,4 +150,26 @@ public interface IScriptUnit { * Set the error state of this script unit. */ void setErrored(boolean errored); + + // ==================== EXECUTION ==================== + + /** + * Execute this script unit for the given hook type and event. + * For ScriptContainer: calls run(EnumScriptType, Event) + * For JaninoScript: calls callByHookType(EnumScriptType, Event) + * + * @param type The script hook type + * @param event The event object + */ + void run(EnumScriptType type, Event event); + + /** + * Execute this script unit for the given hook name and event. + * For ScriptContainer: calls run(String, Event) + * For JaninoScript: converts hookName to EnumScriptType and calls callByHookType + * + * @param hookName The hook name (e.g., "init", "interact") + * @param event The event object + */ + void run(String hookName, Object event); } diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java index d13e64c50..a2f3a3025 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -8,7 +8,6 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraftforge.common.util.Constants; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import java.io.IOException; @@ -56,8 +55,9 @@ public class LinkedItemScript implements IScriptHandler { if (!this.isEnabled()) { return; } - if (container instanceof ScriptContainer) - ((ScriptContainer) container).run(s, event); + if (container != null) { + container.run(s, event); + } } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index 7fd2eb644..c8dd007f7 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -121,18 +121,15 @@ public class PlayerDataScript implements IScriptHandler { IScriptUnit script = scripts.get(i); if (errored.contains(i)) continue; - if (script instanceof ScriptContainer) { - ScriptContainer container = (ScriptContainer) script; - container.run(hookName, event); - if (container.errored) { - errored.add(i); - } - for (Entry entry : container.console.entrySet()) { - if (!console.containsKey(entry.getKey())) - console.put(entry.getKey(), " tab " + (i + 1) + ":\n" + entry.getValue()); - } - container.console.clear(); + script.run(hookName, event); + if (script.hasErrored()) { + errored.add(i); } + for (Entry entry : script.getConsole().entrySet()) { + if (!console.containsKey(entry.getKey())) + console.put(entry.getKey(), " tab " + (i + 1) + ":\n" + entry.getValue()); + } + script.clearConsole(); } } else { if (ScriptController.Instance.lastLoaded > this.lastInited || ScriptController.Instance.lastPlayerUpdate > this.lastPlayerUpdate) { @@ -151,12 +148,9 @@ public class PlayerDataScript implements IScriptHandler { } for (IScriptUnit script : this.scripts) { - if (script == null || script.hasErrored()|| !script.hasCode()) + if (script == null || script.hasErrored() || !script.hasCode()) continue; - if (script instanceof ScriptContainer) { - ScriptContainer container = (ScriptContainer) script; - container.run(hookName, event); - } + script.run(hookName, event); } } } diff --git a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java index ff9f681ad..ee1b32c08 100644 --- a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java +++ b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java @@ -8,7 +8,6 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraftforge.common.util.Constants; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import java.io.IOException; @@ -53,8 +52,9 @@ public class RecipeScript implements IScriptHandler { public void callScript(String s, Event event) { if (!this.isEnabled()) return; - if (container instanceof ScriptContainer) - ((ScriptContainer) container).run(s, event); + if (container != null) { + container.run(s, event); + } } @Override diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 13a7a33c9..a5e1f3274 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -1,12 +1,14 @@ package noppes.npcs.janino; import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.eventhandler.Event; import io.github.somehussar.janinoloader.api.script.IScriptBodyBuilder; import io.github.somehussar.janinoloader.api.script.IScriptClassBody; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.NBTTags; import noppes.npcs.config.ConfigScript; +import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.IScriptUnit; import org.codehaus.commons.compiler.InternalCompilerException; @@ -51,6 +53,8 @@ public abstract class JaninoScript implements IScriptUnit { private List hookList = new ArrayList<>(); // Map from display name (with parameters if overloaded) to Method private Map hookMap = new HashMap<>(); + // Map from EnumScriptType to Method for OLD tab-based script system + private Map hookTypeMap = new HashMap<>(); protected JaninoScript(Class type, Consumer> buildSettings) { this.type = type; @@ -63,6 +67,9 @@ public abstract class JaninoScript implements IScriptUnit { sandbox = new Sandbox(permissions); this.scriptBody = builder.build(); + + // Build hook type map from @ScriptHook annotations + initializeHookTypeMap(); } protected T getUnsafe() { @@ -195,9 +202,85 @@ public abstract class JaninoScript implements IScriptUnit { sb.append(code).append("\n"); } } - + + /** + * Scans the Functions interface for @ScriptHook annotations and builds the hookTypeMap. + * This allows direct method invocation by EnumScriptType without switch statements. + */ + private void initializeHookTypeMap() { + for (Method method : type.getDeclaredMethods()) { + ScriptHook annotation = method.getAnnotation(ScriptHook.class); + if (annotation != null) { + for (EnumScriptType hookType : annotation.value()) { + hookTypeMap.put(hookType, method); + } + } + } + } + + /** + * Call a hook method by its EnumScriptType. + * Used by the OLD tab-based script system to execute hooks directly. + * + * @param hookType The script type/hook to execute + * @param event The event object to pass to the hook method + */ + public void callByHookType(EnumScriptType hookType, Object event) { + Method method = hookTypeMap.get(hookType); + if (method == null) { + // Hook not implemented or not annotated + return; + } + + ensureCompiled(); + T t = getUnsafe(); + if (t == null) + return; + + try { + sandbox.confine((PrivilegedAction) () -> { + try { + method.invoke(t, event); + } catch (Exception e) { + appendConsole("Error calling hook " + hookType.name() + ": " + e.getMessage()); + } + return null; + }); + } catch (Exception e) { + appendConsole("Runtime Error in hook " + hookType.name() + ": " + e.getMessage()); + } + } + // ==================== IScriptUnit IMPLEMENTATION ==================== + /** + * Execute this script unit for the given hook type and event. + * Implementation of IScriptUnit.run(EnumScriptType, Event) + * + * @param type The script hook type + * @param event The event object + */ + @Override + public void run(EnumScriptType type, Event event) { + callByHookType(type, event); + } + + /** + * Execute this script unit for the given hook name and event. + * Implementation of IScriptUnit.run(String, Event) + * + * @param hookName The hook name (e.g., "init", "interact") + * @param event The event object + */ + @Override + public void run(String hookName, Object event) { + try { + EnumScriptType enumType = EnumScriptType.valueOfIgnoreCase(hookName); + callByHookType(enumType, event); + } catch (IllegalArgumentException ignored) { + // Hook name doesn't map to an EnumScriptType + } + } @Override public String getScript() { return this.script; diff --git a/src/main/java/noppes/npcs/janino/ScriptHook.java b/src/main/java/noppes/npcs/janino/ScriptHook.java new file mode 100644 index 000000000..1b1ba598b --- /dev/null +++ b/src/main/java/noppes/npcs/janino/ScriptHook.java @@ -0,0 +1,22 @@ +package noppes.npcs.janino; + +import noppes.npcs.constants.EnumScriptType; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to mark script hook methods and associate them with their EnumScriptType. + * Used by JaninoScript to automatically build a map of hook types to methods. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface ScriptHook { + /** + * The script type(s) this method handles. + * Multiple types can be specified for methods that handle multiple hooks. + */ + EnumScriptType[] value(); +} diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java index 87d233540..02f27b71c 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java @@ -8,7 +8,6 @@ import noppes.npcs.NBTTags; import noppes.npcs.api.item.IItemCustom; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.data.IScriptUnit; -import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.scripted.CustomNPCsException; @@ -101,9 +100,7 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus if (script == null || script.hasErrored() || !script.hasCode()) continue; - if (script instanceof ScriptContainer) { - ((ScriptContainer) script).run(hookName, event); - } + script.run(hookName, event); if (script.hasErrored()) { this.errored.add(i); From 54d0fe2b1b73a37d5df6e200e023f7d857f6e997 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 14 Jan 2026 00:24:38 +0200 Subject: [PATCH 223/358] Very first implementation of JaninoNpcScript --- .../npcs/controllers/data/DataScript.java | 5 + .../java/noppes/npcs/janino/JaninoScript.java | 2 + .../npcs/janino/annotations/ParamName.java | 20 +++ .../janino/{ => annotations}/ScriptHook.java | 2 +- .../npcs/janino/impl/JaninoNpcScript.java | 120 ++++++++++++++++++ 5 files changed, 148 insertions(+), 1 deletion(-) create mode 100644 src/main/java/noppes/npcs/janino/annotations/ParamName.java rename src/main/java/noppes/npcs/janino/{ => annotations}/ScriptHook.java (94%) create mode 100644 src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index 014959919..6e07223a8 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -19,6 +19,7 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.janino.impl.JaninoNpcScript; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.ScriptWorld; import noppes.npcs.scripted.constants.EntityType; @@ -312,4 +313,8 @@ public class DataScript implements IScriptHandler { public Collection getNPCScripts() { return this.scripts.values(); } + @Override + public IScriptUnit createJaninoScriptUnit() { + return new JaninoNpcScript(); + } } diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index a5e1f3274..0f9d5c9f1 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -11,6 +11,8 @@ import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.IScriptUnit; +import noppes.npcs.janino.annotations.ParamName; +import noppes.npcs.janino.annotations.ScriptHook; import org.codehaus.commons.compiler.InternalCompilerException; import org.codehaus.commons.compiler.Sandbox; diff --git a/src/main/java/noppes/npcs/janino/annotations/ParamName.java b/src/main/java/noppes/npcs/janino/annotations/ParamName.java new file mode 100644 index 000000000..6b1e9ec25 --- /dev/null +++ b/src/main/java/noppes/npcs/janino/annotations/ParamName.java @@ -0,0 +1,20 @@ +package noppes.npcs.janino.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Specifies the parameter name and type to use when generating method stubs. + * Applied to interface method parameters to preserve their names and types in generated code. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +public @interface ParamName { + /** The parameter name (e.g., "event") */ + String value(); + + /** The fully qualified type name (e.g., "noppes.npcs.api.event.INpcEvent.InitEvent") */ + String type() default ""; +} diff --git a/src/main/java/noppes/npcs/janino/ScriptHook.java b/src/main/java/noppes/npcs/janino/annotations/ScriptHook.java similarity index 94% rename from src/main/java/noppes/npcs/janino/ScriptHook.java rename to src/main/java/noppes/npcs/janino/annotations/ScriptHook.java index 1b1ba598b..d4720b5d8 100644 --- a/src/main/java/noppes/npcs/janino/ScriptHook.java +++ b/src/main/java/noppes/npcs/janino/annotations/ScriptHook.java @@ -1,4 +1,4 @@ -package noppes.npcs.janino; +package noppes.npcs.janino.annotations; import noppes.npcs.constants.EnumScriptType; diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java new file mode 100644 index 000000000..b41e2aac8 --- /dev/null +++ b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java @@ -0,0 +1,120 @@ +package noppes.npcs.janino.impl; + +import noppes.npcs.api.event.IAbilityEvent; +import noppes.npcs.api.event.INpcEvent; +import noppes.npcs.api.event.IProjectileEvent; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.janino.JaninoScript; +import noppes.npcs.janino.annotations.ParamName; +import noppes.npcs.janino.annotations.ScriptHook; + +/** + * Janino (Java) script implementation for NPC events. + * This allows NPCs to use compiled Java code instead of ECMAScript. + */ +public class JaninoNpcScript extends JaninoScript { + + public JaninoNpcScript() { + super(Functions.class, (builder) -> builder + .setDefaultImports( + "noppes.npcs.api.*", + "noppes.npcs.api.entity.*", + "noppes.npcs.api.event.*", + "noppes.npcs.api.handler.*", + "noppes.npcs.api.handler.data.*", + "noppes.npcs.api.item.*", + "noppes.npcs.api.ability.*" + ) + ); + } + + /** + * Interface defining the hook methods available for NPC scripts. + * Each method corresponds to an NPC event from ScriptHookController CONTEXT_NPC. + * Methods are annotated with @ScriptHook to map them to their EnumScriptType. + */ + public interface Functions { + // Core lifecycle events + @ScriptHook(EnumScriptType.INIT) + default void init(@ParamName("event") INpcEvent.InitEvent event) {} + + @ScriptHook(EnumScriptType.TICK) + default void tick(@ParamName("event") INpcEvent.UpdateEvent event) {} + + + // Interaction events + @ScriptHook(EnumScriptType.INTERACT) + default void interact(@ParamName("event") INpcEvent.InteractEvent event) {} + + @ScriptHook(EnumScriptType.DIALOG) + default void dialog(@ParamName("event") INpcEvent.DialogEvent event) {} + + @ScriptHook(EnumScriptType.DIALOG_CLOSE) + default void dialogClose(@ParamName("event") INpcEvent.DialogClosedEvent event) {} + + + // Combat events - damage + @ScriptHook(EnumScriptType.DAMAGED) + default void damaged(@ParamName("event") INpcEvent.DamagedEvent event) {} + + @ScriptHook(EnumScriptType.KILLED) + default void killed(@ParamName("event") INpcEvent.DiedEvent event) {} + + @ScriptHook(EnumScriptType.KILLS) + default void kills(@ParamName("event") INpcEvent.KilledEntityEvent event) {} + + + // Combat events - attacks + @ScriptHook(EnumScriptType.ATTACK_MELEE) + default void meleeAttack(@ParamName("event") INpcEvent.MeleeAttackEvent event) {} + + @ScriptHook(EnumScriptType.ATTACK_SWING) + default void meleeSwing(@ParamName("event") INpcEvent.SwingEvent event) {} + + @ScriptHook(EnumScriptType.RANGED_LAUNCHED) + default void rangedLaunched(@ParamName("event") INpcEvent.RangedLaunchedEvent event) {} + + + // Targeting events + @ScriptHook(EnumScriptType.TARGET) + default void target(@ParamName("event") INpcEvent.TargetEvent event) {} + + @ScriptHook(EnumScriptType.TARGET_LOST) + default void targetLost(@ParamName("event") INpcEvent.TargetLostEvent event) {} + + // Other NPC events + @ScriptHook(EnumScriptType.COLLIDE) + default void collide(@ParamName("event") INpcEvent.CollideEvent event) {} + + @ScriptHook(EnumScriptType.TIMER) + default void timer(@ParamName("event") INpcEvent.TimerEvent event) {} + + + // Projectile events + @ScriptHook(EnumScriptType.PROJECTILE_TICK) + default void projectileTick(@ParamName("event") IProjectileEvent.UpdateEvent event) {} + + @ScriptHook(EnumScriptType.PROJECTILE_IMPACT) + default void projectileImpact(@ParamName("event") IProjectileEvent.ImpactEvent event) {} + + + // Ability events + @ScriptHook(EnumScriptType.ABILITY_START) + default void abilityStart(@ParamName("event") IAbilityEvent.StartEvent event) {} + + @ScriptHook(EnumScriptType.ABILITY_EXECUTE) + default void abilityExecute(@ParamName("event") IAbilityEvent.ExecuteEvent event) {} + + @ScriptHook(EnumScriptType.ABILITY_HIT) + default void abilityHit(@ParamName("event") IAbilityEvent.HitEvent event) {} + + @ScriptHook(EnumScriptType.ABILITY_TICK) + default void abilityTick(@ParamName("event") IAbilityEvent.TickEvent event) {} + + @ScriptHook(EnumScriptType.ABILITY_INTERRUPT) + default void abilityInterrupt(@ParamName("event") IAbilityEvent.InterruptEvent event) {} + + @ScriptHook(EnumScriptType.ABILITY_COMPLETE) + default void abilityComplete(@ParamName("event") IAbilityEvent.CompleteEvent event) {} + } +} From a44cc4f1bbfa44e37501b2e102af04f2568d34a4 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 14 Jan 2026 02:49:20 +0200 Subject: [PATCH 224/358] Passed default imports to JaninoScript --- .../java/noppes/npcs/janino/JaninoScript.java | 11 +++++---- .../npcs/janino/impl/JaninoNpcScript.java | 23 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 0f9d5c9f1..22fb9ee95 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -58,11 +58,14 @@ public abstract class JaninoScript implements IScriptUnit { // Map from EnumScriptType to Method for OLD tab-based script system private Map hookTypeMap = new HashMap<>(); - protected JaninoScript(Class type, Consumer> buildSettings) { + // Default imports for this script type (e.g., noppes.npcs.api.*) + private final String[] defaultImports; + + protected JaninoScript(Class type, String[] defaultImports) { this.type = type; - this.builder = IScriptBodyBuilder - .getBuilder(type, CustomNpcs.getClientCompiler()); - buildSettings.accept(builder); + this.defaultImports = defaultImports != null ? defaultImports : new String[0]; + this.builder = IScriptBodyBuilder.getBuilder(type, CustomNpcs.getClientCompiler()) + .setDefaultImports(defaultImports); Permissions permissions = new Permissions(); permissions.setReadOnly(); diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java index b41e2aac8..4b051b37e 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java @@ -14,18 +14,19 @@ import noppes.npcs.janino.annotations.ScriptHook; */ public class JaninoNpcScript extends JaninoScript { + /** Default imports available to NPC scripts without explicit import statements. */ + public static final String[] DEFAULT_IMPORTS = { + "noppes.npcs.api.*", + "noppes.npcs.api.entity.*", + "noppes.npcs.api.event.*", + "noppes.npcs.api.handler.*", + "noppes.npcs.api.handler.data.*", + "noppes.npcs.api.item.*", + "noppes.npcs.api.ability.*" + }; + public JaninoNpcScript() { - super(Functions.class, (builder) -> builder - .setDefaultImports( - "noppes.npcs.api.*", - "noppes.npcs.api.entity.*", - "noppes.npcs.api.event.*", - "noppes.npcs.api.handler.*", - "noppes.npcs.api.handler.data.*", - "noppes.npcs.api.item.*", - "noppes.npcs.api.ability.*" - ) - ); + super(Functions.class, DEFAULT_IMPORTS); } /** From d17534120b7f501ccae88551755f8ac5eca7d7d0 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 14 Jan 2026 03:41:24 +0200 Subject: [PATCH 225/358] Merged GuiJaninoScript with GuiScriptInterface and moved usages --- .../client/gui/script/GuiScriptInterface.java | 75 +++++++++++++++++-- .../npcs/controllers/ScriptContainer.java | 5 +- .../npcs/controllers/data/IScriptUnit.java | 6 ++ 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 3e16377f4..0dc4036b0 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -1,9 +1,9 @@ package noppes.npcs.client.gui.script; +import net.minecraft.client.Minecraft; import net.minecraft.client.gui.*; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; -import net.minecraft.util.ResourceLocation; import net.minecraft.util.StatCollector; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.NoppesUtil; @@ -29,7 +29,6 @@ import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.opengl.Display; -import org.lwjgl.opengl.GL11; import java.util.ArrayList; import java.util.Date; @@ -50,6 +49,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected Map textAreas = new HashMap<>(); protected GuiScreen parent; + /** Single script being edited (handler-less mode) */ + protected IScriptUnit singleScript; + public boolean singleContainer = false; /** If true, uses scroll-based tab selection instead of top buttons */ @@ -88,6 +90,39 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.setBackground("menubg.png"); } + /** + * Constructor for single JaninoScript editing (handler-less mode). + * Used for scripts like OverlayScript that don't have an IScriptHandler. + * + * @param parent The parent GUI to return to when closing + * @param script The JaninoScript to edit + */ + public GuiScriptInterface(GuiScreen parent, IScriptUnit script) { + this(); + this.parent = parent; + this.singleScript = script; + this.singleContainer = true; + this.loaded = true; + + // Start on script tab (tab 1) + this.activeTab = 1; + + // Build hook list from the script's type using reflection + // this.hookList = script.getHookList(); + } + + /** + * Create and initialize a GuiScriptInterface for a JaninoScript. + * This is the main entry point for handler-less script editing. + */ + public static GuiScriptInterface create(GuiScreen parent, IScriptUnit script) { + GuiScriptInterface gui = new GuiScriptInterface(parent, script); + gui.setWorldAndResolution(Minecraft.getMinecraft(), gui.width, gui.height); + Minecraft.getMinecraft().currentScreen = gui; + gui.initGui(); + return gui; + } + public void initGui() { // ==================== BASE LAYOUT CALCULATION ==================== this.ySize = (int) ((double) this.xSize * 0.56D); @@ -302,6 +337,12 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.addButton(new GuiNpcButton(111, var9 + 81, this.guiTop + 99, 80, 20, "script.apisrc")); } + public GuiScriptInterface setDimensions(int x, int y) { + this.xSize = x; + this.ySize = y; + return this; + } + // ==================== RENDERING ==================== @Override @@ -448,6 +489,10 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb * Override in subclasses with different container access patterns. */ protected IScriptUnit getCurrentContainer() { + // Handler-less single JaninoScript mode + if (singleScript != null) + return singleScript; + int idx = getActiveScriptIndex(); if (idx >= 0 && idx < this.handler.getScripts().size()) return this.handler.getScripts().get(idx); @@ -475,6 +520,11 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.setScript(); this.activeTab = guibutton.id; this.initGui(); + + // Ensure JaninoScript is compiled after editing + IScriptUnit current = getCurrentContainer(); + if (current != null) + current.ensureCompiled(); } if (guibutton.id == scriptLimit) { @@ -590,9 +640,11 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } protected void setScript() { - if (this.activeTab > 0 || useScrollTabs) { + if (this.activeTab > 0 || useScrollTabs || singleScript != null) { IScriptUnit container = getCurrentContainer(); - if (container == null) { + + // Skip container creation for single JaninoScript mode + if (container == null && singleScript == null) { container = new ScriptContainer(this.handler); if (singleContainer) setHandlerContainer(container); @@ -600,10 +652,12 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.handler.getScripts().add(container); } - String text = (this.getTextField(2)).getText(); - text = text.replace("\r\n", "\n"); - text = text.replace("\r", "\n"); - container.setScript(text); + if (container != null) { + String text = (this.getTextField(2)).getText(); + text = text.replace("\r\n", "\n"); + text = text.replace("\r", "\n"); + container.setScript(text); + } } } @@ -753,6 +807,11 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb @Override public void close() { + // Ensure JaninoScript is compiled after editing + IScriptUnit current = getCurrentContainer(); + if (current != null) + current.ensureCompiled(); + if (parent != null) { this.save(); parent.setWorldAndResolution(mc, width, height); diff --git a/src/main/java/noppes/npcs/controllers/ScriptContainer.java b/src/main/java/noppes/npcs/controllers/ScriptContainer.java index b85bb1407..eb0b8b1c4 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptContainer.java +++ b/src/main/java/noppes/npcs/controllers/ScriptContainer.java @@ -328,7 +328,10 @@ public class ScriptContainer implements IScriptUnit { // ECMAScript function template return "function " + hookName + "(event) {\n \n}\n"; } - + + @Override + public void ensureCompiled() {} + @Override public boolean hasErrored() { return this.errored; diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java index 392901ede..1cd661a6e 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java @@ -135,6 +135,12 @@ public interface IScriptUnit { void readFromNBT(NBTTagCompound compound); // ==================== STATE ==================== + + /** + * Ensure this script unit is compiled (for Janino). + * No-op for ScriptContainer. + */ + void ensureCompiled(); /** * Check if this script unit has any code (script or external scripts). From 2c2e13c2f74557b2625f4c51aa478036ad2b7a3b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 14 Jan 2026 03:50:47 +0200 Subject: [PATCH 226/358] Morea ScriptContainer -> IScriptUnit abstraction --- .../client/gui/script/EventGuiScriptList.java | 24 +++++++++++-------- .../client/gui/script/GuiScriptInterface.java | 9 ++++--- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java b/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java index 530a3a2be..cd47fed55 100644 --- a/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java +++ b/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java @@ -5,7 +5,7 @@ import noppes.npcs.client.gui.util.GuiCustomScroll; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.SubGuiInterface; -import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.data.IScriptUnit; import java.util.ArrayList; import java.util.Iterator; @@ -14,10 +14,10 @@ import java.util.List; public class EventGuiScriptList extends SubGuiInterface { private GuiCustomScroll scroll1; private GuiCustomScroll scroll2; - private ScriptContainer container; + private IScriptUnit container; private List scripts; - public EventGuiScriptList(List scripts, ScriptContainer container) { + public EventGuiScriptList(List scripts, IScriptUnit container) { this.container = container; this.setBackground("menubg.png"); this.xSize = 346; @@ -50,9 +50,9 @@ public class EventGuiScriptList extends SubGuiInterface { this.addScroll(this.scroll2); this.addLabel(new GuiNpcLabel(2, "script.loadedScripts", this.guiLeft + 200, this.guiTop + 4)); ArrayList temp = new ArrayList(this.scripts); - temp.removeAll(this.container.scripts); + temp.removeAll(getScripts()); this.scroll1.setList(temp); - this.scroll2.setList(this.container.scripts); + this.scroll2.setList(getScripts()); this.addButton(new GuiNpcButton(1, this.guiLeft + 145, this.guiTop + 40, 55, 20, ">")); this.addButton(new GuiNpcButton(2, this.guiLeft + 145, this.guiTop + 62, 55, 20, "<")); this.addButton(new GuiNpcButton(3, this.guiLeft + 145, this.guiTop + 90, 55, 20, ">>")); @@ -63,25 +63,25 @@ public class EventGuiScriptList extends SubGuiInterface { protected void actionPerformed(GuiButton guibutton) { GuiNpcButton button = (GuiNpcButton) guibutton; if (button.id == 1 && this.scroll1.hasSelected()) { - this.container.scripts.add(this.scroll1.getSelected()); + getScripts().add(this.scroll1.getSelected()); this.scroll1.selected = -1; this.scroll2.selected = -1; this.initGui(); } if (button.id == 2 && this.scroll2.hasSelected()) { - this.container.scripts.remove(this.scroll2.getSelected()); + getScripts().remove(this.scroll2.getSelected()); this.scroll2.selected = -1; this.initGui(); } if (button.id == 3) { - this.container.scripts.clear(); + getScripts().clear(); Iterator var3 = this.scripts.iterator(); while (var3.hasNext()) { String script = (String) var3.next(); - this.container.scripts.add(script); + getScripts().add(script); } this.scroll1.selected = -1; @@ -90,7 +90,7 @@ public class EventGuiScriptList extends SubGuiInterface { } if (button.id == 4) { - this.container.scripts.clear(); + getScripts().clear(); this.scroll1.selected = -1; this.scroll2.selected = -1; this.initGui(); @@ -102,6 +102,10 @@ public class EventGuiScriptList extends SubGuiInterface { } + public List getScripts() { + return this.container.getExternalScripts(); + } + public void save() { } } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 0dc4036b0..c20c653d5 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -452,10 +452,10 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.setScript(); } if (i == 102) { - ScriptContainer container; + IScriptUnit container; if (this.activeTab > 0) { - container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); - container.script = ""; + container = this.handler.getScripts().get(this.activeTab - 1); + container.setScript(""); } else { this.handler.clearConsole(); if (this.handler instanceof ScriptCustomItem) { @@ -566,7 +566,6 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb displayGuiScreen(guiyesno); } - ScriptContainer container; if (guibutton.id == 102) { GuiYesNo guiyesno = new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), StatCollector.translateToLocal("gui.sure"), 102); displayGuiScreen(guiyesno); @@ -592,7 +591,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } if (guibutton.id == 107) { - container = (ScriptContainer) this.handler.getScripts().get(this.activeTab - 1); + IScriptUnit container = this.handler.getScripts().get(this.activeTab - 1); if (container == null) { container = new ScriptContainer(this.handler); if (singleContainer) From 23c44cc5d4f3fff64474b8a1f9791edc79fbaad5 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Wed, 14 Jan 2026 03:52:04 +0200 Subject: [PATCH 227/358] Refactor --- .../npcs/client/gui/script/GuiScriptEffect.java | 2 +- .../npcs/client/gui/script/GuiScriptInterface.java | 12 ++++++------ .../npcs/client/gui/script/GuiScriptLinkedItem.java | 2 +- .../npcs/client/gui/script/GuiScriptRecipe.java | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java index e50b31ade..22c51b207 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java @@ -36,7 +36,7 @@ public class GuiScriptEffect extends GuiScriptInterface { return ScriptContext.GLOBAL; } - protected void setHandlerContainer(IScriptUnit container) { + protected void setHandlerUnit(IScriptUnit container) { this.scriptHandler.container = container; } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index c20c653d5..69dcbb1b2 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -442,7 +442,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } if (i == 10) { if (singleContainer) - setHandlerContainer(null); + setHandlerUnit(null); else this.handler.getScripts().remove(this.activeTab - 1); this.activeTab = 0; @@ -530,7 +530,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb if (guibutton.id == scriptLimit) { if (singleContainer) { if (getFirst() == null) - setHandlerContainer(new ScriptContainer(handler)); + setHandlerUnit(new ScriptContainer(handler)); else this.setScript(); this.activeTab = 1; @@ -595,7 +595,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb if (container == null) { container = new ScriptContainer(this.handler); if (singleContainer) - setHandlerContainer(container); + setHandlerUnit(container); else this.handler.getScripts().add(container); } @@ -646,7 +646,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb if (container == null && singleScript == null) { container = new ScriptContainer(this.handler); if (singleContainer) - setHandlerContainer(container); + setHandlerUnit(container); else this.handler.getScripts().add(container); } @@ -711,7 +711,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb int tab = compound.getInteger("Tab"); NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, this.handler); - this.setHandlerContainer(container); + this.setHandlerUnit(container); this.initGui(); } } @@ -743,7 +743,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb /** * Set the handler's container. Override if handler is not IScriptHandler. */ - protected void setHandlerContainer(IScriptUnit container) { + protected void setHandlerUnit(IScriptUnit container) { // Default implementation - subclasses may need to cast and set differently // e.g., ((LinkedItemScript) handler).container = container; } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java index 2a001b732..881e3dd6b 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java @@ -35,7 +35,7 @@ public class GuiScriptLinkedItem extends GuiScriptInterface { return ScriptContext.ITEM; } - protected void setHandlerContainer(IScriptUnit container) { + protected void setHandlerUnit(IScriptUnit container) { ((LinkedItemScript) handler).container = container; } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java index 176294f7e..83316a48d 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java @@ -49,7 +49,7 @@ public class GuiScriptRecipe extends GuiScriptInterface { return ScriptContext.GLOBAL; } - protected void setHandlerContainer(IScriptUnit container) { + protected void setHandlerUnit(IScriptUnit container) { ((RecipeScript) handler).container = container; } From f6e589ac67936b4734fe4f2a53fdaa40eb97cdc8 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 15 Jan 2026 04:13:59 +0200 Subject: [PATCH 228/358] temp SingleScriptHandlers tuff --- .../client/gui/script/GuiScriptInterface.java | 47 ++------- .../controllers/data/SingleScriptHandler.java | 95 +++++++++++++++++++ 2 files changed, 105 insertions(+), 37 deletions(-) create mode 100644 src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 69dcbb1b2..8da3d089c 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -27,6 +27,7 @@ import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.IScriptUnit; +import noppes.npcs.controllers.data.SingleScriptHandler; import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.opengl.Display; @@ -49,9 +50,6 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected Map textAreas = new HashMap<>(); protected GuiScreen parent; - /** Single script being edited (handler-less mode) */ - protected IScriptUnit singleScript; - public boolean singleContainer = false; /** If true, uses scroll-based tab selection instead of top buttons */ @@ -91,35 +89,15 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } /** - * Constructor for single JaninoScript editing (handler-less mode). - * Used for scripts like OverlayScript that don't have an IScriptHandler. - * - * @param parent The parent GUI to return to when closing - * @param script The JaninoScript to edit - */ - public GuiScriptInterface(GuiScreen parent, IScriptUnit script) { - this(); - this.parent = parent; - this.singleScript = script; - this.singleContainer = true; - this.loaded = true; - - // Start on script tab (tab 1) - this.activeTab = 1; - - // Build hook list from the script's type using reflection - // this.hookList = script.getHookList(); - } - - /** - * Create and initialize a GuiScriptInterface for a JaninoScript. - * This is the main entry point for handler-less script editing. + * Create a GuiScriptInterface for a single script without a handler. + * Wraps the script in a SingleScriptHandler automatically. */ public static GuiScriptInterface create(GuiScreen parent, IScriptUnit script) { - GuiScriptInterface gui = new GuiScriptInterface(parent, script); - gui.setWorldAndResolution(Minecraft.getMinecraft(), gui.width, gui.height); - Minecraft.getMinecraft().currentScreen = gui; - gui.initGui(); + GuiScriptInterface gui = new GuiScriptInterface(); + gui.handler = new SingleScriptHandler(script); + gui.parent = parent; + gui.singleContainer = true; + gui.displayGuiScreen(gui); return gui; } @@ -489,10 +467,6 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb * Override in subclasses with different container access patterns. */ protected IScriptUnit getCurrentContainer() { - // Handler-less single JaninoScript mode - if (singleScript != null) - return singleScript; - int idx = getActiveScriptIndex(); if (idx >= 0 && idx < this.handler.getScripts().size()) return this.handler.getScripts().get(idx); @@ -639,11 +613,10 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } protected void setScript() { - if (this.activeTab > 0 || useScrollTabs || singleScript != null) { + if (this.activeTab > 0 || useScrollTabs) { IScriptUnit container = getCurrentContainer(); - // Skip container creation for single JaninoScript mode - if (container == null && singleScript == null) { + if (container == null) { container = new ScriptContainer(this.handler); if (singleContainer) setHandlerUnit(container); diff --git a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java new file mode 100644 index 000000000..165952090 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java @@ -0,0 +1,95 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.FMLCommonHandler; +import cpw.mods.fml.common.eventhandler.Event; +import noppes.npcs.constants.EnumScriptType; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Simple IScriptHandler implementation for single scripts that don't have their own handler. + * Useful for standalone scripts like OverlayScript that need to be edited in GuiScriptInterface. + */ +public class SingleScriptHandler implements IScriptHandler { + private final IScriptUnit script; + private final List scripts = new ArrayList<>(); + + public SingleScriptHandler(IScriptUnit script) { + this.script = script; + this.scripts.add(script); + } + + @Override + public void callScript(EnumScriptType type, Event event) { + // No-op: single scripts don't respond to standard hook calls + } + + @Override + public void callScript(String hookName, Event event) { + // No-op: single scripts don't respond to standard hook calls + } + + public boolean isClient() { + return FMLCommonHandler.instance() + .getEffectiveSide() + .isClient(); + } + + @Override + public boolean getEnabled() { + return true; + } + + @Override + public void setEnabled(boolean enabled) { + // No-op: single scripts are always enabled + } + + @Override + public String getLanguage() { + return script.getLanguage(); + } + + @Override + public void setLanguage(String language) { + // No-op: language is determined by script type + } + + @Override + public void setScripts(List list) { + // No-op: single script handler only manages one script + } + + @Override + public List getScripts() { + return scripts; + } + + @Override + public String noticeString() { + return ""; + } + + @Override + public Map getConsoleText() { + return script.getConsole(); + } + + @Override + public void clearConsole() { + script.clearConsole(); + } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return null; // Single script handlers don't create new scripts + } + + @Override + public boolean supportsJanino() { + return script.isJanino(); + } +} From 68bc02a5d4b6ad05442c88c192308a99f2510b62 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 18 Jan 2026 03:01:55 +0200 Subject: [PATCH 229/358] Removed SingleScriptHandler --- .../client/gui/script/GuiScriptInterface.java | 5 +- .../npcs/controllers/data/DataScript.java | 18 ---- .../controllers/data/SingleScriptHandler.java | 95 ------------------- 3 files changed, 2 insertions(+), 116 deletions(-) delete mode 100644 src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 8da3d089c..7e1bfa0e1 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -27,7 +27,6 @@ import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.IScriptUnit; -import noppes.npcs.controllers.data.SingleScriptHandler; import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.opengl.Display; @@ -92,9 +91,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb * Create a GuiScriptInterface for a single script without a handler. * Wraps the script in a SingleScriptHandler automatically. */ - public static GuiScriptInterface create(GuiScreen parent, IScriptUnit script) { + public static GuiScriptInterface create(GuiScreen parent, IScriptHandler handler) { GuiScriptInterface gui = new GuiScriptInterface(); - gui.handler = new SingleScriptHandler(script); + gui.handler = handler; gui.parent = parent; gui.singleContainer = true; gui.displayGuiScreen(gui); diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index 6e07223a8..cd6f4bb54 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -222,24 +222,6 @@ public class DataScript implements IScriptHandler { return map; } - public Map getConsoleText() { - TreeMap map = new TreeMap<>(); - int tab = 0; - for (IScriptUnit script : this.getScripts()) { - ++tab; - - for (Map.Entry longStringEntry : script.getConsole().entrySet()) { - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + longStringEntry.getValue()); - } - } - return map; - } - - public void clearConsole() { - for (IScriptUnit script : this.getScripts()) { - script.clearConsole(); - } - } @Override public void callScript(EnumScriptType type, Event event) { diff --git a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java deleted file mode 100644 index 165952090..000000000 --- a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java +++ /dev/null @@ -1,95 +0,0 @@ -package noppes.npcs.controllers.data; - -import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.eventhandler.Event; -import noppes.npcs.constants.EnumScriptType; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Simple IScriptHandler implementation for single scripts that don't have their own handler. - * Useful for standalone scripts like OverlayScript that need to be edited in GuiScriptInterface. - */ -public class SingleScriptHandler implements IScriptHandler { - private final IScriptUnit script; - private final List scripts = new ArrayList<>(); - - public SingleScriptHandler(IScriptUnit script) { - this.script = script; - this.scripts.add(script); - } - - @Override - public void callScript(EnumScriptType type, Event event) { - // No-op: single scripts don't respond to standard hook calls - } - - @Override - public void callScript(String hookName, Event event) { - // No-op: single scripts don't respond to standard hook calls - } - - public boolean isClient() { - return FMLCommonHandler.instance() - .getEffectiveSide() - .isClient(); - } - - @Override - public boolean getEnabled() { - return true; - } - - @Override - public void setEnabled(boolean enabled) { - // No-op: single scripts are always enabled - } - - @Override - public String getLanguage() { - return script.getLanguage(); - } - - @Override - public void setLanguage(String language) { - // No-op: language is determined by script type - } - - @Override - public void setScripts(List list) { - // No-op: single script handler only manages one script - } - - @Override - public List getScripts() { - return scripts; - } - - @Override - public String noticeString() { - return ""; - } - - @Override - public Map getConsoleText() { - return script.getConsole(); - } - - @Override - public void clearConsole() { - script.clearConsole(); - } - - @Override - public IScriptUnit createJaninoScriptUnit() { - return null; // Single script handlers don't create new scripts - } - - @Override - public boolean supportsJanino() { - return script.isJanino(); - } -} From cda43fe67a4f64ebb517393a08d11e7524b5292d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 18 Jan 2026 03:05:02 +0200 Subject: [PATCH 230/358] Defaulted IScriptHandler methods to clean up duplicate code across implementations --- .../npcs/blocks/tiles/TileScripted.java | 21 +---------- .../controllers/data/ForgeDataScript.java | 28 --------------- .../controllers/data/GlobalNPCDataScript.java | 21 ----------- .../npcs/controllers/data/IScriptHandler.java | 35 ++++++++++++++++--- .../controllers/data/LinkedItemScript.java | 29 --------------- .../controllers/data/PlayerDataScript.java | 29 +++++---------- 6 files changed, 39 insertions(+), 124 deletions(-) diff --git a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java index 7174bac6e..969ea0bac 100644 --- a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java +++ b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java @@ -416,26 +416,7 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler { public List getScripts() { return scripts; } - - @Override - public Map getConsoleText() { - Map map = new TreeMap<>(); - int tab = 0; - for (IScriptUnit script : getScripts()) { - tab++; - for (Entry entry : script.getConsole().entrySet()) { - map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); - } - } - return map; - } - - @Override - public void clearConsole() { - for (IScriptUnit script : getScripts()) { - script.clearConsole(); - } - } + @SideOnly(Side.CLIENT) @Override diff --git a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java index 043e56efe..c7f345d73 100644 --- a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java @@ -5,15 +5,11 @@ import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.EventHooks; import noppes.npcs.NBTTags; import noppes.npcs.config.ConfigScript; -import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import java.util.ArrayList; import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; public class ForgeDataScript implements IScriptHandler { private List scripts = new ArrayList<>(); @@ -56,10 +52,6 @@ public class ForgeDataScript implements IScriptHandler { return compound; } - @Override - public void callScript(EnumScriptType var1, Event var2) { - this.callScript(var1.function, var2); - } public void callScript(String type, Event event) { if (this.isEnabled()) { @@ -117,25 +109,5 @@ public class ForgeDataScript implements IScriptHandler { return this.scripts; } - public String noticeString() { - return "ForgeScript"; - } - public Map getConsoleText() { - TreeMap map = new TreeMap<>(); - int tab = 0; - for (IScriptUnit script : this.getScripts()) { - ++tab; - for (Entry entry : script.getConsole().entrySet()) { - map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); - } - } - return map; - } - - public void clearConsole() { - for (IScriptUnit script : this.getScripts()) { - script.clearConsole(); - } - } } diff --git a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java index c4dc15913..f8580963b 100644 --- a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java @@ -7,7 +7,6 @@ import net.minecraft.util.math.BlockPos; import noppes.npcs.NBTTags; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.config.ConfigScript; -import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.entity.EntityNPCInterface; @@ -76,9 +75,6 @@ public class GlobalNPCDataScript implements IScriptHandler { return ConfigScript.GlobalNPCScripts && this.enabled && ScriptController.HasStart && this.scripts.size() > 0; } - public void callScript(EnumScriptType type, Event event) { - this.callScript(type.function, event); - } @Override public void callScript(String hookName, Event event) { @@ -145,23 +141,6 @@ public class GlobalNPCDataScript implements IScriptHandler { return this.npcAPI; } - public Map getConsoleText() { - TreeMap map = new TreeMap<>(); - int tab = 0; - for (IScriptUnit script : this.getScripts()) { - ++tab; - for (Map.Entry entry : script.getConsole().entrySet()) { - map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); - } - } - return map; - } - - public void clearConsole() { - for (IScriptUnit script : this.getScripts()) { - script.clearConsole(); - } - } public static final class ToStringHelper { private final String className; diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java index 6b551feb9..69d17beb2 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -1,17 +1,25 @@ package noppes.npcs.controllers.data; +import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.Event; import noppes.npcs.constants.EnumScriptType; import java.util.List; import java.util.Map; +import java.util.TreeMap; public interface IScriptHandler { - void callScript(EnumScriptType type, Event event); + default void callScript(EnumScriptType type, Event event) { + if (type != null) + callScript(type.function, event); + + } void callScript(String hookName, Event event); - boolean isClient(); + default boolean isClient() { + return FMLCommonHandler.instance().getEffectiveSide().isClient(); + } boolean getEnabled(); @@ -25,11 +33,28 @@ public interface IScriptHandler { List getScripts(); - String noticeString(); + default String noticeString() { + return ""; + } - Map getConsoleText(); + default Map getConsoleText() { + Map map = new TreeMap<>(); + int tab = 0; + + for (IScriptUnit script : getScripts()) { + ++tab; + for (Map.Entry entry : script.getConsole().entrySet()) { + map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); + } + } + return map; + } - void clearConsole(); + default void clearConsole() { + for (IScriptUnit script : getScripts()) + script.clearConsole(); + + } /** * Create a new Janino (Java) script unit for this handler. diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java index a2f3a3025..2796586a2 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -1,21 +1,12 @@ package noppes.npcs.controllers.data; -import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.eventhandler.Event; import io.netty.buffer.ByteBuf; import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.nbt.NBTTagCompound; import net.minecraftforge.common.util.Constants; -import noppes.npcs.config.ConfigScript; -import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptController; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; public class LinkedItemScript implements IScriptHandler { public IScriptUnit container; @@ -106,26 +97,6 @@ public class LinkedItemScript implements IScriptHandler { return "LinkedItem"; } - @Override - public Map getConsoleText() { - TreeMap map = new TreeMap<>(); - int tab = 0; - for (IScriptUnit script : this.getScripts()) { - ++tab; - for (Map.Entry entry : script.getConsole().entrySet()) { - map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); - } - } - return map; - } - - @Override - public void clearConsole() { - for (IScriptUnit script : this.getScripts()) { - script.clearConsole(); - } - } - public void saveScript(ByteBuf buffer) throws IOException { int tab = buffer.readInt(); int totalScripts = buffer.readInt(); diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index c8dd007f7..1fd06d927 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -89,9 +89,6 @@ public class PlayerDataScript implements IScriptHandler { return ConfigScript.GlobalPlayerScripts && ScriptController.Instance.playerScripts.enabled && ScriptController.HasStart && (this.player == null || !this.player.worldObj.isRemote); } - public void callScript(EnumScriptType type, Event event) { - this.callScript(type.function, event); - } @Override public void callScript(String hookName, Event event) { @@ -187,10 +184,9 @@ public class PlayerDataScript implements IScriptHandler { public String noticeString() { if (this.player == null) { return "Global script"; - } else { - BlockPos pos = new BlockPos(this.player); - return PlayerDataScript.toStringHelper(this.player).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); } + BlockPos pos = new BlockPos(this.player); + return PlayerDataScript.toStringHelper(this.player).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); } public IPlayer getPlayer() { @@ -201,28 +197,19 @@ public class PlayerDataScript implements IScriptHandler { } public Map getConsoleText() { - if (ConfigScript.IndividualPlayerScripts) + if (ConfigScript.IndividualPlayerScripts) { return console; - - TreeMap map = new TreeMap<>(); - int tab = 0; - for (IScriptUnit script : this.getScripts()) { - ++tab; - for (Entry entry : script.getConsole().entrySet()) { - map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); - } } - return map; + return IScriptHandler.super.getConsoleText(); } + @Override public void clearConsole() { - if (ConfigScript.IndividualPlayerScripts) + if (ConfigScript.IndividualPlayerScripts) { console.clear(); - else { - for (IScriptUnit script : this.getScripts()) { - script.clearConsole(); - } + return; } + IScriptHandler.super.clearConsole(); } public static final class ToStringHelper { From f829926f00260eb962b255a38a58f258f8a98e83 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 18 Jan 2026 03:19:30 +0200 Subject: [PATCH 231/358] New ScriptHandler, which is an abstract IScriptHandler implementation --- .../npcs/controllers/data/EffectScript.java | 98 +------------------ .../controllers/data/LinkedItemScript.java | 65 +----------- .../npcs/controllers/data/RecipeScript.java | 93 +----------------- .../npcs/controllers/data/ScriptHandler.java | 64 ++++++++++++ 4 files changed, 67 insertions(+), 253 deletions(-) create mode 100644 src/main/java/noppes/npcs/controllers/data/ScriptHandler.java diff --git a/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/src/main/java/noppes/npcs/controllers/data/EffectScript.java index dcd497fc8..ee5f63d90 100644 --- a/src/main/java/noppes/npcs/controllers/data/EffectScript.java +++ b/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -1,27 +1,15 @@ package noppes.npcs.controllers.data; -import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.eventhandler.Event; import io.netty.buffer.ByteBuf; import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.nbt.NBTTagCompound; import net.minecraftforge.common.util.Constants; -import noppes.npcs.config.ConfigScript; -import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptController; import noppes.npcs.scripted.event.player.PlayerEvent; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -public class EffectScript implements IScriptHandler { - public IScriptUnit container; - public String scriptLanguage = "ECMAScript"; - public boolean enabled = false; +public class EffectScript extends ScriptHandler { public NBTTagCompound writeToNBT(NBTTagCompound compound) { @@ -47,90 +35,6 @@ public class EffectScript implements IScriptHandler { callScript(type.function, event); } - public boolean isEnabled() { - return this.enabled && ScriptController.HasStart && container != null && ConfigScript.ScriptingEnabled; - } - - @Override - public void callScript(EnumScriptType type, Event event) { - this.callScript(type.function, event); - } - - @Override - public void callScript(String s, Event event) { - if (!this.isEnabled()) { - return; - } - if (container != null) { - container.run(s, event); - } - } - - @Override - public boolean isClient() { - return FMLCommonHandler.instance().getEffectiveSide().isClient(); - } - - @Override - public boolean getEnabled() { - return this.enabled; - } - - @Override - public void setEnabled(boolean b) { - this.enabled = b; - } - - @Override - public String getLanguage() { - return this.scriptLanguage; - } - - @Override - public void setLanguage(String s) { - this.scriptLanguage = s; - } - - @Override - public void setScripts(List list) { - if (list == null || list.isEmpty()) { - container = null; - return; - } - container = list.get(0); - } - - @Override - public List getScripts() { - if (container == null) - return new ArrayList<>(); - return Collections.singletonList(container); - } - - @Override - public String noticeString() { - return ""; - } - - @Override - public Map getConsoleText() { - TreeMap map = new TreeMap<>(); - int tab = 0; - for (IScriptUnit script : this.getScripts()) { - ++tab; - for (Map.Entry entry : script.getConsole().entrySet()) { - map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); - } - } - return map; - } - - @Override - public void clearConsole() { - for (IScriptUnit script : this.getScripts()) { - script.clearConsole(); - } - } public void saveScript(ByteBuf buffer) throws IOException { int tab = buffer.readInt(); diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java index 2796586a2..3b09c5766 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -8,10 +8,7 @@ import noppes.npcs.controllers.ScriptController; import java.io.IOException; -public class LinkedItemScript implements IScriptHandler { - public IScriptUnit container; - public String scriptLanguage = "ECMAScript"; - public boolean enabled = false; +public class LinkedItemScript extends ScriptHandler { public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setString("ScriptLanguage", scriptLanguage); @@ -32,66 +29,6 @@ public class LinkedItemScript implements IScriptHandler { return this; } - public boolean isEnabled() { - return this.enabled && ScriptController.HasStart && container != null && ConfigScript.ScriptingEnabled; - } - - @Override - public void callScript(EnumScriptType type, Event event) { - this.callScript(type.function, event); - } - - @Override - public void callScript(String s, Event event) { - if (!this.isEnabled()) { - return; - } - if (container != null) { - container.run(s, event); - } - } - - @Override - public boolean isClient() { - return FMLCommonHandler.instance().getEffectiveSide().isClient(); - } - - @Override - public boolean getEnabled() { - return this.enabled; - } - - @Override - public void setEnabled(boolean b) { - this.enabled = b; - } - - @Override - public String getLanguage() { - return this.scriptLanguage; - } - - @Override - public void setLanguage(String s) { - this.scriptLanguage = s; - } - - @Override - public void setScripts(List list) { - if (list == null || list.isEmpty()) { - container = null; - return; - } - container = list.get(0); - } - - @Override - public List getScripts() { - if (container == null) - return new ArrayList<>(); - return Collections.singletonList(container); - } - @Override public String noticeString() { return "LinkedItem"; diff --git a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java index ee1b32c08..b498d233c 100644 --- a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java +++ b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java @@ -1,26 +1,14 @@ package noppes.npcs.controllers.data; -import cpw.mods.fml.common.FMLCommonHandler; -import cpw.mods.fml.common.eventhandler.Event; import io.netty.buffer.ByteBuf; import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.nbt.NBTTagCompound; import net.minecraftforge.common.util.Constants; -import noppes.npcs.config.ConfigScript; -import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptController; import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -public class RecipeScript implements IScriptHandler { - public IScriptUnit container; - public String scriptLanguage = "ECMAScript"; - public boolean enabled = false; +public class RecipeScript extends ScriptHandler { public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setString("ScriptLanguage", scriptLanguage); @@ -39,90 +27,11 @@ public class RecipeScript implements IScriptHandler { return this; } - public boolean isEnabled() { - return this.enabled && ScriptController.HasStart && container != null && ConfigScript.ScriptingEnabled; - } - - @Override - public void callScript(EnumScriptType type, Event event) { - this.callScript(type.function, event); - } - - @Override - public void callScript(String s, Event event) { - if (!this.isEnabled()) - return; - if (container != null) { - container.run(s, event); - } - } - - @Override - public boolean isClient() { - return FMLCommonHandler.instance().getEffectiveSide().isClient(); - } - - @Override - public boolean getEnabled() { - return this.enabled; - } - - @Override - public void setEnabled(boolean b) { - this.enabled = b; - } - - @Override - public String getLanguage() { - return this.scriptLanguage; - } - - @Override - public void setLanguage(String s) { - this.scriptLanguage = s; - } - - @Override - public void setScripts(List list) { - if (list == null || list.isEmpty()) { - container = null; - return; - } - container = list.get(0); - } - - @Override - public List getScripts() { - if (container == null) - return new ArrayList<>(); - return Collections.singletonList(container); - } - @Override public String noticeString() { return "RecipeScript"; } - @Override - public Map getConsoleText() { - TreeMap map = new TreeMap<>(); - int tab = 0; - for (IScriptUnit script : this.getScripts()) { - ++tab; - for (Map.Entry entry : script.getConsole().entrySet()) { - map.put(entry.getKey(), " tab " + tab + ":\n" + entry.getValue()); - } - } - return map; - } - - @Override - public void clearConsole() { - for (IScriptUnit script : this.getScripts()) { - script.clearConsole(); - } - } - public void saveScript(ByteBuf buffer) throws IOException { int tab = buffer.readInt(); int totalScripts = buffer.readInt(); diff --git a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java new file mode 100644 index 000000000..f5ea44383 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java @@ -0,0 +1,64 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.eventhandler.Event; +import noppes.npcs.config.ConfigScript; +import noppes.npcs.controllers.ScriptController; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class ScriptHandler implements IScriptHandler { + public IScriptUnit container; + protected String scriptLanguage = "ECMAScript"; + protected boolean enabled = false; + + protected boolean canRunScripts() { + return enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled && container != null; + } + + @Override + public void callScript(String hookName, Event event) { + if (!canRunScripts()) { + return; + } + container.run(hookName, event); + } + + @Override + public boolean getEnabled() { + return enabled; + } + + @Override + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @Override + public String getLanguage() { + return scriptLanguage; + } + + @Override + public void setLanguage(String language) { + this.scriptLanguage = language; + } + + @Override + public void setScripts(List list) { + if (list == null || list.isEmpty()) { + container = null; + return; + } + container = list.get(0); + } + + @Override + public List getScripts() { + if (container == null) { + return new ArrayList<>(); + } + return Collections.singletonList(container); + } +} From 86285ceff5ae21d5a92a502034bdb998be934a9b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 18 Jan 2026 03:26:31 +0200 Subject: [PATCH 232/358] - New JaninoScriptHandler thats a concrete IScriptHandler impl and replaces JaninoScriptable - Removed ScriptHandler based methods from JaninoScript --- .../client/gui/script/GuiJaninoScript.java | 7 +- .../controllers/data/JaninoScriptHandler.java | 121 ++++++++++++++++++ .../java/noppes/npcs/janino/JaninoScript.java | 57 ++------- .../noppes/npcs/janino/JaninoScriptable.java | 38 ------ 4 files changed, 139 insertions(+), 84 deletions(-) create mode 100644 src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java delete mode 100644 src/main/java/noppes/npcs/janino/JaninoScriptable.java diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java index 2f609b205..04d078fda 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java @@ -166,7 +166,8 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback this.getScriptIndex())); // this.getButton(103).enabled = !this.languages.isEmpty(); this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); - this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, this.container.getEnabled() ? 1 : 0)); + // Note: GuiJaninoScript is deprecated; enabled button disabled + this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, 0)); this.addButton(new GuiNpcButton(109, var9, this.guiTop + 78, 80, 20, "gui.website")); this.addButton(new GuiNpcButton(112, var9 + 81, this.guiTop + 78, 80, 20, "gui.forum")); @@ -280,7 +281,7 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback } //Enable else if (guibutton.id == 104) { - this.container.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); + // Deprecated GUI - enabled is now per-handler, not per-unit } //Remove else if (guibutton.id == 105) { @@ -347,7 +348,7 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback } private String getConsoleText() { - Map map = this.container.getConsoleText(); + Map map = this.container.getConsole(); StringBuilder builder = new StringBuilder(); Iterator var3 = map.entrySet() .iterator(); diff --git a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java new file mode 100644 index 000000000..81046844e --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java @@ -0,0 +1,121 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.eventhandler.Event; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.janino.JaninoScript; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +/** + * Reusable IScriptHandler implementation for objects that own an optional single JaninoScript unit. + * + * This is meant for "function provider" scripts (not EnumScriptType driven). Execution is typically + * initiated by the owning system calling script.call(...) directly. + * + * NBT storage is kept under the parent compound key "Script" (same convention as JaninoScriptable). + */ +public class JaninoScriptHandler> extends ScriptHandler { + + protected S script; + + private final Supplier factory; + private final Class scriptClass; + + public JaninoScriptHandler(Supplier factory, Class scriptClass) { + this.factory = factory; + this.scriptClass = scriptClass; + scriptLanguage = "Java"; + } + + // -------------------- Script ownership (JaninoScriptable) -------------------- + + public boolean hasScript() { + return script != null; + } + + public S getScript() { + return script; + } + + public JaninoScript createScript() { + if (script == null) + script = factory.get(); + + return script; + } + + public void deleteScript() { + unloadScript(); + script = null; + } + + public void unloadScript() { + if (script != null) { + script.unload(); + } + } + + // -------------------- NBT helpers -------------------- + + public void readFromNBT(NBTTagCompound compound) { + // JaninoScriptable's helper generics assume JaninoScript, but most of our scripts are JaninoScript. + // Keep it simple and safe: treat it as raw. + script = (S) JaninoScript.readFromNBT(compound, (JaninoScript) script, (Supplier) factory); + } + + public void writeToNBT(NBTTagCompound compound) { + JaninoScript.writeToNBT(compound, (JaninoScript) script); + } + + // -------------------- IScriptHandler -------------------- + + @Override + public void callScript(String hookName, Event event) { + // No-op: these scripts are not executed through the global hook dispatcher. + } + + @Override + public void callScript(EnumScriptType type, Event event) { + // No-op + } + + @Override + public void setScripts(List list) { + if (list == null || list.isEmpty() || list.get(0) == null) { + deleteScript(); + return; + } + + IScriptUnit unit = list.get(0); + if (scriptClass.isInstance(unit)) { + this.script = scriptClass.cast(unit); + return; + } + + // Fallback: copy code into a real instance of this script type. + S target = scriptClass.cast(createScript()); + target.setScript(unit.getScript()); + target.setExternalScripts(unit.getExternalScripts()); + } + + @Override + public List getScripts() { + if (script == null) + return Collections.emptyList(); + + return Collections.singletonList(script); + } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return factory.get(); + } + + @Override + public boolean supportsJanino() { + return true; + } +} diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 22fb9ee95..bc3d8e915 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -22,10 +22,10 @@ import java.security.*; import java.util.*; import java.util.function.Consumer; import java.util.function.Function; +import java.util.function.Supplier; import java.util.stream.Collectors; public abstract class JaninoScript implements IScriptUnit { - public boolean enabled; public boolean errored = false; private String language = "Java"; public TreeMap console = new TreeMap(); @@ -157,14 +157,6 @@ public abstract class JaninoScript implements IScriptUnit { } public void ensureCompiled() { - if (!isEnabled()) { - if (!evaluated) { - compileScript(""); - evaluated = true; - } - return; - } - int global = ScriptController.Instance.globalRevision; if (!evaluated || global != lastSeenGlobalRevision) { @@ -182,9 +174,6 @@ public abstract class JaninoScript implements IScriptUnit { private int lastSeenGlobalRevision; private String getFullCode() { - if (!isEnabled()) - return ""; - StringBuilder sb = new StringBuilder(); if (ConfigScript.RunLoadedScriptsFirst) @@ -306,6 +295,8 @@ public abstract class JaninoScript implements IScriptUnit { public void setExternalScripts(List externalScripts) { this.externalScripts = externalScripts; this.evaluated = false; + this.hookNameMap.clear(); + this.missingHookNames.clear(); } @Override @@ -470,7 +461,6 @@ public abstract class JaninoScript implements IScriptUnit { @Override public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setString(IScriptUnit.NBT_TYPE_KEY, IScriptUnit.TYPE_JANINO); - compound.setBoolean("enabled", enabled); compound.setTag("console", NBTTags.NBTLongStringMap(this.console)); compound.setTag("externalScripts", NBTTags.nbtStringList(this.externalScripts)); compound.setString("script", script); @@ -479,46 +469,27 @@ public abstract class JaninoScript implements IScriptUnit { @Override public void readFromNBT(NBTTagCompound compound) { - this.enabled = compound.getBoolean("enabled"); + // Read legacy enabled field but ignore it (handler owns enabled now) this.console = NBTTags.GetLongStringMap(compound.getTagList("console", 10)); setExternalScripts(NBTTags.getStringList(compound.getTagList("externalScripts", 10))); setScript(compound.getString("script")); } - public boolean isEnabled() { - return this.enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled; - } + public static > S readFromNBT(NBTTagCompound compound, S script, Supplier factory) { + if (compound.hasKey("Script")) { + if (script == null) + script = factory.get(); - public boolean isClient() { - return FMLCommonHandler.instance() - .getEffectiveSide() - .isClient(); - } - - public boolean getEnabled() { - return this.enabled; - } - - public void setEnabled(boolean b) { - if (this.enabled != b) { - this.enabled = b; - this.evaluated = false; + script.readFromNBT(compound.getCompoundTag("Script")); } + return script; } - public Map getConsoleText() { - TreeMap map = new TreeMap(); - int tab = 0; + public static NBTTagCompound writeToNBT(NBTTagCompound compound, JaninoScript script) { + if (script != null) + compound.setTag("Script", script.writeToNBT(new NBTTagCompound())); - Iterator var5 = console.entrySet() - .iterator(); - - while (var5.hasNext()) { - Map.Entry longStringEntry = (Map.Entry) var5.next(); - map.put(longStringEntry.getKey(), " tab " + tab + ":\n" + (String) longStringEntry.getValue()); - } - - return map; + return compound; } @Override diff --git a/src/main/java/noppes/npcs/janino/JaninoScriptable.java b/src/main/java/noppes/npcs/janino/JaninoScriptable.java deleted file mode 100644 index 9700bf893..000000000 --- a/src/main/java/noppes/npcs/janino/JaninoScriptable.java +++ /dev/null @@ -1,38 +0,0 @@ -package noppes.npcs.janino; - -import net.minecraft.nbt.NBTTagCompound; - -import java.util.function.Supplier; - -/** - - */ -public interface JaninoScriptable> { - - boolean hasScript(); - - JaninoScript getScript(); - - JaninoScript createScript(); - - void deleteScript(); - - void unloadScript(); - - static > S readFromNBT(NBTTagCompound compound, S script, Supplier factory) { - if (compound.hasKey("Script")) { - if (script == null) - script = factory.get(); - - script.readFromNBT(compound.getCompoundTag("Script")); - } - return script; - } - - static NBTTagCompound writeToNBT(NBTTagCompound compound, JaninoScript script) { - if (script != null) - compound.setTag("Script", script.writeToNBT(new NBTTagCompound())); - - return compound; - } -} From 44f71ddb814d6fd7f44d5a4808bd3ca653a6c12c Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 18 Jan 2026 04:04:17 +0200 Subject: [PATCH 233/358] Handled script language correctly for external script selector list + added forgotten singleContainer logic --- .../client/gui/script/GuiScriptInterface.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 7e1bfa0e1..49d4e56f7 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -564,7 +564,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } if (guibutton.id == 107) { - IScriptUnit container = this.handler.getScripts().get(this.activeTab - 1); + IScriptUnit container = getCurrentContainer(); if (container == null) { container = new ScriptContainer(this.handler); if (singleContainer) @@ -573,7 +573,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.handler.getScripts().add(container); } - this.setSubGui(new EventGuiScriptList((List) this.languages.get(this.handler.getLanguage()), container)); + String language = container != null ? container.getLanguage() : this.handler.getLanguage(); + this.setSubGui(new EventGuiScriptList((List) this.languages.get(language), container)); } // Language toggle button - switch between ECMAScript and Java @@ -598,10 +599,13 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return; // Handler doesn't support Janino } } - - // Replace the script unit in the list - handler.getScripts().set(idx, newUnit); - + + if (singleContainer) { + setHandlerUnit(newUnit); + } else { + // Replace the script unit in the list + handler.getScripts().set(idx, newUnit); + } // Clear the cached text area so it recreates with new language textAreas.remove(idx); From eecb5b0b3bf0e13bfbdb862c53bbbda7994fc502 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 18 Jan 2026 17:50:04 +0200 Subject: [PATCH 234/358] ScriptHandler refactor: - ScriptHandler now holds a list of containers - Delegated single container handling to SingleScriptHandler IScriptHandler refactor: - Moved ScriptContext/Hook logic to it - More default methods + hooked them to GuIScriptInterface --- .../client/gui/script/GuiScriptInterface.java | 69 ++++---- .../npcs/controllers/data/IScriptHandler.java | 88 ++++++++++ .../npcs/controllers/data/ScriptHandler.java | 35 ++-- .../controllers/data/SingleScriptHandler.java | 153 ++++++++++++++++++ 4 files changed, 294 insertions(+), 51 deletions(-) create mode 100644 src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 49d4e56f7..4eb7ccca2 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -27,6 +27,7 @@ import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.IScriptUnit; +import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.opengl.Display; @@ -49,7 +50,6 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected Map textAreas = new HashMap<>(); protected GuiScreen parent; - public boolean singleContainer = false; /** If true, uses scroll-based tab selection instead of top buttons */ protected boolean useScrollTabs = false; @@ -97,6 +97,12 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb gui.parent = parent; gui.singleContainer = true; gui.displayGuiScreen(gui); + + // Initialize hooks from handler + gui.hookList = new ArrayList<>(handler.getHooks()); + + + Minecraft.getMinecraft().displayGuiScreen(gui); return gui; } @@ -113,6 +119,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.guiTop += 10; int yoffset = (int) ((double) this.ySize * 0.02D); + if (hookList.isEmpty()) + hookList = new ArrayList<>(handler.getHooks()); + // ==================== TAB BUTTONS ==================== // Skip standard tab creation for GUIs that use custom toggle/scroll tabs if (!useSettingsToggle) { @@ -123,7 +132,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb int topXoffset = 0; int topYoffset = 0; - if(!singleContainer) { + boolean isSingle = handler.isSingleContainer(); + if (!isSingle) { for (int ta = 0; ta < this.handler.getScripts().size(); ++ta) { if (ta % 20 == 0 && ta > 0) { topYoffset -= 20; @@ -137,7 +147,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } } - if (singleContainer && getFirst() != null) + if (isSingle && handler.getSingleScript() != null) this.addTopButton( top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, "Script")); @@ -233,10 +243,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb // Set the scripting language for proper syntax highlighting // Use the container's language if available, otherwise fall back to handler's language String language = (container != null) ? container.getLanguage() : this.handler.getLanguage(); - activeArea.setLanguage(language); - - // Set the script context for context-aware hook autocomplete - activeArea.setScriptContext(getScriptContext()); + activeArea.setLanguage(language); // Set the script context for context-aware hook autocomplete activeArea.setScriptContext(getScriptContext()); @@ -320,6 +327,16 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return this; } + /** + * Get the script context for this GUI. + * Override in subclasses to return a different context. + * + * @return The script context (default: GLOBAL, or handler's context if available) + */ + protected ScriptContext getScriptContext() { + return handler.getContext(); + } + // ==================== RENDERING ==================== @Override @@ -418,10 +435,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.openLink("http://www.minecraftforge.net/forum/index.php/board,122.0.html"); } if (i == 10) { - if (singleContainer) - setHandlerUnit(null); - else - this.handler.getScripts().remove(this.activeTab - 1); + handler.removeScriptUnit(this.activeTab - 1); this.activeTab = 0; } if (i == 101) { @@ -482,11 +496,6 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return super.getTextField(id); } - public IScriptUnit getFirst() { - if (this.handler.getScripts().isEmpty()) - return null; - return this.handler.getScripts().get(0); - } @Override protected void actionPerformed(GuiButton guibutton) { if (guibutton.id >= 0 && guibutton.id < scriptLimit) { @@ -501,14 +510,15 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } if (guibutton.id == scriptLimit) { - if (singleContainer) { - if (getFirst() == null) - setHandlerUnit(new ScriptContainer(handler)); - else + if (handler.isSingleContainer()) { + if (handler.getSingleScript() == null) { + handler.addScriptUnit(new ScriptContainer(handler)); + } else { this.setScript(); + } this.activeTab = 1; } else { - this.handler.getScripts().add(new ScriptContainer(this.handler)); + handler.addScriptUnit(new ScriptContainer(this.handler)); this.activeTab = this.handler.getScripts().size(); } this.initGui(); @@ -567,10 +577,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb IScriptUnit container = getCurrentContainer(); if (container == null) { container = new ScriptContainer(this.handler); - if (singleContainer) - setHandlerUnit(container); - else - this.handler.getScripts().add(container); + handler.addScriptUnit(container); } String language = container != null ? container.getLanguage() : this.handler.getLanguage(); @@ -600,12 +607,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } } - if (singleContainer) { - setHandlerUnit(newUnit); - } else { - // Replace the script unit in the list - handler.getScripts().set(idx, newUnit); - } + handler.replaceScriptUnit(idx, newUnit); // Clear the cached text area so it recreates with new language textAreas.remove(idx); @@ -621,10 +623,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb if (container == null) { container = new ScriptContainer(this.handler); - if (singleContainer) - setHandlerUnit(container); - else - this.handler.getScripts().add(container); + handler.addScriptUnit(container); } if (container != null) { diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java index 69d17beb2..cc47eb1c8 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -3,7 +3,10 @@ package noppes.npcs.controllers.data; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.Event; import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; +import noppes.npcs.controllers.ScriptHookController; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -74,4 +77,89 @@ public interface IScriptHandler { default boolean supportsJanino() { return createJaninoScriptUnit() != null; } + + /** + * Get the script context for autocomplete and type resolution. + * + * @return The script context (default: GLOBAL) + */ + default ScriptContext getContext() { + return ScriptContext.GLOBAL; + } + + /** + * Get the hook context identifier for this handler. + * Used by ScriptHookController to look up available hooks. + * + * @return The context string (e.g., IScriptHookHandler.CONTEXT_EFFECT), or empty if none + */ + default String getHookContext() { + return ""; + } + + /** + * Get the list of available hooks for this handler. + * Uses getHookContext() to look up hooks from ScriptHookController. + * + * @return List of hook names, or empty list if no context + */ + default List getHooks() { + String context = getHookContext(); + if (context == null || context.isEmpty()) + return Collections.emptyList(); + + return ScriptHookController.Instance.getAllHooks(context); + } + + /** + * Check if this handler manages only a single script container. + * Single-container handlers use simplified UI (no numbered tabs). + * + * @return true if this handler uses a single container + */ + default boolean isSingleContainer() { + return false; + } + + /** + * Get the single script unit for single-container handlers. + * Convenience method that returns the first script or null. + * + * @return The single script unit, or null if none exists + */ + default IScriptUnit getSingleScript() { + List scripts = getScripts(); + return (scripts != null && !scripts.isEmpty()) ? scripts.get(0) : null; + } + + /** + * Add a script unit to this handler. + * For single-container handlers, replaces the current unit. + */ + default void addScriptUnit(IScriptUnit unit) { + if (unit != null) + getScripts().add(unit); + } + + /** + * Replace a script unit at the given index. + * For single-container handlers, replaces the current unit. + */ + default void replaceScriptUnit(int index, IScriptUnit unit) { + List scripts = getScripts(); + if (index < 0 || index >= scripts.size()) + scripts.add(unit); + else + scripts.set(index, unit); + } + + /** + * Remove a script unit at the given index. + * For single-container handlers, clears the unit. + */ + default void removeScriptUnit(int index) { + List scripts = getScripts(); + if (index >= 0 && index < scripts.size()) + scripts.remove(index); + } } diff --git a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java index f5ea44383..def6ca7cf 100644 --- a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java @@ -5,24 +5,36 @@ import noppes.npcs.config.ConfigScript; import noppes.npcs.controllers.ScriptController; import java.util.ArrayList; -import java.util.Collections; import java.util.List; public abstract class ScriptHandler implements IScriptHandler { - public IScriptUnit container; + public List scripts = new ArrayList<>(); protected String scriptLanguage = "ECMAScript"; protected boolean enabled = false; + protected long lastInited = -1; protected boolean canRunScripts() { - return enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled && container != null; + return enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled && scripts != null && !scripts.isEmpty(); } @Override public void callScript(String hookName, Event event) { - if (!canRunScripts()) { + if (!canRunScripts()) return; + + if (ScriptController.Instance.lastLoaded > lastInited) { + lastInited = ScriptController.Instance.lastLoaded; + for (IScriptUnit script : scripts) { + if (script != null) + script.setErrored(false); + } + } + + for (IScriptUnit script : scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) + continue; + script.run(hookName, event); } - container.run(hookName, event); } @Override @@ -45,20 +57,11 @@ public abstract class ScriptHandler implements IScriptHandler { this.scriptLanguage = language; } - @Override public void setScripts(List list) { - if (list == null || list.isEmpty()) { - container = null; - return; - } - container = list.get(0); + this.scripts = list; } - @Override public List getScripts() { - if (container == null) { - return new ArrayList<>(); - } - return Collections.singletonList(container); + return this.scripts; } } diff --git a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java new file mode 100644 index 000000000..a047b41b6 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java @@ -0,0 +1,153 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.eventhandler.Event; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraftforge.common.util.Constants; +import noppes.npcs.config.ConfigScript; +import noppes.npcs.controllers.ScriptController; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Base class for script handlers that manage a single script container. + * Provides shared NBT/ByteBuf serialization and packet handling. + * + * Subclasses should: + * - Store their parent object (e.g., CustomEffect, LinkedItem) + * - Override getHookContext() to return the appropriate context string + * - Override getContext() if needed (default is GLOBAL) + * - Override requestData() and sendSavePacket() for packet communication + */ +public abstract class SingleScriptHandler extends ScriptHandler { + public IScriptUnit container; + + @Override + public boolean isSingleContainer() { + return true; + } + + protected boolean canRunScripts() { + return enabled && ScriptController.HasStart && ConfigScript.ScriptingEnabled && container != null; + } + + @Override + public void callScript(String hookName, Event event) { + if (!canRunScripts()) { + return; + } + container.run(hookName, event); + } + + @Override + public void setScripts(List list) { + if (list == null || list.isEmpty()) { + container = null; + this.scripts = new ArrayList<>(); + } else { + container = list.get(0); + this.scripts = new ArrayList<>(1); + this.scripts.add(container); + } + } + + @Override + public List getScripts() { + if(container == null) + return new ArrayList<>(); + + return Collections.singletonList(container); + } + + public void addScriptUnit(IScriptUnit unit) { + this.container = unit; + this.scripts.clear(); + if (unit != null) + this.scripts.add(unit); + } + + public void replaceScriptUnit(int index, IScriptUnit unit) { + this.container = unit; + this.scripts.clear(); + if (unit != null) + this.scripts.add(unit); + } + + public void removeScriptUnit(int index) { + this.container = null; + this.scripts.clear(); + } + + /** + * Write script data to NBT. + * Uses standard keys: ScriptLanguage, ScriptEnabled, ScriptContent + */ + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString("ScriptLanguage", scriptLanguage); + compound.setBoolean("ScriptEnabled", enabled); + + if (container != null) { + compound.setTag("ScriptContent", container.writeToNBT(new NBTTagCompound())); + } + return compound; + } + + /** + * Read script data from NBT. + * Uses standard keys: ScriptLanguage, ScriptEnabled, ScriptContent + */ + public void readFromNBT(NBTTagCompound compound) { + scriptLanguage = compound.getString("ScriptLanguage"); + enabled = compound.getBoolean("ScriptEnabled"); + + if (compound.hasKey("ScriptContent", Constants.NBT.TAG_COMPOUND)) { + container = IScriptUnit.createFromNBT(compound.getCompoundTag("ScriptContent"), this); + } + } + + /** + * Save script data from a ByteBuf packet. + * Handles the tab/totalScripts protocol used by script packets. + * + * Protocol: + * - tab == 0: Script content NBT follows + * - tab != 0 (typically -1): Script metadata (language, enabled) follows + */ + public void saveScript(ByteBuf buffer) throws IOException { + int tab = buffer.readInt(); + int totalScripts = buffer.readInt(); + + if (totalScripts == 0) { + this.container = null; + } + + if (tab == 0) { + // Script content + NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); + this.container = IScriptUnit.createFromNBT(tabCompound, this); + } else { + // Metadata (language, enabled) + NBTTagCompound compound = ByteBufUtils.readNBT(buffer); + this.setLanguage(compound.getString("ScriptLanguage")); + normalizeLanguage(); + this.setEnabled(compound.getBoolean("ScriptEnabled")); + } + } + + /** + * Normalize the language to a valid one if the current language is not available. + */ + protected void normalizeLanguage() { + if (!ScriptController.Instance.languages.containsKey(this.getLanguage())) { + if (!ScriptController.Instance.languages.isEmpty()) { + this.setLanguage((String) ScriptController.Instance.languages.keySet().toArray()[0]); + } else { + this.setLanguage("ECMAScript"); + } + } + } +} From f5dbe19de2010404a5f939c9a473e2795d2b51ee Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 18 Jan 2026 17:57:10 +0200 Subject: [PATCH 235/358] =?UTF-8?q?New=20IScriptHandlerPacket:=20-=20Defin?= =?UTF-8?q?es=20the=20packet-backed=20script=20GUI=20contract=20used=20by?= =?UTF-8?q?=20GuiScriptInterface=20for=20=E2=80=9Cstreamed=E2=80=9D=20scri?= =?UTF-8?q?pt=20loading/saving.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated EffectScript, LinkedItemScript and RecipeScript to implement IScriptHandlerPacket - Removed GuiScriptEffect, GuiScriptLinkedItem and GuiScriptRecipe as their functionality got replaced with new interface handled by GuiScriptInterface --- .../npcs/client/gui/SubGuiEffectGeneral.java | 8 +- .../gui/global/GuiNpcManageRecipes.java | 13 +- .../client/gui/item/SubGuiLinkedItem.java | 8 +- .../client/gui/script/GuiScriptEffect.java | 56 -------- .../client/gui/script/GuiScriptInterface.java | 126 +++++------------- .../gui/script/GuiScriptLinkedItem.java | 55 -------- .../client/gui/script/GuiScriptRecipe.java | 69 ---------- .../npcs/controllers/data/EffectScript.java | 94 ++++++------- .../data/IScriptHandlerPacket.java | 86 ++++++++++++ .../controllers/data/LinkedItemScript.java | 94 +++++++------ .../npcs/controllers/data/RecipeScript.java | 88 ++++++------ 11 files changed, 260 insertions(+), 437 deletions(-) delete mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java delete mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java delete mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java create mode 100644 src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java b/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java index 19903229b..db1b516ea 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java @@ -7,7 +7,8 @@ import net.minecraft.client.renderer.texture.TextureManager; import net.minecraft.util.ResourceLocation; import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.client.gui.global.GuiNPCManageEffects; -import noppes.npcs.client.gui.script.GuiScriptEffect; +import noppes.npcs.client.gui.script.GuiScriptInterface; +import noppes.npcs.controllers.data.EffectScript; import noppes.npcs.client.gui.util.GuiMenuTopButton; import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -202,10 +203,7 @@ public class SubGuiEffectGeneral extends SubGuiInterface implements ITextfieldLi } if (id == -2) { PacketClient.sendClient(new EffectSavePacket(effect.writeToNBT(false), originalName)); - GuiScriptEffect scriptGUI = new GuiScriptEffect(parent, effect); - scriptGUI.setWorldAndResolution(mc, width, height); - scriptGUI.initGui(); - mc.currentScreen = scriptGUI; + GuiScriptInterface.open(parent, new EffectScript(effect.id)); } if (id == 10) { GuiNpcButtonYesNo button = (GuiNpcButtonYesNo) guibutton; diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java index 1b83b0639..f246cb417 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java @@ -12,7 +12,8 @@ import net.minecraft.util.StatCollector; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.NoppesUtil; import noppes.npcs.client.gui.SubGuiNpcAvailability; -import noppes.npcs.client.gui.script.GuiScriptRecipe; +import noppes.npcs.client.gui.script.GuiScriptInterface; +import noppes.npcs.controllers.data.RecipeScript; import noppes.npcs.client.gui.util.GuiButtonBiDirectional; import noppes.npcs.client.gui.util.GuiContainerNPCInterface2; import noppes.npcs.client.gui.util.GuiCustomScroll; @@ -222,15 +223,9 @@ public class GuiNpcManageRecipes extends GuiContainerNPCInterface2 implements IS if (button.id == 16) { save(); if (container.width == 1) { - GuiScriptRecipe gui = new GuiScriptRecipe(this, container.recipeAnvil); - gui.setWorldAndResolution(mc, width, height); - gui.initGui(); - mc.currentScreen = gui; + GuiScriptInterface.open(this, new RecipeScript(container.recipeAnvil.id, true)); } else if (!container.recipe.isGlobal) { - GuiScriptRecipe gui = new GuiScriptRecipe(this, container.recipe); - gui.setWorldAndResolution(mc, width, height); - gui.initGui(); - mc.currentScreen = gui; + GuiScriptInterface.open(this, new RecipeScript(container.recipe.id, false)); } } } diff --git a/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java b/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java index 227f7cecc..fd57c0171 100644 --- a/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java @@ -12,7 +12,8 @@ import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.global.GuiNPCManageLinked; -import noppes.npcs.client.gui.script.GuiScriptLinkedItem; +import noppes.npcs.client.gui.script.GuiScriptInterface; +import noppes.npcs.controllers.data.LinkedItemScript; import noppes.npcs.client.gui.util.GuiButtonBiDirectional; import noppes.npcs.client.gui.util.GuiMenuTopButton; import noppes.npcs.client.gui.util.GuiNpcButton; @@ -307,10 +308,7 @@ public class SubGuiLinkedItem extends SubGuiInterface implements ITextfieldListe return; } else if (id == -3) { PacketClient.sendClient(new LinkedItemSavePacket(linkedItem.writeToNBT(false), originalName)); - GuiScriptLinkedItem scriptGUI = new GuiScriptLinkedItem((GuiNPCManageLinked) this.parent, linkedItem); - scriptGUI.setWorldAndResolution(mc, width, height); - scriptGUI.initGui(); - mc.currentScreen = scriptGUI; + GuiScriptInterface.open(this.parent, new LinkedItemScript(linkedItem.id)); return; } else if (id == 20) { // Open confirmation for version bump. diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java deleted file mode 100644 index 22c51b207..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptEffect.java +++ /dev/null @@ -1,56 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.client.gui.global.GuiNPCManageEffects; -import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.CustomEffect; -import noppes.npcs.controllers.data.EffectScript; -import noppes.npcs.controllers.data.IScriptUnit; - -import java.util.ArrayList; - -public class GuiScriptEffect extends GuiScriptInterface { - - public final CustomEffect effect; - private final EffectScript scriptHandler; - - public GuiScriptEffect(GuiNPCManageEffects parent, CustomEffect effect) { - super(); - this.parent = parent; - this.effect = effect; - this.scriptHandler = new EffectScript(); - this.handler = this.scriptHandler; - this.singleContainer = true; - - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_EFFECT)); - - EffectScriptPacket.Get(effect.id); - } - - @Override - protected ScriptContext getScriptContext() { - return ScriptContext.GLOBAL; - } - - protected void setHandlerUnit(IScriptUnit container) { - this.scriptHandler.container = container; - } - - @Override - public void setGuiData(NBTTagCompound compound) { - setGuiDataWithOldContainer(compound); - } - - protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { - EffectScriptPacket.Save(effect.id, index, totalCount, scriptNBT); - } - - @Override - public void save() { - saveWithPackets(); - } -} \ No newline at end of file diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 4eb7ccca2..76819ab56 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -91,16 +91,17 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb * Create a GuiScriptInterface for a single script without a handler. * Wraps the script in a SingleScriptHandler automatically. */ - public static GuiScriptInterface create(GuiScreen parent, IScriptHandler handler) { + public static GuiScriptInterface open(GuiScreen parent, IScriptHandler handler) { GuiScriptInterface gui = new GuiScriptInterface(); gui.handler = handler; gui.parent = parent; - gui.singleContainer = true; - gui.displayGuiScreen(gui); // Initialize hooks from handler gui.hookList = new ArrayList<>(handler.getHooks()); + // Request data from server + if (handler instanceof IScriptHandlerPacket) + ((IScriptHandlerPacket) handler).requestData(); Minecraft.getMinecraft().displayGuiScreen(gui); return gui; @@ -637,60 +638,29 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } public void setGuiData(NBTTagCompound compound) { - NBTTagList data = compound.getTagList("Languages", 10); - HashMap languages = new HashMap(); - - for (int i = 0; i < data.tagCount(); ++i) { - NBTTagCompound comp = data.getCompoundTagAt(i); - ArrayList scripts = new ArrayList(); - NBTTagList list = comp.getTagList("Scripts", 8); - - for (int j = 0; j < list.tagCount(); ++j) { - scripts.add(list.getStringTagAt(j)); + if (handler instanceof IScriptHandlerPacket) { + IScriptHandlerPacket.GuiDataResult result = ((IScriptHandlerPacket) handler).setGuiData(compound); + if (result.kind == IScriptHandlerPacket.GuiDataKind.LOAD_COMPLETE) { + loaded = true; + return; + } + if (result.kind == IScriptHandlerPacket.GuiDataKind.METADATA) { + loadLanguagesData(compound); + return; + } + if (result.kind == IScriptHandlerPacket.GuiDataKind.TAB) { + initGui(); + return; } - - languages.put(comp.getString("Language"), scripts); - } - - this.languages = languages; - this.initGui(); - } - - /** - * Get the script context for this GUI. - * Override in subclasses to return the appropriate context. - * - * @return The script context (default: GLOBAL) - */ - protected ScriptContext getScriptContext() { - return ScriptContext.GLOBAL; - } - - // ==================== UNIFIED SCRIPT DATA HANDLING ==================== - - /** - * Unified setGuiData for script GUIs with old container system. - * Handles both language data and tab-specific script loading. - */ - protected void setGuiDataWithOldContainer(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; return; } - - if (!compound.hasKey("Tab")) { - this.handler.setLanguage(compound.getString("ScriptLanguage")); - this.handler.setEnabled(compound.getBoolean("ScriptEnabled")); - this.loadLanguagesData(compound); - } else { - int tab = compound.getInteger("Tab"); - NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); - IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, this.handler); - this.setHandlerUnit(container); - this.initGui(); - } + + loadLanguagesData(compound); } + + // ==================== UNIFIED SCRIPT DATA HANDLING ==================== + /** * Load languages data from NBT. * Separated for potential override in subclasses if needed. @@ -715,50 +685,16 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.initGui(); } - /** - * Set the handler's container. Override if handler is not IScriptHandler. - */ - protected void setHandlerUnit(IScriptUnit container) { - // Default implementation - subclasses may need to cast and set differently - // e.g., ((LinkedItemScript) handler).container = container; - } - - /** - * Unified save method for script GUIs with packet-based saving. - * Subclasses only need to override sendScriptPackets() and sendMetadataPacket(). - */ - protected void saveWithPackets() { - if (loaded) { - this.setScript(); - - List containers = this.handler.getScripts(); - for (int i = 0; i < containers.size(); i++) { - IScriptUnit container = containers.get(i); - sendSavePacket(i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.handler.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.handler.getEnabled()); - scriptData.setTag("ScriptConsole", noppes.npcs.NBTTags.NBTLongStringMap(this.handler.getConsoleText())); - - sendSavePacket(-1, containers.size(), scriptData); - } - } - - /** - * Send a script container packet. Override in subclasses. - * @param index The index of this script - * @param totalCount Total number of scripts - * @param scriptNBT The script container NBT data - */ - protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { - // Default: do nothing (for non-packet-based GUIs like GuiScriptItem) - } - public void save() { - if (loaded) - this.setScript(); + if (!loaded) + return; + + this.setScript(); + + if (handler instanceof IScriptHandlerPacket) + ((IScriptHandlerPacket) handler).sync(); + + } public void textUpdate(String text) { diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java deleted file mode 100644 index 881e3dd6b..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptLinkedItem.java +++ /dev/null @@ -1,55 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.item.LinkedItemScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.client.gui.global.GuiNPCManageLinked; -import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.IScriptUnit; -import noppes.npcs.controllers.data.LinkedItem; -import noppes.npcs.controllers.data.LinkedItemScript; - -import java.util.ArrayList; - -public class GuiScriptLinkedItem extends GuiScriptInterface { - - public final LinkedItem linkedItem; - - public GuiScriptLinkedItem(GuiNPCManageLinked parent, LinkedItem linkedItem) { - super(); - this.parent = parent; - this.linkedItem = linkedItem; - this.handler = new LinkedItemScript(); - this.singleContainer = true; - - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_LINKED_ITEM)); - - LinkedItemScriptPacket.Get(linkedItem.id); - } - - @Override - protected ScriptContext getScriptContext() { - return ScriptContext.ITEM; - } - - protected void setHandlerUnit(IScriptUnit container) { - ((LinkedItemScript) handler).container = container; - } - - @Override - public void setGuiData(NBTTagCompound compound) { - setGuiDataWithOldContainer(compound); - } - - protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { - LinkedItemScriptPacket.Save(linkedItem.id, index, totalCount, scriptNBT); - } - - @Override - public void save() { - saveWithPackets(); - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java deleted file mode 100644 index 83316a48d..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptRecipe.java +++ /dev/null @@ -1,69 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.RecipeScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.client.gui.global.GuiNpcManageRecipes; -import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.IScriptUnit; -import noppes.npcs.controllers.data.RecipeAnvil; -import noppes.npcs.controllers.data.RecipeCarpentry; -import noppes.npcs.controllers.data.RecipeScript; - -import java.util.ArrayList; - -public class GuiScriptRecipe extends GuiScriptInterface { - - public final int recipeId; - public final boolean anvil; - - public GuiScriptRecipe(GuiNpcManageRecipes parent, RecipeCarpentry recipe) { - super(); - this.parent = parent; - this.handler = new RecipeScript(); - this.anvil = false; - this.recipeId = recipe.id; - this.singleContainer = true; - - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_RECIPE)); - - RecipeScriptPacket.Get(false, recipeId); - } - - public GuiScriptRecipe(GuiNpcManageRecipes parent, RecipeAnvil recipe) { - super(); - this.parent = parent; - this.handler = new RecipeScript(); - this.anvil = true; - this.recipeId = recipe.id; - - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_RECIPE)); - - RecipeScriptPacket.Get(true, recipeId); - } - - @Override - protected ScriptContext getScriptContext() { - return ScriptContext.GLOBAL; - } - - protected void setHandlerUnit(IScriptUnit container) { - ((RecipeScript) handler).container = container; - } - - @Override - public void setGuiData(NBTTagCompound compound) { - setGuiDataWithOldContainer(compound); - } - - protected void sendSavePacket(int index, int totalCount, NBTTagCompound scriptNBT) { - RecipeScriptPacket.Save(anvil, recipeId, index, totalCount, scriptNBT); - } - - @Override - public void save() { - saveWithPackets(); - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/src/main/java/noppes/npcs/controllers/data/EffectScript.java index ee5f63d90..8169dbbd7 100644 --- a/src/main/java/noppes/npcs/controllers/data/EffectScript.java +++ b/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -1,63 +1,55 @@ package noppes.npcs.controllers.data; -import io.netty.buffer.ByteBuf; -import kamkeel.npcs.util.ByteBufUtils; +import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.common.util.Constants; -import noppes.npcs.controllers.ScriptController; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.scripted.event.player.PlayerEvent; -import java.io.IOException; - -public class EffectScript extends ScriptHandler { - - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setString("ScriptLanguage", scriptLanguage); - compound.setBoolean("ScriptEnabled", enabled); - - if (container != null) - compound.setTag("ScriptContent", container.writeToNBT(new NBTTagCompound())); - return compound; +/** + * Script handler for CustomEffect scripts. + * Manages a single script container for effect hooks (onEffectAdd, onEffectTick, onEffectRemove). + */ +public class EffectScript extends SingleScriptHandler implements IScriptHandlerPacket { + + /** The effect ID for packet communication. -1 if not bound to an effect. */ + private int effectId = -1; + + /** + * Create an unbound EffectScript (for server-side use). + */ + public EffectScript() { } - - public EffectScript readFromNBT(NBTTagCompound compound) { - scriptLanguage = compound.getString("ScriptLanguage"); - enabled = compound.getBoolean("ScriptEnabled"); - - if (compound.hasKey("ScriptContent", Constants.NBT.TAG_COMPOUND)) { - container = IScriptUnit.createFromNBT(compound.getCompoundTag("ScriptContent"), this); - } - return this; + + /** + * Create an EffectScript bound to a specific effect (for GUI use). + * @param effectId The ID of the CustomEffect + */ + public EffectScript(int effectId) { + this.effectId = effectId; } - + + @Override + public String getHookContext() { + return IScriptHookHandler.CONTEXT_EFFECT; + } + + /** + * Convenience method to call a script with a typed event. + */ public void callScript(ScriptType type, PlayerEvent.EffectEvent event) { callScript(type.function, event); } - - - public void saveScript(ByteBuf buffer) throws IOException { - int tab = buffer.readInt(); - int totalScripts = buffer.readInt(); - if (totalScripts == 0) { - this.container = null; - } - - if (tab == 0) { - NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); - this.container = IScriptUnit.createFromNBT(tabCompound, this); - } else { - NBTTagCompound compound = ByteBufUtils.readNBT(buffer); - this.setLanguage(compound.getString("ScriptLanguage")); - if (!ScriptController.Instance.languages.containsKey(this.getLanguage())) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.setLanguage((String) ScriptController.Instance.languages.keySet().toArray()[0]); - } else { - this.setLanguage("ECMAScript"); - } - } - this.setEnabled(compound.getBoolean("ScriptEnabled")); - } + + @Override + public void requestData() { + if (effectId >= 0) + EffectScriptPacket.Get(effectId); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + if (effectId >= 0) + EffectScriptPacket.Save(effectId, index, totalCount, nbt); } public enum ScriptType { @@ -71,4 +63,4 @@ public class EffectScript extends ScriptHandler { this.function = functionName; } } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java new file mode 100644 index 000000000..c307f508d --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java @@ -0,0 +1,86 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.NBTTags; + +import java.util.List; + +public interface IScriptHandlerPacket extends IScriptHandler { + + /** + * Client-side: request initial script data from the server (typically a {@code ...Packet.Get(...)}). + */ + void requestData(); + + /** + * Client-side: send a script container packet to the server. + * + * @param index {@code >= 0} for a script tab, {@code -1} for handler metadata (language/enabled/console) + * @param totalCount Total number of scripts for this handler + * @param nbt The script container NBT (tab content or metadata) + */ + void sendSavePacket(int index, int totalCount, NBTTagCompound nbt); + + enum GuiDataKind { + LOAD_COMPLETE, + METADATA, + TAB + } + + class GuiDataResult { + public final GuiDataKind kind; + public final int tabIndex; + + public GuiDataResult(GuiDataKind kind, int tabIndex) { + this.kind = kind; + this.tabIndex = tabIndex; + } + } + + /** + * Apply one incoming gui-data payload to this handler. + *

+ * Notes: + *

+ * + * @return A small classification result so GuiScriptInterface can update GUI state. + */ + default GuiDataResult setGuiData(NBTTagCompound compound) { + if (compound.hasKey("LoadComplete")) { + return new GuiDataResult(GuiDataKind.LOAD_COMPLETE, -1); + } + + if (!compound.hasKey("Tab")) { + setLanguage(compound.getString("ScriptLanguage")); + setEnabled(compound.getBoolean("ScriptEnabled")); + return new GuiDataResult(GuiDataKind.METADATA, -1); + } + + int tab = compound.getInteger("Tab"); + NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); + IScriptUnit unit = IScriptUnit.createFromNBT(scriptCompound, this); + replaceScriptUnit(tab, unit); + return new GuiDataResult(GuiDataKind.TAB, tab); + } + + /** + * Emit save packets for all tabs plus one metadata packet. + * GuiScriptInterface is responsible for updating the active {@link IScriptUnit} with current editor text first. + */ + default void sync() { + List containers = getScripts(); + for (int i = 0; i < containers.size(); i++) { + IScriptUnit container = containers.get(i); + sendSavePacket(i, containers.size(), container.writeToNBT(new NBTTagCompound())); + } + + NBTTagCompound scriptData = new NBTTagCompound(); + scriptData.setString("ScriptLanguage", getLanguage()); + scriptData.setBoolean("ScriptEnabled", getEnabled()); + scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(getConsoleText())); + sendSavePacket(-1, containers.size(), scriptData); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java index 3b09c5766..4d01e1c46 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -1,60 +1,58 @@ package noppes.npcs.controllers.data; -import io.netty.buffer.ByteBuf; -import kamkeel.npcs.util.ByteBufUtils; +import kamkeel.npcs.network.packets.request.script.item.LinkedItemScriptPacket; import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.common.util.Constants; -import noppes.npcs.controllers.ScriptController; +import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.constants.ScriptContext; -import java.io.IOException; - -public class LinkedItemScript extends ScriptHandler { - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setString("ScriptLanguage", scriptLanguage); - compound.setBoolean("ScriptEnabled", enabled); - - if (container != null) - compound.setTag("ScriptContent", container.writeToNBT(new NBTTagCompound())); - return compound; +/** + * Script handler for LinkedItem scripts. + * Manages a single script container for linked item hooks. + */ +public class LinkedItemScript extends SingleScriptHandler implements IScriptHandlerPacket { + + /** The linked item ID for packet communication. -1 if not bound. */ + private int linkedItemId = -1; + + /** + * Create an unbound LinkedItemScript (for server-side use). + */ + public LinkedItemScript() { } - - public LinkedItemScript readFromNBT(NBTTagCompound compound) { - scriptLanguage = compound.getString("ScriptLanguage"); - enabled = compound.getBoolean("ScriptEnabled"); - - if (compound.hasKey("ScriptContent", Constants.NBT.TAG_COMPOUND)) { - container = IScriptUnit.createFromNBT(compound.getCompoundTag("ScriptContent"), this); - } - return this; + + /** + * Create a LinkedItemScript bound to a specific linked item (for GUI use). + * @param linkedItemId The ID of the LinkedItem + */ + public LinkedItemScript(int linkedItemId) { + this.linkedItemId = linkedItemId; } - + + @Override + public String getHookContext() { + return IScriptHookHandler.CONTEXT_LINKED_ITEM; + } + + @Override + public ScriptContext getContext() { + return ScriptContext.ITEM; + } + @Override public String noticeString() { return "LinkedItem"; } - - public void saveScript(ByteBuf buffer) throws IOException { - int tab = buffer.readInt(); - int totalScripts = buffer.readInt(); - if (totalScripts == 0) { - this.container = null; - } - - if (tab == 0) { - NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); - this.container = IScriptUnit.createFromNBT(tabCompound, this); - } else { - NBTTagCompound compound = ByteBufUtils.readNBT(buffer); - this.setLanguage(compound.getString("ScriptLanguage")); - if (!ScriptController.Instance.languages.containsKey(this.getLanguage())) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.setLanguage((String) ScriptController.Instance.languages.keySet().toArray()[0]); - } else { - this.setLanguage("ECMAScript"); - } - } - this.setEnabled(compound.getBoolean("ScriptEnabled")); - } + + @Override + public void requestData() { + if (linkedItemId >= 0) + LinkedItemScriptPacket.Get(linkedItemId); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + if (linkedItemId >= 0) + LinkedItemScriptPacket.Save(linkedItemId, index, totalCount, nbt); + } } diff --git a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java index b498d233c..ab5c67fb5 100644 --- a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java +++ b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java @@ -1,57 +1,57 @@ package noppes.npcs.controllers.data; -import io.netty.buffer.ByteBuf; -import kamkeel.npcs.util.ByteBufUtils; +import kamkeel.npcs.network.packets.request.script.RecipeScriptPacket; import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.common.util.Constants; -import noppes.npcs.controllers.ScriptController; +import noppes.npcs.api.handler.IScriptHookHandler; -import java.io.IOException; - -public class RecipeScript extends ScriptHandler { - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setString("ScriptLanguage", scriptLanguage); - compound.setBoolean("ScriptEnabled", enabled); - if (container != null) - compound.setTag("ScriptContent", container.writeToNBT(new NBTTagCompound())); - return compound; +/** + * Script handler for Recipe scripts (both carpentry and anvil). + * Manages a single script container for recipe hooks (pre, post). + */ +public class RecipeScript extends SingleScriptHandler implements IScriptHandlerPacket { + + /** The recipe ID for packet communication. -1 if not bound. */ + private int recipeId = -1; + + /** Whether this is an anvil recipe (vs carpentry). */ + private boolean anvil = false; + + /** + * Create an unbound RecipeScript (for server-side use). + */ + public RecipeScript() { } - - public RecipeScript readFromNBT(NBTTagCompound compound) { - scriptLanguage = compound.getString("ScriptLanguage"); - enabled = compound.getBoolean("ScriptEnabled"); - if (compound.hasKey("ScriptContent", Constants.NBT.TAG_COMPOUND)) { - container = IScriptUnit.createFromNBT(compound.getCompoundTag("ScriptContent"), this); - } - return this; + + /** + * Create a RecipeScript bound to a specific recipe (for GUI use). + * @param recipeId The ID of the recipe + * @param anvil true for anvil recipe, false for carpentry recipe + */ + public RecipeScript(int recipeId, boolean anvil) { + this.recipeId = recipeId; + this.anvil = anvil; } - + + @Override + public String getHookContext() { + return IScriptHookHandler.CONTEXT_RECIPE; + } + @Override public String noticeString() { return "RecipeScript"; } - - public void saveScript(ByteBuf buffer) throws IOException { - int tab = buffer.readInt(); - int totalScripts = buffer.readInt(); - if (totalScripts == 0) - this.container = null; - if (tab == 0) { - NBTTagCompound tabCompound = ByteBufUtils.readNBT(buffer); - this.container = IScriptUnit.createFromNBT(tabCompound, this); - } else { - NBTTagCompound compound = ByteBufUtils.readNBT(buffer); - this.setLanguage(compound.getString("ScriptLanguage")); - if (!ScriptController.Instance.languages.containsKey(this.getLanguage())) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.setLanguage((String) ScriptController.Instance.languages.keySet().toArray()[0]); - } else { - this.setLanguage("ECMAScript"); - } - } - this.setEnabled(compound.getBoolean("ScriptEnabled")); - } + + @Override + public void requestData() { + if (recipeId >= 0) + RecipeScriptPacket.Get(anvil, recipeId); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + if (recipeId >= 0) + RecipeScriptPacket.Save(anvil, recipeId, index, totalCount, nbt); } public enum ScriptType { From 4a8e6df034ed48d1c53069fd22791be4af553f5a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Sun, 18 Jan 2026 18:20:13 +0200 Subject: [PATCH 236/358] Add gradle-plugins submodule --- .gitignore | 1 - .gitmodules | 3 +++ gradle-plugins | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) create mode 160000 gradle-plugins diff --git a/.gitignore b/.gitignore index ed907b3d2..f24532312 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ /out/ /build/ /eclipse/ -/gradle-plugins/ .classpath .project /bin/ diff --git a/.gitmodules b/.gitmodules index d8252cb6a..d3428cb3d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = src/api url = https://github.com/KAMKEEL/CustomNPC-Plus-API.git branch = dev +[submodule "gradle-plugins"] + path = gradle-plugins + url = https://github.com/bigguy345/dts-gradle-plugin diff --git a/gradle-plugins b/gradle-plugins new file mode 160000 index 000000000..e193079bf --- /dev/null +++ b/gradle-plugins @@ -0,0 +1 @@ +Subproject commit e193079bf7392e83d233ce2e120ee0f2b645021f From a032d24d6576f40ac6f20aa42c646ab748fc143d Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 19 Jan 2026 03:18:41 +0200 Subject: [PATCH 237/358] Fixed crash --- .../controllers/data/JaninoScriptHandler.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java index 81046844e..4dfd5540f 100644 --- a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java @@ -109,6 +109,21 @@ public class JaninoScriptHandler> extends ScriptHandle return Collections.singletonList(script); } + @Override + public void addScriptUnit(IScriptUnit unit) { + setScripts(Collections.singletonList(unit)); + } + + @Override + public void replaceScriptUnit(int index, IScriptUnit unit) { + setScripts(Collections.singletonList(unit)); + } + + @Override + public void removeScriptUnit(int index) { + deleteScript(); + } + @Override public IScriptUnit createJaninoScriptUnit() { return factory.get(); From af62868387eb2cca9182b87b8e25a5980d3c7f80 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 19 Jan 2026 03:26:02 +0200 Subject: [PATCH 238/358] 1. JaninoHookResolver 2. Janino reflection based function calls like: Color script = scriptHandler.callFunction("getColor", Color.class, ctx); Compared to current: Color script = scriptHandler.getScript().call(f -> f.getColor(ctx)); --- .../npcs/controllers/data/IScriptHandler.java | 45 ++- .../npcs/controllers/data/IScriptUnit.java | 29 +- .../controllers/data/JaninoScriptHandler.java | 7 + .../npcs/janino/JaninoHookResolver.java | 319 ++++++++++++++++++ .../java/noppes/npcs/janino/JaninoScript.java | 144 +++----- 5 files changed, 435 insertions(+), 109 deletions(-) create mode 100644 src/main/java/noppes/npcs/janino/JaninoHookResolver.java diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java index cc47eb1c8..b89c725a9 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -17,9 +17,52 @@ public interface IScriptHandler { callScript(type.function, event); } - + void callScript(String hookName, Event event); + /** + * Call a function by name and return its result. + * + * Default implementation calls the function on each script unit (in order) and returns + * the first non-null value. + */ + default Object callFunction(String hookName, Object... args) { + if (hookName == null || hookName.isEmpty()) + return null; + + if (!getEnabled()) + return null; + + List scripts = getScripts(); + if (scripts == null || scripts.isEmpty()) + return null; + + for (IScriptUnit script : scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) + continue; + + Object result = script.callFunction(hookName, args); + if (result != null) + return result; + } + + return null; + } + + /** + * Typed convenience wrapper around {@link #callFunction(String, Object...)}. + */ + default S callFunction(String hookName, Class returnType, Object... args) { + Object result = callFunction(hookName, args); + if (result == null || returnType == null) + return null; + + if (returnType.isInstance(result)) + return returnType.cast(result); + + return null; + } + default boolean isClient() { return FMLCommonHandler.instance().getEffectiveSide().isClient(); } diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java index 1cd661a6e..258b584b8 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java @@ -161,8 +161,6 @@ public interface IScriptUnit { /** * Execute this script unit for the given hook type and event. - * For ScriptContainer: calls run(EnumScriptType, Event) - * For JaninoScript: calls callByHookType(EnumScriptType, Event) * * @param type The script hook type * @param event The event object @@ -171,11 +169,32 @@ public interface IScriptUnit { /** * Execute this script unit for the given hook name and event. - * For ScriptContainer: calls run(String, Event) - * For JaninoScript: converts hookName to EnumScriptType and calls callByHookType - * + * * @param hookName The hook name (e.g., "init", "interact") * @param event The event object */ void run(String hookName, Object event); + + /** + * Call a script function by name and return its result. + * + * Default implementation returns null (not supported). + */ + default Object callFunction(String hookName, Object... args) { + return null; + } + + /** + * Typed convenience wrapper around {@link #callFunction(String, Object...)}. + */ + default S callFunction(String hookName, Class returnType, Object... args) { + Object result = callFunction(hookName, args); + if (result == null || returnType == null) + return null; + + if (returnType.isInstance(result)) + return returnType.cast(result); + + return null; + } } diff --git a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java index 4dfd5540f..28bd3af4f 100644 --- a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java @@ -124,6 +124,13 @@ public class JaninoScriptHandler> extends ScriptHandle deleteScript(); } + public List getHooks() { + if (script != null) + return script.getHookList(); + + return Collections.emptyList(); + } + @Override public IScriptUnit createJaninoScriptUnit() { return factory.get(); diff --git a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java new file mode 100644 index 000000000..2c8eeb543 --- /dev/null +++ b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java @@ -0,0 +1,319 @@ +package noppes.npcs.janino; + +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.janino.annotations.ScriptHook; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.stream.Collectors; + +/** + * Resolves script hook methods for a given interface type using runtime + * argument values. Resolution favors exact matches and then the "closest" + * assignable parameter types (measured by inheritance distance). + * + * The resolver keeps simple caches for previously resolved signatures and + * supports mapping enum-based hook names (via the {@code @ScriptHook} + * annotation) to methods. + */ +public final class JaninoHookResolver { + private final Class interfaceType; + + /* Methods explicitly mapped from EnumScriptType via @ScriptHook */ + private final Map enumHookLookup = new HashMap<>(); + + /** + * Cache for resolved hook methods. Keys are generated by {@link #buildSignatureKey} + * and represent the unique combination of hook name and runtime argument types. + * This avoids expensive recursive reflection lookups on every hook call. + */ + private final Map signatureCache = new HashMap<>(); + + /* Lazily built list of available hooks and a mapping for display names */ + private boolean hookListInitialized; + /* List of names displayed in GUI hooks list (accounts for overload names) */ + private final List displayHookList = new ArrayList<>(); + /* Display name -> Method map */ + private final Map displayHookMethods = new HashMap<>(); + + /** + * Create a resolver for the provided script interface type. + * @param interfaceType non-null interface class that scripts implement + */ + public JaninoHookResolver(Class interfaceType) { + this.interfaceType = Objects.requireNonNull(interfaceType, "interfaceType"); + initializeEnumHookMethods(); + } + + /** + * Clears internal resolution caches so future calls perform fresh + * method resolution. Call when scripts or available methods change. + */ + public void clearResolutionCaches() { + signatureCache.clear(); + } + + /** + * Resolve a hook method by name and runtime argument values. + * + * This first checks any enum-mapped methods (via {@code @ScriptHook}), + * then a signature-based cache and finally attempts full candidate + * resolution using {@link #resolveHookMethodInternal}. + * + * @param hookName hook method name (or enum function name) + * @param args runtime arguments to use for overload resolution + * @return the resolved {@link Method} or {@code null} if none matched + */ + public Method resolveHookMethod(String hookName, Object[] args) { + if (hookName == null || hookName.isEmpty()) + return null; + + Method method = enumHookLookup.get(hookName); + if (method != null) + return method; + + Object[] safeArgs = args == null ? new Object[0] : args; + + String signatureKey = buildSignatureKey(hookName, safeArgs); + method = signatureCache.get(signatureKey); + if (method != null) + return method; + + method = resolveHookMethodInternal(interfaceType, hookName, safeArgs); + if (method != null) { + signatureCache.put(signatureKey, method); + return method; + } + + String noArgSignatureKey = buildSignatureKey(hookName, new Object[0]); + method = signatureCache.get(noArgSignatureKey); + if (method != null) + return method; + + method = resolveHookMethodInternal(interfaceType, hookName, new Object[0]); + if (method != null) { + signatureCache.put(noArgSignatureKey, method); + return method; + } + + return null; + } + + /** + * Returns an immutable copy of the available hook display names. + */ + public List getDisplayHookList() { + ensureHookListCreated(); + return new ArrayList<>(displayHookList); + } + + public Method getMethodForDisplayName(String displayName) { + if (displayName == null || displayName.isEmpty()) + return null; + + ensureHookListCreated(); + return displayHookMethods.get(displayName); + } + + /** + * Builds the human-readable hook list and display-name -> Method map. + * The list is computed lazily on first access and cached. + */ + private void ensureHookListCreated() { + if (hookListInitialized) + return; + + Map> grouped = Arrays.stream(interfaceType.getDeclaredMethods()) + .collect(Collectors.groupingBy(Method::getName)); + + for (Map.Entry> entry : grouped.entrySet()) { + List methods = entry.getValue(); + + methods.sort(Comparator.comparingInt(Method::getParameterCount)); + + boolean first = true; + for (Method m : methods) { + if (Modifier.isFinal(m.getModifiers())) + continue; + + String displayName = first ? m.getName() : + m.getName() + "(" + Arrays.stream(m.getParameterTypes()) + .map(Class::getSimpleName) + .collect(Collectors.joining(", ")) + ")"; + + displayHookList.add(displayName); + displayHookMethods.put(displayName, m); + first = false; + } + } + + hookListInitialized = true; + } + + /** + * Maps EnumScriptType common names to their respective methods using + * the {@code @ScriptHook} annotation. + */ + private void initializeEnumHookMethods() { + for (Method method : interfaceType.getDeclaredMethods()) { + ScriptHook annotation = method.getAnnotation(ScriptHook.class); + if (annotation == null) + continue; + + for (EnumScriptType hookType : annotation.value()) { + enumHookLookup.put(hookType.function, method); + } + } + } + + /** + * Build a cache key for a hook name and runtime argument list. This key + * encodes the runtime classes of the provided arguments and is used for + * quick cache lookups. Note: keys are based on concrete runtime classes + * which may cause fragmentation when many subclasses are used. + * + * @param hookName hook name + * @param args runtime arguments + * @return a string key representing the hook signature + */ + public static String buildSignatureKey(String hookName, Object[] args) { + StringBuilder sb = new StringBuilder(); + sb.append(hookName).append('#'); + + if (args == null || args.length == 0) + return sb.append('0').toString(); + + sb.append(args.length).append(':'); + for (int i = 0; i < args.length; i++) { + Object arg = args[i]; + if (i > 0) + sb.append(','); + + sb.append(arg == null ? "null" : arg.getClass().getName()); + } + return sb.toString(); + } + + /** + * Internal resolution routine. Attempts to find the best matching method + * on the provided interface by comparing parameter counts and a simple + * match scoring function (see {@link #scoreMethodMatch}). + */ + private static Method resolveHookMethodInternal(Class interfaceType, String hookName, Object[] args) { + Object[] safeArgs = args == null ? new Object[0] : args; + + Method best = null; + int bestScore = Integer.MAX_VALUE; + + for (Method method : interfaceType.getDeclaredMethods()) { + if (!method.getName().equals(hookName) || Modifier.isFinal(method.getModifiers())) + continue; + + Class[] paramTypes = method.getParameterTypes(); + if (paramTypes.length != safeArgs.length) + continue; + + int score = scoreMethodMatch(paramTypes, safeArgs); + if (score == Integer.MAX_VALUE) + continue; + + if (score < bestScore) { + bestScore = score; + best = method; + if (score == 0) + return best; + } + } + + return best; + } + + /** + * Score how well a method's parameter types match the provided runtime + * arguments. Lower scores are better; {@link Integer#MAX_VALUE} indicates + * an incompatible parameter. + * + * @param paramTypes method parameter types + * @param args runtime argument values + * @return score (lower is better) or Integer.MAX_VALUE for no match + */ + private static int scoreMethodMatch(Class[] paramTypes, Object[] args) { + int score = 0; + for (int i = 0; i < paramTypes.length; i++) { + Class paramType = paramTypes[i]; + Object arg = args[i]; + + if (arg == null) { + if (paramType.isPrimitive()) + return Integer.MAX_VALUE; + + score += 1; + continue; + } + + Class argType = arg.getClass(); + Class boxedParam = boxType(paramType); + + if (boxedParam.equals(argType)) + continue; + + if (boxedParam.isAssignableFrom(argType)) { + score += inheritanceDistance(argType, boxedParam); + continue; + } + + return Integer.MAX_VALUE; + } + + return score; + } + + private static Class boxType(Class type) { + if (!type.isPrimitive()) + return type; + if (type == boolean.class) + return Boolean.class; + if (type == byte.class) + return Byte.class; + if (type == short.class) + return Short.class; + if (type == int.class) + return Integer.class; + if (type == long.class) + return Long.class; + if (type == float.class) + return Float.class; + if (type == double.class) + return Double.class; + if (type == char.class) + return Character.class; + return type; + } + + /** + * Compute a simple inheritance distance between a child and parent class + * by walking superclasses. Returns a large value if {@code parent} is + * not assignable from {@code child}. + * + * Note: the matching algorithm uses superclass traversal to compute a + * distance metric; interface inheritance is not explored which can make + * matches against interfaces less optimal. Consider improving + * `inheritanceDistance` if interface-based scoring is required. + */ + private static int inheritanceDistance(Class child, Class parent) { + if (child.equals(parent)) + return 0; + + if (!parent.isAssignableFrom(child)) + return 1000; + + int distance = 0; + Class current = child; + while (current != null && !current.equals(parent)) { + current = current.getSuperclass(); + distance++; + } + return distance; + } +} diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index bc3d8e915..8a217b8d0 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -1,6 +1,5 @@ package noppes.npcs.janino; -import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.Event; import io.github.somehussar.janinoloader.api.script.IScriptBodyBuilder; import io.github.somehussar.janinoloader.api.script.IScriptClassBody; @@ -12,7 +11,6 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.janino.annotations.ParamName; -import noppes.npcs.janino.annotations.ScriptHook; import org.codehaus.commons.compiler.InternalCompilerException; import org.codehaus.commons.compiler.Sandbox; @@ -50,15 +48,8 @@ public abstract class JaninoScript implements IScriptUnit { public final IScriptBodyBuilder builder; protected IScriptClassBody scriptBody; - private boolean hooksInitialized = false; - // Hook list for GUI display - private List hookList = new ArrayList<>(); - // Map from display name (with parameters if overloaded) to Method - private Map hookMap = new HashMap<>(); - // Map from EnumScriptType to Method for OLD tab-based script system - private Map hookTypeMap = new HashMap<>(); + private final JaninoHookResolver hookResolver; - // Default imports for this script type (e.g., noppes.npcs.api.*) private final String[] defaultImports; protected JaninoScript(Class type, String[] defaultImports) { @@ -73,8 +64,7 @@ public abstract class JaninoScript implements IScriptUnit { this.scriptBody = builder.build(); - // Build hook type map from @ScriptHook annotations - initializeHookTypeMap(); + this.hookResolver = new JaninoHookResolver(type); } protected T getUnsafe() { @@ -147,8 +137,10 @@ public abstract class JaninoScript implements IScriptUnit { appendConsole(parentCause.getMessage()); appendConsole(rootCause.getMessage()); } + hookResolver.clearResolutionCaches(); } catch (Exception e) { appendConsole("Unknown error: " + e.getMessage()); + hookResolver.clearResolutionCaches(); } } @@ -197,54 +189,43 @@ public abstract class JaninoScript implements IScriptUnit { } } - /** - * Scans the Functions interface for @ScriptHook annotations and builds the hookTypeMap. - * This allows direct method invocation by EnumScriptType without switch statements. - */ - private void initializeHookTypeMap() { - for (Method method : type.getDeclaredMethods()) { - ScriptHook annotation = method.getAnnotation(ScriptHook.class); - if (annotation != null) { - for (EnumScriptType hookType : annotation.value()) { - hookTypeMap.put(hookType, method); - } - } - } + public void callByHookName(String hookName, Object arg) { + callFunction(hookName, arg); } - /** - * Call a hook method by its EnumScriptType. - * Used by the OLD tab-based script system to execute hooks directly. - * - * @param hookType The script type/hook to execute - * @param event The event object to pass to the hook method - */ - public void callByHookType(EnumScriptType hookType, Object event) { - Method method = hookTypeMap.get(hookType); - if (method == null) { - // Hook not implemented or not annotated - return; - } + @Override + public Object callFunction(String hookName, Object... args) { + if (hookName == null || hookName.isEmpty()) + return null; + + Method method = hookResolver.resolveHookMethod(hookName, args); + if (method == null) + return null; ensureCompiled(); T t = getUnsafe(); if (t == null) - return; + return null; + + Method invokeMethod = method; + Object[] invokeArgs = args == null ? new Object[0] : args; try { - sandbox.confine((PrivilegedAction) () -> { + return sandbox.confine((PrivilegedAction) () -> { try { - method.invoke(t, event); + return invokeMethod.invoke(t, invokeArgs); } catch (Exception e) { - appendConsole("Error calling hook " + hookType.name() + ": " + e.getMessage()); + appendConsole("Error calling hook " + hookName + ": " + e.getMessage()); + return null; } - return null; }); } catch (Exception e) { - appendConsole("Runtime Error in hook " + hookType.name() + ": " + e.getMessage()); + appendConsole("Runtime Error in hook " + hookName + ": " + e.getMessage()); + return null; } } + // ==================== IScriptUnit IMPLEMENTATION ==================== /** @@ -256,7 +237,10 @@ public abstract class JaninoScript implements IScriptUnit { */ @Override public void run(EnumScriptType type, Event event) { - callByHookType(type, event); + if (type == null) + return; + + callFunction(type.function, event); } /** @@ -268,13 +252,9 @@ public abstract class JaninoScript implements IScriptUnit { */ @Override public void run(String hookName, Object event) { - try { - EnumScriptType enumType = EnumScriptType.valueOfIgnoreCase(hookName); - callByHookType(enumType, event); - } catch (IllegalArgumentException ignored) { - // Hook name doesn't map to an EnumScriptType - } + callFunction(hookName, event); } + @Override public String getScript() { return this.script; @@ -284,6 +264,7 @@ public abstract class JaninoScript implements IScriptUnit { public void setScript(String script) { this.script = script; this.evaluated = false; + hookResolver.clearResolutionCaches(); } @Override @@ -295,8 +276,7 @@ public abstract class JaninoScript implements IScriptUnit { public void setExternalScripts(List externalScripts) { this.externalScripts = externalScripts; this.evaluated = false; - this.hookNameMap.clear(); - this.missingHookNames.clear(); + hookResolver.clearResolutionCaches(); } @Override @@ -343,19 +323,14 @@ public abstract class JaninoScript implements IScriptUnit { return script != null && !script.isEmpty(); } - @Override - public String generateHookStub(String hookName, Object hookData) { - // Build hook list and map - if (!hooksInitialized) { - createHookList(); - hooksInitialized = true; - } - - // Look up in hookMap (handles both simple names and overloaded display names) - Method method = hookMap.get(hookName); - if (method != null) - return generateMethodStub(method); + @Override + public String generateHookStub(String hookName, Object hookData) { + hookResolver.clearResolutionCaches(); + Method method = hookResolver.getMethodForDisplayName(hookName); + if (method != null) + return generateMethodStub(method); + // Fallback for unknown hook names - generate basic override return String.format("public void %s() {\n \n}\n", hookName); } @@ -416,45 +391,8 @@ public abstract class JaninoScript implements IScriptUnit { } - /** - * Create the hook list and hook map for GUI display and generation. - * Groups methods by name and creates display names with parameters for overloaded methods. - */ - private void createHookList() { - // Group all declared methods by their name - Map> grouped = Arrays.stream(type.getDeclaredMethods()) - .collect(Collectors.groupingBy(Method::getName)); - - for (Map.Entry> entry : grouped.entrySet()) { - List methods = entry.getValue(); - - // Sort overloads by parameter count (ascending) - methods.sort(Comparator.comparingInt(Method::getParameterCount)); - boolean first = true; - - for (Method m : methods) { - if (Modifier.isFinal(m.getModifiers())) - continue; // Skip final methods - - // For first (simplest) overload, use just the method name - // For additional overloads, include parameter types in display name - String displayName = first ? m.getName() : - m.getName() + "(" + Arrays.stream(m.getParameterTypes()) - .map(Class::getSimpleName) - .collect(Collectors.joining(", ")) + ")"; - - hookList.add(displayName); - hookMap.put(displayName, m); - first = false; - } - } - } - - /** - * Get the list of hook names for GUI display. - */ public List getHookList() { - return new ArrayList<>(hookList); + return hookResolver.getDisplayHookList(); } From f8c9faac0b394db22a33806f8581d554af1b921a Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 19 Jan 2026 04:06:11 +0200 Subject: [PATCH 239/358] - Implemented `getLanguageOptions()` with server-to-client fallback logic to ensure "Java/Janino" is correctly listed and synchronized in the script GUI. - Renamed `getScriptIndex` to `getLanguageIndex` in `GuiScriptInterface` and its subclasses to resolve naming ambiguity between tabs and engine selection. --- .../noppes/npcs/client/gui/GuiScript.java | 5 +- .../client/gui/script/GuiJaninoScript.java | 4 +- .../client/gui/script/GuiScriptInterface.java | 77 ++++++++++++++++--- 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index 916a0ecd3..2b6f6d7c8 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -204,9 +204,10 @@ public class GuiScript extends GuiScriptInterface { consoleOptions.toArray(new String[0]), activeConsole)); addLabel(new GuiNpcLabel(1, "script.language", guiLeft + 232, guiTop + 30)); + List languageOptions = getLanguageOptions(); addButton(new GuiNpcButton(103, guiLeft + 294, guiTop + 25, 80, 20, - languages.keySet().toArray(new String[0]), getScriptIndex())); - getButton(103).enabled = languages.size() > 0; + languageOptions.toArray(new String[0]), getLanguageIndex(languageOptions))); + getButton(103).enabled = languageOptions.size() > 0; addLabel(new GuiNpcLabel(2, "gui.enabled", guiLeft + 232, guiTop + 53)); addButton(new GuiNpcButton(104, guiLeft + 294, guiTop + 48, 50, 20, diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java index 04d078fda..acfac310a 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java @@ -163,7 +163,7 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); String containerLang = container.getLanguage(); this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, new String[]{containerLang.equals("Java") ? "Janino (JVM 8)" : containerLang}, - this.getScriptIndex())); + this.getLanguageIndex())); // this.getButton(103).enabled = !this.languages.isEmpty(); this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); // Note: GuiJaninoScript is deprecated; enabled button disabled @@ -361,7 +361,7 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback return builder.toString(); } - private int getScriptIndex() { + private int getLanguageIndex() { return 0; } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 76819ab56..f4d9b2c13 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -304,10 +304,11 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.addButton(new GuiNpcButton(100, var9, this.guiTop + 125, 60, 20, "gui.copy")); this.addButton(new GuiNpcButton(102, var9, this.guiTop + 146, 60, 20, "gui.clear")); this.addLabel(new GuiNpcLabel(1, "script.language", var9, this.guiTop + 15)); + List languageOptions = getLanguageOptions(); this.addButton(new GuiNpcButton(103, var9 + 60, this.guiTop + 10, 80, 20, - (String[]) this.languages.keySet().toArray(new String[this.languages.keySet().size()]), - this.getScriptIndex())); - this.getButton(103).enabled = this.languages.size() > 0; + languageOptions.toArray(new String[0]), + this.getLanguageIndex(languageOptions))); + this.getButton(103).enabled = languageOptions.size() > 0; this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, new String[]{"gui.no", "gui.yes"}, this.handler.getEnabled() ? 1 : 0)); @@ -405,19 +406,66 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return builder.toString(); } - public int getScriptIndex() { - int i = 0; - for (Iterator var2 = this.languages.keySet().iterator(); var2.hasNext(); ++i) { - String language = (String) var2.next(); - if (language.equalsIgnoreCase(this.handler.getLanguage())) { + /** + * Finds the index of the current handler's language within the available options. + * + * @return The 0-based index, or 0 if not found. + */ + public int getLanguageIndex() { + return getLanguageIndex(getLanguageOptions()); + } + + /** + * Finds the index of the current handler's language within the provided list. + * + * @param languageOptions The list of available languages to search. + * @return The 0-based index, or 0 if not found. + */ + protected int getLanguageIndex(List languageOptions) { + if (languageOptions == null || languageOptions.isEmpty()) + return 0; + + int i = 0; + String current = this.handler != null ? this.handler.getLanguage() : null; + for (String language : languageOptions) { + if (current != null && language.equalsIgnoreCase(current)) return i; - } + i++; } return 0; } + /** + * Retrieves the list of available scripting languages for this handler. + *

+ * This method prioritizes languages provided by the server through the {@link #languages} map. + * If "Java" is not present but the handler supports Janino, it injects "Java" into the options + * and ensures the client-side Janino script list is synchronized for the {@link EventGuiScriptList}. + * + * @return A list of language names available for selection. + */ + protected List getLanguageOptions() { + List options = new ArrayList<>(); + if (this.languages != null && !this.languages.isEmpty()) + options.addAll(this.languages.keySet()); + + if ((options.isEmpty() || !options.contains("Java")) && this.handler != null && this.handler.supportsJanino()) { + List scripts = new ArrayList<>(); + if (ScriptController.Instance != null && ScriptController.Instance.scripts != null) { + scripts.addAll(ScriptController.Instance.scripts.keySet()); + } + if (this.languages == null) + this.languages = new HashMap(); + if (!this.languages.containsKey("Java")) + this.languages.put("Java", scripts); + options.add("Java"); + } + + return options; + } + public void confirmClicked(boolean flag, int i) { if (flag) { if (i == 0) { @@ -681,6 +729,17 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb languages.put(comp.getString("Language"), scripts); } + if (handler != null && handler.supportsJanino()) { + List javaScripts = (List) languages.get("Java"); + if (javaScripts == null || javaScripts.isEmpty()) { + javaScripts = new ArrayList<>(); + if (ScriptController.Instance != null && ScriptController.Instance.scripts != null) { + javaScripts.addAll(ScriptController.Instance.scripts.keySet()); + } + languages.put("Java", javaScripts); + } + } + this.languages = languages; this.initGui(); } From 4cb1eee0174ac50fd4e639e54ccf1f642df5c439 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Mon, 19 Jan 2026 04:46:26 +0200 Subject: [PATCH 240/358] Only showed language button in script tab when script supports multiple languages --- .../noppes/npcs/client/gui/script/GuiScriptInterface.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index f4d9b2c13..f3173a2a2 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -271,10 +271,9 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); - // Language toggle button (only if handler supports Janino) - if (handler.supportsJanino() && container != null) { - String langLabel = container.isJanino() ? "Java" : "ECMAScript"; - this.addButton(new GuiNpcButton(113, left1, this.guiTop + 42 + yoffset, 121, 20, langLabel)); + // Language toggle button (only if handler supports Janino and there is more than one option) + if (handler.supportsJanino() && getLanguageOptions().size() > 1 && container != null) { + this.addButton(new GuiNpcButton(113, left1, this.guiTop + 42 + yoffset, 121, 20, container.getLanguage())); } this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); From 50e9f1b3e5a54f5d273c5ee4a06dfb7b61e258de Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 22 Jan 2026 20:05:07 +0200 Subject: [PATCH 241/358] HUGE IScriptHandlerPacket merge. All ScriptHandlers that send packets now directly define their data through the ScriptHandler class itself, NOT through their own GuiScript extended class. --- .../request/script/BlockScriptPacket.java | 4 + .../request/script/item/ItemScriptPacket.java | 4 + .../npcs/blocks/tiles/TileScripted.java | 40 +++++- .../java/noppes/npcs/client/ClientProxy.java | 25 +++- .../noppes/npcs/client/gui/GuiScript.java | 3 +- .../client/gui/script/GuiNPCEventScripts.java | 60 +-------- .../client/gui/script/GuiScriptAllNPCs.java | 63 ---------- .../client/gui/script/GuiScriptBlock.java | 39 ------ .../client/gui/script/GuiScriptForge.java | 118 ------------------ .../client/gui/script/GuiScriptGlobal.java | 9 +- .../client/gui/script/GuiScriptInterface.java | 15 ++- .../npcs/client/gui/script/GuiScriptItem.java | 74 ----------- .../client/gui/script/GuiScriptPlayers.java | 71 ----------- .../npcs/controllers/data/DataScript.java | 25 +++- .../controllers/data/ForgeDataScript.java | 107 +++++++++++++++- .../controllers/data/GlobalNPCDataScript.java | 25 +++- .../controllers/data/PlayerDataScript.java | 29 ++++- .../npcs/scripted/item/ScriptCustomItem.java | 55 +++++++- 18 files changed, 323 insertions(+), 443 deletions(-) delete mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java delete mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java delete mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java delete mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java delete mode 100644 src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/BlockScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/BlockScriptPacket.java index 0fd7de927..ce94c98f9 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/BlockScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/BlockScriptPacket.java @@ -84,6 +84,10 @@ public final class BlockScriptPacket extends AbstractPacket { NBTTagCompound compound = ((TileScripted) tile).getNBT(new NBTTagCompound()); compound.setTag("Languages", ScriptController.Instance.nbtLanguages()); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + + NBTTagCompound loadComplete = new NBTTagCompound(); + loadComplete.setInteger("LoadComplete", 1); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, loadComplete); } else { if (!player.capabilities.isCreativeMode) { return; diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptPacket.java index 478340a63..7d21242bd 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/item/ItemScriptPacket.java @@ -75,6 +75,10 @@ public final class ItemScriptPacket extends AbstractPacket { NBTTagCompound compound = iw.getMCNbt(); compound.setTag("Languages", ScriptController.Instance.nbtLanguages()); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + + NBTTagCompound loadComplete = new NBTTagCompound(); + loadComplete.setInteger("LoadComplete", 1); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, loadComplete); } else { if (!player.capabilities.isCreativeMode) { return; diff --git a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java index 969ea0bac..c26a2f20a 100644 --- a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java +++ b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java @@ -3,6 +3,7 @@ package noppes.npcs.blocks.tiles; import cpw.mods.fml.common.eventhandler.Event; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.network.packets.request.script.BlockScriptPacket; import net.minecraft.block.Block; import net.minecraft.client.renderer.texture.ITickable; import net.minecraft.item.Item; @@ -25,7 +26,9 @@ import noppes.npcs.api.IBlock; import noppes.npcs.api.block.ITextPlane; import noppes.npcs.client.renderer.blocks.BlockScriptedRenderer; import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptController; +import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.controllers.data.IScriptBlockHandler; import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.entity.data.DataTimers; @@ -39,7 +42,7 @@ import java.util.Map; import java.util.Map.Entry; import java.util.TreeMap; -public class TileScripted extends TileEntity implements IScriptBlockHandler { +public class TileScripted extends TileEntity implements IScriptBlockHandler, IScriptHandlerPacket { public List scripts = new ArrayList<>(); public Map tempData = new HashMap<>(); @@ -161,6 +164,41 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler { text6.setNBT(compound.getCompoundTag("Text6")); } + @Override + public ScriptContext getContext() { + return ScriptContext.BLOCK; + } + + @Override + public String getHookContext() { + return noppes.npcs.api.handler.IScriptHookHandler.CONTEXT_BLOCK; + } + + @Override + public void requestData() { + BlockScriptPacket.Get(this.xCoord, this.yCoord, this.zCoord); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + BlockScriptPacket.Save(this.xCoord, this.yCoord, this.zCoord, nbt); + } + + @Override + public GuiDataResult setGuiData(NBTTagCompound compound) { + if (compound.hasKey("LoadComplete")) { + return new GuiDataResult(GuiDataKind.LOAD_COMPLETE, -1); + } + + setNBT(compound); + return new GuiDataResult(GuiDataKind.METADATA, -1); + } + + @Override + public void sync() { + sendSavePacket(-1, getScripts().size(), getNBT(new NBTTagCompound())); + } + @Override public void writeToNBT(NBTTagCompound compound) { super.writeToNBT(compound); diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index d6169cbbd..249836b80 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -21,7 +21,9 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.Container; import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; import net.minecraft.stats.Achievement; +import net.minecraft.tileentity.TileEntity; import net.minecraft.util.ChatComponentText; import net.minecraft.util.ReportedException; import net.minecraft.util.ResourceLocation; @@ -120,9 +122,8 @@ import noppes.npcs.client.gui.roles.GuiNpcFollowerSetup; import noppes.npcs.client.gui.roles.GuiNpcItemGiver; import noppes.npcs.client.gui.roles.GuiNpcTraderSetup; import noppes.npcs.client.gui.roles.GuiNpcTransporter; -import noppes.npcs.client.gui.script.GuiScriptBlock; import noppes.npcs.client.gui.script.GuiScriptGlobal; -import noppes.npcs.client.gui.script.GuiScriptItem; +import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.script.PackageFinder; import noppes.npcs.client.model.ModelNPCGolem; import noppes.npcs.client.model.ModelNpcCrystal; @@ -138,6 +139,7 @@ import noppes.npcs.client.renderer.RenderNpcDragon; import noppes.npcs.client.renderer.RenderNpcSlime; import noppes.npcs.client.renderer.RenderProjectile; import kamkeel.npcs.client.renderer.TelegraphRenderer; +import noppes.npcs.scripted.item.ScriptCustomItem; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; import noppes.npcs.client.renderer.blocks.BlockBannerRenderer; import noppes.npcs.client.renderer.blocks.BlockBarrelRenderer; @@ -492,7 +494,7 @@ public class ClientProxy extends CommonProxy { return new GuiScript(npc); else if (gui == EnumGuiType.ScriptItem) - return new GuiScriptItem(); + return GuiScriptInterface.create(null, new ScriptCustomItem(new ItemStack(CustomItems.scripted_item))); else if (gui == EnumGuiType.PlayerCarpentryBench) return new GuiNpcCarpentryBench((ContainerCarpentryBench) container); @@ -567,7 +569,7 @@ public class ClientProxy extends CommonProxy { return new GuiCustom((ContainerCustomGui) container); else if (gui == EnumGuiType.ScriptBlock) - return new GuiScriptBlock(x, y, z); + return getScriptBlockGui(x, y, z); else if (gui == EnumGuiType.Paintbrush) return new GuiNpcPaintbrush(); @@ -619,9 +621,22 @@ public class ClientProxy extends CommonProxy { if (guiscreen != null) { minecraft.displayGuiScreen((GuiScreen) guiscreen); } - } + + private GuiScreen getScriptBlockGui(int x, int y, int z) { + World world = Minecraft.getMinecraft().theWorld; + if (world == null) + return null; + + TileEntity tile = world.getTileEntity(x, y, z); + if (!(tile instanceof TileScripted)) + return null; + + return GuiScriptInterface.create(null, (TileScripted) tile); + } + + @Override public void spawnParticle(EntityLivingBase player, String string, Object... ob) { if (string.equals("Spell")) { diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index 2b6f6d7c8..b98e94ac4 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -10,7 +10,6 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.util.StatCollector; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.NoppesUtil; -import noppes.npcs.client.gui.script.GuiNPCEventScripts; import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.*; import noppes.npcs.constants.ScriptContext; @@ -270,7 +269,7 @@ public class GuiScript extends GuiScriptInterface { } if (guibutton.id == 16) { close(); - mc.displayGuiScreen(new GuiNPCEventScripts(npc)); + GuiScriptInterface.open(this, new DataScript(npc)); } if (guibutton.id == 17) { close(); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java index e0ad70596..cff2c111e 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java @@ -1,65 +1,13 @@ package noppes.npcs.client.gui.script; -import kamkeel.npcs.network.packets.request.script.EventScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; +import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.controllers.data.DataScript; -import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.entity.EntityNPCInterface; -import java.util.ArrayList; -import java.util.List; - public class GuiNPCEventScripts extends GuiScriptInterface { - private final DataScript script; - public GuiNPCEventScripts(EntityNPCInterface npc) { - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_NPC)); - this.script = new DataScript(npc); - this.handler = this.script; - EventScriptPacket.Get(); - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - script.setLanguage(compound.getString("ScriptLanguage")); - script.setEnabled(compound.getBoolean("ScriptEnabled")); - super.setGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); - IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, script); - if (script.getScripts().isEmpty()) { - for (int i = 0; i < compound.getInteger("TotalScripts"); i++) { - script.getScripts().add(new ScriptContainer(script)); - } - } - script.getScripts().set(tab, container); - initGui(); - } - } - - public void save() { - if (loaded) { - super.save(); - List containers = this.script.getScripts(); - for (int i = 0; i < containers.size(); i++) { - IScriptUnit container = containers.get(i); - EventScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.script.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.script.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.script.getConsoleText())); - EventScriptPacket.Save(-1, containers.size(), scriptData); - } + this.handler = new DataScript(npc); + if (this.handler instanceof IScriptHandlerPacket) + ((IScriptHandlerPacket) this.handler).requestData(); } } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java deleted file mode 100644 index 71bc73d7d..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptAllNPCs.java +++ /dev/null @@ -1,63 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.GlobalNPCScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.GlobalNPCDataScript; -import noppes.npcs.controllers.data.IScriptUnit; - -import java.util.ArrayList; -import java.util.List; - -public class GuiScriptAllNPCs extends GuiScriptInterface { - private final GlobalNPCDataScript script = new GlobalNPCDataScript(null); - - public GuiScriptAllNPCs() { - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_NPC)); - this.handler = this.script; - GlobalNPCScriptPacket.Get(); - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - script.setLanguage(compound.getString("ScriptLanguage")); - script.setEnabled(compound.getBoolean("ScriptEnabled")); - super.setGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); - IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, script); - if (script.getScripts().isEmpty()) { - for (int i = 0; i < compound.getInteger("TotalScripts"); i++) { - script.getScripts().add(new ScriptContainer(script)); - } - } - script.getScripts().set(tab, container); - initGui(); - } - } - - public void save() { - if (loaded) { - super.save(); - List containers = this.script.getScripts(); - for (int i = 0; i < containers.size(); i++) { - IScriptUnit container = containers.get(i); - GlobalNPCScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.script.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.script.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.script.getConsoleText())); - GlobalNPCScriptPacket.Save(-1, containers.size(), scriptData); - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java deleted file mode 100644 index b11947ee7..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptBlock.java +++ /dev/null @@ -1,39 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.BlockScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.blocks.tiles.TileScripted; -import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.ScriptHookController; - -import java.util.ArrayList; - -public class GuiScriptBlock extends GuiScriptInterface { - private final TileScripted tileScripted; - - public GuiScriptBlock(int x, int y, int z) { - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_BLOCK)); - - this.handler = this.tileScripted = (TileScripted) player.worldObj.getTileEntity(x, y, z); - BlockScriptPacket.Get(tileScripted.xCoord, tileScripted.yCoord, tileScripted.zCoord); - } - - public void setGuiData(NBTTagCompound compound) { - this.tileScripted.setNBT(compound); - super.setGuiData(compound); - loaded = true; - } - - @Override - protected ScriptContext getScriptContext() { - return ScriptContext.BLOCK; - } - - public void save() { - if (loaded) { - super.save(); - BlockScriptPacket.Save(tileScripted.xCoord, tileScripted.yCoord, tileScripted.zCoord, this.tileScripted.getNBT(new NBTTagCompound())); - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java deleted file mode 100644 index 59333eee4..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptForge.java +++ /dev/null @@ -1,118 +0,0 @@ -package noppes.npcs.client.gui.script; - -import com.google.common.reflect.ClassPath; -import cpw.mods.fml.common.eventhandler.Event; -import cpw.mods.fml.common.gameevent.TickEvent; -import cpw.mods.fml.common.network.FMLNetworkEvent; -import kamkeel.npcs.network.packets.request.script.ForgeScriptPacket; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraftforge.event.entity.EntityEvent; -import net.minecraftforge.event.entity.player.ItemTooltipEvent; -import net.minecraftforge.event.world.ChunkDataEvent; -import net.minecraftforge.event.world.ChunkEvent; -import net.minecraftforge.event.world.ChunkWatchEvent; -import net.minecraftforge.event.world.WorldEvent; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.ForgeDataScript; -import noppes.npcs.controllers.data.IScriptUnit; -import org.apache.commons.lang3.StringUtils; - -import java.io.IOException; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; - -public class GuiScriptForge extends GuiScriptInterface { - private final ForgeDataScript script = new ForgeDataScript(); - - public GuiScriptForge() { - // Add built-in "init" hook from controller - hookList.addAll(ScriptHookController.Instance.getBuiltInHooks(IScriptHookHandler.CONTEXT_FORGE)); - - // Dynamically discover Forge event hooks - ArrayList list = new ArrayList(); - try { - list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("cpw.mods.fml.common.gameevent")); - list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("net.minecraftforge.event")); - list.removeAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("net.minecraftforge.event.terraingen")); - } catch (IOException e) { - e.printStackTrace(); - } - - for (ClassPath.ClassInfo e1 : list) { - ClassPath.ClassInfo classLoader = e1; - Class infoClass = classLoader.load(); - ArrayList c = new ArrayList(Arrays.asList(infoClass.getDeclaredClasses())); - if (c.isEmpty()) { - c.add(infoClass); - } - Iterator var10 = c.iterator(); - while (var10.hasNext()) { - Class c1 = (Class) var10.next(); - if (!EntityEvent.EntityConstructing.class.isAssignableFrom(c1) && !WorldEvent.PotentialSpawns.class.isAssignableFrom(c1) && !TickEvent.RenderTickEvent.class.isAssignableFrom(c1) && !TickEvent.ClientTickEvent.class.isAssignableFrom(c1) && !FMLNetworkEvent.ClientCustomPacketEvent.class.isAssignableFrom(c1) && !ItemTooltipEvent.class.isAssignableFrom(c1) && Event.class.isAssignableFrom(c1) && !Modifier.isAbstract(c1.getModifiers()) && Modifier.isPublic(c1.getModifiers()) && !ChunkEvent.class.isAssignableFrom(c1) && !ChunkWatchEvent.class.isAssignableFrom(c1) && !ChunkDataEvent.class.isAssignableFrom(c1)) { - String eventName = c1.getName(); - int i = eventName.lastIndexOf("."); - eventName = StringUtils.uncapitalize(eventName.substring(i + 1).replace("$", "")); - - hookList.add(eventName); - } - } - } - hookList.add("onCNPCNaturalSpawn"); - - // Add any addon-registered hooks for the forge context - for (String addonHook : ScriptHookController.Instance.getAddonHooks(IScriptHookHandler.CONTEXT_FORGE)) { - if (!hookList.contains(addonHook)) { - hookList.add(addonHook); - } - } - - this.handler = this.script; - ForgeScriptPacket.Get(); - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - script.setLanguage(compound.getString("ScriptLanguage")); - script.setEnabled(compound.getBoolean("ScriptEnabled")); - super.setGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); - IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, script); - if (script.getScripts().isEmpty()) { - for (int i = 0; i < compound.getInteger("TotalScripts"); i++) { - script.getScripts().add(new ScriptContainer(script)); - } - } - script.getScripts().set(tab, container); - initGui(); - } - } - - public void save() { - if (loaded) { - super.save(); - List containers = this.script.getScripts(); - for (int i = 0; i < containers.size(); i++) { - IScriptUnit container = containers.get(i); - ForgeScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.script.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.script.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.script.getConsoleText())); - ForgeScriptPacket.Save(-1, containers.size(), scriptData); - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptGlobal.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptGlobal.java index 15aec96dd..52ab86b24 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptGlobal.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptGlobal.java @@ -8,6 +8,9 @@ import net.minecraft.util.ResourceLocation; import noppes.npcs.client.gui.util.GuiNPCInterface; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.controllers.data.ForgeDataScript; +import noppes.npcs.controllers.data.GlobalNPCDataScript; +import noppes.npcs.controllers.data.PlayerDataScript; import org.lwjgl.opengl.GL11; import java.util.HashMap; @@ -65,13 +68,13 @@ public class GuiScriptGlobal extends GuiNPCInterface implements IGuiData { protected void actionPerformed(GuiButton guibutton) { if (guibutton.id == 0) { - this.displayGuiScreen(new GuiScriptPlayers()); + GuiScriptInterface.open(this, new PlayerDataScript(null)); } if (guibutton.id == 1) { - this.displayGuiScreen(new GuiScriptForge()); + GuiScriptInterface.open(this, new ForgeDataScript()); } if (guibutton.id == 2) { - this.displayGuiScreen(new GuiScriptAllNPCs()); + GuiScriptInterface.open(this, new GlobalNPCDataScript(null)); } if (guibutton.id > 2) { try { diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index f3173a2a2..5c3ee2596 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -88,10 +88,10 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } /** - * Create a GuiScriptInterface for a single script without a handler. - * Wraps the script in a SingleScriptHandler automatically. + * Create a GuiScriptInterface for a handler without displaying it. + * Useful when a caller needs to return a GuiScreen (e.g. ClientProxy). */ - public static GuiScriptInterface open(GuiScreen parent, IScriptHandler handler) { + public static GuiScriptInterface create(GuiScreen parent, IScriptHandler handler) { GuiScriptInterface gui = new GuiScriptInterface(); gui.handler = handler; gui.parent = parent; @@ -103,6 +103,15 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb if (handler instanceof IScriptHandlerPacket) ((IScriptHandlerPacket) handler).requestData(); + return gui; + } + + /** + * Open a GuiScriptInterface for a handler. + * Wraps the script in a SingleScriptHandler automatically. + */ + public static GuiScriptInterface open(GuiScreen parent, IScriptHandler handler) { + GuiScriptInterface gui = create(parent, handler); Minecraft.getMinecraft().displayGuiScreen(gui); return gui; } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java deleted file mode 100644 index fceaeb6ed..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptItem.java +++ /dev/null @@ -1,74 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.item.ItemScriptErrorPacket; -import kamkeel.npcs.network.packets.request.script.item.ItemScriptPacket; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.CustomItems; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.scripted.item.ScriptCustomItem; - -import java.util.*; - -public class GuiScriptItem extends GuiScriptInterface { - private ScriptCustomItem item; - public static Map consoleText = new HashMap<>(); - - public GuiScriptItem() { - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_ITEM)); - - this.handler = this.item = new ScriptCustomItem(new ItemStack(CustomItems.scripted_item)); - ItemScriptErrorPacket.Get(); - ItemScriptPacket.Get(); - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("ItemScriptConsole")) { - consoleText = NBTTags.GetLongStringMap(compound.getTagList("ItemScriptConsole", 10)); - initGui(); - } else { - this.item.setMCNbt(compound); - this.item.loadScriptData(); - super.setGuiData(compound); - loaded = true; - } - } - - @Override - protected ScriptContext getScriptContext() { - return ScriptContext.ITEM; - } - - public void save() { - if (loaded) { - super.save(); - ItemScriptPacket.Save(this.item.getMCNbt()); - } - } - - protected String getConsoleText() { - Map map = consoleText; - StringBuilder builder = new StringBuilder(); - Iterator var3 = map.entrySet().iterator(); - - while (var3.hasNext()) { - Map.Entry entry = (Map.Entry) var3.next(); - builder.insert(0, new Date((Long) entry.getKey()) + (String) entry.getValue() + "\n"); - } - - return builder.toString(); - } - - public void confirmClicked(boolean flag, int i) { - if (i == 102) { - if (activeTab <= 0) { - consoleText = new HashMap<>(); - ItemScriptErrorPacket.Clear(); - } - } - super.confirmClicked(flag, i); - } -} diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java deleted file mode 100644 index de884b356..000000000 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptPlayers.java +++ /dev/null @@ -1,71 +0,0 @@ -package noppes.npcs.client.gui.script; - -import kamkeel.npcs.network.packets.request.script.PlayerScriptPacket; -import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.ScriptContainer; -import noppes.npcs.controllers.ScriptHookController; -import noppes.npcs.controllers.data.IScriptUnit; -import noppes.npcs.controllers.data.PlayerDataScript; - -import java.util.ArrayList; -import java.util.List; - -public class GuiScriptPlayers extends GuiScriptInterface { - private PlayerDataScript script = new PlayerDataScript((EntityPlayer) null); - - public GuiScriptPlayers() { - this.hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(IScriptHookHandler.CONTEXT_PLAYER)); - - this.handler = this.script; - PlayerScriptPacket.Get(); - } - - public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("LoadComplete")) { - loaded = true; - return; - } - - if (!compound.hasKey("Tab")) { - script.setLanguage(compound.getString("ScriptLanguage")); - script.setEnabled(compound.getBoolean("ScriptEnabled")); - super.setGuiData(compound); - } else { - int tab = compound.getInteger("Tab"); - NBTTagCompound scriptCompound = compound.getCompoundTag("Script"); - IScriptUnit container = IScriptUnit.createFromNBT(scriptCompound, script); - if (script.getScripts().isEmpty()) { - for (int i = 0; i < compound.getInteger("TotalScripts"); i++) { - script.getScripts().add(new ScriptContainer(script)); - } - } - script.getScripts().set(tab, container); - initGui(); - } - } - - @Override - protected ScriptContext getScriptContext() { - return ScriptContext.PLAYER; - } - - public void save() { - if (loaded) { - super.save(); - List containers = this.script.getScripts(); - for (int i = 0; i < containers.size(); i++) { - IScriptUnit container = containers.get(i); - PlayerScriptPacket.Save(i, containers.size(), container.writeToNBT(new NBTTagCompound())); - } - NBTTagCompound scriptData = new NBTTagCompound(); - scriptData.setString("ScriptLanguage", this.script.getLanguage()); - scriptData.setBoolean("ScriptEnabled", this.script.getEnabled()); - scriptData.setTag("ScriptConsole", NBTTags.NBTLongStringMap(this.script.getConsoleText())); - PlayerScriptPacket.Save(-1, containers.size(), scriptData); - } - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index cd6f4bb54..37f58453d 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -3,6 +3,7 @@ package noppes.npcs.controllers.data; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.eventhandler.Event; import cpw.mods.fml.relauncher.Side; +import kamkeel.npcs.network.packets.request.script.EventScriptPacket; import net.minecraft.entity.Entity; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -12,10 +13,12 @@ import noppes.npcs.EventHooks; import noppes.npcs.LogWriter; import noppes.npcs.NBTTags; import noppes.npcs.api.IWorld; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.config.ConfigDebug; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.entity.EntityNPCInterface; @@ -36,7 +39,7 @@ import java.util.List; import java.util.Map; import java.util.TreeMap; -public class DataScript implements IScriptHandler { +public class DataScript implements IScriptHandlerPacket { public List eventScripts = new ArrayList<>(); private HashMap scripts = new HashMap<>(); @@ -210,6 +213,26 @@ public class DataScript implements IScriptHandler { return enabled && ScriptController.HasStart && !npc.worldObj.isRemote && !scripts.isEmpty() && ConfigScript.ScriptingEnabled; } + @Override + public ScriptContext getContext() { + return ScriptContext.NPC; + } + + @Override + public String getHookContext() { + return IScriptHookHandler.CONTEXT_NPC; + } + + @Override + public void requestData() { + EventScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + EventScriptPacket.Save(index, totalCount, nbt); + } + public Map getOldConsoleText() { Map map = new TreeMap<>(); diff --git a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java index c7f345d73..0ecdf27c0 100644 --- a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java @@ -1,17 +1,37 @@ package noppes.npcs.controllers.data; +import com.google.common.reflect.ClassPath; import cpw.mods.fml.common.eventhandler.Event; +import cpw.mods.fml.common.gameevent.TickEvent; +import cpw.mods.fml.common.network.FMLNetworkEvent; +import kamkeel.npcs.network.packets.request.script.ForgeScriptPacket; import net.minecraft.nbt.NBTTagCompound; +import net.minecraftforge.event.entity.EntityEvent; +import net.minecraftforge.event.entity.player.ItemTooltipEvent; +import net.minecraftforge.event.world.ChunkDataEvent; +import net.minecraftforge.event.world.ChunkEvent; +import net.minecraftforge.event.world.ChunkWatchEvent; +import net.minecraftforge.event.world.WorldEvent; import noppes.npcs.EventHooks; import noppes.npcs.NBTTags; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.config.ConfigScript; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; +import noppes.npcs.controllers.ScriptHookController; +import org.apache.commons.lang3.StringUtils; +import java.io.IOException; +import java.lang.reflect.Modifier; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; import java.util.List; -public class ForgeDataScript implements IScriptHandler { +public class ForgeDataScript implements IScriptHandlerPacket { + private static final Object HOOK_LOCK = new Object(); + private static List cachedHooks; private List scripts = new ArrayList<>(); private String scriptLanguage = "ECMAScript"; public long lastInited = -1L; @@ -81,6 +101,91 @@ public class ForgeDataScript implements IScriptHandler { return this.enabled && ConfigScript.GlobalForgeScripts && ScriptController.HasStart && this.scripts.size() > 0; } + @Override + public ScriptContext getContext() { + return ScriptContext.FORGE; + } + + @Override + public String getHookContext() { + return IScriptHookHandler.CONTEXT_FORGE; + } + + @Override + public List getHooks() { + if (cachedHooks != null) + return new ArrayList<>(cachedHooks); + + synchronized (HOOK_LOCK) { + if (cachedHooks != null) + return new ArrayList<>(cachedHooks); + + List hookList = new ArrayList<>(ScriptHookController.Instance.getBuiltInHooks(IScriptHookHandler.CONTEXT_FORGE)); + + ArrayList list = new ArrayList(); + try { + list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("cpw.mods.fml.common.gameevent")); + list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("net.minecraftforge.event")); + list.removeAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("net.minecraftforge.event.terraingen")); + } catch (IOException e) { + e.printStackTrace(); + } + + for (ClassPath.ClassInfo e1 : list) { + ClassPath.ClassInfo classLoader = e1; + Class infoClass = classLoader.load(); + ArrayList c = new ArrayList(Arrays.asList(infoClass.getDeclaredClasses())); + if (c.isEmpty()) { + c.add(infoClass); + } + Iterator var10 = c.iterator(); + while (var10.hasNext()) { + Class c1 = (Class) var10.next(); + if (!EntityEvent.EntityConstructing.class.isAssignableFrom(c1) + && !WorldEvent.PotentialSpawns.class.isAssignableFrom(c1) + && !TickEvent.RenderTickEvent.class.isAssignableFrom(c1) + && !TickEvent.ClientTickEvent.class.isAssignableFrom(c1) + && !FMLNetworkEvent.ClientCustomPacketEvent.class.isAssignableFrom(c1) + && !ItemTooltipEvent.class.isAssignableFrom(c1) + && Event.class.isAssignableFrom(c1) + && !Modifier.isAbstract(c1.getModifiers()) + && Modifier.isPublic(c1.getModifiers()) + && !ChunkEvent.class.isAssignableFrom(c1) + && !ChunkWatchEvent.class.isAssignableFrom(c1) + && !ChunkDataEvent.class.isAssignableFrom(c1)) { + String eventName = c1.getName(); + int i = eventName.lastIndexOf("."); + eventName = StringUtils.uncapitalize(eventName.substring(i + 1).replace("$", "")); + + hookList.add(eventName); + } + } + } + + hookList.add("onCNPCNaturalSpawn"); + + for (String addonHook : ScriptHookController.Instance.getAddonHooks(IScriptHookHandler.CONTEXT_FORGE)) { + if (!hookList.contains(addonHook)) { + hookList.add(addonHook); + } + } + + cachedHooks = hookList; + } + + return new ArrayList<>(cachedHooks); + } + + @Override + public void requestData() { + ForgeScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + ForgeScriptPacket.Save(index, totalCount, nbt); + } + public boolean isClient() { return false; } diff --git a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java index f8580963b..5561c255c 100644 --- a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java @@ -2,11 +2,14 @@ package noppes.npcs.controllers.data; import com.google.common.base.Preconditions; import cpw.mods.fml.common.eventhandler.Event; +import kamkeel.npcs.network.packets.request.script.GlobalNPCScriptPacket; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.math.BlockPos; import noppes.npcs.NBTTags; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.config.ConfigScript; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.entity.EntityNPCInterface; @@ -25,7 +28,7 @@ import java.util.OptionalInt; import java.util.OptionalLong; import java.util.TreeMap; -public class GlobalNPCDataScript implements IScriptHandler { +public class GlobalNPCDataScript implements IScriptHandlerPacket { public List scripts = new ArrayList<>(); public String scriptLanguage = "ECMAScript"; private EntityNPCInterface npc; @@ -75,6 +78,26 @@ public class GlobalNPCDataScript implements IScriptHandler { return ConfigScript.GlobalNPCScripts && this.enabled && ScriptController.HasStart && this.scripts.size() > 0; } + @Override + public ScriptContext getContext() { + return ScriptContext.NPC; + } + + @Override + public String getHookContext() { + return IScriptHookHandler.CONTEXT_NPC; + } + + @Override + public void requestData() { + GlobalNPCScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + GlobalNPCScriptPacket.Save(index, totalCount, nbt); + } + @Override public void callScript(String hookName, Event event) { diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index 1fd06d927..3e794a37f 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -2,14 +2,17 @@ package noppes.npcs.controllers.data; import com.google.common.base.Preconditions; import cpw.mods.fml.common.eventhandler.Event; +import kamkeel.npcs.network.packets.request.script.PlayerScriptPacket; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.math.BlockPos; import noppes.npcs.EventHooks; import noppes.npcs.NBTTags; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.scripted.NpcAPI; @@ -30,7 +33,7 @@ import java.util.OptionalInt; import java.util.OptionalLong; import java.util.TreeMap; -public class PlayerDataScript implements IScriptHandler { +public class PlayerDataScript implements IScriptHandlerPacket { public List scripts = new ArrayList<>(); public String scriptLanguage = "ECMAScript"; private EntityPlayer player; @@ -89,6 +92,26 @@ public class PlayerDataScript implements IScriptHandler { return ConfigScript.GlobalPlayerScripts && ScriptController.Instance.playerScripts.enabled && ScriptController.HasStart && (this.player == null || !this.player.worldObj.isRemote); } + @Override + public ScriptContext getContext() { + return ScriptContext.PLAYER; + } + + @Override + public String getHookContext() { + return IScriptHookHandler.CONTEXT_PLAYER; + } + + @Override + public void requestData() { + PlayerScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + PlayerScriptPacket.Save(index, totalCount, nbt); + } + @Override public void callScript(String hookName, Event event) { @@ -200,7 +223,7 @@ public class PlayerDataScript implements IScriptHandler { if (ConfigScript.IndividualPlayerScripts) { return console; } - return IScriptHandler.super.getConsoleText(); + return IScriptHandlerPacket.super.getConsoleText(); } @Override @@ -209,7 +232,7 @@ public class PlayerDataScript implements IScriptHandler { console.clear(); return; } - IScriptHandler.super.clearConsole(); + IScriptHandlerPacket.super.clearConsole(); } public static final class ToStringHelper { diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java index 02f27b71c..2e17555d3 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java @@ -1,15 +1,20 @@ package noppes.npcs.scripted.item; import cpw.mods.fml.common.eventhandler.Event; +import kamkeel.npcs.network.packets.request.script.item.ItemScriptErrorPacket; +import kamkeel.npcs.network.packets.request.script.item.ItemScriptPacket; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.EventHooks; import noppes.npcs.NBTTags; import noppes.npcs.api.item.IItemCustom; +import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.IScriptHandler; +import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.scripted.CustomNPCsException; import java.util.ArrayList; @@ -18,13 +23,15 @@ import java.util.Map; import java.util.Objects; import java.util.TreeMap; -public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCustom, IScriptHandler { +public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCustom, IScriptHandlerPacket { public List scripts = new ArrayList(); public List errored = new ArrayList(); public String scriptLanguage = "ECMAScript"; public boolean enabled = false; public boolean loaded = false; + private Map clientConsoleText = new TreeMap<>(); + public double durabilityValue = 1.0D; public int stackSize = 64; @@ -60,6 +67,48 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus } } + @Override + public ScriptContext getContext() { + return ScriptContext.ITEM; + } + + @Override + public String getHookContext() { + return IScriptHookHandler.CONTEXT_ITEM; + } + + @Override + public void requestData() { + ItemScriptErrorPacket.Get(); + ItemScriptPacket.Get(); + } + + @Override + public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { + ItemScriptPacket.Save(nbt); + } + + @Override + public GuiDataResult setGuiData(NBTTagCompound compound) { + if (compound.hasKey("LoadComplete")) { + return new GuiDataResult(GuiDataKind.LOAD_COMPLETE, -1); + } + + if (compound.hasKey("ItemScriptConsole")) { + clientConsoleText = NBTTags.GetLongStringMap(compound.getTagList("ItemScriptConsole", 10)); + return new GuiDataResult(GuiDataKind.TAB, -1); + } + + setMCNbt(compound); + loadScriptData(); + return new GuiDataResult(GuiDataKind.METADATA, -1); + } + + @Override + public void sync() { + sendSavePacket(-1, getScripts().size(), getMCNbt()); + } + public int getType() { return 6; } @@ -142,10 +191,12 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus } public Map getConsoleText() { - return new TreeMap<>(); + return clientConsoleText; } public void clearConsole() { + clientConsoleText.clear(); + ItemScriptErrorPacket.Clear(); } public int getMaxStackSize() { From d835d2acb04a2c18975147ffcfcdcb1fb8442513 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 22 Jan 2026 20:48:17 +0200 Subject: [PATCH 242/358] Migrated all EffectScript.ScriptType hooks to EnumScriptType --- src/main/java/noppes/npcs/EventHooks.java | 6 +++--- .../noppes/npcs/constants/EnumScriptType.java | 4 ++++ .../npcs/controllers/ScriptHookController.java | 13 ++++++------- .../npcs/controllers/data/CustomEffect.java | 7 ++++--- .../npcs/controllers/data/EffectScript.java | 18 ------------------ 5 files changed, 17 insertions(+), 31 deletions(-) diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index a0c3d2b57..f72bbdab7 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -491,21 +491,21 @@ public class EventHooks { public static boolean onEffectAdded(IPlayer player, IPlayerEffect effect) { PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); PlayerEvent.EffectEvent.Added event = new PlayerEvent.EffectEvent.Added(player, effect); - handler.callScript(EffectScript.ScriptType.OnEffectAdd.function, event); + handler.callScript(EnumScriptType.ON_EFFECT_ADD.function, event); return NpcAPI.EVENT_BUS.post(event); } public static boolean onEffectTick(IPlayer player, IPlayerEffect effect) { PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); PlayerEvent.EffectEvent.Ticked event = new PlayerEvent.EffectEvent.Ticked(player, effect); - handler.callScript(EffectScript.ScriptType.OnEffectTick.function, event); + handler.callScript(EnumScriptType.ON_EFFECT_TICK.function, event); return NpcAPI.EVENT_BUS.post(event); } public static boolean onEffectRemove(IPlayer player, IPlayerEffect effect, EffectEvent.ExpirationType type) { PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); PlayerEvent.EffectEvent.Removed event = new PlayerEvent.EffectEvent.Removed(player, effect, type); - handler.callScript(EffectScript.ScriptType.OnEffectRemove.function, event); + handler.callScript(EnumScriptType.ON_EFFECT_REMOVE.function, event); return NpcAPI.EVENT_BUS.post(event); } diff --git a/src/main/java/noppes/npcs/constants/EnumScriptType.java b/src/main/java/noppes/npcs/constants/EnumScriptType.java index b1052c86c..99a8b4091 100644 --- a/src/main/java/noppes/npcs/constants/EnumScriptType.java +++ b/src/main/java/noppes/npcs/constants/EnumScriptType.java @@ -116,6 +116,10 @@ public enum EnumScriptType { PROFILE_REMOVE("profileRemove"), PROFILE_CREATE("profileCreate"), + ON_EFFECT_ADD("onEffectAdd"), + ON_EFFECT_TICK("onEffectTick"), + ON_EFFECT_REMOVE("onEffectRemove"), + ABILITY_START("abilityStart"), ABILITY_EXECUTE("abilityExecute"), ABILITY_INTERRUPT("abilityInterrupt"), diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index 0a64be83c..b599250a8 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -2,7 +2,6 @@ package noppes.npcs.controllers; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.controllers.data.EffectScript; import noppes.npcs.controllers.data.RecipeScript; import java.util.ArrayList; @@ -145,9 +144,9 @@ public class ScriptHookController implements IScriptHookHandler { EnumScriptType.PROFILE_CHANGE.function, EnumScriptType.PROFILE_REMOVE.function, EnumScriptType.PROFILE_CREATE.function, - EffectScript.ScriptType.OnEffectAdd.function, - EffectScript.ScriptType.OnEffectTick.function, - EffectScript.ScriptType.OnEffectRemove.function + EnumScriptType.ON_EFFECT_ADD.function, + EnumScriptType.ON_EFFECT_TICK.function, + EnumScriptType.ON_EFFECT_REMOVE.function ); // Block hooks @@ -209,9 +208,9 @@ public class ScriptHookController implements IScriptHookHandler { // Effect hooks registerBuiltIn(CONTEXT_EFFECT, - EffectScript.ScriptType.OnEffectAdd.function, - EffectScript.ScriptType.OnEffectTick.function, - EffectScript.ScriptType.OnEffectRemove.function + EnumScriptType.ON_EFFECT_ADD.function, + EnumScriptType.ON_EFFECT_TICK.function, + EnumScriptType.ON_EFFECT_REMOVE.function ); // Forge hooks - just init, the rest are dynamically discovered diff --git a/src/main/java/noppes/npcs/controllers/data/CustomEffect.java b/src/main/java/noppes/npcs/controllers/data/CustomEffect.java index 3834bef08..9b621234d 100644 --- a/src/main/java/noppes/npcs/controllers/data/CustomEffect.java +++ b/src/main/java/noppes/npcs/controllers/data/CustomEffect.java @@ -7,6 +7,7 @@ import noppes.npcs.EventHooks; import noppes.npcs.NoppesUtilServer; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.api.handler.data.ICustomEffect; +import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.CustomEffectController; import noppes.npcs.scripted.event.player.PlayerEvent; @@ -193,7 +194,7 @@ public class CustomEffect implements ICustomEffect { return; } - script.callScript(EffectScript.ScriptType.OnEffectAdd, event); + script.callScript(EnumScriptType.ON_EFFECT_ADD, event); } EventHooks.onEffectAdded(iPlayer, playerEffect); @@ -213,7 +214,7 @@ public class CustomEffect implements ICustomEffect { return; } - script.callScript(EffectScript.ScriptType.OnEffectTick, event); + script.callScript(EnumScriptType.ON_EFFECT_TICK, event); } EventHooks.onEffectTick(iPlayer, playerEffect); @@ -234,7 +235,7 @@ public class CustomEffect implements ICustomEffect { return; } - script.callScript(EffectScript.ScriptType.OnEffectRemove, event); + script.callScript(EnumScriptType.ON_EFFECT_REMOVE, event); } EventHooks.onEffectRemove(iPlayer, playerEffect, type); diff --git a/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/src/main/java/noppes/npcs/controllers/data/EffectScript.java index 8169dbbd7..b2ae04b98 100644 --- a/src/main/java/noppes/npcs/controllers/data/EffectScript.java +++ b/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -3,7 +3,6 @@ package noppes.npcs.controllers.data; import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.api.handler.IScriptHookHandler; -import noppes.npcs.scripted.event.player.PlayerEvent; /** * Script handler for CustomEffect scripts. @@ -33,12 +32,6 @@ public class EffectScript extends SingleScriptHandler implements IScriptHandlerP return IScriptHookHandler.CONTEXT_EFFECT; } - /** - * Convenience method to call a script with a typed event. - */ - public void callScript(ScriptType type, PlayerEvent.EffectEvent event) { - callScript(type.function, event); - } @Override public void requestData() { @@ -52,15 +45,4 @@ public class EffectScript extends SingleScriptHandler implements IScriptHandlerP EffectScriptPacket.Save(effectId, index, totalCount, nbt); } - public enum ScriptType { - OnEffectAdd("onEffectAdd"), - OnEffectTick("onEffectTick"), - OnEffectRemove("onEffectRemove"); - - public final String function; - - ScriptType(String functionName) { - this.function = functionName; - } - } } From 7050e9b7d4dbb541fec634a41b46cdb14c940ba3 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 22 Jan 2026 20:51:46 +0200 Subject: [PATCH 243/358] Implemented Janino scripting for player events. via JaninoPlayerScript --- .../controllers/data/PlayerDataScript.java | 6 + .../npcs/janino/impl/JaninoPlayerScript.java | 269 ++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index 3e794a37f..c329c4c6e 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -15,6 +15,7 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; +import noppes.npcs.janino.impl.JaninoPlayerScript; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.player.PlayerEvent; @@ -427,4 +428,9 @@ public class PlayerDataScript implements IScriptHandlerPacket { public static PlayerDataScript.ToStringHelper toStringHelper(Object self) { return new PlayerDataScript.ToStringHelper(self.getClass().getSimpleName()); } + + @Override + public IScriptUnit createJaninoScriptUnit() { + return new JaninoPlayerScript(); + } } diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java new file mode 100644 index 000000000..e5f61cdc9 --- /dev/null +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -0,0 +1,269 @@ +package noppes.npcs.janino.impl; + +import noppes.npcs.api.event.IAnimationEvent; +import noppes.npcs.api.event.ICustomGuiEvent; +import noppes.npcs.api.event.ICustomNPCsEvent; +import noppes.npcs.api.event.IDialogEvent; +import noppes.npcs.api.event.IFactionEvent; +import noppes.npcs.api.event.IPartyEvent; +import noppes.npcs.api.event.IPlayerEvent; +import noppes.npcs.api.event.IQuestEvent; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.janino.JaninoScript; +import noppes.npcs.janino.annotations.ParamName; +import noppes.npcs.janino.annotations.ScriptHook; + +/** + * Janino (Java) script implementation for player events. + */ +public class JaninoPlayerScript extends JaninoScript { + + /** Default imports available to player scripts without explicit import statements. */ + public static final String[] DEFAULT_IMPORTS = { + "noppes.npcs.api.*", + "noppes.npcs.api.entity.*", + "noppes.npcs.api.event.*", + "noppes.npcs.api.handler.*", + "noppes.npcs.api.handler.data.*", + "noppes.npcs.api.item.*", + "noppes.npcs.api.gui.*" + }; + + public JaninoPlayerScript() { + super(Functions.class, DEFAULT_IMPORTS); + } + + /** + * Interface defining the hook methods available for player scripts. + * Each method corresponds to a player hook from ScriptHookController CONTEXT_PLAYER. + */ + public interface Functions { + // Core lifecycle + @ScriptHook(EnumScriptType.INIT) + default void init(@ParamName("event") IPlayerEvent.InitEvent event) {} + + @ScriptHook(EnumScriptType.TICK) + default void tick(@ParamName("event") IPlayerEvent.UpdateEvent event) {} + + // Interaction + @ScriptHook(EnumScriptType.INTERACT) + default void interact(@ParamName("event") IPlayerEvent.InteractEvent event) {} + + @ScriptHook(EnumScriptType.RIGHT_CLICK) + default void rightClick(@ParamName("event") IPlayerEvent.RightClickEvent event) {} + + // Combat + @ScriptHook(EnumScriptType.ATTACK) + default void attack(@ParamName("event") IPlayerEvent.AttackEvent event) {} + + @ScriptHook(EnumScriptType.ATTACKED) + default void attacked(@ParamName("event") IPlayerEvent.AttackedEvent event) {} + + @ScriptHook(EnumScriptType.DAMAGED_ENTITY) + default void damagedEntity(@ParamName("event") IPlayerEvent.DamagedEntityEvent event) {} + + @ScriptHook(EnumScriptType.DAMAGED) + default void damaged(@ParamName("event") IPlayerEvent.DamagedEvent event) {} + + @ScriptHook(EnumScriptType.KILLS) + default void kills(@ParamName("event") IPlayerEvent.KilledEntityEvent event) {} + + @ScriptHook(EnumScriptType.KILLED) + default void killed(@ParamName("event") IPlayerEvent.DiedEvent event) {} + + // Inventory and drops + @ScriptHook(EnumScriptType.DROP) + default void drop(@ParamName("event") IPlayerEvent.DropEvent event) {} + + @ScriptHook(EnumScriptType.RESPAWN) + default void respawn(@ParamName("event") IPlayerEvent.RespawnEvent event) {} + + @ScriptHook(EnumScriptType.BREAK_BLOCK) + default void breakBlock(@ParamName("event") IPlayerEvent.BreakEvent event) {} + + // Communication + @ScriptHook(EnumScriptType.CHAT) + default void chat(@ParamName("event") IPlayerEvent.ChatEvent event) {} + + @ScriptHook(EnumScriptType.LOGIN) + default void login(@ParamName("event") IPlayerEvent.LoginEvent event) {} + + @ScriptHook(EnumScriptType.LOGOUT) + default void logout(@ParamName("event") IPlayerEvent.LogoutEvent event) {} + + @ScriptHook(EnumScriptType.KEY_PRESSED) + default void keyPressed(@ParamName("event") IPlayerEvent.KeyPressedEvent event) {} + + @ScriptHook(EnumScriptType.MOUSE_CLICKED) + default void mouseClicked(@ParamName("event") IPlayerEvent.MouseClickedEvent event) {} + + // Item interactions + @ScriptHook(EnumScriptType.TOSS) + default void toss(@ParamName("event") IPlayerEvent.TossEvent event) {} + + @ScriptHook(EnumScriptType.PICKUP) + default void pickUp(@ParamName("event") IPlayerEvent.PickUpEvent event) {} + + @ScriptHook(EnumScriptType.PICKUP_XP) + default void pickupXP(@ParamName("event") IPlayerEvent.PickupXPEvent event) {} + + @ScriptHook(EnumScriptType.RANGED_CHARGE) + default void rangedCharge(@ParamName("event") IPlayerEvent.RangedChargeEvent event) {} + + @ScriptHook(EnumScriptType.RANGED_LAUNCHED) + default void rangedLaunched(@ParamName("event") IPlayerEvent.RangedLaunchedEvent event) {} + + @ScriptHook(EnumScriptType.TIMER) + default void timer(@ParamName("event") IPlayerEvent.TimerEvent event) {} + + // Item usage + @ScriptHook(EnumScriptType.START_USING_ITEM) + default void startItem(@ParamName("event") IPlayerEvent.StartUsingItem event) {} + + @ScriptHook(EnumScriptType.USING_ITEM) + default void usingItem(@ParamName("event") IPlayerEvent.UsingItem event) {} + + @ScriptHook(EnumScriptType.STOP_USING_ITEM) + default void stopItem(@ParamName("event") IPlayerEvent.StopUsingItem event) {} + + @ScriptHook(EnumScriptType.FINISH_USING_ITEM) + default void finishItem(@ParamName("event") IPlayerEvent.FinishUsingItem event) {} + + // Containers and world interaction + @ScriptHook(EnumScriptType.CONTAINER_OPEN) + default void containerOpen(@ParamName("event") IPlayerEvent.ContainerOpen event) {} + + @ScriptHook(EnumScriptType.USE_HOE) + default void useHoe(@ParamName("event") IPlayerEvent.UseHoeEvent event) {} + + @ScriptHook(EnumScriptType.BONEMEAL) + default void bonemeal(@ParamName("event") IPlayerEvent.BonemealEvent event) {} + + @ScriptHook(EnumScriptType.FILL_BUCKET) + default void fillBucket(@ParamName("event") IPlayerEvent.FillBucketEvent event) {} + + // Movement and status + @ScriptHook(EnumScriptType.JUMP) + default void jump(@ParamName("event") IPlayerEvent.JumpEvent event) {} + + @ScriptHook(EnumScriptType.FALL) + default void fall(@ParamName("event") IPlayerEvent.FallEvent event) {} + + @ScriptHook(EnumScriptType.WAKE_UP) + default void wakeUp(@ParamName("event") IPlayerEvent.WakeUpEvent event) {} + + @ScriptHook(EnumScriptType.SLEEP) + default void sleep(@ParamName("event") IPlayerEvent.SleepEvent event) {} + + @ScriptHook(EnumScriptType.PLAYSOUND) + default void playSound(@ParamName("event") IPlayerEvent.SoundEvent event) {} + + @ScriptHook(EnumScriptType.LIGHTNING) + default void lightning(@ParamName("event") IPlayerEvent.LightningEvent event) {} + + @ScriptHook(EnumScriptType.CHANGED_DIM) + default void changedDim(@ParamName("event") IPlayerEvent.ChangedDimension event) {} + + // Quest + @ScriptHook(EnumScriptType.QUEST_START) + default void questStart(@ParamName("event") IQuestEvent.QuestStartEvent event) {} + + @ScriptHook(EnumScriptType.QUEST_COMPLETED) + default void questCompleted(@ParamName("event") IQuestEvent.QuestCompletedEvent event) {} + + @ScriptHook(EnumScriptType.QUEST_TURNIN) + default void questTurnIn(@ParamName("event") IQuestEvent.QuestTurnedInEvent event) {} + + // Faction + @ScriptHook(EnumScriptType.FACTION_POINTS) + default void factionPoints(@ParamName("event") IFactionEvent.FactionPoints event) {} + + // Dialog + @ScriptHook(EnumScriptType.DIALOG_OPEN) + default void dialogOpen(@ParamName("event") IDialogEvent.DialogOpen event) {} + + @ScriptHook(EnumScriptType.DIALOG_OPTION) + default void dialogOption(@ParamName("event") IDialogEvent.DialogOption event) {} + + @ScriptHook(EnumScriptType.DIALOG_CLOSE) + default void dialogClose(@ParamName("event") IDialogEvent.DialogClosed event) {} + + // Commands + @ScriptHook(EnumScriptType.SCRIPT_COMMAND) + default void scriptCommand(@ParamName("event") ICustomNPCsEvent.ScriptedCommandEvent event) {} + + // Custom GUI + @ScriptHook(EnumScriptType.CUSTOM_GUI_CLOSED) + default void customGuiClosed(@ParamName("event") ICustomGuiEvent.CloseEvent event) {} + + @ScriptHook(EnumScriptType.CUSTOM_GUI_BUTTON) + default void customGuiButton(@ParamName("event") ICustomGuiEvent.ButtonEvent event) {} + + @ScriptHook(EnumScriptType.CUSTOM_GUI_SLOT) + default void customGuiSlot(@ParamName("event") ICustomGuiEvent.SlotEvent event) {} + + @ScriptHook(EnumScriptType.CUSTOM_GUI_SLOT_CLICKED) + default void customGuiSlotClicked(@ParamName("event") ICustomGuiEvent.SlotClickEvent event) {} + + @ScriptHook(EnumScriptType.CUSTOM_GUI_SCROLL) + default void customGuiScroll(@ParamName("event") ICustomGuiEvent.ScrollEvent event) {} + + @ScriptHook(EnumScriptType.CUSTOM_GUI_TEXTFIELD) + default void customGuiTextfield(@ParamName("event") ICustomGuiEvent.UnfocusedEvent event) {} + + // Party + @ScriptHook(EnumScriptType.PARTY_QUEST_COMPLETED) + default void partyQuestCompleted(@ParamName("event") IPartyEvent.PartyQuestCompletedEvent event) {} + + @ScriptHook(EnumScriptType.PARTY_QUEST_SET) + default void partyQuestSet(@ParamName("event") IPartyEvent.PartyQuestSetEvent event) {} + + @ScriptHook(EnumScriptType.PARTY_QUEST_TURNED_IN) + default void partyQuestTurnedIn(@ParamName("event") IPartyEvent.PartyQuestTurnedInEvent event) {} + + @ScriptHook(EnumScriptType.PARTY_INVITE) + default void partyInvite(@ParamName("event") IPartyEvent.PartyInviteEvent event) {} + + @ScriptHook(EnumScriptType.PARTY_KICK) + default void partyKick(@ParamName("event") IPartyEvent.PartyKickEvent event) {} + + @ScriptHook(EnumScriptType.PARTY_LEAVE) + default void partyLeave(@ParamName("event") IPartyEvent.PartyLeaveEvent event) {} + + @ScriptHook(EnumScriptType.PARTY_DISBAND) + default void partyDisband(@ParamName("event") IPartyEvent.PartyDisbandEvent event) {} + + // Animation + @ScriptHook(EnumScriptType.ANIMATION_START) + default void animationStart(@ParamName("event") IAnimationEvent.Started event) {} + + @ScriptHook(EnumScriptType.ANIMATION_END) + default void animationEnd(@ParamName("event") IAnimationEvent.Ended event) {} + + @ScriptHook(EnumScriptType.ANIMATION_FRAME_ENTER) + default void frameEnter(@ParamName("event") IAnimationEvent.IFrameEvent.Entered event) {} + + @ScriptHook(EnumScriptType.ANIMATION_FRAME_EXIT) + default void frameExit(@ParamName("event") IAnimationEvent.IFrameEvent.Exited event) {} + + // Profile + @ScriptHook(EnumScriptType.PROFILE_CHANGE) + default void profileChange(@ParamName("event") IPlayerEvent.ProfileEvent.Changed event) {} + + @ScriptHook(EnumScriptType.PROFILE_REMOVE) + default void profileRemove(@ParamName("event") IPlayerEvent.ProfileEvent.Removed event) {} + + @ScriptHook(EnumScriptType.PROFILE_CREATE) + default void profileCreate(@ParamName("event") IPlayerEvent.ProfileEvent.Create event) {} + + // Effects + @ScriptHook(EnumScriptType.ON_EFFECT_ADD) + default void onEffectAdd(@ParamName("event") IPlayerEvent.EffectEvent.Added event) {} + + @ScriptHook(EnumScriptType.ON_EFFECT_TICK) + default void onEffectTick(@ParamName("event") IPlayerEvent.EffectEvent.Ticked event) {} + + @ScriptHook(EnumScriptType.ON_EFFECT_REMOVE) + default void onEffectRemove(@ParamName("event") IPlayerEvent.EffectEvent.Removed event) {} + } +} From e2ca76e626afc4437f8b31f7649a48f5242454e6 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 22 Jan 2026 21:02:24 +0200 Subject: [PATCH 244/358] Removed default imports for server event janino scripts --- .../noppes/npcs/janino/impl/JaninoNpcScript.java | 13 +------------ .../npcs/janino/impl/JaninoPlayerScript.java | 15 ++------------- 2 files changed, 3 insertions(+), 25 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java index 4b051b37e..5b3ec4dbd 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java @@ -14,19 +14,8 @@ import noppes.npcs.janino.annotations.ScriptHook; */ public class JaninoNpcScript extends JaninoScript { - /** Default imports available to NPC scripts without explicit import statements. */ - public static final String[] DEFAULT_IMPORTS = { - "noppes.npcs.api.*", - "noppes.npcs.api.entity.*", - "noppes.npcs.api.event.*", - "noppes.npcs.api.handler.*", - "noppes.npcs.api.handler.data.*", - "noppes.npcs.api.item.*", - "noppes.npcs.api.ability.*" - }; - public JaninoNpcScript() { - super(Functions.class, DEFAULT_IMPORTS); + super(Functions.class, null); } /** diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java index e5f61cdc9..15d822615 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -17,20 +17,9 @@ import noppes.npcs.janino.annotations.ScriptHook; * Janino (Java) script implementation for player events. */ public class JaninoPlayerScript extends JaninoScript { - - /** Default imports available to player scripts without explicit import statements. */ - public static final String[] DEFAULT_IMPORTS = { - "noppes.npcs.api.*", - "noppes.npcs.api.entity.*", - "noppes.npcs.api.event.*", - "noppes.npcs.api.handler.*", - "noppes.npcs.api.handler.data.*", - "noppes.npcs.api.item.*", - "noppes.npcs.api.gui.*" - }; - + public JaninoPlayerScript() { - super(Functions.class, DEFAULT_IMPORTS); + super(Functions.class, null); } /** From 19b01e20991cbd6c6a5449925d30d1a3793ab678 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 14:27:48 -0500 Subject: [PATCH 245/358] Compress Script Hooks and Context System --- src/api | 2 +- .../npcs/blocks/tiles/TileScripted.java | 5 - .../noppes/npcs/constants/ScriptContext.java | 65 +++- .../npcs/controllers/ScriptController.java | 8 +- .../controllers/ScriptHookController.java | 54 +-- .../npcs/controllers/data/DataScript.java | 6 - .../npcs/controllers/data/EffectScript.java | 29 +- .../controllers/data/ForgeDataScript.java | 204 +++++------- .../controllers/data/GlobalNPCDataScript.java | 315 +----------------- .../npcs/controllers/data/IScriptHandler.java | 8 +- .../controllers/data/LinkedItemScript.java | 8 +- .../controllers/data/MultiScriptHandler.java | 141 ++++++++ .../controllers/data/PlayerDataScript.java | 291 ++-------------- .../npcs/controllers/data/RecipeScript.java | 6 +- .../npcs/controllers/data/ScriptHandler.java | 7 + .../npcs/scripted/item/ScriptCustomItem.java | 6 - .../npcs/util/ScriptToStringHelper.java | 210 ++++++++++++ 17 files changed, 595 insertions(+), 770 deletions(-) create mode 100644 src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java create mode 100644 src/main/java/noppes/npcs/util/ScriptToStringHelper.java diff --git a/src/api b/src/api index 339e0f164..c962f678a 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 339e0f164e8c7b08c3469f61027fa25aa9e5c118 +Subproject commit c962f678a4f1bb83194d805cf3b5681fd5c70b1a diff --git a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java index c26a2f20a..b9558dd93 100644 --- a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java +++ b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java @@ -169,11 +169,6 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler, ISc return ScriptContext.BLOCK; } - @Override - public String getHookContext() { - return noppes.npcs.api.handler.IScriptHookHandler.CONTEXT_BLOCK; - } - @Override public void requestData() { BlockScriptPacket.Get(this.xCoord, this.yCoord, this.zCoord); diff --git a/src/main/java/noppes/npcs/constants/ScriptContext.java b/src/main/java/noppes/npcs/constants/ScriptContext.java index 942c406e6..f2f614a1e 100644 --- a/src/main/java/noppes/npcs/constants/ScriptContext.java +++ b/src/main/java/noppes/npcs/constants/ScriptContext.java @@ -16,10 +16,13 @@ import java.util.concurrent.ConcurrentHashMap; * - ICustomGuiEvent (custom GUI hooks) * - etc. * + * Each context also has a hookContext string used by ScriptHookController for + * hook registration and lookup. + * * This is a registerable system - any mod can register their own contexts: * * // Register a new context with multiple namespaces: - * ScriptContext.register("DBC", "IDBCEvent", "IDBCPlayerEvent", "IDBCFormEvent"); + * ScriptContext.register("DBC", "dbc", IDBCEvent.class, IDBCPlayerEvent.class); * * // Or add namespaces to an existing context: * ScriptContext.PLAYER.addNamespace("IDBCPlayerEvent"); @@ -31,12 +34,12 @@ public class ScriptContext { // ==================== BUILT-IN CONTEXTS ==================== - public static final ScriptContext NPC = register("NPC", + public static final ScriptContext NPC = register("NPC", "npc", INpcEvent.class, IProjectileEvent.class ); - public static final ScriptContext PLAYER = register("PLAYER", + public static final ScriptContext PLAYER = register("PLAYER", "player", IPlayerEvent.class, IAnimationEvent.class, IPartyEvent.class, @@ -46,19 +49,31 @@ public class ScriptContext { ICustomGuiEvent.class ); - public static final ScriptContext BLOCK = register("BLOCK", + public static final ScriptContext BLOCK = register("BLOCK", "block", IBlockEvent.class ); - public static final ScriptContext ITEM = register("ITEM", + public static final ScriptContext ITEM = register("ITEM", "item", IItemEvent.class ); - public static final ScriptContext FORGE = register("FORGE", + public static final ScriptContext FORGE = register("FORGE", "forge", IForgeEvent.class ); - public static final ScriptContext GLOBAL = register("GLOBAL", + public static final ScriptContext LINKED_ITEM = register("LINKED_ITEM", "linked_item", + IItemEvent.class + ); + + public static final ScriptContext RECIPE = register("RECIPE", "recipe", + "Recipe" + ); + + public static final ScriptContext EFFECT = register("EFFECT", "effect", + "Effect" + ); + + public static final ScriptContext GLOBAL = register("GLOBAL", "", "Global" // Special case: Global namespace doesn't have a corresponding event class ); @@ -67,13 +82,17 @@ public class ScriptContext { /** Unique identifier for this context (e.g., "NPC", "PLAYER", "DBC") */ public final String id; + /** Hook context identifier used by ScriptHookController (e.g., "npc", "player") */ + public final String hookContext; + /** The event interface namespaces this context supports */ private final List namespaces; // ==================== CONSTRUCTOR ==================== - private ScriptContext(String id, String... namespaces) { + private ScriptContext(String id, String hookContext, String... namespaces) { this.id = id; + this.hookContext = hookContext != null ? hookContext : ""; this.namespaces = new ArrayList<>(Arrays.asList(namespaces)); } @@ -123,14 +142,15 @@ public class ScriptContext { // ==================== REGISTRATION API ==================== /** - * Register a new script context with one or more namespaces. + * Register a new script context with a hook context and one or more namespaces. * * @param id Unique identifier (e.g., "DBC", "CUSTOM") + * @param hookContext The hook context string for ScriptHookController (e.g., "dbc") * @param namespaces The event interface names (e.g., "IDBCEvent", "IDBCPlayerEvent") * @return The registered ScriptContext */ - public static ScriptContext register(String id, String... namespaces) { - ScriptContext context = new ScriptContext(id, namespaces); + public static ScriptContext register(String id, String hookContext, String... namespaces) { + ScriptContext context = new ScriptContext(id, hookContext, namespaces); REGISTRY.put(id, context); return context; } @@ -140,18 +160,19 @@ public class ScriptContext { * Automatically extracts the simple name from each class. * * Example: - * ScriptContext.register("NPC", INpcEvent.class, IProjectileEvent.class); + * ScriptContext.register("NPC", "npc", INpcEvent.class, IProjectileEvent.class); * * @param id Unique identifier (e.g., "DBC", "CUSTOM") + * @param hookContext The hook context string for ScriptHookController (e.g., "npc") * @param eventClasses The event interface classes (simple names will be extracted) * @return The registered ScriptContext */ - public static ScriptContext register(String id, Class... eventClasses) { + public static ScriptContext register(String id, String hookContext, Class... eventClasses) { String[] namespaces = new String[eventClasses.length]; for (int i = 0; i < eventClasses.length; i++) { namespaces[i] = eventClasses[i].getSimpleName(); } - return register(id, namespaces); + return register(id, hookContext, namespaces); } /** @@ -180,6 +201,22 @@ public class ScriptContext { return GLOBAL; } + /** + * Find a script context by its hook context string. + * + * @param hookContext The hook context string (e.g., "npc", "player") + * @return The ScriptContext with that hook context, or GLOBAL if not found + */ + public static ScriptContext byHookContext(String hookContext) { + if (hookContext == null || hookContext.isEmpty()) return GLOBAL; + for (ScriptContext ctx : REGISTRY.values()) { + if (hookContext.equals(ctx.hookContext)) { + return ctx; + } + } + return GLOBAL; + } + /** * Get all registered script contexts. * diff --git a/src/main/java/noppes/npcs/controllers/ScriptController.java b/src/main/java/noppes/npcs/controllers/ScriptController.java index b5d21563c..af2153cff 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptController.java @@ -134,7 +134,7 @@ public class ScriptController { File file = this.forgeScriptsFile(); try { NBTJsonUtil.SaveFile(file, this.forgeScripts.writeToNBT(new NBTTagCompound())); - this.forgeScripts.lastInited = -1L; + this.forgeScripts.resetLastInited(); } catch (IOException | JsonException var4) { var4.printStackTrace(); } @@ -170,7 +170,7 @@ public class ScriptController { File file = this.playerScriptsFile(); try { NBTJsonUtil.SaveFile(file, this.playerScripts.writeToNBT(new NBTTagCompound())); - this.playerScripts.lastInited = -1L; + this.playerScripts.resetLastInited(); } catch (IOException | JsonException var4) { var4.printStackTrace(); } @@ -212,7 +212,7 @@ public class ScriptController { File file = this.npcScriptsFile(); try { NBTJsonUtil.SaveFile(file, this.globalNpcScripts.writeToNBT(new NBTTagCompound())); - this.globalNpcScripts.lastInited = -1L; + this.globalNpcScripts.resetLastInited(); } catch (IOException | JsonException var4) { var4.printStackTrace(); } @@ -227,7 +227,7 @@ public class ScriptController { File file = this.playerScriptsFile(); try { NBTJsonUtil.SaveFile(file, this.playerScripts.writeToNBT(new NBTTagCompound())); - this.playerScripts.lastInited = -1L; + this.playerScripts.resetLastInited(); } catch (IOException | JsonException var4) { var4.printStackTrace(); } diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index b599250a8..bc20eccff 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -2,6 +2,8 @@ package noppes.npcs.controllers; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; +import noppes.npcs.controllers.data.EffectScript; import noppes.npcs.controllers.data.RecipeScript; import java.util.ArrayList; @@ -24,32 +26,22 @@ public class ScriptHookController implements IScriptHookHandler { // Addon-registered hooks per context private final Map> addonHooks = new HashMap<>(); - // All available contexts - private static final String[] CONTEXTS = { - CONTEXT_NPC, - CONTEXT_PLAYER, - CONTEXT_BLOCK, - CONTEXT_ITEM, - CONTEXT_LINKED_ITEM, - CONTEXT_FORGE, - CONTEXT_RECIPE, - CONTEXT_EFFECT - }; - public ScriptHookController() { Instance = this; initializeBuiltInHooks(); } private void initializeBuiltInHooks() { - // Initialize empty lists for all contexts - for (String context : CONTEXTS) { - builtInHooks.put(context, new ArrayList<>()); - addonHooks.put(context, new ArrayList<>()); + // Initialize empty lists for all contexts from ScriptContext + for (ScriptContext context : ScriptContext.values()) { + if (!context.hookContext.isEmpty()) { + builtInHooks.put(context.hookContext, new ArrayList<>()); + addonHooks.put(context.hookContext, new ArrayList<>()); + } } // NPC hooks - registerBuiltIn(CONTEXT_NPC, + registerBuiltIn(ScriptContext.NPC, EnumScriptType.INIT.function, EnumScriptType.TICK.function, EnumScriptType.INTERACT.function, @@ -76,7 +68,7 @@ public class ScriptHookController implements IScriptHookHandler { ); // Player hooks - registerBuiltIn(CONTEXT_PLAYER, + registerBuiltIn(ScriptContext.PLAYER, EnumScriptType.INIT.function, EnumScriptType.TICK.function, EnumScriptType.INTERACT.function, @@ -150,7 +142,7 @@ public class ScriptHookController implements IScriptHookHandler { ); // Block hooks - registerBuiltIn(CONTEXT_BLOCK, + registerBuiltIn(ScriptContext.BLOCK, EnumScriptType.INIT.function, EnumScriptType.TICK.function, EnumScriptType.INTERACT.function, @@ -167,7 +159,7 @@ public class ScriptHookController implements IScriptHookHandler { ); // Item hooks - registerBuiltIn(CONTEXT_ITEM, + registerBuiltIn(ScriptContext.ITEM, EnumScriptType.INIT.function, EnumScriptType.TICK.function, EnumScriptType.TOSSED.function, @@ -183,7 +175,7 @@ public class ScriptHookController implements IScriptHookHandler { ); // Linked item hooks - registerBuiltIn(CONTEXT_LINKED_ITEM, + registerBuiltIn(ScriptContext.LINKED_ITEM, EnumScriptType.LINKED_ITEM_BUILD.function, EnumScriptType.LINKED_ITEM_VERSION.function, EnumScriptType.INIT.function, @@ -201,24 +193,32 @@ public class ScriptHookController implements IScriptHookHandler { ); // Recipe hooks - registerBuiltIn(CONTEXT_RECIPE, + registerBuiltIn(ScriptContext.RECIPE, RecipeScript.ScriptType.PRE.function, RecipeScript.ScriptType.POST.function ); // Effect hooks - registerBuiltIn(CONTEXT_EFFECT, + registerBuiltIn(ScriptContext.EFFECT, EnumScriptType.ON_EFFECT_ADD.function, EnumScriptType.ON_EFFECT_TICK.function, EnumScriptType.ON_EFFECT_REMOVE.function ); - // Forge hooks - just init, the rest are dynamically discovered - registerBuiltIn(CONTEXT_FORGE, - EnumScriptType.INIT.function + // Forge hooks - init + CNPC-specific hooks, the rest are dynamically discovered from Forge event classes + registerBuiltIn(ScriptContext.FORGE, + EnumScriptType.INIT.function, + EnumScriptType.CNPC_NATURAL_SPAWN.function ); } + /** + * Register built-in hooks for a ScriptContext. + */ + private void registerBuiltIn(ScriptContext context, String... hooks) { + registerBuiltIn(context.hookContext, hooks); + } + private void registerBuiltIn(String context, String... hooks) { List list = builtInHooks.get(context); if (list != null) { @@ -322,7 +322,7 @@ public class ScriptHookController implements IScriptHookHandler { @Override public String[] getContexts() { - return CONTEXTS.clone(); + return builtInHooks.keySet().toArray(new String[0]); } private boolean isBuiltInHook(String context, String functionName) { diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index 37f58453d..677b3efde 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -13,7 +13,6 @@ import noppes.npcs.EventHooks; import noppes.npcs.LogWriter; import noppes.npcs.NBTTags; import noppes.npcs.api.IWorld; -import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.config.ConfigDebug; import noppes.npcs.config.ConfigScript; @@ -218,11 +217,6 @@ public class DataScript implements IScriptHandlerPacket { return ScriptContext.NPC; } - @Override - public String getHookContext() { - return IScriptHookHandler.CONTEXT_NPC; - } - @Override public void requestData() { EventScriptPacket.Get(); diff --git a/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/src/main/java/noppes/npcs/controllers/data/EffectScript.java index b2ae04b98..821e542b4 100644 --- a/src/main/java/noppes/npcs/controllers/data/EffectScript.java +++ b/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -3,22 +3,24 @@ package noppes.npcs.controllers.data; import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.constants.ScriptContext; +import noppes.npcs.scripted.event.player.PlayerEvent; /** * Script handler for CustomEffect scripts. * Manages a single script container for effect hooks (onEffectAdd, onEffectTick, onEffectRemove). */ public class EffectScript extends SingleScriptHandler implements IScriptHandlerPacket { - + /** The effect ID for packet communication. -1 if not bound to an effect. */ private int effectId = -1; - + /** * Create an unbound EffectScript (for server-side use). */ public EffectScript() { } - + /** * Create an EffectScript bound to a specific effect (for GUI use). * @param effectId The ID of the CustomEffect @@ -26,22 +28,27 @@ public class EffectScript extends SingleScriptHandler implements IScriptHandlerP public EffectScript(int effectId) { this.effectId = effectId; } - + @Override - public String getHookContext() { - return IScriptHookHandler.CONTEXT_EFFECT; + public ScriptContext getContext() { + return ScriptContext.EFFECT; } - - + + @Override + public String noticeString() { + return effectId >= 0 ? "CustomEffect[" + effectId + "]" : "CustomEffect"; + } + + @Override public void requestData() { - if (effectId >= 0) + if (effectId >= 0) EffectScriptPacket.Get(effectId); } - + @Override public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { - if (effectId >= 0) + if (effectId >= 0) EffectScriptPacket.Save(effectId, index, totalCount, nbt); } diff --git a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java index 0ecdf27c0..1c6f09da7 100644 --- a/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/ForgeDataScript.java @@ -13,8 +13,6 @@ import net.minecraftforge.event.world.ChunkEvent; import net.minecraftforge.event.world.ChunkWatchEvent; import net.minecraftforge.event.world.WorldEvent; import noppes.npcs.EventHooks; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; @@ -26,89 +24,65 @@ import java.io.IOException; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; -import java.util.Iterator; import java.util.List; -public class ForgeDataScript implements IScriptHandlerPacket { +public class ForgeDataScript extends MultiScriptHandler { private static final Object HOOK_LOCK = new Object(); private static List cachedHooks; - private List scripts = new ArrayList<>(); - private String scriptLanguage = "ECMAScript"; - public long lastInited = -1L; + private long lastForgeUpdate = -1L; - private boolean enabled = false; public ForgeDataScript() { } - public void clear() { - this.scripts = new ArrayList<>(); - } - - public void readFromNBT(NBTTagCompound compound) { - if (compound.hasKey("Scripts")) { - this.scripts = new ArrayList<>(NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this)); - } else { - this.scripts = new ArrayList<>(NBTTags.GetScript(compound, this)); - } - this.scriptLanguage = compound.getString("ScriptLanguage"); - if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0]; - } else { - this.scriptLanguage = "ECMAScript"; - } - } - this.enabled = compound.getBoolean("ScriptEnabled"); - } - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setInteger("TotalScripts", this.scripts.size()); - for (int i = 0; i < this.scripts.size(); i++) { - compound.setTag("Tab" + i, this.scripts.get(i).writeToNBT(new NBTTagCompound())); - } - compound.setString("ScriptLanguage", this.scriptLanguage); - compound.setBoolean("ScriptEnabled", this.enabled); - return compound; - } - - - public void callScript(String type, Event event) { - if (this.isEnabled()) { - if (ScriptController.Instance.lastLoaded > this.lastInited || ScriptController.Instance.lastForgeUpdate > this.lastForgeUpdate) { - this.lastInited = ScriptController.Instance.lastLoaded; - this.lastForgeUpdate = ScriptController.Instance.lastForgeUpdate; - - for (IScriptUnit script : this.scripts) { - if (script instanceof ScriptContainer) - ((ScriptContainer) script).errored = false; - } - - if (!type.equals("init")) { - EventHooks.onForgeInit(this); - } - } - - for (IScriptUnit script : this.scripts) { - if (script == null || script.hasErrored() || !script.hasCode()) - continue; - script.run(type, event); - } - } - } - public boolean isEnabled() { return this.enabled && ConfigScript.GlobalForgeScripts && ScriptController.HasStart && this.scripts.size() > 0; } + @Override + protected boolean canRunScripts() { + return isEnabled(); + } + @Override public ScriptContext getContext() { return ScriptContext.FORGE; } @Override - public String getHookContext() { - return IScriptHookHandler.CONTEXT_FORGE; + protected boolean needsReInit() { + return ScriptController.Instance.lastLoaded > lastInited || ScriptController.Instance.lastForgeUpdate > lastForgeUpdate; + } + + @Override + protected void reInitScripts() { + lastInited = ScriptController.Instance.lastLoaded; + lastForgeUpdate = ScriptController.Instance.lastForgeUpdate; + + for (IScriptUnit script : this.scripts) { + if (script instanceof ScriptContainer) + ((ScriptContainer) script).errored = false; + } + } + + @Override + public void callScript(String type, Event event) { + if (!canRunScripts()) { + return; + } + + if (needsReInit()) { + reInitScripts(); + if (!type.equals("init")) { + EventHooks.onForgeInit(this); + } + } + + for (IScriptUnit script : this.scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) + continue; + script.run(type, event); + } } @Override @@ -120,9 +94,11 @@ public class ForgeDataScript implements IScriptHandlerPacket { if (cachedHooks != null) return new ArrayList<>(cachedHooks); - List hookList = new ArrayList<>(ScriptHookController.Instance.getBuiltInHooks(IScriptHookHandler.CONTEXT_FORGE)); + // Start with built-in + addon hooks from ScriptHookController + List hookList = new ArrayList<>(ScriptHookController.Instance.getAllHooks(ScriptContext.FORGE.hookContext)); - ArrayList list = new ArrayList(); + // Dynamically discover Forge event classes + ArrayList list = new ArrayList<>(); try { list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("cpw.mods.fml.common.gameevent")); list.addAll(ClassPath.from(this.getClass().getClassLoader()).getTopLevelClassesRecursive("net.minecraftforge.event")); @@ -131,51 +107,50 @@ public class ForgeDataScript implements IScriptHandlerPacket { e.printStackTrace(); } - for (ClassPath.ClassInfo e1 : list) { - ClassPath.ClassInfo classLoader = e1; - Class infoClass = classLoader.load(); - ArrayList c = new ArrayList(Arrays.asList(infoClass.getDeclaredClasses())); - if (c.isEmpty()) { - c.add(infoClass); + for (ClassPath.ClassInfo classInfo : list) { + Class infoClass = classInfo.load(); + List> classes = new ArrayList<>(Arrays.asList(infoClass.getDeclaredClasses())); + if (classes.isEmpty()) { + classes.add(infoClass); } - Iterator var10 = c.iterator(); - while (var10.hasNext()) { - Class c1 = (Class) var10.next(); - if (!EntityEvent.EntityConstructing.class.isAssignableFrom(c1) - && !WorldEvent.PotentialSpawns.class.isAssignableFrom(c1) - && !TickEvent.RenderTickEvent.class.isAssignableFrom(c1) - && !TickEvent.ClientTickEvent.class.isAssignableFrom(c1) - && !FMLNetworkEvent.ClientCustomPacketEvent.class.isAssignableFrom(c1) - && !ItemTooltipEvent.class.isAssignableFrom(c1) - && Event.class.isAssignableFrom(c1) - && !Modifier.isAbstract(c1.getModifiers()) - && Modifier.isPublic(c1.getModifiers()) - && !ChunkEvent.class.isAssignableFrom(c1) - && !ChunkWatchEvent.class.isAssignableFrom(c1) - && !ChunkDataEvent.class.isAssignableFrom(c1)) { - String eventName = c1.getName(); - int i = eventName.lastIndexOf("."); - eventName = StringUtils.uncapitalize(eventName.substring(i + 1).replace("$", "")); - hookList.add(eventName); + for (Class eventClass : classes) { + if (isValidForgeEvent(eventClass)) { + String eventName = eventClass.getName(); + int lastDot = eventName.lastIndexOf("."); + eventName = StringUtils.uncapitalize(eventName.substring(lastDot + 1).replace("$", "")); + + if (!hookList.contains(eventName)) { + hookList.add(eventName); + } } } } - hookList.add("onCNPCNaturalSpawn"); - - for (String addonHook : ScriptHookController.Instance.getAddonHooks(IScriptHookHandler.CONTEXT_FORGE)) { - if (!hookList.contains(addonHook)) { - hookList.add(addonHook); - } - } - cachedHooks = hookList; } return new ArrayList<>(cachedHooks); } + /** + * Check if a class is a valid Forge event that should be exposed as a hook. + */ + private boolean isValidForgeEvent(Class eventClass) { + return Event.class.isAssignableFrom(eventClass) + && Modifier.isPublic(eventClass.getModifiers()) + && !Modifier.isAbstract(eventClass.getModifiers()) + && !EntityEvent.EntityConstructing.class.isAssignableFrom(eventClass) + && !WorldEvent.PotentialSpawns.class.isAssignableFrom(eventClass) + && !TickEvent.RenderTickEvent.class.isAssignableFrom(eventClass) + && !TickEvent.ClientTickEvent.class.isAssignableFrom(eventClass) + && !FMLNetworkEvent.ClientCustomPacketEvent.class.isAssignableFrom(eventClass) + && !ItemTooltipEvent.class.isAssignableFrom(eventClass) + && !ChunkEvent.class.isAssignableFrom(eventClass) + && !ChunkWatchEvent.class.isAssignableFrom(eventClass) + && !ChunkDataEvent.class.isAssignableFrom(eventClass); + } + @Override public void requestData() { ForgeScriptPacket.Get(); @@ -186,33 +161,8 @@ public class ForgeDataScript implements IScriptHandlerPacket { ForgeScriptPacket.Save(index, totalCount, nbt); } + @Override public boolean isClient() { return false; } - - public boolean getEnabled() { - return this.enabled; - } - - public void setEnabled(boolean bo) { - this.enabled = bo; - } - - public String getLanguage() { - return this.scriptLanguage; - } - - public void setLanguage(String lang) { - this.scriptLanguage = lang; - } - - public void setScripts(List list) { - this.scripts = list; - } - - public List getScripts() { - return this.scripts; - } - - } diff --git a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java index 5561c255c..805ae7c8d 100644 --- a/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/GlobalNPCDataScript.java @@ -1,41 +1,20 @@ package noppes.npcs.controllers.data; -import com.google.common.base.Preconditions; -import cpw.mods.fml.common.eventhandler.Event; import kamkeel.npcs.network.packets.request.script.GlobalNPCScriptPacket; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.math.BlockPos; -import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.scripted.NpcAPI; +import noppes.npcs.util.ScriptToStringHelper; -import javax.annotation.CheckForNull; -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.OptionalDouble; -import java.util.OptionalInt; -import java.util.OptionalLong; -import java.util.TreeMap; - -public class GlobalNPCDataScript implements IScriptHandlerPacket { - public List scripts = new ArrayList<>(); - public String scriptLanguage = "ECMAScript"; +public class GlobalNPCDataScript extends MultiScriptHandler { private EntityNPCInterface npc; private ICustomNpc npcAPI; - public long lastInited = -1L; private long lastNpcUpdate = -1L; - public boolean enabled = false; public GlobalNPCDataScript(EntityNPCInterface npc) { if (npc != null) { @@ -43,35 +22,9 @@ public class GlobalNPCDataScript implements IScriptHandlerPacket { } } - public void clear() { - this.scripts = new ArrayList<>(); - } - - public void readFromNBT(NBTTagCompound compound) { - if (compound.hasKey("Scripts")) { - this.scripts = new ArrayList<>(NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this)); - } else { - this.scripts = new ArrayList<>(NBTTags.GetScript(compound, this)); - } - this.scriptLanguage = compound.getString("ScriptLanguage"); - if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0]; - } else { - this.scriptLanguage = "ECMAScript"; - } - } - this.enabled = compound.getBoolean("ScriptEnabled"); - } - - public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setInteger("TotalScripts", this.scripts.size()); - for (int i = 0; i < this.scripts.size(); i++) { - compound.setTag("Tab" + i, this.scripts.get(i).writeToNBT(new NBTTagCompound())); - } - compound.setString("ScriptLanguage", this.scriptLanguage); - compound.setBoolean("ScriptEnabled", this.enabled); - return compound; + @Override + protected boolean canRunScripts() { + return isEnabled(); } public boolean isEnabled() { @@ -84,8 +37,14 @@ public class GlobalNPCDataScript implements IScriptHandlerPacket { } @Override - public String getHookContext() { - return IScriptHookHandler.CONTEXT_NPC; + protected boolean needsReInit() { + return ScriptController.Instance.lastLoaded > lastInited || ScriptController.Instance.lastGlobalNpcUpdate > lastNpcUpdate; + } + + @Override + protected void reInitScripts() { + super.reInitScripts(); + lastNpcUpdate = ScriptController.Instance.lastGlobalNpcUpdate; } @Override @@ -98,62 +57,18 @@ public class GlobalNPCDataScript implements IScriptHandlerPacket { GlobalNPCScriptPacket.Save(index, totalCount, nbt); } + @Override + public boolean isClient() { + return this.npc != null && this.npc.isClientWorld(); + } @Override - public void callScript(String hookName, Event event) { - if (this.isEnabled()) { - if (ScriptController.Instance.lastLoaded > this.lastInited || ScriptController.Instance.lastGlobalNpcUpdate > this.lastNpcUpdate) { - this.lastInited = ScriptController.Instance.lastLoaded; - this.lastNpcUpdate = ScriptController.Instance.lastGlobalNpcUpdate; - - for (IScriptUnit script : this.scripts) { - if (script instanceof ScriptContainer) - ((ScriptContainer) script).errored = false; - } - } - - for (IScriptUnit script : this.scripts) { - if (script == null || script.hasErrored() || !script.hasCode()) - continue; - script.run(hookName, event); - } - } - } - - public boolean isClient() { - return this.npc.isClientWorld(); - } - - public boolean getEnabled() { - return this.enabled; - } - - public void setEnabled(boolean bo) { - this.enabled = bo; - } - - public String getLanguage() { - return this.scriptLanguage; - } - - public void setLanguage(String lang) { - this.scriptLanguage = lang; - } - - public void setScripts(List list) { - this.scripts = list; - } - - public List getScripts() { - return this.scripts; - } - public String noticeString() { if (this.npc == null) { return "Global script"; } else { BlockPos pos = new BlockPos(this.npc); - return GlobalNPCDataScript.toStringHelper(this.npc).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); + return ScriptToStringHelper.toStringHelper(this.npc).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); } } @@ -163,198 +78,4 @@ public class GlobalNPCDataScript implements IScriptHandlerPacket { } return this.npcAPI; } - - - public static final class ToStringHelper { - private final String className; - private final GlobalNPCDataScript.ToStringHelper.ValueHolder holderHead; - private GlobalNPCDataScript.ToStringHelper.ValueHolder holderTail; - private boolean omitNullValues; - private boolean omitEmptyValues; - - private ToStringHelper(String className) { - this.holderHead = new GlobalNPCDataScript.ToStringHelper.ValueHolder(); - this.holderTail = this.holderHead; - this.omitNullValues = false; - this.omitEmptyValues = false; - this.className = (String) Preconditions.checkNotNull(className); - } - - public GlobalNPCDataScript.ToStringHelper omitNullValues() { - this.omitNullValues = true; - return this; - } - - public GlobalNPCDataScript.ToStringHelper add(String name, @CheckForNull Object value) { - return this.addHolder(name, value); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, boolean value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, char value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, double value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, float value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, int value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper add(String name, long value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(@CheckForNull Object value) { - return this.addHolder(value); - } - - public GlobalNPCDataScript.ToStringHelper addValue(boolean value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(char value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(double value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(float value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(int value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public GlobalNPCDataScript.ToStringHelper addValue(long value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - private static boolean isEmpty(Object value) { - if (value instanceof CharSequence) { - return ((CharSequence) value).length() == 0; - } else if (value instanceof Collection) { - return ((Collection) value).isEmpty(); - } else if (value instanceof Map) { - return ((Map) value).isEmpty(); - } else if (value instanceof Optional) { - return !((Optional) value).isPresent(); - } else if (value instanceof OptionalInt) { - return !((OptionalInt) value).isPresent(); - } else if (value instanceof OptionalLong) { - return !((OptionalLong) value).isPresent(); - } else if (value instanceof OptionalDouble) { - return !((OptionalDouble) value).isPresent(); - } else if (value instanceof com.google.common.base.Optional) { - return !((com.google.common.base.Optional) value).isPresent(); - } else if (value.getClass().isArray()) { - return Array.getLength(value) == 0; - } else { - return false; - } - } - - public String toString() { - boolean omitNullValuesSnapshot = this.omitNullValues; - boolean omitEmptyValuesSnapshot = this.omitEmptyValues; - String nextSeparator = ""; - StringBuilder builder = (new StringBuilder(32)).append(this.className).append('{'); - for (GlobalNPCDataScript.ToStringHelper.ValueHolder valueHolder = this.holderHead.next; valueHolder != null; valueHolder = valueHolder.next) { - Object value = valueHolder.value; - if (!(valueHolder instanceof GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder)) { - if (value == null) { - if (omitNullValuesSnapshot) { - continue; - } - } else if (omitEmptyValuesSnapshot && isEmpty(value)) { - continue; - } - } - builder.append(nextSeparator); - nextSeparator = ", "; - if (valueHolder.name != null) { - builder.append(valueHolder.name).append('='); - } - if (value != null && value.getClass().isArray()) { - Object[] objectArray = new Object[]{value}; - String arrayString = Arrays.deepToString(objectArray); - builder.append(arrayString, 1, arrayString.length() - 1); - } else { - builder.append(value); - } - } - return builder.append('}').toString(); - } - - private GlobalNPCDataScript.ToStringHelper.ValueHolder addHolder() { - GlobalNPCDataScript.ToStringHelper.ValueHolder valueHolder = new GlobalNPCDataScript.ToStringHelper.ValueHolder(); - this.holderTail = this.holderTail.next = valueHolder; - return valueHolder; - } - - private GlobalNPCDataScript.ToStringHelper addHolder(@CheckForNull Object value) { - GlobalNPCDataScript.ToStringHelper.ValueHolder valueHolder = this.addHolder(); - valueHolder.value = value; - return this; - } - - private GlobalNPCDataScript.ToStringHelper addHolder(String name, @CheckForNull Object value) { - GlobalNPCDataScript.ToStringHelper.ValueHolder valueHolder = this.addHolder(); - valueHolder.value = value; - valueHolder.name = (String) Preconditions.checkNotNull(name); - return this; - } - - private GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder addUnconditionalHolder() { - GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = new GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder(); - this.holderTail = this.holderTail.next = valueHolder; - return valueHolder; - } - - private GlobalNPCDataScript.ToStringHelper addUnconditionalHolder(Object value) { - GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); - valueHolder.value = value; - return this; - } - - private GlobalNPCDataScript.ToStringHelper addUnconditionalHolder(String name, Object value) { - GlobalNPCDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); - valueHolder.value = value; - valueHolder.name = (String) Preconditions.checkNotNull(name); - return this; - } - - private static final class UnconditionalValueHolder extends GlobalNPCDataScript.ToStringHelper.ValueHolder { - private UnconditionalValueHolder() { - super(); - } - } - - private static class ValueHolder { - @CheckForNull - String name; - @CheckForNull - Object value; - @CheckForNull - GlobalNPCDataScript.ToStringHelper.ValueHolder next; - - private ValueHolder() { - } - } - } - - public static GlobalNPCDataScript.ToStringHelper toStringHelper(Object self) { - return new GlobalNPCDataScript.ToStringHelper(self.getClass().getSimpleName()); - } } diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java index b89c725a9..93892f2a6 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -134,10 +134,14 @@ public interface IScriptHandler { * Get the hook context identifier for this handler. * Used by ScriptHookController to look up available hooks. * - * @return The context string (e.g., IScriptHookHandler.CONTEXT_EFFECT), or empty if none + * By default, derives from getContext().hookContext. + * Override if a different hook context is needed. + * + * @return The context string (e.g., "effect", "npc"), or empty if none */ default String getHookContext() { - return ""; + ScriptContext ctx = getContext(); + return ctx != null ? ctx.hookContext : ""; } /** diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java index 4d01e1c46..1ee4537fb 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -2,7 +2,6 @@ package noppes.npcs.controllers.data; import kamkeel.npcs.network.packets.request.script.item.LinkedItemScriptPacket; import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.ScriptContext; /** @@ -28,14 +27,9 @@ public class LinkedItemScript extends SingleScriptHandler implements IScriptHand this.linkedItemId = linkedItemId; } - @Override - public String getHookContext() { - return IScriptHookHandler.CONTEXT_LINKED_ITEM; - } - @Override public ScriptContext getContext() { - return ScriptContext.ITEM; + return ScriptContext.LINKED_ITEM; } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java new file mode 100644 index 000000000..19758708d --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java @@ -0,0 +1,141 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.eventhandler.Event; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.NBTTags; +import noppes.npcs.controllers.ScriptContainer; +import noppes.npcs.controllers.ScriptController; + +import java.util.ArrayList; +import java.util.Map; +import java.util.TreeMap; + +/** + * Base class for script handlers that manage multiple script containers (tabs). + * Provides shared NBT serialization and common script execution patterns. + * + * Subclasses should: + * - Override getContext() to return the appropriate ScriptContext + * - Override getHookContext() if not using ScriptContext's hookContext + * - Override requestData() and sendSavePacket() for packet communication + * - Override isEnabled() if custom enable logic is needed + */ +public abstract class MultiScriptHandler extends ScriptHandler implements IScriptHandlerPacket { + + /** Tracks when scripts were last updated (for reload detection) */ + protected long lastUpdate = -1L; + + /** Console output storage */ + protected Map console = new TreeMap<>(); + + // ==================== NBT SERIALIZATION ==================== + + /** + * Read script data from NBT using the Tab0/Tab1/etc. format. + */ + public void readFromNBT(NBTTagCompound compound) { + if (compound.hasKey("Scripts")) { + this.scripts = new ArrayList<>(NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this)); + } else { + this.scripts = new ArrayList<>(NBTTags.GetScript(compound, this)); + } + + this.scriptLanguage = compound.getString("ScriptLanguage"); + normalizeLanguage(); + + this.enabled = compound.getBoolean("ScriptEnabled"); + + if (compound.hasKey("ScriptConsole")) { + this.console = NBTTags.GetLongStringMap(compound.getTagList("ScriptConsole", 10)); + } + } + + /** + * Write script data to NBT using the Tab0/Tab1/etc. format. + */ + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setInteger("TotalScripts", this.scripts.size()); + for (int i = 0; i < this.scripts.size(); i++) { + compound.setTag("Tab" + i, this.scripts.get(i).writeToNBT(new NBTTagCompound())); + } + compound.setString("ScriptLanguage", this.scriptLanguage); + compound.setBoolean("ScriptEnabled", this.enabled); + compound.setTag("ScriptConsole", NBTTags.NBTLongStringMap(console)); + return compound; + } + + /** + * Normalize the language to a valid one if the current language is not available. + */ + protected void normalizeLanguage() { + if (!ScriptController.Instance.languages.containsKey(this.scriptLanguage)) { + if (!ScriptController.Instance.languages.isEmpty()) { + this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0]; + } else { + this.scriptLanguage = "ECMAScript"; + } + } + } + + // ==================== SCRIPT EXECUTION ==================== + + /** + * Check if scripts need to be re-initialized due to reload. + * Subclasses can override to add additional update tracking (e.g., lastPlayerUpdate). + * + * @return true if scripts should be re-initialized + */ + protected boolean needsReInit() { + return ScriptController.Instance.lastLoaded > lastInited; + } + + /** + * Called when scripts need re-initialization. + * Clears error state on all script containers. + */ + protected void reInitScripts() { + lastInited = ScriptController.Instance.lastLoaded; + for (IScriptUnit script : this.scripts) { + if (script instanceof ScriptContainer) { + ((ScriptContainer) script).errored = false; + } + } + } + + @Override + public void callScript(String hookName, Event event) { + if (!canRunScripts()) { + return; + } + + if (needsReInit()) { + reInitScripts(); + } + + for (IScriptUnit script : this.scripts) { + if (script == null || script.hasErrored() || !script.hasCode()) { + continue; + } + script.run(hookName, event); + } + } + + // ==================== CONSOLE HANDLING ==================== + + @Override + public Map getConsoleText() { + return console; + } + + @Override + public void clearConsole() { + console.clear(); + } + + // ==================== UTILITY ==================== + + public void clear() { + this.scripts = new ArrayList<>(); + this.console = new TreeMap<>(); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index c329c4c6e..c5721f4c5 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -1,6 +1,5 @@ package noppes.npcs.controllers.data; -import com.google.common.base.Preconditions; import cpw.mods.fml.common.eventhandler.Event; import kamkeel.npcs.network.packets.request.script.PlayerScriptPacket; import net.minecraft.entity.player.EntityPlayer; @@ -8,7 +7,6 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.math.BlockPos; import noppes.npcs.EventHooks; import noppes.npcs.NBTTags; -import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; @@ -18,35 +16,23 @@ import noppes.npcs.controllers.ScriptController; import noppes.npcs.janino.impl.JaninoPlayerScript; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.player.PlayerEvent; +import noppes.npcs.util.ScriptToStringHelper; -import javax.annotation.CheckForNull; -import java.lang.reflect.Array; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; -import java.util.Optional; -import java.util.OptionalDouble; -import java.util.OptionalInt; -import java.util.OptionalLong; import java.util.TreeMap; -public class PlayerDataScript implements IScriptHandlerPacket { - public List scripts = new ArrayList<>(); - public String scriptLanguage = "ECMAScript"; +public class PlayerDataScript extends MultiScriptHandler { private EntityPlayer player; private IPlayer playerAPI; private long lastPlayerUpdate = 0L; - public long lastInited = -1; - public boolean hadInteract = true; - private boolean enabled = false; - - private static Map console = new TreeMap(); - private static List errored = new ArrayList(); + // Static console/errored for IndividualPlayerScripts mode + private static Map staticConsole = new TreeMap<>(); + private static List errored = new ArrayList<>(); public PlayerDataScript(EntityPlayer player) { if (player != null) { @@ -54,38 +40,23 @@ public class PlayerDataScript implements IScriptHandlerPacket { } } + @Override public void clear() { - console = new TreeMap(); - errored = new ArrayList(); + staticConsole = new TreeMap<>(); + errored = new ArrayList<>(); scripts = new ArrayList<>(); } + @Override public void readFromNBT(NBTTagCompound compound) { - if (compound.hasKey("Scripts")) { - this.scripts = new ArrayList<>(NBTTags.GetScriptOld(compound.getTagList("Scripts", 10), this)); - } else { - this.scripts = new ArrayList<>(NBTTags.GetScript(compound, this)); - } - this.scriptLanguage = compound.getString("ScriptLanguage"); - if (!ScriptController.Instance.languages.containsKey(scriptLanguage)) { - if (!ScriptController.Instance.languages.isEmpty()) { - this.scriptLanguage = (String) ScriptController.Instance.languages.keySet().toArray()[0]; - } else { - this.scriptLanguage = "ECMAScript"; - } - } - this.enabled = compound.getBoolean("ScriptEnabled"); - console = NBTTags.GetLongStringMap(compound.getTagList("ScriptConsole", 10)); + super.readFromNBT(compound); + staticConsole = NBTTags.GetLongStringMap(compound.getTagList("ScriptConsole", 10)); } + @Override public NBTTagCompound writeToNBT(NBTTagCompound compound) { - compound.setInteger("TotalScripts", this.scripts.size()); - for (int i = 0; i < this.scripts.size(); i++) { - compound.setTag("Tab" + i, this.scripts.get(i).writeToNBT(new NBTTagCompound())); - } - compound.setString("ScriptLanguage", this.scriptLanguage); - compound.setBoolean("ScriptEnabled", this.enabled); - compound.setTag("ScriptConsole", NBTTags.NBTLongStringMap(console)); + super.writeToNBT(compound); + compound.setTag("ScriptConsole", NBTTags.NBTLongStringMap(staticConsole)); return compound; } @@ -94,13 +65,13 @@ public class PlayerDataScript implements IScriptHandlerPacket { } @Override - public ScriptContext getContext() { - return ScriptContext.PLAYER; + protected boolean canRunScripts() { + return isEnabled(); } @Override - public String getHookContext() { - return IScriptHookHandler.CONTEXT_PLAYER; + public ScriptContext getContext() { + return ScriptContext.PLAYER; } @Override @@ -113,7 +84,6 @@ public class PlayerDataScript implements IScriptHandlerPacket { PlayerScriptPacket.Save(index, totalCount, nbt); } - @Override public void callScript(String hookName, Event event) { if (!isEnabled()) @@ -147,8 +117,8 @@ public class PlayerDataScript implements IScriptHandlerPacket { errored.add(i); } for (Entry entry : script.getConsole().entrySet()) { - if (!console.containsKey(entry.getKey())) - console.put(entry.getKey(), " tab " + (i + 1) + ":\n" + entry.getValue()); + if (!staticConsole.containsKey(entry.getKey())) + staticConsole.put(entry.getKey(), " tab " + (i + 1) + ":\n" + entry.getValue()); } script.clearConsole(); } @@ -176,41 +146,34 @@ public class PlayerDataScript implements IScriptHandlerPacket { } } + @Override public boolean isClient() { return this.player != null && this.player.isClientWorld(); } + @Override public boolean getEnabled() { return ScriptController.Instance.playerScripts.enabled; } + @Override public void setEnabled(boolean bo) { ScriptController.Instance.playerScripts.enabled = bo; this.enabled = bo; } + @Override public String getLanguage() { return ScriptController.Instance.playerScripts.scriptLanguage; } - public void setLanguage(String lang) { - this.scriptLanguage = lang; - } - - public void setScripts(List list) { - this.scripts = list; - } - - public List getScripts() { - return this.scripts; - } - + @Override public String noticeString() { if (this.player == null) { return "Global script"; } BlockPos pos = new BlockPos(this.player); - return PlayerDataScript.toStringHelper(this.player).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); + return ScriptToStringHelper.toStringHelper(this.player).add("x", pos.getX()).add("y", pos.getY()).add("z", pos.getZ()).toString(); } public IPlayer getPlayer() { @@ -220,213 +183,21 @@ public class PlayerDataScript implements IScriptHandlerPacket { return this.playerAPI; } + @Override public Map getConsoleText() { if (ConfigScript.IndividualPlayerScripts) { - return console; + return staticConsole; } - return IScriptHandlerPacket.super.getConsoleText(); + return super.getConsoleText(); } @Override public void clearConsole() { if (ConfigScript.IndividualPlayerScripts) { - console.clear(); + staticConsole.clear(); return; } - IScriptHandlerPacket.super.clearConsole(); - } - - public static final class ToStringHelper { - private final String className; - private final PlayerDataScript.ToStringHelper.ValueHolder holderHead; - private PlayerDataScript.ToStringHelper.ValueHolder holderTail; - private boolean omitNullValues; - private boolean omitEmptyValues; - - private ToStringHelper(String className) { - this.holderHead = new PlayerDataScript.ToStringHelper.ValueHolder(); - this.holderTail = this.holderHead; - this.omitNullValues = false; - this.omitEmptyValues = false; - this.className = (String) Preconditions.checkNotNull(className); - } - - public PlayerDataScript.ToStringHelper omitNullValues() { - this.omitNullValues = true; - return this; - } - - public PlayerDataScript.ToStringHelper add(String name, @CheckForNull Object value) { - return this.addHolder(name, value); - } - - public PlayerDataScript.ToStringHelper add(String name, boolean value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper add(String name, char value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper add(String name, double value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper add(String name, float value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper add(String name, int value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper add(String name, long value) { - return this.addUnconditionalHolder(name, String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(@CheckForNull Object value) { - return this.addHolder(value); - } - - public PlayerDataScript.ToStringHelper addValue(boolean value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(char value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(double value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(float value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(int value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - public PlayerDataScript.ToStringHelper addValue(long value) { - return this.addUnconditionalHolder(String.valueOf(value)); - } - - private static boolean isEmpty(Object value) { - if (value instanceof CharSequence) { - return ((CharSequence) value).length() == 0; - } else if (value instanceof Collection) { - return ((Collection) value).isEmpty(); - } else if (value instanceof Map) { - return ((Map) value).isEmpty(); - } else if (value instanceof Optional) { - return !((Optional) value).isPresent(); - } else if (value instanceof OptionalInt) { - return !((OptionalInt) value).isPresent(); - } else if (value instanceof OptionalLong) { - return !((OptionalLong) value).isPresent(); - } else if (value instanceof OptionalDouble) { - return !((OptionalDouble) value).isPresent(); - } else if (value instanceof com.google.common.base.Optional) { - return !((com.google.common.base.Optional) value).isPresent(); - } else if (value.getClass().isArray()) { - return Array.getLength(value) == 0; - } else { - return false; - } - } - - public String toString() { - boolean omitNullValuesSnapshot = this.omitNullValues; - boolean omitEmptyValuesSnapshot = this.omitEmptyValues; - String nextSeparator = ""; - StringBuilder builder = (new StringBuilder(32)).append(this.className).append('{'); - for (PlayerDataScript.ToStringHelper.ValueHolder valueHolder = this.holderHead.next; valueHolder != null; valueHolder = valueHolder.next) { - Object value = valueHolder.value; - if (!(valueHolder instanceof PlayerDataScript.ToStringHelper.UnconditionalValueHolder)) { - if (value == null) { - if (omitNullValuesSnapshot) { - continue; - } - } else if (omitEmptyValuesSnapshot && isEmpty(value)) { - continue; - } - } - builder.append(nextSeparator); - nextSeparator = ", "; - if (valueHolder.name != null) { - builder.append(valueHolder.name).append('='); - } - if (value != null && value.getClass().isArray()) { - Object[] objectArray = new Object[]{value}; - String arrayString = Arrays.deepToString(objectArray); - builder.append(arrayString, 1, arrayString.length() - 1); - } else { - builder.append(value); - } - } - return builder.append('}').toString(); - } - - private PlayerDataScript.ToStringHelper.ValueHolder addHolder() { - PlayerDataScript.ToStringHelper.ValueHolder valueHolder = new PlayerDataScript.ToStringHelper.ValueHolder(); - this.holderTail = this.holderTail.next = valueHolder; - return valueHolder; - } - - private PlayerDataScript.ToStringHelper addHolder(@CheckForNull Object value) { - PlayerDataScript.ToStringHelper.ValueHolder valueHolder = this.addHolder(); - valueHolder.value = value; - return this; - } - - private PlayerDataScript.ToStringHelper addHolder(String name, @CheckForNull Object value) { - PlayerDataScript.ToStringHelper.ValueHolder valueHolder = this.addHolder(); - valueHolder.value = value; - valueHolder.name = (String) Preconditions.checkNotNull(name); - return this; - } - - private PlayerDataScript.ToStringHelper.UnconditionalValueHolder addUnconditionalHolder() { - PlayerDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = new PlayerDataScript.ToStringHelper.UnconditionalValueHolder(); - this.holderTail = this.holderTail.next = valueHolder; - return valueHolder; - } - - private PlayerDataScript.ToStringHelper addUnconditionalHolder(Object value) { - PlayerDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); - valueHolder.value = value; - return this; - } - - private PlayerDataScript.ToStringHelper addUnconditionalHolder(String name, Object value) { - PlayerDataScript.ToStringHelper.UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); - valueHolder.value = value; - valueHolder.name = (String) Preconditions.checkNotNull(name); - return this; - } - - private static final class UnconditionalValueHolder extends PlayerDataScript.ToStringHelper.ValueHolder { - private UnconditionalValueHolder() { - super(); - } - } - - private static class ValueHolder { - @CheckForNull - String name; - @CheckForNull - Object value; - @CheckForNull - PlayerDataScript.ToStringHelper.ValueHolder next; - - private ValueHolder() { - } - } - } - - public static PlayerDataScript.ToStringHelper toStringHelper(Object self) { - return new PlayerDataScript.ToStringHelper(self.getClass().getSimpleName()); + super.clearConsole(); } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java index ab5c67fb5..803a1b30d 100644 --- a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java +++ b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java @@ -2,7 +2,7 @@ package noppes.npcs.controllers.data; import kamkeel.npcs.network.packets.request.script.RecipeScriptPacket; import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.constants.ScriptContext; /** * Script handler for Recipe scripts (both carpentry and anvil). @@ -33,8 +33,8 @@ public class RecipeScript extends SingleScriptHandler implements IScriptHandlerP } @Override - public String getHookContext() { - return IScriptHookHandler.CONTEXT_RECIPE; + public ScriptContext getContext() { + return ScriptContext.RECIPE; } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java index def6ca7cf..4bb555375 100644 --- a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java @@ -64,4 +64,11 @@ public abstract class ScriptHandler implements IScriptHandler { public List getScripts() { return this.scripts; } + + /** + * Reset the lastInited timestamp to force re-initialization on next script call. + */ + public void resetLastInited() { + this.lastInited = -1; + } } diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java index 2e17555d3..0ca2fa778 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java @@ -8,7 +8,6 @@ import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.EventHooks; import noppes.npcs.NBTTags; import noppes.npcs.api.item.IItemCustom; -import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.IScriptUnit; @@ -72,11 +71,6 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus return ScriptContext.ITEM; } - @Override - public String getHookContext() { - return IScriptHookHandler.CONTEXT_ITEM; - } - @Override public void requestData() { ItemScriptErrorPacket.Get(); diff --git a/src/main/java/noppes/npcs/util/ScriptToStringHelper.java b/src/main/java/noppes/npcs/util/ScriptToStringHelper.java new file mode 100644 index 000000000..d34e5c1cd --- /dev/null +++ b/src/main/java/noppes/npcs/util/ScriptToStringHelper.java @@ -0,0 +1,210 @@ +package noppes.npcs.util; + +import com.google.common.base.Preconditions; + +import javax.annotation.CheckForNull; +import java.lang.reflect.Array; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; +import java.util.Optional; +import java.util.OptionalDouble; +import java.util.OptionalInt; +import java.util.OptionalLong; + +/** + * Utility class for building toString representations of script handlers. + * Used by GlobalNPCDataScript and PlayerDataScript for notice strings. + */ +public final class ScriptToStringHelper { + private final String className; + private final ValueHolder holderHead; + private ValueHolder holderTail; + private boolean omitNullValues; + private boolean omitEmptyValues; + + private ScriptToStringHelper(String className) { + this.holderHead = new ValueHolder(); + this.holderTail = this.holderHead; + this.omitNullValues = false; + this.omitEmptyValues = false; + this.className = Preconditions.checkNotNull(className); + } + + public static ScriptToStringHelper toStringHelper(Object self) { + return new ScriptToStringHelper(self.getClass().getSimpleName()); + } + + public ScriptToStringHelper omitNullValues() { + this.omitNullValues = true; + return this; + } + + public ScriptToStringHelper add(String name, @CheckForNull Object value) { + return this.addHolder(name, value); + } + + public ScriptToStringHelper add(String name, boolean value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, char value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, double value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, float value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, int value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper add(String name, long value) { + return this.addUnconditionalHolder(name, String.valueOf(value)); + } + + public ScriptToStringHelper addValue(@CheckForNull Object value) { + return this.addHolder(value); + } + + public ScriptToStringHelper addValue(boolean value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(char value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(double value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(float value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(int value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + public ScriptToStringHelper addValue(long value) { + return this.addUnconditionalHolder(String.valueOf(value)); + } + + private static boolean isEmpty(Object value) { + if (value instanceof CharSequence) { + return ((CharSequence) value).length() == 0; + } else if (value instanceof Collection) { + return ((Collection) value).isEmpty(); + } else if (value instanceof Map) { + return ((Map) value).isEmpty(); + } else if (value instanceof Optional) { + return !((Optional) value).isPresent(); + } else if (value instanceof OptionalInt) { + return !((OptionalInt) value).isPresent(); + } else if (value instanceof OptionalLong) { + return !((OptionalLong) value).isPresent(); + } else if (value instanceof OptionalDouble) { + return !((OptionalDouble) value).isPresent(); + } else if (value instanceof com.google.common.base.Optional) { + return !((com.google.common.base.Optional) value).isPresent(); + } else if (value.getClass().isArray()) { + return Array.getLength(value) == 0; + } else { + return false; + } + } + + public String toString() { + boolean omitNullValuesSnapshot = this.omitNullValues; + boolean omitEmptyValuesSnapshot = this.omitEmptyValues; + String nextSeparator = ""; + StringBuilder builder = (new StringBuilder(32)).append(this.className).append('{'); + for (ValueHolder valueHolder = this.holderHead.next; valueHolder != null; valueHolder = valueHolder.next) { + Object value = valueHolder.value; + if (!(valueHolder instanceof UnconditionalValueHolder)) { + if (value == null) { + if (omitNullValuesSnapshot) { + continue; + } + } else if (omitEmptyValuesSnapshot && isEmpty(value)) { + continue; + } + } + builder.append(nextSeparator); + nextSeparator = ", "; + if (valueHolder.name != null) { + builder.append(valueHolder.name).append('='); + } + if (value != null && value.getClass().isArray()) { + Object[] objectArray = new Object[]{value}; + String arrayString = Arrays.deepToString(objectArray); + builder.append(arrayString, 1, arrayString.length() - 1); + } else { + builder.append(value); + } + } + return builder.append('}').toString(); + } + + private ValueHolder addHolder() { + ValueHolder valueHolder = new ValueHolder(); + this.holderTail = this.holderTail.next = valueHolder; + return valueHolder; + } + + private ScriptToStringHelper addHolder(@CheckForNull Object value) { + ValueHolder valueHolder = this.addHolder(); + valueHolder.value = value; + return this; + } + + private ScriptToStringHelper addHolder(String name, @CheckForNull Object value) { + ValueHolder valueHolder = this.addHolder(); + valueHolder.value = value; + valueHolder.name = Preconditions.checkNotNull(name); + return this; + } + + private UnconditionalValueHolder addUnconditionalHolder() { + UnconditionalValueHolder valueHolder = new UnconditionalValueHolder(); + this.holderTail = this.holderTail.next = valueHolder; + return valueHolder; + } + + private ScriptToStringHelper addUnconditionalHolder(Object value) { + UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); + valueHolder.value = value; + return this; + } + + private ScriptToStringHelper addUnconditionalHolder(String name, Object value) { + UnconditionalValueHolder valueHolder = this.addUnconditionalHolder(); + valueHolder.value = value; + valueHolder.name = Preconditions.checkNotNull(name); + return this; + } + + private static final class UnconditionalValueHolder extends ValueHolder { + private UnconditionalValueHolder() { + super(); + } + } + + private static class ValueHolder { + @CheckForNull + String name; + @CheckForNull + Object value; + @CheckForNull + ValueHolder next; + + private ValueHolder() { + } + } +} From 9ed588728b6af0196aa03166d8678fb9aec746ad Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 15:04:44 -0500 Subject: [PATCH 246/358] Update ScriptForgeEventHandler.java --- src/main/java/noppes/npcs/ScriptForgeEventHandler.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/noppes/npcs/ScriptForgeEventHandler.java b/src/main/java/noppes/npcs/ScriptForgeEventHandler.java index 649557b43..4294a6d9e 100644 --- a/src/main/java/noppes/npcs/ScriptForgeEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptForgeEventHandler.java @@ -36,6 +36,10 @@ public class ScriptForgeEventHandler { @SubscribeEvent public void forgeEntity(Event event) { if (CustomNpcs.getServer() != null && ScriptController.Instance.forgeScripts.isEnabled()) { + if(FMLCommonHandler.instance().getEffectiveSide() == Side.CLIENT){ + return; + } + if (event instanceof TickEvent && !(((TickEvent) event).side == Side.SERVER && ((TickEvent) event).phase == TickEvent.Phase.START)) { return; } From 73f0d4e441409257056e640c4c36dccb9aff6c12 Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Thu, 22 Jan 2026 21:14:50 +0100 Subject: [PATCH 247/358] Bought goatee some braincells He lost his from AI usage. --- .../java/noppes/npcs/janino/JaninoScript.java | 42 ++++++++++--------- .../npcs/janino/impl/JaninoPlayerScript.java | 4 +- 2 files changed, 25 insertions(+), 21 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 8a217b8d0..a2202271e 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -52,11 +52,11 @@ public abstract class JaninoScript implements IScriptUnit { private final String[] defaultImports; - protected JaninoScript(Class type, String[] defaultImports) { + protected JaninoScript(Class type, String[] defaultImports, boolean isClient) { this.type = type; this.defaultImports = defaultImports != null ? defaultImports : new String[0]; - this.builder = IScriptBodyBuilder.getBuilder(type, CustomNpcs.getClientCompiler()) - .setDefaultImports(defaultImports); + this.builder = IScriptBodyBuilder.getBuilder(type, isClient ? CustomNpcs.getClientCompiler() : CustomNpcs.getDynamicCompiler()) + .setDefaultImports(defaultImports); Permissions permissions = new Permissions(); permissions.setReadOnly(); @@ -67,6 +67,10 @@ public abstract class JaninoScript implements IScriptUnit { this.hookResolver = new JaninoHookResolver(type); } + protected JaninoScript(Class type, String[] defaultImports) { + this(type, defaultImports, true); + } + protected T getUnsafe() { return scriptBody.get(); } @@ -237,9 +241,9 @@ public abstract class JaninoScript implements IScriptUnit { */ @Override public void run(EnumScriptType type, Event event) { - if (type == null) + if (type == null) return; - + callFunction(type.function, event); } @@ -254,7 +258,7 @@ public abstract class JaninoScript implements IScriptUnit { public void run(String hookName, Object event) { callFunction(hookName, event); } - + @Override public String getScript() { return this.script; @@ -266,7 +270,7 @@ public abstract class JaninoScript implements IScriptUnit { this.evaluated = false; hookResolver.clearResolutionCaches(); } - + @Override public List getExternalScripts() { return this.externalScripts; @@ -278,17 +282,17 @@ public abstract class JaninoScript implements IScriptUnit { this.evaluated = false; hookResolver.clearResolutionCaches(); } - + @Override public TreeMap getConsole() { return this.console; } - + @Override public void clearConsole() { console.clear(); } - + @Override public void appendConsole(String message) { if (message != null && !message.isEmpty()) { @@ -304,18 +308,18 @@ public abstract class JaninoScript implements IScriptUnit { } } } - + @Override public String getLanguage() { return this.language; } - + @Override public void setLanguage(String language) { // JaninoScript is always Java, ignore attempts to change // This method exists for IScriptUnit interface compatibility } - + @Override public boolean hasCode() { if (!externalScripts.isEmpty()) @@ -330,11 +334,11 @@ public abstract class JaninoScript implements IScriptUnit { Method method = hookResolver.getMethodForDisplayName(hookName); if (method != null) return generateMethodStub(method); - + // Fallback for unknown hook names - generate basic override return String.format("public void %s() {\n \n}\n", hookName); } - + /** * Generates a stub string for a given Method. */ @@ -367,7 +371,7 @@ public abstract class JaninoScript implements IScriptUnit { typeCount.put(baseName, count); paramName = count == 1 ? baseName : baseName + (count - 1); } - + return typeName + " " + paramName; }) .collect(Collectors.joining(", ")); @@ -429,14 +433,14 @@ public abstract class JaninoScript implements IScriptUnit { return compound; } - + @Override public boolean hasErrored() { return this.errored; } - + @Override public void setErrored(boolean errored) { this.errored = errored; } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java index 15d822615..0e1f3b004 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -17,9 +17,9 @@ import noppes.npcs.janino.annotations.ScriptHook; * Janino (Java) script implementation for player events. */ public class JaninoPlayerScript extends JaninoScript { - + public JaninoPlayerScript() { - super(Functions.class, null); + super(Functions.class, null, false); } /** From cb364823a9aef9486e1fee917f8661e1264f853b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 22 Jan 2026 22:28:20 +0200 Subject: [PATCH 248/358] Added ScriptContext based registerHook --- .../noppes/npcs/controllers/ScriptHookController.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index bc20eccff..a919de972 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -3,7 +3,6 @@ package noppes.npcs.controllers; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.data.EffectScript; import noppes.npcs.controllers.data.RecipeScript; import java.util.ArrayList; @@ -230,6 +229,16 @@ public class ScriptHookController implements IScriptHookHandler { } } + @Override + public void registerHooks(ScriptContext context, String functionName) { + registerHook(context.hookContext, functionName); + } + + @Override + public void registerHooks(ScriptContext context, String... functionNames) { + registerHooks(context.hookContext, functionNames); + } + @Override public void registerHook(String context, String functionName) { if (context == null || functionName == null || functionName.isEmpty()) { From a18292b8ed579a9626159ff80d6b85e5666ee580 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 22 Jan 2026 22:29:26 +0200 Subject: [PATCH 249/358] Used dynamic janino compiler for server side script handlers --- src/main/java/noppes/npcs/janino/JaninoScript.java | 2 +- src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index a2202271e..f1a8f58e7 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -68,7 +68,7 @@ public abstract class JaninoScript implements IScriptUnit { } protected JaninoScript(Class type, String[] defaultImports) { - this(type, defaultImports, true); + this(type, defaultImports, false); } protected T getUnsafe() { diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java index 0e1f3b004..5c644665d 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -19,7 +19,7 @@ import noppes.npcs.janino.annotations.ScriptHook; public class JaninoPlayerScript extends JaninoScript { public JaninoPlayerScript() { - super(Functions.class, null, false); + super(Functions.class, null); } /** From 13cb94d5e2e180404308247571b8537979aa3028 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 22 Jan 2026 22:57:04 +0200 Subject: [PATCH 250/358] Forgot API ScriptHookController --- .../controllers/data/MultiScriptHandler.java | 32 +------------------ 1 file changed, 1 insertion(+), 31 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java index 19758708d..d175eb3b9 100644 --- a/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java @@ -21,13 +21,7 @@ import java.util.TreeMap; * - Override isEnabled() if custom enable logic is needed */ public abstract class MultiScriptHandler extends ScriptHandler implements IScriptHandlerPacket { - - /** Tracks when scripts were last updated (for reload detection) */ - protected long lastUpdate = -1L; - - /** Console output storage */ - protected Map console = new TreeMap<>(); - + // ==================== NBT SERIALIZATION ==================== /** @@ -44,10 +38,6 @@ public abstract class MultiScriptHandler extends ScriptHandler implements IScrip normalizeLanguage(); this.enabled = compound.getBoolean("ScriptEnabled"); - - if (compound.hasKey("ScriptConsole")) { - this.console = NBTTags.GetLongStringMap(compound.getTagList("ScriptConsole", 10)); - } } /** @@ -60,7 +50,6 @@ public abstract class MultiScriptHandler extends ScriptHandler implements IScrip } compound.setString("ScriptLanguage", this.scriptLanguage); compound.setBoolean("ScriptEnabled", this.enabled); - compound.setTag("ScriptConsole", NBTTags.NBTLongStringMap(console)); return compound; } @@ -119,23 +108,4 @@ public abstract class MultiScriptHandler extends ScriptHandler implements IScrip script.run(hookName, event); } } - - // ==================== CONSOLE HANDLING ==================== - - @Override - public Map getConsoleText() { - return console; - } - - @Override - public void clearConsole() { - console.clear(); - } - - // ==================== UTILITY ==================== - - public void clear() { - this.scripts = new ArrayList<>(); - this.console = new TreeMap<>(); - } } From dfcab1e413a9c024bba42fb0013e3e6fa83b776b Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Thu, 22 Jan 2026 23:05:47 +0200 Subject: [PATCH 251/358] Moved clear to ScriptHandler --- .../java/noppes/npcs/controllers/data/PlayerDataScript.java | 3 +-- src/main/java/noppes/npcs/controllers/data/ScriptHandler.java | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index c5721f4c5..6549ed1e5 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -40,11 +40,10 @@ public class PlayerDataScript extends MultiScriptHandler { } } - @Override public void clear() { + super.clear(); staticConsole = new TreeMap<>(); errored = new ArrayList<>(); - scripts = new ArrayList<>(); } @Override diff --git a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java index 4bb555375..dd57b05bb 100644 --- a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java @@ -65,6 +65,9 @@ public abstract class ScriptHandler implements IScriptHandler { return this.scripts; } + public void clear() { + this.scripts = new ArrayList<>(); + } /** * Reset the lastInited timestamp to force re-initialization on next script call. */ From e6221c094070eeac161dc1a59defac592be13b38 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 16:24:48 -0500 Subject: [PATCH 252/358] Test Janino --- .../npcs/janino/JaninoHookResolver.java | 113 +++++++- .../java/noppes/npcs/janino/JaninoScript.java | 11 +- .../npcs/janino/impl/JaninoPlayerScript.java | 252 +++++------------- 3 files changed, 184 insertions(+), 192 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java index 2c8eeb543..b77233b12 100644 --- a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java +++ b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java @@ -16,6 +16,15 @@ import java.util.stream.Collectors; * The resolver keeps simple caches for previously resolved signatures and * supports mapping enum-based hook names (via the {@code @ScriptHook} * annotation) to methods. + * + *

Dynamic Hook Support

+ * This resolver supports two modes: + *
    + *
  • Interface-only: Only methods defined in the interface are found (legacy behavior)
  • + *
  • Compiled class: Methods on the actual compiled script class are found, + * allowing users to define custom methods for Forge events, addon hooks, etc.
  • + *
+ * Use {@link #resolveHookMethod(String, Object[], Object)} to search the compiled instance. */ public final class JaninoHookResolver { private final Class interfaceType; @@ -30,6 +39,18 @@ public final class JaninoHookResolver { */ private final Map signatureCache = new HashMap<>(); + /** + * Cache for methods found on compiled classes (keyed by class + signature). + * Separate from signatureCache to handle recompilation. + */ + private final Map compiledClassCache = new HashMap<>(); + + /** + * Track which compiled class we've cached methods for. + * If the class changes (recompilation), we clear the cache. + */ + private Class lastCompiledClass; + /* Lazily built list of available hooks and a mapping for display names */ private boolean hookListInitialized; /* List of names displayed in GUI hooks list (accounts for overload names) */ @@ -52,10 +73,12 @@ public final class JaninoHookResolver { */ public void clearResolutionCaches() { signatureCache.clear(); + compiledClassCache.clear(); + lastCompiledClass = null; } /** - * Resolve a hook method by name and runtime argument values. + * Resolve a hook method by name and runtime argument values (interface-only mode). * * This first checks any enum-mapped methods (via {@code @ScriptHook}), * then a signature-based cache and finally attempts full candidate @@ -66,16 +89,79 @@ public final class JaninoHookResolver { * @return the resolved {@link Method} or {@code null} if none matched */ public Method resolveHookMethod(String hookName, Object[] args) { + return resolveHookMethod(hookName, args, null); + } + + /** + * Resolve a hook method by name, searching both the compiled class and interface. + * + *

This enables dynamic hook support: users can define methods for any hook + * (Forge events, addon hooks, etc.) without needing them in the interface definition.

+ * + *

Resolution order:

+ *
    + *
  1. Enum-mapped methods (via {@code @ScriptHook})
  2. + *
  3. Compiled class methods (user-defined, including custom hooks)
  4. + *
  5. Interface methods (base hooks with defaults)
  6. + *
  7. Fallback to no-arg versions
  8. + *
+ * + *

Performance: Method lookups are cached. First call per hook does reflection + * (~microseconds), subsequent calls use cache (essentially free).

+ * + * @param hookName hook method name (or enum function name) + * @param args runtime arguments to use for overload resolution + * @param compiledInstance the compiled script instance (or null for interface-only) + * @return the resolved {@link Method} or {@code null} if none matched + */ + public Method resolveHookMethod(String hookName, Object[] args, Object compiledInstance) { if (hookName == null || hookName.isEmpty()) return null; + // 1. Check enum-mapped methods first (fastest path) Method method = enumHookLookup.get(hookName); if (method != null) return method; Object[] safeArgs = args == null ? new Object[0] : args; - String signatureKey = buildSignatureKey(hookName, safeArgs); + + // 2. If we have a compiled instance, search its class first + if (compiledInstance != null) { + Class compiledClass = compiledInstance.getClass(); + + // Clear cache if compiled class changed (recompilation) + if (lastCompiledClass != compiledClass) { + compiledClassCache.clear(); + lastCompiledClass = compiledClass; + } + + // Check compiled class cache + method = compiledClassCache.get(signatureKey); + if (method != null) + return method; + + // Search compiled class for matching method + method = resolveHookMethodInternal(compiledClass, hookName, safeArgs); + if (method != null) { + compiledClassCache.put(signatureKey, method); + return method; + } + + // Try no-arg version on compiled class + String noArgKey = buildSignatureKey(hookName, new Object[0]); + method = compiledClassCache.get(noArgKey); + if (method != null) + return method; + + method = resolveHookMethodInternal(compiledClass, hookName, new Object[0]); + if (method != null) { + compiledClassCache.put(noArgKey, method); + return method; + } + } + + // 3. Fall back to interface-only search method = signatureCache.get(signatureKey); if (method != null) return method; @@ -197,17 +283,32 @@ public final class JaninoHookResolver { /** * Internal resolution routine. Attempts to find the best matching method - * on the provided interface by comparing parameter counts and a simple + * on the provided class by comparing parameter counts and a simple * match scoring function (see {@link #scoreMethodMatch}). + * + *

This searches all public methods (including inherited) to support + * both interface-defined hooks and user-defined custom methods.

+ * + * @param targetClass the class to search (interface or compiled class) + * @param hookName the method name to find + * @param args runtime arguments for overload resolution + * @return the best matching method, or null if none found */ - private static Method resolveHookMethodInternal(Class interfaceType, String hookName, Object[] args) { + private static Method resolveHookMethodInternal(Class targetClass, String hookName, Object[] args) { Object[] safeArgs = args == null ? new Object[0] : args; Method best = null; int bestScore = Integer.MAX_VALUE; - for (Method method : interfaceType.getDeclaredMethods()) { - if (!method.getName().equals(hookName) || Modifier.isFinal(method.getModifiers())) + // Use getMethods() to include inherited methods (important for compiled classes) + for (Method method : targetClass.getMethods()) { + if (!method.getName().equals(hookName)) + continue; + + // Skip final methods and Object methods + if (Modifier.isFinal(method.getModifiers())) + continue; + if (method.getDeclaringClass() == Object.class) continue; Class[] paramTypes = method.getParameterTypes(); diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index a2202271e..955aed3bc 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -202,15 +202,18 @@ public abstract class JaninoScript implements IScriptUnit { if (hookName == null || hookName.isEmpty()) return null; - Method method = hookResolver.resolveHookMethod(hookName, args); - if (method == null) - return null; - + // Ensure compiled first so we can search the compiled class ensureCompiled(); T t = getUnsafe(); if (t == null) return null; + // Pass compiled instance to enable dynamic hook resolution + // This allows users to define methods for ANY hook (Forge events, addon hooks, etc.) + Method method = hookResolver.resolveHookMethod(hookName, args, t); + if (method == null) + return null; + Method invokeMethod = method; Object[] invokeArgs = args == null ? new Object[0] : args; diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java index 0e1f3b004..10cb6d556 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -1,13 +1,6 @@ package noppes.npcs.janino.impl; -import noppes.npcs.api.event.IAnimationEvent; -import noppes.npcs.api.event.ICustomGuiEvent; -import noppes.npcs.api.event.ICustomNPCsEvent; -import noppes.npcs.api.event.IDialogEvent; -import noppes.npcs.api.event.IFactionEvent; -import noppes.npcs.api.event.IPartyEvent; import noppes.npcs.api.event.IPlayerEvent; -import noppes.npcs.api.event.IQuestEvent; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.janino.JaninoScript; import noppes.npcs.janino.annotations.ParamName; @@ -15,6 +8,35 @@ import noppes.npcs.janino.annotations.ScriptHook; /** * Janino (Java) script implementation for player events. + * + *

Dynamic Hook Support

+ * With the new dynamic hook resolution, users can define methods for ANY hook - + * not just those defined in the Functions interface. Simply write a public method + * with the hook name and matching parameter type: + * + *
{@code
+ * // Standard hook (defined in interface)
+ * public void init(IPlayerEvent.InitEvent event) {
+ *     // ...
+ * }
+ *
+ * // Custom addon hook (NOT in interface - still works!)
+ * public void myAddonHook(MyAddonEvent event) {
+ *     // ...
+ * }
+ *
+ * // Any hook by name
+ * public void someCustomHook(Object event) {
+ *     // ...
+ * }
+ * }
+ * + * The interface below defines core hooks for: + *
    + *
  • EnumScriptType mapping (so "init" maps to init() method)
  • + *
  • GUI hook list display
  • + *
  • Stub generation with correct parameter types
  • + *
*/ public class JaninoPlayerScript extends JaninoScript { @@ -23,236 +45,102 @@ public class JaninoPlayerScript extends JaninoScript Date: Thu, 22 Jan 2026 16:59:13 -0500 Subject: [PATCH 253/358] Potential Fix to Usuable Type names --- .../controllers/ScriptHookController.java | 18 +++++++--- .../java/noppes/npcs/janino/JaninoScript.java | 34 ++++++++++++++++--- .../npcs/janino/impl/JaninoNpcScript.java | 15 +++++++- .../npcs/janino/impl/JaninoPlayerScript.java | 19 ++++++++++- 4 files changed, 76 insertions(+), 10 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index a919de972..c5af0929b 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -229,14 +229,24 @@ public class ScriptHookController implements IScriptHookHandler { } } - @Override - public void registerHooks(ScriptContext context, String functionName) { + /** + * Convenience method to register a hook using ScriptContext. + * @param context The script context + * @param functionName The hook function name + */ + public void registerHook(ScriptContext context, String functionName) { registerHook(context.hookContext, functionName); } - @Override + /** + * Convenience method to register multiple hooks using ScriptContext. + * @param context The script context + * @param functionNames The hook function names + */ public void registerHooks(ScriptContext context, String... functionNames) { - registerHooks(context.hookContext, functionNames); + for (String fn : functionNames) { + registerHook(context.hookContext, fn); + } } @Override diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 7d61ead92..a7e6389aa 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -344,6 +344,7 @@ public abstract class JaninoScript implements IScriptUnit { /** * Generates a stub string for a given Method. + * Handles nested classes properly (e.g., IPlayerEvent.InitEvent instead of just InitEvent). */ public static String generateMethodStub(Method method) { String mods = Modifier.toString(method.getModifiers()); @@ -352,13 +353,14 @@ public abstract class JaninoScript implements IScriptUnit { if (!mods.isEmpty()) mods += " "; - String returnTypeStr = method.getReturnType().getSimpleName(); + String returnTypeStr = getUsableTypeName(method.getReturnType()); String name = method.getName(); Map typeCount = new HashMap<>(); String params = Arrays.stream(method.getParameters()) .map(p -> { - String typeName = p.getType().getSimpleName(); + String typeName = getUsableTypeName(p.getType()); + String simpleTypeName = p.getType().getSimpleName(); // Check for @ParamName annotation first ParamName paramNameAnnotation = p.getAnnotation(ParamName.class); @@ -368,8 +370,8 @@ public abstract class JaninoScript implements IScriptUnit { // Use annotated name paramName = paramNameAnnotation.value(); } else { - // Fall back to generated name - String baseName = Character.toLowerCase(typeName.charAt(0)) + typeName.substring(1); + // Fall back to generated name based on simple name + String baseName = Character.toLowerCase(simpleTypeName.charAt(0)) + simpleTypeName.substring(1); int count = typeCount.getOrDefault(baseName, 0) + 1; typeCount.put(baseName, count); paramName = count == 1 ? baseName : baseName + (count - 1); @@ -397,6 +399,30 @@ public abstract class JaninoScript implements IScriptUnit { return String.format("%s%s %s(%s) {\n%s\n}\n", mods, returnTypeStr, name, params, body); } + /** + * Gets a usable type name for stub generation. + * For nested classes like IPlayerEvent.InitEvent, returns "IPlayerEvent.InitEvent" + * For top-level classes, returns the simple name. + * For primitives, returns the primitive name. + */ + private static String getUsableTypeName(Class type) { + if (type.isPrimitive() || type.getEnclosingClass() == null) { + return type.getSimpleName(); + } + + // Build the nested class chain: OuterClass.InnerClass.DeeperClass + StringBuilder sb = new StringBuilder(); + Class current = type; + while (current != null) { + if (sb.length() > 0) { + sb.insert(0, "."); + } + sb.insert(0, current.getSimpleName()); + current = current.getEnclosingClass(); + } + return sb.toString(); + } + public List getHookList() { return hookResolver.getDisplayHookList(); diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java index 5b3ec4dbd..1af8dea35 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java @@ -14,8 +14,21 @@ import noppes.npcs.janino.annotations.ScriptHook; */ public class JaninoNpcScript extends JaninoScript { + /** + * Default imports for NPC scripts. + * These are automatically available in user code without explicit import statements. + */ + private static final String[] DEFAULT_IMPORTS = { + "noppes.npcs.api.event.INpcEvent", + "noppes.npcs.api.event.IProjectileEvent", + "noppes.npcs.api.event.IAbilityEvent", + "noppes.npcs.api.entity.ICustomNpc", + "noppes.npcs.api.entity.IPlayer", + "noppes.npcs.api.NpcAPI" + }; + public JaninoNpcScript() { - super(Functions.class, null); + super(Functions.class, DEFAULT_IMPORTS); } /** diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java index d535f2e24..c755c700c 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -40,8 +40,25 @@ import noppes.npcs.janino.annotations.ScriptHook; */ public class JaninoPlayerScript extends JaninoScript { + /** + * Default imports for player scripts. + * These are automatically available in user code without explicit import statements. + */ + private static final String[] DEFAULT_IMPORTS = { + "noppes.npcs.api.event.IPlayerEvent", + "noppes.npcs.api.event.IQuestEvent", + "noppes.npcs.api.event.IDialogEvent", + "noppes.npcs.api.event.IFactionEvent", + "noppes.npcs.api.event.IPartyEvent", + "noppes.npcs.api.event.ICustomGuiEvent", + "noppes.npcs.api.event.IAnimationEvent", + "noppes.npcs.api.event.ICustomNPCsEvent", + "noppes.npcs.api.entity.IPlayer", + "noppes.npcs.api.NpcAPI" + }; + public JaninoPlayerScript() { - super(Functions.class, null); + super(Functions.class, DEFAULT_IMPORTS, false); } /** From f4a5bbd1f6405de4f1aa6202e470506218e08691 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 17:28:12 -0500 Subject: [PATCH 254/358] Janino Kam --- src/api | 2 +- .../java/noppes/npcs/janino/JaninoScript.java | 53 ++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/api b/src/api index c962f678a..b90df20fc 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit c962f678a4f1bb83194d805cf3b5681fd5c70b1a +Subproject commit b90df20fc42f11f7ff69e79792881ebc209735eb diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index a7e6389aa..8734304d9 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -338,8 +338,57 @@ public abstract class JaninoScript implements IScriptUnit { if (method != null) return generateMethodStub(method); - // Fallback for unknown hook names - generate basic override - return String.format("public void %s() {\n \n}\n", hookName); + // Fallback: try to find the event class by naming convention + String eventType = findEventTypeForHook(hookName); + if (eventType != null) { + return String.format("public void %s(%s event) {\n \n}\n", hookName, eventType); + } + + // Last resort: generic Object parameter + return String.format("public void %s(Object event) {\n \n}\n", hookName); + } + + /** + * Tries to find the event type for a hook by searching API event interfaces. + * Uses naming convention: hookName "questStart" -> event class "QuestStartEvent" + * + * @param hookName The hook name (e.g., "questStart", "dialogOpen") + * @return The usable type name (e.g., "IQuestEvent.QuestStartEvent") or null if not found + */ + private String findEventTypeForHook(String hookName) { + if (hookName == null || hookName.isEmpty()) + return null; + + // Convert hook name to expected event class name + // questStart -> QuestStartEvent + String expectedEventName = Character.toUpperCase(hookName.charAt(0)) + hookName.substring(1) + "Event"; + + // Search in known API event interfaces + Class[] eventInterfaces = { + noppes.npcs.api.event.IPlayerEvent.class, + noppes.npcs.api.event.INpcEvent.class, + noppes.npcs.api.event.IQuestEvent.class, + noppes.npcs.api.event.IDialogEvent.class, + noppes.npcs.api.event.IFactionEvent.class, + noppes.npcs.api.event.IPartyEvent.class, + noppes.npcs.api.event.ICustomGuiEvent.class, + noppes.npcs.api.event.IAnimationEvent.class, + noppes.npcs.api.event.IBlockEvent.class, + noppes.npcs.api.event.IItemEvent.class, + noppes.npcs.api.event.IProjectileEvent.class, + noppes.npcs.api.event.IAbilityEvent.class, + noppes.npcs.api.event.IForgeEvent.class + }; + + for (Class eventInterface : eventInterfaces) { + for (Class nested : eventInterface.getDeclaredClasses()) { + if (nested.getSimpleName().equals(expectedEventName)) { + return getUsableTypeName(nested); + } + } + } + + return null; } /** From bee4d18bc6aeef536c99d5b74f602bb77b262627 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 17:59:03 -0500 Subject: [PATCH 255/358] Hook Context --- src/api | 2 +- .../npcs/controllers/HookDefinition.java | 307 +++++++++ .../controllers/ScriptHookController.java | 624 +++++++++++++----- .../java/noppes/npcs/janino/JaninoScript.java | 156 +++-- .../npcs/janino/impl/JaninoNpcScript.java | 6 + .../npcs/janino/impl/JaninoPlayerScript.java | 6 + 6 files changed, 903 insertions(+), 198 deletions(-) create mode 100644 src/main/java/noppes/npcs/controllers/HookDefinition.java diff --git a/src/api b/src/api index b90df20fc..7a337a570 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit b90df20fc42f11f7ff69e79792881ebc209735eb +Subproject commit 7a337a5704c442fb98a0fd4e0ba26210cb54af98 diff --git a/src/main/java/noppes/npcs/controllers/HookDefinition.java b/src/main/java/noppes/npcs/controllers/HookDefinition.java new file mode 100644 index 000000000..b8b78fe70 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/HookDefinition.java @@ -0,0 +1,307 @@ +package noppes.npcs.controllers; + +import cpw.mods.fml.common.eventhandler.Cancelable; +import noppes.npcs.api.handler.IHookDefinition; +import noppes.npcs.janino.annotations.ParamName; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; + +/** + * Implementation of {@link IHookDefinition} with a fluent builder API. + * + *

Usage Examples

+ *
{@code
+ * // Full definition with class reference
+ * HookDefinition.builder("onDBCTransform")
+ *     .eventClass(IDBCEvent.TransformEvent.class)
+ *     .paramNames("event")
+ *     .requiredImports("com.dbc.api.event.IDBCEvent")
+ *     .cancelable(true)
+ *     .build();
+ *
+ * // String-based (avoids classloading issues)
+ * HookDefinition.builder("onDBCPowerUp")
+ *     .eventClass("com.dbc.api.event.IDBCEvent$PowerUpEvent")
+ *     .requiredImports("com.dbc.api.event.IDBCEvent")
+ *     .build();
+ *
+ * // Simple hook (backward compat, no metadata)
+ * HookDefinition.simple("legacyHook");
+ * }
+ */ +public class HookDefinition implements IHookDefinition { + + private final String hookName; + private final String eventClassName; + private final String[] paramNames; + private final String[] requiredImports; + private final boolean cancelable; + + // Cached resolved class (transient - not serialized) + private transient Class cachedEventClass; + private transient boolean classResolutionAttempted; + + private HookDefinition(Builder builder) { + this.hookName = builder.hookName; + this.eventClassName = builder.eventClassName; + this.paramNames = builder.paramNames != null ? builder.paramNames : new String[]{"event"}; + this.requiredImports = builder.requiredImports != null ? builder.requiredImports : new String[0]; + this.cancelable = builder.cancelable; + } + + // ==================== IHookDefinition Implementation ==================== + + @Override + public String hookName() { + return hookName; + } + + @Override + public String eventClassName() { + return eventClassName; + } + + @Override + public String[] paramNames() { + return paramNames; + } + + @Override + public String[] requiredImports() { + return requiredImports; + } + + @Override + public boolean isCancelable() { + return cancelable; + } + + @Override + public Class getEventClass() { + if (classResolutionAttempted) { + return cachedEventClass; + } + + classResolutionAttempted = true; + if (eventClassName == null || eventClassName.isEmpty()) { + return null; + } + + try { + cachedEventClass = Class.forName(eventClassName); + } catch (ClassNotFoundException e) { + cachedEventClass = null; + } + + return cachedEventClass; + } + + // ==================== Factory Methods ==================== + + /** + * Create a new builder for a hook definition. + * + * @param hookName The hook function name + * @return A new builder instance + */ + public static Builder builder(String hookName) { + return new Builder(hookName); + } + + /** + * Create a simple hook definition with just a name (no metadata). + * For backward compatibility with legacy hook registration. + * + * @param hookName The hook function name + * @return A minimal hook definition + */ + public static HookDefinition simple(String hookName) { + return builder(hookName).build(); + } + + /** + * Create a hook definition from an interface method. + * Automatically extracts metadata from the method signature and annotations. + * + * @param hookName The hook name (may differ from method name) + * @param method The method to extract metadata from + * @return A hook definition with extracted metadata + */ + public static HookDefinition fromMethod(String hookName, Method method) { + Builder builder = builder(hookName); + + // Extract event type from first parameter + if (method.getParameterCount() > 0) { + Class eventType = method.getParameterTypes()[0]; + builder.eventClass(eventType); + + // Extract @Cancelable annotation + if (eventType.isAnnotationPresent(Cancelable.class)) { + builder.cancelable(true); + } + + // Build required imports from event type + String importName = getImportForClass(eventType); + if (importName != null) { + builder.requiredImports(importName); + } + } + + // Extract parameter names from @ParamName annotations + Parameter[] params = method.getParameters(); + if (params.length > 0) { + String[] names = new String[params.length]; + for (int i = 0; i < params.length; i++) { + ParamName annotation = params[i].getAnnotation(ParamName.class); + if (annotation != null) { + names[i] = annotation.value(); + } else { + names[i] = params[i].getType().getSimpleName().toLowerCase(); + } + } + builder.paramNames(names); + } + + return builder.build(); + } + + /** + * Get the import statement needed for a class. + * For nested classes, returns the enclosing class. + */ + private static String getImportForClass(Class clazz) { + if (clazz == null || clazz.isPrimitive()) { + return null; + } + + // For nested classes, get the top-level enclosing class + Class enclosing = clazz; + while (enclosing.getEnclosingClass() != null) { + enclosing = enclosing.getEnclosingClass(); + } + + String name = enclosing.getName(); + // Skip java.lang classes + if (name.startsWith("java.lang.")) { + return null; + } + + return name; + } + + // ==================== Builder ==================== + + /** + * Builder for creating HookDefinition instances with a fluent API. + */ + public static class Builder { + private final String hookName; + private String eventClassName; + private String[] paramNames; + private String[] requiredImports; + private boolean cancelable; + + private Builder(String hookName) { + if (hookName == null || hookName.isEmpty()) { + throw new IllegalArgumentException("hookName cannot be null or empty"); + } + this.hookName = hookName; + } + + /** + * Set the event class by name (avoids classloading issues). + * + * @param className Full qualified class name (use $ for nested classes) + * @return this builder + */ + public Builder eventClass(String className) { + this.eventClassName = className; + return this; + } + + /** + * Set the event class directly (convenience method). + * The class name will be extracted automatically. + * + * @param clazz The event class + * @return this builder + */ + public Builder eventClass(Class clazz) { + if (clazz != null) { + this.eventClassName = clazz.getName(); + // Auto-detect cancelable + if (clazz.isAnnotationPresent(Cancelable.class)) { + this.cancelable = true; + } + } + return this; + } + + /** + * Set parameter names for stub generation. + * + * @param names Parameter names + * @return this builder + */ + public Builder paramNames(String... names) { + this.paramNames = names; + return this; + } + + /** + * Set required imports for this hook's event type. + * + * @param imports Fully qualified class names to import + * @return this builder + */ + public Builder requiredImports(String... imports) { + this.requiredImports = imports; + return this; + } + + /** + * Set whether the event is cancelable. + * + * @param cancelable true if the event can be canceled + * @return this builder + */ + public Builder cancelable(boolean cancelable) { + this.cancelable = cancelable; + return this; + } + + /** + * Build the hook definition. + * + * @return A new HookDefinition instance + */ + public HookDefinition build() { + return new HookDefinition(this); + } + } + + // ==================== Object Methods ==================== + + @Override + public String toString() { + return "HookDefinition{" + + "hookName='" + hookName + '\'' + + ", eventClassName='" + eventClassName + '\'' + + ", cancelable=" + cancelable + + '}'; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof HookDefinition)) return false; + HookDefinition other = (HookDefinition) obj; + return hookName.equals(other.hookName); + } + + @Override + public int hashCode() { + return hookName.hashCode(); + } +} diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index c5af0929b..abefb0930 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -1,5 +1,6 @@ package noppes.npcs.controllers; +import noppes.npcs.api.handler.IHookDefinition; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; @@ -14,17 +15,33 @@ import java.util.Map; /** * Controller for managing script hooks. * Allows addon mods to register custom hooks that appear in script editor GUIs. + * + *

Rich Hook Registration

+ * Supports full hook definitions with metadata for proper stub generation: + *
{@code
+ * ScriptHookController.Instance.registerHookDefinition("player",
+ *     HookDefinition.builder("onDBCTransform")
+ *         .eventClass(IDBCEvent.TransformEvent.class)
+ *         .requiredImports("com.dbc.api.event.IDBCEvent")
+ *         .build());
+ * }
*/ public class ScriptHookController implements IScriptHookHandler { public static ScriptHookController Instance; - // Built-in hooks per context + // Built-in hooks per context (legacy string list) private final Map> builtInHooks = new HashMap<>(); - // Addon-registered hooks per context + // Addon-registered hooks per context (legacy string list) private final Map> addonHooks = new HashMap<>(); + // Rich hook definitions per context (NEW) + private final Map> hookDefinitions = new HashMap<>(); + + // Revision counter for cache invalidation (NEW) + private int hookRevision = 0; + public ScriptHookController() { Instance = this; initializeBuiltInHooks(); @@ -36,160 +53,24 @@ public class ScriptHookController implements IScriptHookHandler { if (!context.hookContext.isEmpty()) { builtInHooks.put(context.hookContext, new ArrayList<>()); addonHooks.put(context.hookContext, new ArrayList<>()); + hookDefinitions.put(context.hookContext, new HashMap<>()); } } - // NPC hooks - registerBuiltIn(ScriptContext.NPC, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.INTERACT.function, - EnumScriptType.DIALOG.function, - EnumScriptType.DAMAGED.function, - EnumScriptType.KILLED.function, - EnumScriptType.ATTACK_MELEE.function, - EnumScriptType.ATTACK_SWING.function, - EnumScriptType.RANGED_LAUNCHED.function, - EnumScriptType.TARGET.function, - EnumScriptType.COLLIDE.function, - EnumScriptType.KILLS.function, - EnumScriptType.DIALOG_CLOSE.function, - EnumScriptType.TIMER.function, - EnumScriptType.TARGET_LOST.function, - EnumScriptType.PROJECTILE_TICK.function, - EnumScriptType.PROJECTILE_IMPACT.function, - EnumScriptType.ABILITY_TICK.function, - EnumScriptType.ABILITY_EXECUTE.function, - EnumScriptType.ABILITY_HIT.function, - EnumScriptType.ABILITY_START.function, - EnumScriptType.ABILITY_INTERRUPT.function, - EnumScriptType.ABILITY_COMPLETE.function - ); + // NPC hooks with full definitions + initializeNpcHooks(); - // Player hooks - registerBuiltIn(ScriptContext.PLAYER, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.INTERACT.function, - EnumScriptType.RIGHT_CLICK.function, - EnumScriptType.ATTACK.function, - EnumScriptType.ATTACKED.function, - EnumScriptType.DAMAGED_ENTITY.function, - EnumScriptType.DAMAGED.function, - EnumScriptType.KILLS.function, - EnumScriptType.KILLED.function, - EnumScriptType.DROP.function, - EnumScriptType.RESPAWN.function, - EnumScriptType.BREAK_BLOCK.function, - EnumScriptType.CHAT.function, - EnumScriptType.LOGIN.function, - EnumScriptType.LOGOUT.function, - EnumScriptType.KEY_PRESSED.function, - EnumScriptType.MOUSE_CLICKED.function, - EnumScriptType.TOSS.function, - EnumScriptType.PICKUP.function, - EnumScriptType.PICKUP_XP.function, - EnumScriptType.RANGED_CHARGE.function, - EnumScriptType.RANGED_LAUNCHED.function, - EnumScriptType.TIMER.function, - EnumScriptType.START_USING_ITEM.function, - EnumScriptType.USING_ITEM.function, - EnumScriptType.STOP_USING_ITEM.function, - EnumScriptType.FINISH_USING_ITEM.function, - EnumScriptType.CONTAINER_OPEN.function, - EnumScriptType.USE_HOE.function, - EnumScriptType.BONEMEAL.function, - EnumScriptType.FILL_BUCKET.function, - EnumScriptType.JUMP.function, - EnumScriptType.FALL.function, - EnumScriptType.WAKE_UP.function, - EnumScriptType.SLEEP.function, - EnumScriptType.PLAYSOUND.function, - EnumScriptType.LIGHTNING.function, - EnumScriptType.CHANGED_DIM.function, - EnumScriptType.QUEST_START.function, - EnumScriptType.QUEST_COMPLETED.function, - EnumScriptType.QUEST_TURNIN.function, - EnumScriptType.FACTION_POINTS.function, - EnumScriptType.DIALOG_OPEN.function, - EnumScriptType.DIALOG_OPTION.function, - EnumScriptType.DIALOG_CLOSE.function, - EnumScriptType.SCRIPT_COMMAND.function, - EnumScriptType.CUSTOM_GUI_CLOSED.function, - EnumScriptType.CUSTOM_GUI_BUTTON.function, - EnumScriptType.CUSTOM_GUI_SLOT.function, - EnumScriptType.CUSTOM_GUI_SLOT_CLICKED.function, - EnumScriptType.CUSTOM_GUI_SCROLL.function, - EnumScriptType.CUSTOM_GUI_TEXTFIELD.function, - EnumScriptType.PARTY_QUEST_COMPLETED.function, - EnumScriptType.PARTY_QUEST_SET.function, - EnumScriptType.PARTY_QUEST_TURNED_IN.function, - EnumScriptType.PARTY_INVITE.function, - EnumScriptType.PARTY_KICK.function, - EnumScriptType.PARTY_LEAVE.function, - EnumScriptType.PARTY_DISBAND.function, - EnumScriptType.ANIMATION_START.function, - EnumScriptType.ANIMATION_END.function, - EnumScriptType.ANIMATION_FRAME_ENTER.function, - EnumScriptType.ANIMATION_FRAME_EXIT.function, - EnumScriptType.PROFILE_CHANGE.function, - EnumScriptType.PROFILE_REMOVE.function, - EnumScriptType.PROFILE_CREATE.function, - EnumScriptType.ON_EFFECT_ADD.function, - EnumScriptType.ON_EFFECT_TICK.function, - EnumScriptType.ON_EFFECT_REMOVE.function - ); + // Player hooks with full definitions + initializePlayerHooks(); - // Block hooks - registerBuiltIn(ScriptContext.BLOCK, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.INTERACT.function, - EnumScriptType.FALLEN_UPON.function, - EnumScriptType.REDSTONE.function, - EnumScriptType.BROKEN.function, - EnumScriptType.EXPLODED.function, - EnumScriptType.RAIN_FILLED.function, - EnumScriptType.NEIGHBOR_CHANGED.function, - EnumScriptType.CLICKED.function, - EnumScriptType.HARVESTED.function, - EnumScriptType.COLLIDE.function, - EnumScriptType.TIMER.function - ); + // Block hooks with full definitions + initializeBlockHooks(); - // Item hooks - registerBuiltIn(ScriptContext.ITEM, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.TOSSED.function, - EnumScriptType.PICKEDUP.function, - EnumScriptType.SPAWN.function, - EnumScriptType.INTERACT.function, - EnumScriptType.RIGHT_CLICK.function, - EnumScriptType.ATTACK.function, - EnumScriptType.START_USING_ITEM.function, - EnumScriptType.USING_ITEM.function, - EnumScriptType.STOP_USING_ITEM.function, - EnumScriptType.FINISH_USING_ITEM.function - ); + // Item hooks with full definitions + initializeItemHooks(); - // Linked item hooks - registerBuiltIn(ScriptContext.LINKED_ITEM, - EnumScriptType.LINKED_ITEM_BUILD.function, - EnumScriptType.LINKED_ITEM_VERSION.function, - EnumScriptType.INIT.function, - EnumScriptType.TICK.function, - EnumScriptType.TOSSED.function, - EnumScriptType.PICKEDUP.function, - EnumScriptType.SPAWN.function, - EnumScriptType.INTERACT.function, - EnumScriptType.RIGHT_CLICK.function, - EnumScriptType.ATTACK.function, - EnumScriptType.START_USING_ITEM.function, - EnumScriptType.USING_ITEM.function, - EnumScriptType.STOP_USING_ITEM.function, - EnumScriptType.FINISH_USING_ITEM.function - ); + // Linked item hooks with full definitions + initializeLinkedItemHooks(); // Recipe hooks registerBuiltIn(ScriptContext.RECIPE, @@ -211,6 +92,372 @@ public class ScriptHookController implements IScriptHookHandler { ); } + private void initializeNpcHooks() { + String ctx = ScriptContext.NPC.hookContext; + String npcImport = "noppes.npcs.api.event.INpcEvent"; + String projImport = "noppes.npcs.api.event.IProjectileEvent"; + String abilityImport = "noppes.npcs.api.event.IAbilityEvent"; + + // Core lifecycle + registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, + "noppes.npcs.api.event.INpcEvent$InitEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, + "noppes.npcs.api.event.INpcEvent$UpdateEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.TIMER.function, + "noppes.npcs.api.event.INpcEvent$TimerEvent", npcImport); + + // Interaction + registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, + "noppes.npcs.api.event.INpcEvent$InteractEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.DIALOG.function, + "noppes.npcs.api.event.INpcEvent$DialogEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_CLOSE.function, + "noppes.npcs.api.event.INpcEvent$DialogClosedEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.COLLIDE.function, + "noppes.npcs.api.event.INpcEvent$CollideEvent", npcImport); + + // Combat + registerBuiltInDefinition(ctx, EnumScriptType.DAMAGED.function, + "noppes.npcs.api.event.INpcEvent$DamagedEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.KILLED.function, + "noppes.npcs.api.event.INpcEvent$DiedEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.KILLS.function, + "noppes.npcs.api.event.INpcEvent$KilledEntityEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.ATTACK_MELEE.function, + "noppes.npcs.api.event.INpcEvent$MeleeAttackEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.ATTACK_SWING.function, + "noppes.npcs.api.event.INpcEvent$SwingEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.RANGED_LAUNCHED.function, + "noppes.npcs.api.event.INpcEvent$RangedLaunchedEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.TARGET.function, + "noppes.npcs.api.event.INpcEvent$TargetEvent", npcImport); + registerBuiltInDefinition(ctx, EnumScriptType.TARGET_LOST.function, + "noppes.npcs.api.event.INpcEvent$TargetLostEvent", npcImport); + + // Projectile events + registerBuiltInDefinition(ctx, EnumScriptType.PROJECTILE_TICK.function, + "noppes.npcs.api.event.IProjectileEvent$UpdateEvent", projImport); + registerBuiltInDefinition(ctx, EnumScriptType.PROJECTILE_IMPACT.function, + "noppes.npcs.api.event.IProjectileEvent$ImpactEvent", projImport); + + // Ability events + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_START.function, + "noppes.npcs.api.event.IAbilityEvent$StartEvent", abilityImport); + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_EXECUTE.function, + "noppes.npcs.api.event.IAbilityEvent$ExecuteEvent", abilityImport); + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_HIT.function, + "noppes.npcs.api.event.IAbilityEvent$HitEvent", abilityImport); + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_TICK.function, + "noppes.npcs.api.event.IAbilityEvent$TickEvent", abilityImport); + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_INTERRUPT.function, + "noppes.npcs.api.event.IAbilityEvent$InterruptEvent", abilityImport); + registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_COMPLETE.function, + "noppes.npcs.api.event.IAbilityEvent$CompleteEvent", abilityImport); + } + + private void initializePlayerHooks() { + String ctx = ScriptContext.PLAYER.hookContext; + String playerImport = "noppes.npcs.api.event.IPlayerEvent"; + String questImport = "noppes.npcs.api.event.IQuestEvent"; + String dialogImport = "noppes.npcs.api.event.IDialogEvent"; + String factionImport = "noppes.npcs.api.event.IFactionEvent"; + String partyImport = "noppes.npcs.api.event.IPartyEvent"; + String guiImport = "noppes.npcs.api.event.ICustomGuiEvent"; + String animImport = "noppes.npcs.api.event.IAnimationEvent"; + + // Core lifecycle + registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, + "noppes.npcs.api.event.IPlayerEvent$InitEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, + "noppes.npcs.api.event.IPlayerEvent$UpdateEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.TIMER.function, + "noppes.npcs.api.event.IPlayerEvent$TimerEvent", playerImport); + + // Combat + registerBuiltInDefinition(ctx, EnumScriptType.ATTACK.function, + "noppes.npcs.api.event.IPlayerEvent$AttackEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.ATTACKED.function, + "noppes.npcs.api.event.IPlayerEvent$AttackedEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.DAMAGED.function, + "noppes.npcs.api.event.IPlayerEvent$DamagedEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.DAMAGED_ENTITY.function, + "noppes.npcs.api.event.IPlayerEvent$DamagedEntityEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.KILLS.function, + "noppes.npcs.api.event.IPlayerEvent$KilledEntityEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.KILLED.function, + "noppes.npcs.api.event.IPlayerEvent$DiedEvent", playerImport); + + // Interaction + registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, + "noppes.npcs.api.event.IPlayerEvent$InteractEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.RIGHT_CLICK.function, + "noppes.npcs.api.event.IPlayerEvent$RightClickEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.BREAK_BLOCK.function, + "noppes.npcs.api.event.IPlayerEvent$BreakEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.CHAT.function, + "noppes.npcs.api.event.IPlayerEvent$ChatEvent", playerImport); + + // Connection + registerBuiltInDefinition(ctx, EnumScriptType.LOGIN.function, + "noppes.npcs.api.event.IPlayerEvent$LoginEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.LOGOUT.function, + "noppes.npcs.api.event.IPlayerEvent$LogoutEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.RESPAWN.function, + "noppes.npcs.api.event.IPlayerEvent$RespawnEvent", playerImport); + + // Input + registerBuiltInDefinition(ctx, EnumScriptType.KEY_PRESSED.function, + "noppes.npcs.api.event.IPlayerEvent$KeyPressedEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.MOUSE_CLICKED.function, + "noppes.npcs.api.event.IPlayerEvent$MouseClickedEvent", playerImport); + + // Items + registerBuiltInDefinition(ctx, EnumScriptType.PICKUP.function, + "noppes.npcs.api.event.IPlayerEvent$PickUpEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.TOSS.function, + "noppes.npcs.api.event.IPlayerEvent$TossEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.DROP.function, + "noppes.npcs.api.event.IPlayerEvent$DropEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.PICKUP_XP.function, + "noppes.npcs.api.event.IPlayerEvent$PickupXPEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.START_USING_ITEM.function, + "noppes.npcs.api.event.IPlayerEvent$StartUsingItem", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.USING_ITEM.function, + "noppes.npcs.api.event.IPlayerEvent$UsingItem", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.STOP_USING_ITEM.function, + "noppes.npcs.api.event.IPlayerEvent$StopUsingItem", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.FINISH_USING_ITEM.function, + "noppes.npcs.api.event.IPlayerEvent$FinishUsingItem", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.CONTAINER_OPEN.function, + "noppes.npcs.api.event.IPlayerEvent$ContainerOpen", playerImport); + + // Movement + registerBuiltInDefinition(ctx, EnumScriptType.JUMP.function, + "noppes.npcs.api.event.IPlayerEvent$JumpEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.FALL.function, + "noppes.npcs.api.event.IPlayerEvent$FallEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.CHANGED_DIM.function, + "noppes.npcs.api.event.IPlayerEvent$ChangedDimension", playerImport); + + // World + registerBuiltInDefinition(ctx, EnumScriptType.RANGED_CHARGE.function, + "noppes.npcs.api.event.IPlayerEvent$RangedChargeEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.RANGED_LAUNCHED.function, + "noppes.npcs.api.event.IPlayerEvent$RangedLaunchedEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.USE_HOE.function, + "noppes.npcs.api.event.IPlayerEvent$UseHoeEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.BONEMEAL.function, + "noppes.npcs.api.event.IPlayerEvent$BonemealEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.FILL_BUCKET.function, + "noppes.npcs.api.event.IPlayerEvent$FillBucketEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.WAKE_UP.function, + "noppes.npcs.api.event.IPlayerEvent$WakeUpEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.SLEEP.function, + "noppes.npcs.api.event.IPlayerEvent$SleepEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.PLAYSOUND.function, + "noppes.npcs.api.event.IPlayerEvent$SoundEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.LIGHTNING.function, + "noppes.npcs.api.event.IPlayerEvent$LightningEvent", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.SCRIPT_COMMAND.function, + "noppes.npcs.api.event.ICustomNPCsEvent$ScriptedCommandEvent", "noppes.npcs.api.event.ICustomNPCsEvent"); + + // Quest events + registerBuiltInDefinition(ctx, EnumScriptType.QUEST_START.function, + "noppes.npcs.api.event.IQuestEvent$QuestStartEvent", questImport); + registerBuiltInDefinition(ctx, EnumScriptType.QUEST_COMPLETED.function, + "noppes.npcs.api.event.IQuestEvent$QuestCompletedEvent", questImport); + registerBuiltInDefinition(ctx, EnumScriptType.QUEST_TURNIN.function, + "noppes.npcs.api.event.IQuestEvent$QuestTurnedInEvent", questImport); + + // Dialog events + registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_OPEN.function, + "noppes.npcs.api.event.IDialogEvent$DialogOpen", dialogImport); + registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_OPTION.function, + "noppes.npcs.api.event.IDialogEvent$DialogOption", dialogImport); + registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_CLOSE.function, + "noppes.npcs.api.event.IDialogEvent$DialogClosed", dialogImport); + + // Faction events + registerBuiltInDefinition(ctx, EnumScriptType.FACTION_POINTS.function, + "noppes.npcs.api.event.IFactionEvent$FactionPoints", factionImport); + + // Custom GUI events + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_CLOSED.function, + "noppes.npcs.api.event.ICustomGuiEvent$CloseEvent", guiImport); + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_BUTTON.function, + "noppes.npcs.api.event.ICustomGuiEvent$ButtonEvent", guiImport); + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_SLOT.function, + "noppes.npcs.api.event.ICustomGuiEvent$SlotEvent", guiImport); + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_SLOT_CLICKED.function, + "noppes.npcs.api.event.ICustomGuiEvent$SlotClickEvent", guiImport); + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_SCROLL.function, + "noppes.npcs.api.event.ICustomGuiEvent$ScrollEvent", guiImport); + registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_TEXTFIELD.function, + "noppes.npcs.api.event.ICustomGuiEvent$UnfocusedEvent", guiImport); + + // Party events + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_QUEST_COMPLETED.function, + "noppes.npcs.api.event.IPartyEvent$PartyQuestCompletedEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_QUEST_SET.function, + "noppes.npcs.api.event.IPartyEvent$PartyQuestSetEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_QUEST_TURNED_IN.function, + "noppes.npcs.api.event.IPartyEvent$PartyQuestTurnedInEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_INVITE.function, + "noppes.npcs.api.event.IPartyEvent$PartyInviteEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_KICK.function, + "noppes.npcs.api.event.IPartyEvent$PartyKickEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_LEAVE.function, + "noppes.npcs.api.event.IPartyEvent$PartyLeaveEvent", partyImport); + registerBuiltInDefinition(ctx, EnumScriptType.PARTY_DISBAND.function, + "noppes.npcs.api.event.IPartyEvent$PartyDisbandEvent", partyImport); + + // Animation events + registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_START.function, + "noppes.npcs.api.event.IAnimationEvent$Started", animImport); + registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_END.function, + "noppes.npcs.api.event.IAnimationEvent$Ended", animImport); + registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_FRAME_ENTER.function, + "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Entered", animImport); + registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_FRAME_EXIT.function, + "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Exited", animImport); + + // Profile events + registerBuiltInDefinition(ctx, EnumScriptType.PROFILE_CHANGE.function, + "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Changed", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.PROFILE_REMOVE.function, + "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Removed", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.PROFILE_CREATE.function, + "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Create", playerImport); + + // Effect events (for player context) + registerBuiltInDefinition(ctx, EnumScriptType.ON_EFFECT_ADD.function, + "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Added", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.ON_EFFECT_TICK.function, + "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Ticked", playerImport); + registerBuiltInDefinition(ctx, EnumScriptType.ON_EFFECT_REMOVE.function, + "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Removed", playerImport); + } + + private void initializeBlockHooks() { + String ctx = ScriptContext.BLOCK.hookContext; + String blockImport = "noppes.npcs.api.event.IBlockEvent"; + + registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, + "noppes.npcs.api.event.IBlockEvent$InitEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, + "noppes.npcs.api.event.IBlockEvent$UpdateEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, + "noppes.npcs.api.event.IBlockEvent$InteractEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.FALLEN_UPON.function, + "noppes.npcs.api.event.IBlockEvent$EntityFallenUponEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.REDSTONE.function, + "noppes.npcs.api.event.IBlockEvent$RedstoneEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.BROKEN.function, + "noppes.npcs.api.event.IBlockEvent$BreakEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.EXPLODED.function, + "noppes.npcs.api.event.IBlockEvent$ExplodedEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.RAIN_FILLED.function, + "noppes.npcs.api.event.IBlockEvent$RainFillEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.NEIGHBOR_CHANGED.function, + "noppes.npcs.api.event.IBlockEvent$NeighborChangedEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.CLICKED.function, + "noppes.npcs.api.event.IBlockEvent$ClickedEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.HARVESTED.function, + "noppes.npcs.api.event.IBlockEvent$HarvestedEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.COLLIDE.function, + "noppes.npcs.api.event.IBlockEvent$CollidedEvent", blockImport); + registerBuiltInDefinition(ctx, EnumScriptType.TIMER.function, + "noppes.npcs.api.event.IBlockEvent$TimerEvent", blockImport); + } + + private void initializeItemHooks() { + String ctx = ScriptContext.ITEM.hookContext; + String itemImport = "noppes.npcs.api.event.IItemEvent"; + + registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, + "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, + "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.TOSSED.function, + "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.PICKEDUP.function, + "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.SPAWN.function, + "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, + "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.RIGHT_CLICK.function, + "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.ATTACK.function, + "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.START_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.STOP_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.FINISH_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); + } + + private void initializeLinkedItemHooks() { + String ctx = ScriptContext.LINKED_ITEM.hookContext; + String itemImport = "noppes.npcs.api.event.IItemEvent"; + String linkedImport = "noppes.npcs.api.event.ILinkedItemEvent"; + + // Linked item specific hooks + registerBuiltInDefinition(ctx, EnumScriptType.LINKED_ITEM_BUILD.function, + "noppes.npcs.api.event.ILinkedItemEvent$BuildEvent", linkedImport); + registerBuiltInDefinition(ctx, EnumScriptType.LINKED_ITEM_VERSION.function, + "noppes.npcs.api.event.ILinkedItemEvent$VersionChangeEvent", linkedImport); + + // Standard item hooks + registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, + "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, + "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.TOSSED.function, + "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.PICKEDUP.function, + "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.SPAWN.function, + "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, + "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.RIGHT_CLICK.function, + "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.ATTACK.function, + "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.START_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.STOP_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); + registerBuiltInDefinition(ctx, EnumScriptType.FINISH_USING_ITEM.function, + "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); + } + + /** + * Register a built-in hook with full definition metadata. + */ + private void registerBuiltInDefinition(String context, String hookName, String eventClassName, String... imports) { + HookDefinition def = HookDefinition.builder(hookName) + .eventClass(eventClassName) + .requiredImports(imports) + .build(); + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new HashMap<>(); + hookDefinitions.put(context, contextDefs); + } + contextDefs.put(hookName, def); + + // Also register in legacy string list + registerBuiltIn(context, hookName); + } + /** * Register built-in hooks for a ScriptContext. */ @@ -229,6 +476,72 @@ public class ScriptHookController implements IScriptHookHandler { } } + // ==================== RICH HOOK REGISTRATION (NEW API) ==================== + + @Override + public void registerHookDefinition(String context, IHookDefinition definition) { + if (context == null || definition == null || definition.hookName() == null) { + return; + } + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new HashMap<>(); + hookDefinitions.put(context, contextDefs); + } + + // Store as HookDefinition (implementation) + if (definition instanceof HookDefinition) { + contextDefs.put(definition.hookName(), (HookDefinition) definition); + } else { + // Convert foreign IHookDefinition to HookDefinition + HookDefinition def = HookDefinition.builder(definition.hookName()) + .eventClass(definition.eventClassName()) + .paramNames(definition.paramNames()) + .requiredImports(definition.requiredImports()) + .cancelable(definition.isCancelable()) + .build(); + contextDefs.put(definition.hookName(), def); + } + + // Also add to legacy addon hooks list for backward compatibility + registerHook(context, definition.hookName()); + + // Increment revision for cache invalidation + hookRevision++; + } + + @Override + public IHookDefinition getHookDefinition(String context, String hookName) { + if (context == null || hookName == null) { + return null; + } + + Map contextDefs = hookDefinitions.get(context); + return contextDefs != null ? contextDefs.get(hookName) : null; + } + + @Override + public List getAllHookDefinitions(String context) { + if (context == null) { + return Collections.emptyList(); + } + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null || contextDefs.isEmpty()) { + return Collections.emptyList(); + } + + return new ArrayList<>(contextDefs.values()); + } + + @Override + public int getHookRevision() { + return hookRevision; + } + + // ==================== CONVENIENCE METHODS ==================== + /** * Convenience method to register a hook using ScriptContext. * @param context The script context @@ -248,7 +561,18 @@ public class ScriptHookController implements IScriptHookHandler { registerHook(context.hookContext, fn); } } - + + /** + * Convenience method to register a hook definition using ScriptContext. + * @param context The script context + * @param definition The hook definition + */ + public void registerHookDefinition(ScriptContext context, IHookDefinition definition) { + registerHookDefinition(context.hookContext, definition); + } + + // ==================== LEGACY HOOK REGISTRATION ==================== + @Override public void registerHook(String context, String functionName) { if (context == null || functionName == null || functionName.isEmpty()) { diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 8734304d9..6f6d6e55b 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -6,9 +6,11 @@ import io.github.somehussar.janinoloader.api.script.IScriptClassBody; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.NBTTags; +import noppes.npcs.api.handler.IHookDefinition; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptController; +import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.janino.annotations.ParamName; import org.codehaus.commons.compiler.InternalCompilerException; @@ -52,6 +54,10 @@ public abstract class JaninoScript implements IScriptUnit { private final String[] defaultImports; + // Hook definition cache for fast lookup (NEW) + private Map hookDefinitionCache; + private int lastHookRevision = -1; + protected JaninoScript(Class type, String[] defaultImports, boolean isClient) { this.type = type; this.defaultImports = defaultImports != null ? defaultImports : new String[0]; @@ -71,6 +77,79 @@ public abstract class JaninoScript implements IScriptUnit { this(type, defaultImports, false); } + // ==================== HOOK CONTEXT (Abstract) ==================== + + /** + * Get the hook context identifier for this script type. + * Used to look up hook definitions from the registry. + * + * @return Context string, e.g., "npc", "player", "block" + */ + protected abstract String getHookContext(); + + // ==================== HOOK DEFINITION CACHE ==================== + + /** + * Get hook definition from cache, rebuilding cache if revision changed. + * + * @param hookName The hook name to look up + * @return The hook definition, or null if not found + */ + protected IHookDefinition getHookDefinition(String hookName) { + if (ScriptHookController.Instance == null) { + return null; + } + + int currentRevision = ScriptHookController.Instance.getHookRevision(); + if (hookDefinitionCache == null || currentRevision != lastHookRevision) { + rebuildHookDefinitionCache(); + lastHookRevision = currentRevision; + } + + return hookDefinitionCache.get(hookName); + } + + /** + * Rebuild the hook definition cache from the registry. + */ + private void rebuildHookDefinitionCache() { + hookDefinitionCache = new HashMap<>(); + String context = getHookContext(); + + if (context == null || context.isEmpty() || ScriptHookController.Instance == null) { + return; + } + + for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { + hookDefinitionCache.put(def.hookName(), def); + } + } + + /** + * Get all required imports from hook definitions plus default imports. + * + * @return Array of all import statements needed + */ + protected String[] collectAllImports() { + Set allImports = new LinkedHashSet<>(); + + // Add default imports first + Collections.addAll(allImports, defaultImports); + + // Add imports from all hook definitions for this context + String context = getHookContext(); + if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { + for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { + String[] imports = def.requiredImports(); + if (imports != null) { + Collections.addAll(allImports, imports); + } + } + } + + return allImports.toArray(new String[0]); + } + protected T getUnsafe() { return scriptBody.get(); } @@ -119,11 +198,16 @@ public abstract class JaninoScript implements IScriptUnit { } /** - * Feed the code into the engine and compile it + * Feed the code into the engine and compile it. + * Rebuilds imports from hook definitions before compilation. */ public void compileScript(String code) { try { + // Rebuild imports from current hook definitions + defaults + String[] allImports = collectAllImports(); + builder.setDefaultImports(allImports); + scriptBody.setScript(code); } catch (InternalCompilerException e ) { Throwable parentCause = null; @@ -330,65 +414,43 @@ public abstract class JaninoScript implements IScriptUnit { return script != null && !script.isEmpty(); } - @Override - public String generateHookStub(String hookName, Object hookData) { - hookResolver.clearResolutionCaches(); + @Override + public String generateHookStub(String hookName, Object hookData) { + hookResolver.clearResolutionCaches(); - Method method = hookResolver.getMethodForDisplayName(hookName); - if (method != null) - return generateMethodStub(method); + // 1. Try interface method (existing @ScriptHook annotated methods) + Method method = hookResolver.getMethodForDisplayName(hookName); + if (method != null) + return generateMethodStub(method); - // Fallback: try to find the event class by naming convention - String eventType = findEventTypeForHook(hookName); - if (eventType != null) { - return String.format("public void %s(%s event) {\n \n}\n", hookName, eventType); + // 2. Look up in hook definition registry (addon hooks, dynamic hooks) + IHookDefinition def = getHookDefinition(hookName); + if (def != null) { + return generateStubFromDefinition(def); } - // Last resort: generic Object parameter + // 3. Last resort: generic Object parameter return String.format("public void %s(Object event) {\n \n}\n", hookName); } /** - * Tries to find the event type for a hook by searching API event interfaces. - * Uses naming convention: hookName "questStart" -> event class "QuestStartEvent" + * Generate a method stub from a hook definition. * - * @param hookName The hook name (e.g., "questStart", "dialogOpen") - * @return The usable type name (e.g., "IQuestEvent.QuestStartEvent") or null if not found + * @param def The hook definition containing type info + * @return Generated stub code */ - private String findEventTypeForHook(String hookName) { - if (hookName == null || hookName.isEmpty()) - return null; + private String generateStubFromDefinition(IHookDefinition def) { + String typeName = def.getUsableTypeName(); + String[] params = def.paramNames(); + String paramName = (params != null && params.length > 0) ? params[0] : "event"; - // Convert hook name to expected event class name - // questStart -> QuestStartEvent - String expectedEventName = Character.toUpperCase(hookName.charAt(0)) + hookName.substring(1) + "Event"; - - // Search in known API event interfaces - Class[] eventInterfaces = { - noppes.npcs.api.event.IPlayerEvent.class, - noppes.npcs.api.event.INpcEvent.class, - noppes.npcs.api.event.IQuestEvent.class, - noppes.npcs.api.event.IDialogEvent.class, - noppes.npcs.api.event.IFactionEvent.class, - noppes.npcs.api.event.IPartyEvent.class, - noppes.npcs.api.event.ICustomGuiEvent.class, - noppes.npcs.api.event.IAnimationEvent.class, - noppes.npcs.api.event.IBlockEvent.class, - noppes.npcs.api.event.IItemEvent.class, - noppes.npcs.api.event.IProjectileEvent.class, - noppes.npcs.api.event.IAbilityEvent.class, - noppes.npcs.api.event.IForgeEvent.class - }; - - for (Class eventInterface : eventInterfaces) { - for (Class nested : eventInterface.getDeclaredClasses()) { - if (nested.getSimpleName().equals(expectedEventName)) { - return getUsableTypeName(nested); - } - } + if (typeName == null || typeName.isEmpty()) { + return String.format("public void %s(Object %s) {\n \n}\n", + def.hookName(), paramName); } - return null; + return String.format("public void %s(%s %s) {\n \n}\n", + def.hookName(), typeName, paramName); } /** diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java index 1af8dea35..637374775 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java @@ -17,6 +17,7 @@ public class JaninoNpcScript extends JaninoScript { /** * Default imports for NPC scripts. * These are automatically available in user code without explicit import statements. + * Additional imports from registered hook definitions are added at compile time. */ private static final String[] DEFAULT_IMPORTS = { "noppes.npcs.api.event.INpcEvent", @@ -31,6 +32,11 @@ public class JaninoNpcScript extends JaninoScript { super(Functions.class, DEFAULT_IMPORTS); } + @Override + protected String getHookContext() { + return "npc"; + } + /** * Interface defining the hook methods available for NPC scripts. * Each method corresponds to an NPC event from ScriptHookController CONTEXT_NPC. diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java index c755c700c..d29b1b15e 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -43,6 +43,7 @@ public class JaninoPlayerScript extends JaninoScript Date: Thu, 22 Jan 2026 18:44:41 -0500 Subject: [PATCH 256/358] No @ScriptHook needed anymore --- .../npcs/janino/JaninoHookResolver.java | 273 +++++------------- .../java/noppes/npcs/janino/JaninoScript.java | 154 ++-------- .../npcs/janino/annotations/ScriptHook.java | 22 -- .../npcs/janino/impl/JaninoNpcScript.java | 114 +------- .../npcs/janino/impl/JaninoPlayerScript.java | 154 +--------- 5 files changed, 108 insertions(+), 609 deletions(-) delete mode 100644 src/main/java/noppes/npcs/janino/annotations/ScriptHook.java diff --git a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java index b77233b12..87b969e25 100644 --- a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java +++ b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java @@ -1,47 +1,24 @@ package noppes.npcs.janino; -import noppes.npcs.constants.EnumScriptType; -import noppes.npcs.janino.annotations.ScriptHook; - +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.*; -import java.util.stream.Collectors; /** - * Resolves script hook methods for a given interface type using runtime + * Resolves script hook methods for compiled script classes using runtime * argument values. Resolution favors exact matches and then the "closest" * assignable parameter types (measured by inheritance distance). * - * The resolver keeps simple caches for previously resolved signatures and - * supports mapping enum-based hook names (via the {@code @ScriptHook} - * annotation) to methods. - * - *

Dynamic Hook Support

- * This resolver supports two modes: - *
    - *
  • Interface-only: Only methods defined in the interface are found (legacy behavior)
  • - *
  • Compiled class: Methods on the actual compiled script class are found, - * allowing users to define custom methods for Forge events, addon hooks, etc.
  • - *
- * Use {@link #resolveHookMethod(String, Object[], Object)} to search the compiled instance. + * Methods are resolved dynamically from the compiled class - no interface + * definitions required. */ public final class JaninoHookResolver { private final Class interfaceType; - /* Methods explicitly mapped from EnumScriptType via @ScriptHook */ - private final Map enumHookLookup = new HashMap<>(); - /** - * Cache for resolved hook methods. Keys are generated by {@link #buildSignatureKey} - * and represent the unique combination of hook name and runtime argument types. - * This avoids expensive recursive reflection lookups on every hook call. - */ - private final Map signatureCache = new HashMap<>(); - - /** - * Cache for methods found on compiled classes (keyed by class + signature). - * Separate from signatureCache to handle recompilation. + * Cache for methods found on compiled classes (keyed by signature). */ private final Map compiledClassCache = new HashMap<>(); @@ -51,12 +28,13 @@ public final class JaninoHookResolver { */ private Class lastCompiledClass; - /* Lazily built list of available hooks and a mapping for display names */ - private boolean hookListInitialized; - /* List of names displayed in GUI hooks list (accounts for overload names) */ - private final List displayHookList = new ArrayList<>(); - /* Display name -> Method map */ - private final Map displayHookMethods = new HashMap<>(); + // MethodHandle caches + private final Map compiledHandleCache = new HashMap<>(); + + // Method -> MethodHandle for identity-based deduplication + private final Map methodToHandle = new IdentityHashMap<>(); + + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); /** * Create a resolver for the provided script interface type. @@ -64,7 +42,6 @@ public final class JaninoHookResolver { */ public JaninoHookResolver(Class interfaceType) { this.interfaceType = Objects.requireNonNull(interfaceType, "interfaceType"); - initializeEnumHookMethods(); } /** @@ -72,114 +49,64 @@ public final class JaninoHookResolver { * method resolution. Call when scripts or available methods change. */ public void clearResolutionCaches() { - signatureCache.clear(); compiledClassCache.clear(); + compiledHandleCache.clear(); + methodToHandle.clear(); lastCompiledClass = null; } /** - * Resolve a hook method by name and runtime argument values (interface-only mode). - * - * This first checks any enum-mapped methods (via {@code @ScriptHook}), - * then a signature-based cache and finally attempts full candidate - * resolution using {@link #resolveHookMethodInternal}. - * - * @param hookName hook method name (or enum function name) - * @param args runtime arguments to use for overload resolution - * @return the resolved {@link Method} or {@code null} if none matched - */ - public Method resolveHookMethod(String hookName, Object[] args) { - return resolveHookMethod(hookName, args, null); - } - - /** - * Resolve a hook method by name, searching both the compiled class and interface. - * - *

This enables dynamic hook support: users can define methods for any hook - * (Forge events, addon hooks, etc.) without needing them in the interface definition.

+ * Resolve a hook method by name, searching the compiled class. * *

Resolution order:

*
    - *
  1. Enum-mapped methods (via {@code @ScriptHook})
  2. - *
  3. Compiled class methods (user-defined, including custom hooks)
  4. - *
  5. Interface methods (base hooks with defaults)
  6. + *
  7. Compiled class methods (user-defined)
  8. *
  9. Fallback to no-arg versions
  10. *
* - *

Performance: Method lookups are cached. First call per hook does reflection - * (~microseconds), subsequent calls use cache (essentially free).

- * - * @param hookName hook method name (or enum function name) + * @param hookName hook method name * @param args runtime arguments to use for overload resolution - * @param compiledInstance the compiled script instance (or null for interface-only) + * @param compiledInstance the compiled script instance * @return the resolved {@link Method} or {@code null} if none matched */ public Method resolveHookMethod(String hookName, Object[] args, Object compiledInstance) { - if (hookName == null || hookName.isEmpty()) + if (hookName == null || hookName.isEmpty() || compiledInstance == null) return null; - // 1. Check enum-mapped methods first (fastest path) - Method method = enumHookLookup.get(hookName); - if (method != null) - return method; - Object[] safeArgs = args == null ? new Object[0] : args; String signatureKey = buildSignatureKey(hookName, safeArgs); - // 2. If we have a compiled instance, search its class first - if (compiledInstance != null) { - Class compiledClass = compiledInstance.getClass(); + Class compiledClass = compiledInstance.getClass(); - // Clear cache if compiled class changed (recompilation) - if (lastCompiledClass != compiledClass) { - compiledClassCache.clear(); - lastCompiledClass = compiledClass; - } - - // Check compiled class cache - method = compiledClassCache.get(signatureKey); - if (method != null) - return method; - - // Search compiled class for matching method - method = resolveHookMethodInternal(compiledClass, hookName, safeArgs); - if (method != null) { - compiledClassCache.put(signatureKey, method); - return method; - } - - // Try no-arg version on compiled class - String noArgKey = buildSignatureKey(hookName, new Object[0]); - method = compiledClassCache.get(noArgKey); - if (method != null) - return method; - - method = resolveHookMethodInternal(compiledClass, hookName, new Object[0]); - if (method != null) { - compiledClassCache.put(noArgKey, method); - return method; - } + // Clear cache if compiled class changed (recompilation) + if (lastCompiledClass != compiledClass) { + compiledClassCache.clear(); + compiledHandleCache.clear(); + methodToHandle.clear(); + lastCompiledClass = compiledClass; } - // 3. Fall back to interface-only search - method = signatureCache.get(signatureKey); + // Check compiled class cache + Method method = compiledClassCache.get(signatureKey); if (method != null) return method; - method = resolveHookMethodInternal(interfaceType, hookName, safeArgs); + // Search compiled class for matching method + method = resolveHookMethodInternal(compiledClass, hookName, safeArgs); if (method != null) { - signatureCache.put(signatureKey, method); + compiledClassCache.put(signatureKey, method); return method; } - String noArgSignatureKey = buildSignatureKey(hookName, new Object[0]); - method = signatureCache.get(noArgSignatureKey); + // Try no-arg version on compiled class + String noArgKey = buildSignatureKey(hookName, new Object[0]); + method = compiledClassCache.get(noArgKey); if (method != null) return method; - method = resolveHookMethodInternal(interfaceType, hookName, new Object[0]); + method = resolveHookMethodInternal(compiledClass, hookName, new Object[0]); if (method != null) { - signatureCache.put(noArgSignatureKey, method); + compiledClassCache.put(noArgKey, method); return method; } @@ -187,83 +114,55 @@ public final class JaninoHookResolver { } /** - * Returns an immutable copy of the available hook display names. + * Resolve a MethodHandle for the given hook. Faster than Method.invoke(). + * Uses existing Method resolution, then converts to cached MethodHandle. */ - public List getDisplayHookList() { - ensureHookListCreated(); - return new ArrayList<>(displayHookList); - } - - public Method getMethodForDisplayName(String displayName) { - if (displayName == null || displayName.isEmpty()) + public MethodHandle resolveHookHandle(String hookName, Object[] args, Object compiledInstance) { + if (hookName == null || hookName.isEmpty() || compiledInstance == null) return null; - ensureHookListCreated(); - return displayHookMethods.get(displayName); - } + Object[] safeArgs = args == null ? new Object[0] : args; + String signatureKey = buildSignatureKey(hookName, safeArgs); - /** - * Builds the human-readable hook list and display-name -> Method map. - * The list is computed lazily on first access and cached. - */ - private void ensureHookListCreated() { - if (hookListInitialized) - return; + Class compiledClass = compiledInstance.getClass(); - Map> grouped = Arrays.stream(interfaceType.getDeclaredMethods()) - .collect(Collectors.groupingBy(Method::getName)); + // Clear cache if compiled class changed (recompilation) + if (lastCompiledClass != compiledClass) { + compiledClassCache.clear(); + compiledHandleCache.clear(); + methodToHandle.clear(); + lastCompiledClass = compiledClass; + } - for (Map.Entry> entry : grouped.entrySet()) { - List methods = entry.getValue(); + // Check handle cache + MethodHandle handle = compiledHandleCache.get(signatureKey); + if (handle != null) + return handle; - methods.sort(Comparator.comparingInt(Method::getParameterCount)); + // Resolve Method first + Method method = resolveHookMethod(hookName, args, compiledInstance); + if (method == null) + return null; - boolean first = true; - for (Method m : methods) { - if (Modifier.isFinal(m.getModifiers())) - continue; - - String displayName = first ? m.getName() : - m.getName() + "(" + Arrays.stream(m.getParameterTypes()) - .map(Class::getSimpleName) - .collect(Collectors.joining(", ")) + ")"; - - displayHookList.add(displayName); - displayHookMethods.put(displayName, m); - first = false; + // Convert Method to MethodHandle (deduplicated by Method identity) + handle = methodToHandle.get(method); + if (handle == null) { + try { + handle = LOOKUP.unreflect(method); + methodToHandle.put(method, handle); + } catch (IllegalAccessException e) { + return null; } } - hookListInitialized = true; + compiledHandleCache.put(signatureKey, handle); + return handle; } /** - * Maps EnumScriptType common names to their respective methods using - * the {@code @ScriptHook} annotation. + * Build a cache key for a hook name and runtime argument list. */ - private void initializeEnumHookMethods() { - for (Method method : interfaceType.getDeclaredMethods()) { - ScriptHook annotation = method.getAnnotation(ScriptHook.class); - if (annotation == null) - continue; - - for (EnumScriptType hookType : annotation.value()) { - enumHookLookup.put(hookType.function, method); - } - } - } - - /** - * Build a cache key for a hook name and runtime argument list. This key - * encodes the runtime classes of the provided arguments and is used for - * quick cache lookups. Note: keys are based on concrete runtime classes - * which may cause fragmentation when many subclasses are used. - * - * @param hookName hook name - * @param args runtime arguments - * @return a string key representing the hook signature - */ - public static String buildSignatureKey(String hookName, Object[] args) { + private static String buildSignatureKey(String hookName, Object[] args) { StringBuilder sb = new StringBuilder(); sb.append(hookName).append('#'); @@ -284,15 +183,7 @@ public final class JaninoHookResolver { /** * Internal resolution routine. Attempts to find the best matching method * on the provided class by comparing parameter counts and a simple - * match scoring function (see {@link #scoreMethodMatch}). - * - *

This searches all public methods (including inherited) to support - * both interface-defined hooks and user-defined custom methods.

- * - * @param targetClass the class to search (interface or compiled class) - * @param hookName the method name to find - * @param args runtime arguments for overload resolution - * @return the best matching method, or null if none found + * match scoring function. */ private static Method resolveHookMethodInternal(Class targetClass, String hookName, Object[] args) { Object[] safeArgs = args == null ? new Object[0] : args; @@ -300,7 +191,6 @@ public final class JaninoHookResolver { Method best = null; int bestScore = Integer.MAX_VALUE; - // Use getMethods() to include inherited methods (important for compiled classes) for (Method method : targetClass.getMethods()) { if (!method.getName().equals(hookName)) continue; @@ -331,13 +221,8 @@ public final class JaninoHookResolver { } /** - * Score how well a method's parameter types match the provided runtime - * arguments. Lower scores are better; {@link Integer#MAX_VALUE} indicates - * an incompatible parameter. - * - * @param paramTypes method parameter types - * @param args runtime argument values - * @return score (lower is better) or Integer.MAX_VALUE for no match + * Score how well a method's parameter types match the provided runtime arguments. + * Lower scores are better; Integer.MAX_VALUE indicates an incompatible parameter. */ private static int scoreMethodMatch(Class[] paramTypes, Object[] args) { int score = 0; @@ -392,16 +277,6 @@ public final class JaninoHookResolver { return type; } - /** - * Compute a simple inheritance distance between a child and parent class - * by walking superclasses. Returns a large value if {@code parent} is - * not assignable from {@code child}. - * - * Note: the matching algorithm uses superclass traversal to compute a - * distance metric; interface inheritance is not explored which can make - * matches against interfaces less optimal. Consider improving - * `inheritanceDistance` if interface-based scoring is required. - */ private static int inheritanceDistance(Class child, Class parent) { if (child.equals(parent)) return 0; diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 6f6d6e55b..8093b8224 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -12,18 +12,15 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.IScriptUnit; -import noppes.npcs.janino.annotations.ParamName; import org.codehaus.commons.compiler.InternalCompilerException; import org.codehaus.commons.compiler.Sandbox; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; +import java.lang.invoke.MethodHandle; import java.security.*; import java.util.*; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import java.util.stream.Collectors; public abstract class JaninoScript implements IScriptUnit { public boolean errored = false; @@ -286,26 +283,27 @@ public abstract class JaninoScript implements IScriptUnit { if (hookName == null || hookName.isEmpty()) return null; - // Ensure compiled first so we can search the compiled class ensureCompiled(); T t = getUnsafe(); if (t == null) return null; - // Pass compiled instance to enable dynamic hook resolution - // This allows users to define methods for ANY hook (Forge events, addon hooks, etc.) - Method method = hookResolver.resolveHookMethod(hookName, args, t); - if (method == null) - return null; - - Method invokeMethod = method; Object[] invokeArgs = args == null ? new Object[0] : args; + MethodHandle handle = hookResolver.resolveHookHandle(hookName, invokeArgs, t); + if (handle == null) + return null; + + final MethodHandle finalHandle = handle; try { return sandbox.confine((PrivilegedAction) () -> { try { - return invokeMethod.invoke(t, invokeArgs); - } catch (Exception e) { + // Prepend receiver to args for virtual method call + Object[] fullArgs = new Object[invokeArgs.length + 1]; + fullArgs[0] = t; + System.arraycopy(invokeArgs, 0, fullArgs, 1, invokeArgs.length); + return finalHandle.invokeWithArguments(fullArgs); + } catch (Throwable e) { appendConsole("Error calling hook " + hookName + ": " + e.getMessage()); return null; } @@ -416,127 +414,31 @@ public abstract class JaninoScript implements IScriptUnit { @Override public String generateHookStub(String hookName, Object hookData) { - hookResolver.clearResolutionCaches(); - - // 1. Try interface method (existing @ScriptHook annotated methods) - Method method = hookResolver.getMethodForDisplayName(hookName); - if (method != null) - return generateMethodStub(method); - - // 2. Look up in hook definition registry (addon hooks, dynamic hooks) + // Look up in hook definition registry IHookDefinition def = getHookDefinition(hookName); if (def != null) { - return generateStubFromDefinition(def); - } + String typeName = def.getUsableTypeName(); + String[] params = def.paramNames(); + String paramName = (params != null && params.length > 0) ? params[0] : "event"; - // 3. Last resort: generic Object parameter - return String.format("public void %s(Object event) {\n \n}\n", hookName); - } - - /** - * Generate a method stub from a hook definition. - * - * @param def The hook definition containing type info - * @return Generated stub code - */ - private String generateStubFromDefinition(IHookDefinition def) { - String typeName = def.getUsableTypeName(); - String[] params = def.paramNames(); - String paramName = (params != null && params.length > 0) ? params[0] : "event"; - - if (typeName == null || typeName.isEmpty()) { - return String.format("public void %s(Object %s) {\n \n}\n", - def.hookName(), paramName); - } - - return String.format("public void %s(%s %s) {\n \n}\n", - def.hookName(), typeName, paramName); - } - - /** - * Generates a stub string for a given Method. - * Handles nested classes properly (e.g., IPlayerEvent.InitEvent instead of just InitEvent). - */ - public static String generateMethodStub(Method method) { - String mods = Modifier.toString(method.getModifiers()); - // Remove 'abstract' from modifiers for implementation - mods = mods.replace("abstract ", "").replace("abstract", "").trim(); - if (!mods.isEmpty()) - mods += " "; - - String returnTypeStr = getUsableTypeName(method.getReturnType()); - String name = method.getName(); - - Map typeCount = new HashMap<>(); - String params = Arrays.stream(method.getParameters()) - .map(p -> { - String typeName = getUsableTypeName(p.getType()); - String simpleTypeName = p.getType().getSimpleName(); - - // Check for @ParamName annotation first - ParamName paramNameAnnotation = p.getAnnotation(ParamName.class); - String paramName; - - if (paramNameAnnotation != null) { - // Use annotated name - paramName = paramNameAnnotation.value(); - } else { - // Fall back to generated name based on simple name - String baseName = Character.toLowerCase(simpleTypeName.charAt(0)) + simpleTypeName.substring(1); - int count = typeCount.getOrDefault(baseName, 0) + 1; - typeCount.put(baseName, count); - paramName = count == 1 ? baseName : baseName + (count - 1); - } - - return typeName + " " + paramName; - }) - .collect(Collectors.joining(", ")); - - String body; - Class returnType = method.getReturnType(); - if (returnType == void.class) - body = ""; - else if (returnType.isPrimitive()) { - if (returnType == boolean.class) - body = " return false;"; - else if (returnType == char.class) - body = " return '\\0';"; - else - body = " return 0;"; - } else { - body = " return null;"; - } - - return String.format("%s%s %s(%s) {\n%s\n}\n", mods, returnTypeStr, name, params, body); - } - - /** - * Gets a usable type name for stub generation. - * For nested classes like IPlayerEvent.InitEvent, returns "IPlayerEvent.InitEvent" - * For top-level classes, returns the simple name. - * For primitives, returns the primitive name. - */ - private static String getUsableTypeName(Class type) { - if (type.isPrimitive() || type.getEnclosingClass() == null) { - return type.getSimpleName(); - } - - // Build the nested class chain: OuterClass.InnerClass.DeeperClass - StringBuilder sb = new StringBuilder(); - Class current = type; - while (current != null) { - if (sb.length() > 0) { - sb.insert(0, "."); + if (typeName != null && !typeName.isEmpty()) { + return String.format("public void %s(%s %s) {\n \n}\n", + def.hookName(), typeName, paramName); } - sb.insert(0, current.getSimpleName()); - current = current.getEnclosingClass(); } - return sb.toString(); + + // Fallback: generic Object parameter + return String.format("public void %s(Object event) {\n \n}\n", hookName); } public List getHookList() { - return hookResolver.getDisplayHookList(); + // Get hooks from registry based on context + String context = getHookContext(); + if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { + return ScriptHookController.Instance.getAllHooks(context); + } + return Collections.emptyList(); } diff --git a/src/main/java/noppes/npcs/janino/annotations/ScriptHook.java b/src/main/java/noppes/npcs/janino/annotations/ScriptHook.java deleted file mode 100644 index d4720b5d8..000000000 --- a/src/main/java/noppes/npcs/janino/annotations/ScriptHook.java +++ /dev/null @@ -1,22 +0,0 @@ -package noppes.npcs.janino.annotations; - -import noppes.npcs.constants.EnumScriptType; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation to mark script hook methods and associate them with their EnumScriptType. - * Used by JaninoScript to automatically build a map of hook types to methods. - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface ScriptHook { - /** - * The script type(s) this method handles. - * Multiple types can be specified for methods that handle multiple hooks. - */ - EnumScriptType[] value(); -} diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java index 637374775..4445ed8c6 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java @@ -1,35 +1,15 @@ package noppes.npcs.janino.impl; -import noppes.npcs.api.event.IAbilityEvent; -import noppes.npcs.api.event.INpcEvent; -import noppes.npcs.api.event.IProjectileEvent; -import noppes.npcs.constants.EnumScriptType; import noppes.npcs.janino.JaninoScript; -import noppes.npcs.janino.annotations.ParamName; -import noppes.npcs.janino.annotations.ScriptHook; /** * Janino (Java) script implementation for NPC events. - * This allows NPCs to use compiled Java code instead of ECMAScript. + * Hook methods are resolved dynamically by name - no interface definition required. */ public class JaninoNpcScript extends JaninoScript { - /** - * Default imports for NPC scripts. - * These are automatically available in user code without explicit import statements. - * Additional imports from registered hook definitions are added at compile time. - */ - private static final String[] DEFAULT_IMPORTS = { - "noppes.npcs.api.event.INpcEvent", - "noppes.npcs.api.event.IProjectileEvent", - "noppes.npcs.api.event.IAbilityEvent", - "noppes.npcs.api.entity.ICustomNpc", - "noppes.npcs.api.entity.IPlayer", - "noppes.npcs.api.NpcAPI" - }; - public JaninoNpcScript() { - super(Functions.class, DEFAULT_IMPORTS); + super(Functions.class, null); } @Override @@ -37,93 +17,5 @@ public class JaninoNpcScript extends JaninoScript { return "npc"; } - /** - * Interface defining the hook methods available for NPC scripts. - * Each method corresponds to an NPC event from ScriptHookController CONTEXT_NPC. - * Methods are annotated with @ScriptHook to map them to their EnumScriptType. - */ - public interface Functions { - // Core lifecycle events - @ScriptHook(EnumScriptType.INIT) - default void init(@ParamName("event") INpcEvent.InitEvent event) {} - - @ScriptHook(EnumScriptType.TICK) - default void tick(@ParamName("event") INpcEvent.UpdateEvent event) {} - - - // Interaction events - @ScriptHook(EnumScriptType.INTERACT) - default void interact(@ParamName("event") INpcEvent.InteractEvent event) {} - - @ScriptHook(EnumScriptType.DIALOG) - default void dialog(@ParamName("event") INpcEvent.DialogEvent event) {} - - @ScriptHook(EnumScriptType.DIALOG_CLOSE) - default void dialogClose(@ParamName("event") INpcEvent.DialogClosedEvent event) {} - - - // Combat events - damage - @ScriptHook(EnumScriptType.DAMAGED) - default void damaged(@ParamName("event") INpcEvent.DamagedEvent event) {} - - @ScriptHook(EnumScriptType.KILLED) - default void killed(@ParamName("event") INpcEvent.DiedEvent event) {} - - @ScriptHook(EnumScriptType.KILLS) - default void kills(@ParamName("event") INpcEvent.KilledEntityEvent event) {} - - - // Combat events - attacks - @ScriptHook(EnumScriptType.ATTACK_MELEE) - default void meleeAttack(@ParamName("event") INpcEvent.MeleeAttackEvent event) {} - - @ScriptHook(EnumScriptType.ATTACK_SWING) - default void meleeSwing(@ParamName("event") INpcEvent.SwingEvent event) {} - - @ScriptHook(EnumScriptType.RANGED_LAUNCHED) - default void rangedLaunched(@ParamName("event") INpcEvent.RangedLaunchedEvent event) {} - - - // Targeting events - @ScriptHook(EnumScriptType.TARGET) - default void target(@ParamName("event") INpcEvent.TargetEvent event) {} - - @ScriptHook(EnumScriptType.TARGET_LOST) - default void targetLost(@ParamName("event") INpcEvent.TargetLostEvent event) {} - - // Other NPC events - @ScriptHook(EnumScriptType.COLLIDE) - default void collide(@ParamName("event") INpcEvent.CollideEvent event) {} - - @ScriptHook(EnumScriptType.TIMER) - default void timer(@ParamName("event") INpcEvent.TimerEvent event) {} - - - // Projectile events - @ScriptHook(EnumScriptType.PROJECTILE_TICK) - default void projectileTick(@ParamName("event") IProjectileEvent.UpdateEvent event) {} - - @ScriptHook(EnumScriptType.PROJECTILE_IMPACT) - default void projectileImpact(@ParamName("event") IProjectileEvent.ImpactEvent event) {} - - - // Ability events - @ScriptHook(EnumScriptType.ABILITY_START) - default void abilityStart(@ParamName("event") IAbilityEvent.StartEvent event) {} - - @ScriptHook(EnumScriptType.ABILITY_EXECUTE) - default void abilityExecute(@ParamName("event") IAbilityEvent.ExecuteEvent event) {} - - @ScriptHook(EnumScriptType.ABILITY_HIT) - default void abilityHit(@ParamName("event") IAbilityEvent.HitEvent event) {} - - @ScriptHook(EnumScriptType.ABILITY_TICK) - default void abilityTick(@ParamName("event") IAbilityEvent.TickEvent event) {} - - @ScriptHook(EnumScriptType.ABILITY_INTERRUPT) - default void abilityInterrupt(@ParamName("event") IAbilityEvent.InterruptEvent event) {} - - @ScriptHook(EnumScriptType.ABILITY_COMPLETE) - default void abilityComplete(@ParamName("event") IAbilityEvent.CompleteEvent event) {} - } + public interface Functions {} } diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java index d29b1b15e..79c7718b6 100644 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java @@ -1,65 +1,15 @@ package noppes.npcs.janino.impl; -import noppes.npcs.api.event.IPlayerEvent; -import noppes.npcs.constants.EnumScriptType; import noppes.npcs.janino.JaninoScript; -import noppes.npcs.janino.annotations.ParamName; -import noppes.npcs.janino.annotations.ScriptHook; /** * Janino (Java) script implementation for player events. - * - *

Dynamic Hook Support

- * With the new dynamic hook resolution, users can define methods for ANY hook - - * not just those defined in the Functions interface. Simply write a public method - * with the hook name and matching parameter type: - * - *
{@code
- * // Standard hook (defined in interface)
- * public void init(IPlayerEvent.InitEvent event) {
- *     // ...
- * }
- *
- * // Custom addon hook (NOT in interface - still works!)
- * public void myAddonHook(MyAddonEvent event) {
- *     // ...
- * }
- *
- * // Any hook by name
- * public void someCustomHook(Object event) {
- *     // ...
- * }
- * }
- * - * The interface below defines core hooks for: - *
    - *
  • EnumScriptType mapping (so "init" maps to init() method)
  • - *
  • GUI hook list display
  • - *
  • Stub generation with correct parameter types
  • - *
+ * Hook methods are resolved dynamically by name - no interface definition required. */ public class JaninoPlayerScript extends JaninoScript { - /** - * Default imports for player scripts. - * These are automatically available in user code without explicit import statements. - * Additional imports from registered hook definitions are added at compile time. - */ - private static final String[] DEFAULT_IMPORTS = { - "noppes.npcs.api.event.IPlayerEvent", - "noppes.npcs.api.event.IQuestEvent", - "noppes.npcs.api.event.IDialogEvent", - "noppes.npcs.api.event.IFactionEvent", - "noppes.npcs.api.event.IPartyEvent", - "noppes.npcs.api.event.ICustomGuiEvent", - "noppes.npcs.api.event.IAnimationEvent", - "noppes.npcs.api.event.ICustomNPCsEvent", - "noppes.npcs.api.entity.IPlayer", - "noppes.npcs.api.NpcAPI" - }; - public JaninoPlayerScript() { - super(Functions.class, DEFAULT_IMPORTS, false); + super(Functions.class, null, false); } @Override @@ -67,103 +17,5 @@ public class JaninoPlayerScript extends JaninoScript Date: Thu, 22 Jan 2026 19:03:28 -0500 Subject: [PATCH 257/358] Cache Handle for O(1) lookups --- .../controllers/ScriptHookController.java | 626 ++++++------------ .../controllers/data/JaninoScriptHandler.java | 7 - .../npcs/janino/JaninoHookResolver.java | 240 +++---- .../java/noppes/npcs/janino/JaninoScript.java | 52 +- 4 files changed, 285 insertions(+), 640 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index abefb0930..8ecc8d43f 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -6,11 +6,7 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.RecipeScript; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Controller for managing script hooks. @@ -30,16 +26,10 @@ public class ScriptHookController implements IScriptHookHandler { public static ScriptHookController Instance; - // Built-in hooks per context (legacy string list) - private final Map> builtInHooks = new HashMap<>(); - - // Addon-registered hooks per context (legacy string list) - private final Map> addonHooks = new HashMap<>(); - - // Rich hook definitions per context (NEW) + // Hook definitions per context - single source of truth private final Map> hookDefinitions = new HashMap<>(); - // Revision counter for cache invalidation (NEW) + // Revision counter for cache invalidation private int hookRevision = 0; public ScriptHookController() { @@ -48,48 +38,40 @@ public class ScriptHookController implements IScriptHookHandler { } private void initializeBuiltInHooks() { - // Initialize empty lists for all contexts from ScriptContext + // Initialize empty maps for all contexts from ScriptContext for (ScriptContext context : ScriptContext.values()) { if (!context.hookContext.isEmpty()) { - builtInHooks.put(context.hookContext, new ArrayList<>()); - addonHooks.put(context.hookContext, new ArrayList<>()); - hookDefinitions.put(context.hookContext, new HashMap<>()); + hookDefinitions.put(context.hookContext, new LinkedHashMap<>()); } } - // NPC hooks with full definitions + // NPC hooks initializeNpcHooks(); - // Player hooks with full definitions + // Player hooks initializePlayerHooks(); - // Block hooks with full definitions + // Block hooks initializeBlockHooks(); - // Item hooks with full definitions + // Item hooks initializeItemHooks(); - // Linked item hooks with full definitions + // Linked item hooks initializeLinkedItemHooks(); // Recipe hooks - registerBuiltIn(ScriptContext.RECIPE, - RecipeScript.ScriptType.PRE.function, - RecipeScript.ScriptType.POST.function - ); + registerHookDef(ScriptContext.RECIPE, RecipeScript.ScriptType.PRE.function, null, null); + registerHookDef(ScriptContext.RECIPE, RecipeScript.ScriptType.POST.function, null, null); // Effect hooks - registerBuiltIn(ScriptContext.EFFECT, - EnumScriptType.ON_EFFECT_ADD.function, - EnumScriptType.ON_EFFECT_TICK.function, - EnumScriptType.ON_EFFECT_REMOVE.function - ); + registerHookDef(ScriptContext.EFFECT, EnumScriptType.ON_EFFECT_ADD.function, null, null); + registerHookDef(ScriptContext.EFFECT, EnumScriptType.ON_EFFECT_TICK.function, null, null); + registerHookDef(ScriptContext.EFFECT, EnumScriptType.ON_EFFECT_REMOVE.function, null, null); - // Forge hooks - init + CNPC-specific hooks, the rest are dynamically discovered from Forge event classes - registerBuiltIn(ScriptContext.FORGE, - EnumScriptType.INIT.function, - EnumScriptType.CNPC_NATURAL_SPAWN.function - ); + // Forge hooks + registerHookDef(ScriptContext.FORGE, EnumScriptType.INIT.function, null, null); + registerHookDef(ScriptContext.FORGE, EnumScriptType.CNPC_NATURAL_SPAWN.function, null, null); } private void initializeNpcHooks() { @@ -99,60 +81,37 @@ public class ScriptHookController implements IScriptHookHandler { String abilityImport = "noppes.npcs.api.event.IAbilityEvent"; // Core lifecycle - registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, - "noppes.npcs.api.event.INpcEvent$InitEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, - "noppes.npcs.api.event.INpcEvent$UpdateEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.TIMER.function, - "noppes.npcs.api.event.INpcEvent$TimerEvent", npcImport); + registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.INpcEvent$InitEvent", npcImport); + registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.INpcEvent$UpdateEvent", npcImport); + registerHookDef(ctx, EnumScriptType.TIMER.function, "noppes.npcs.api.event.INpcEvent$TimerEvent", npcImport); // Interaction - registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, - "noppes.npcs.api.event.INpcEvent$InteractEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.DIALOG.function, - "noppes.npcs.api.event.INpcEvent$DialogEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_CLOSE.function, - "noppes.npcs.api.event.INpcEvent$DialogClosedEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.COLLIDE.function, - "noppes.npcs.api.event.INpcEvent$CollideEvent", npcImport); + registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.INpcEvent$InteractEvent", npcImport); + registerHookDef(ctx, EnumScriptType.DIALOG.function, "noppes.npcs.api.event.INpcEvent$DialogEvent", npcImport); + registerHookDef(ctx, EnumScriptType.DIALOG_CLOSE.function, "noppes.npcs.api.event.INpcEvent$DialogClosedEvent", npcImport); + registerHookDef(ctx, EnumScriptType.COLLIDE.function, "noppes.npcs.api.event.INpcEvent$CollideEvent", npcImport); // Combat - registerBuiltInDefinition(ctx, EnumScriptType.DAMAGED.function, - "noppes.npcs.api.event.INpcEvent$DamagedEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.KILLED.function, - "noppes.npcs.api.event.INpcEvent$DiedEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.KILLS.function, - "noppes.npcs.api.event.INpcEvent$KilledEntityEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.ATTACK_MELEE.function, - "noppes.npcs.api.event.INpcEvent$MeleeAttackEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.ATTACK_SWING.function, - "noppes.npcs.api.event.INpcEvent$SwingEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.RANGED_LAUNCHED.function, - "noppes.npcs.api.event.INpcEvent$RangedLaunchedEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.TARGET.function, - "noppes.npcs.api.event.INpcEvent$TargetEvent", npcImport); - registerBuiltInDefinition(ctx, EnumScriptType.TARGET_LOST.function, - "noppes.npcs.api.event.INpcEvent$TargetLostEvent", npcImport); + registerHookDef(ctx, EnumScriptType.DAMAGED.function, "noppes.npcs.api.event.INpcEvent$DamagedEvent", npcImport); + registerHookDef(ctx, EnumScriptType.KILLED.function, "noppes.npcs.api.event.INpcEvent$DiedEvent", npcImport); + registerHookDef(ctx, EnumScriptType.KILLS.function, "noppes.npcs.api.event.INpcEvent$KilledEntityEvent", npcImport); + registerHookDef(ctx, EnumScriptType.ATTACK_MELEE.function, "noppes.npcs.api.event.INpcEvent$MeleeAttackEvent", npcImport); + registerHookDef(ctx, EnumScriptType.ATTACK_SWING.function, "noppes.npcs.api.event.INpcEvent$SwingEvent", npcImport); + registerHookDef(ctx, EnumScriptType.RANGED_LAUNCHED.function, "noppes.npcs.api.event.INpcEvent$RangedLaunchedEvent", npcImport); + registerHookDef(ctx, EnumScriptType.TARGET.function, "noppes.npcs.api.event.INpcEvent$TargetEvent", npcImport); + registerHookDef(ctx, EnumScriptType.TARGET_LOST.function, "noppes.npcs.api.event.INpcEvent$TargetLostEvent", npcImport); // Projectile events - registerBuiltInDefinition(ctx, EnumScriptType.PROJECTILE_TICK.function, - "noppes.npcs.api.event.IProjectileEvent$UpdateEvent", projImport); - registerBuiltInDefinition(ctx, EnumScriptType.PROJECTILE_IMPACT.function, - "noppes.npcs.api.event.IProjectileEvent$ImpactEvent", projImport); + registerHookDef(ctx, EnumScriptType.PROJECTILE_TICK.function, "noppes.npcs.api.event.IProjectileEvent$UpdateEvent", projImport); + registerHookDef(ctx, EnumScriptType.PROJECTILE_IMPACT.function, "noppes.npcs.api.event.IProjectileEvent$ImpactEvent", projImport); // Ability events - registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_START.function, - "noppes.npcs.api.event.IAbilityEvent$StartEvent", abilityImport); - registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_EXECUTE.function, - "noppes.npcs.api.event.IAbilityEvent$ExecuteEvent", abilityImport); - registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_HIT.function, - "noppes.npcs.api.event.IAbilityEvent$HitEvent", abilityImport); - registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_TICK.function, - "noppes.npcs.api.event.IAbilityEvent$TickEvent", abilityImport); - registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_INTERRUPT.function, - "noppes.npcs.api.event.IAbilityEvent$InterruptEvent", abilityImport); - registerBuiltInDefinition(ctx, EnumScriptType.ABILITY_COMPLETE.function, - "noppes.npcs.api.event.IAbilityEvent$CompleteEvent", abilityImport); + registerHookDef(ctx, EnumScriptType.ABILITY_START.function, "noppes.npcs.api.event.IAbilityEvent$StartEvent", abilityImport); + registerHookDef(ctx, EnumScriptType.ABILITY_EXECUTE.function, "noppes.npcs.api.event.IAbilityEvent$ExecuteEvent", abilityImport); + registerHookDef(ctx, EnumScriptType.ABILITY_HIT.function, "noppes.npcs.api.event.IAbilityEvent$HitEvent", abilityImport); + registerHookDef(ctx, EnumScriptType.ABILITY_TICK.function, "noppes.npcs.api.event.IAbilityEvent$TickEvent", abilityImport); + registerHookDef(ctx, EnumScriptType.ABILITY_INTERRUPT.function, "noppes.npcs.api.event.IAbilityEvent$InterruptEvent", abilityImport); + registerHookDef(ctx, EnumScriptType.ABILITY_COMPLETE.function, "noppes.npcs.api.event.IAbilityEvent$CompleteEvent", abilityImport); } private void initializePlayerHooks() { @@ -166,238 +125,143 @@ public class ScriptHookController implements IScriptHookHandler { String animImport = "noppes.npcs.api.event.IAnimationEvent"; // Core lifecycle - registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, - "noppes.npcs.api.event.IPlayerEvent$InitEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, - "noppes.npcs.api.event.IPlayerEvent$UpdateEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.TIMER.function, - "noppes.npcs.api.event.IPlayerEvent$TimerEvent", playerImport); + registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.IPlayerEvent$InitEvent", playerImport); + registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.IPlayerEvent$UpdateEvent", playerImport); + registerHookDef(ctx, EnumScriptType.TIMER.function, "noppes.npcs.api.event.IPlayerEvent$TimerEvent", playerImport); // Combat - registerBuiltInDefinition(ctx, EnumScriptType.ATTACK.function, - "noppes.npcs.api.event.IPlayerEvent$AttackEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.ATTACKED.function, - "noppes.npcs.api.event.IPlayerEvent$AttackedEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.DAMAGED.function, - "noppes.npcs.api.event.IPlayerEvent$DamagedEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.DAMAGED_ENTITY.function, - "noppes.npcs.api.event.IPlayerEvent$DamagedEntityEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.KILLS.function, - "noppes.npcs.api.event.IPlayerEvent$KilledEntityEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.KILLED.function, - "noppes.npcs.api.event.IPlayerEvent$DiedEvent", playerImport); + registerHookDef(ctx, EnumScriptType.ATTACK.function, "noppes.npcs.api.event.IPlayerEvent$AttackEvent", playerImport); + registerHookDef(ctx, EnumScriptType.ATTACKED.function, "noppes.npcs.api.event.IPlayerEvent$AttackedEvent", playerImport); + registerHookDef(ctx, EnumScriptType.DAMAGED.function, "noppes.npcs.api.event.IPlayerEvent$DamagedEvent", playerImport); + registerHookDef(ctx, EnumScriptType.DAMAGED_ENTITY.function, "noppes.npcs.api.event.IPlayerEvent$DamagedEntityEvent", playerImport); + registerHookDef(ctx, EnumScriptType.KILLS.function, "noppes.npcs.api.event.IPlayerEvent$KilledEntityEvent", playerImport); + registerHookDef(ctx, EnumScriptType.KILLED.function, "noppes.npcs.api.event.IPlayerEvent$DiedEvent", playerImport); // Interaction - registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, - "noppes.npcs.api.event.IPlayerEvent$InteractEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.RIGHT_CLICK.function, - "noppes.npcs.api.event.IPlayerEvent$RightClickEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.BREAK_BLOCK.function, - "noppes.npcs.api.event.IPlayerEvent$BreakEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.CHAT.function, - "noppes.npcs.api.event.IPlayerEvent$ChatEvent", playerImport); + registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.IPlayerEvent$InteractEvent", playerImport); + registerHookDef(ctx, EnumScriptType.RIGHT_CLICK.function, "noppes.npcs.api.event.IPlayerEvent$RightClickEvent", playerImport); + registerHookDef(ctx, EnumScriptType.BREAK_BLOCK.function, "noppes.npcs.api.event.IPlayerEvent$BreakEvent", playerImport); + registerHookDef(ctx, EnumScriptType.CHAT.function, "noppes.npcs.api.event.IPlayerEvent$ChatEvent", playerImport); // Connection - registerBuiltInDefinition(ctx, EnumScriptType.LOGIN.function, - "noppes.npcs.api.event.IPlayerEvent$LoginEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.LOGOUT.function, - "noppes.npcs.api.event.IPlayerEvent$LogoutEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.RESPAWN.function, - "noppes.npcs.api.event.IPlayerEvent$RespawnEvent", playerImport); + registerHookDef(ctx, EnumScriptType.LOGIN.function, "noppes.npcs.api.event.IPlayerEvent$LoginEvent", playerImport); + registerHookDef(ctx, EnumScriptType.LOGOUT.function, "noppes.npcs.api.event.IPlayerEvent$LogoutEvent", playerImport); + registerHookDef(ctx, EnumScriptType.RESPAWN.function, "noppes.npcs.api.event.IPlayerEvent$RespawnEvent", playerImport); // Input - registerBuiltInDefinition(ctx, EnumScriptType.KEY_PRESSED.function, - "noppes.npcs.api.event.IPlayerEvent$KeyPressedEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.MOUSE_CLICKED.function, - "noppes.npcs.api.event.IPlayerEvent$MouseClickedEvent", playerImport); + registerHookDef(ctx, EnumScriptType.KEY_PRESSED.function, "noppes.npcs.api.event.IPlayerEvent$KeyPressedEvent", playerImport); + registerHookDef(ctx, EnumScriptType.MOUSE_CLICKED.function, "noppes.npcs.api.event.IPlayerEvent$MouseClickedEvent", playerImport); // Items - registerBuiltInDefinition(ctx, EnumScriptType.PICKUP.function, - "noppes.npcs.api.event.IPlayerEvent$PickUpEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.TOSS.function, - "noppes.npcs.api.event.IPlayerEvent$TossEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.DROP.function, - "noppes.npcs.api.event.IPlayerEvent$DropEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.PICKUP_XP.function, - "noppes.npcs.api.event.IPlayerEvent$PickupXPEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.START_USING_ITEM.function, - "noppes.npcs.api.event.IPlayerEvent$StartUsingItem", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.USING_ITEM.function, - "noppes.npcs.api.event.IPlayerEvent$UsingItem", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.STOP_USING_ITEM.function, - "noppes.npcs.api.event.IPlayerEvent$StopUsingItem", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.FINISH_USING_ITEM.function, - "noppes.npcs.api.event.IPlayerEvent$FinishUsingItem", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.CONTAINER_OPEN.function, - "noppes.npcs.api.event.IPlayerEvent$ContainerOpen", playerImport); + registerHookDef(ctx, EnumScriptType.PICKUP.function, "noppes.npcs.api.event.IPlayerEvent$PickUpEvent", playerImport); + registerHookDef(ctx, EnumScriptType.TOSS.function, "noppes.npcs.api.event.IPlayerEvent$TossEvent", playerImport); + registerHookDef(ctx, EnumScriptType.DROP.function, "noppes.npcs.api.event.IPlayerEvent$DropEvent", playerImport); + registerHookDef(ctx, EnumScriptType.PICKUP_XP.function, "noppes.npcs.api.event.IPlayerEvent$PickupXPEvent", playerImport); + registerHookDef(ctx, EnumScriptType.START_USING_ITEM.function, "noppes.npcs.api.event.IPlayerEvent$StartUsingItem", playerImport); + registerHookDef(ctx, EnumScriptType.USING_ITEM.function, "noppes.npcs.api.event.IPlayerEvent$UsingItem", playerImport); + registerHookDef(ctx, EnumScriptType.STOP_USING_ITEM.function, "noppes.npcs.api.event.IPlayerEvent$StopUsingItem", playerImport); + registerHookDef(ctx, EnumScriptType.FINISH_USING_ITEM.function, "noppes.npcs.api.event.IPlayerEvent$FinishUsingItem", playerImport); + registerHookDef(ctx, EnumScriptType.CONTAINER_OPEN.function, "noppes.npcs.api.event.IPlayerEvent$ContainerOpen", playerImport); // Movement - registerBuiltInDefinition(ctx, EnumScriptType.JUMP.function, - "noppes.npcs.api.event.IPlayerEvent$JumpEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.FALL.function, - "noppes.npcs.api.event.IPlayerEvent$FallEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.CHANGED_DIM.function, - "noppes.npcs.api.event.IPlayerEvent$ChangedDimension", playerImport); + registerHookDef(ctx, EnumScriptType.JUMP.function, "noppes.npcs.api.event.IPlayerEvent$JumpEvent", playerImport); + registerHookDef(ctx, EnumScriptType.FALL.function, "noppes.npcs.api.event.IPlayerEvent$FallEvent", playerImport); + registerHookDef(ctx, EnumScriptType.CHANGED_DIM.function, "noppes.npcs.api.event.IPlayerEvent$ChangedDimension", playerImport); // World - registerBuiltInDefinition(ctx, EnumScriptType.RANGED_CHARGE.function, - "noppes.npcs.api.event.IPlayerEvent$RangedChargeEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.RANGED_LAUNCHED.function, - "noppes.npcs.api.event.IPlayerEvent$RangedLaunchedEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.USE_HOE.function, - "noppes.npcs.api.event.IPlayerEvent$UseHoeEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.BONEMEAL.function, - "noppes.npcs.api.event.IPlayerEvent$BonemealEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.FILL_BUCKET.function, - "noppes.npcs.api.event.IPlayerEvent$FillBucketEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.WAKE_UP.function, - "noppes.npcs.api.event.IPlayerEvent$WakeUpEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.SLEEP.function, - "noppes.npcs.api.event.IPlayerEvent$SleepEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.PLAYSOUND.function, - "noppes.npcs.api.event.IPlayerEvent$SoundEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.LIGHTNING.function, - "noppes.npcs.api.event.IPlayerEvent$LightningEvent", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.SCRIPT_COMMAND.function, - "noppes.npcs.api.event.ICustomNPCsEvent$ScriptedCommandEvent", "noppes.npcs.api.event.ICustomNPCsEvent"); + registerHookDef(ctx, EnumScriptType.RANGED_CHARGE.function, "noppes.npcs.api.event.IPlayerEvent$RangedChargeEvent", playerImport); + registerHookDef(ctx, EnumScriptType.RANGED_LAUNCHED.function, "noppes.npcs.api.event.IPlayerEvent$RangedLaunchedEvent", playerImport); + registerHookDef(ctx, EnumScriptType.USE_HOE.function, "noppes.npcs.api.event.IPlayerEvent$UseHoeEvent", playerImport); + registerHookDef(ctx, EnumScriptType.BONEMEAL.function, "noppes.npcs.api.event.IPlayerEvent$BonemealEvent", playerImport); + registerHookDef(ctx, EnumScriptType.FILL_BUCKET.function, "noppes.npcs.api.event.IPlayerEvent$FillBucketEvent", playerImport); + registerHookDef(ctx, EnumScriptType.WAKE_UP.function, "noppes.npcs.api.event.IPlayerEvent$WakeUpEvent", playerImport); + registerHookDef(ctx, EnumScriptType.SLEEP.function, "noppes.npcs.api.event.IPlayerEvent$SleepEvent", playerImport); + registerHookDef(ctx, EnumScriptType.PLAYSOUND.function, "noppes.npcs.api.event.IPlayerEvent$SoundEvent", playerImport); + registerHookDef(ctx, EnumScriptType.LIGHTNING.function, "noppes.npcs.api.event.IPlayerEvent$LightningEvent", playerImport); + registerHookDef(ctx, EnumScriptType.SCRIPT_COMMAND.function, "noppes.npcs.api.event.ICustomNPCsEvent$ScriptedCommandEvent", "noppes.npcs.api.event.ICustomNPCsEvent"); // Quest events - registerBuiltInDefinition(ctx, EnumScriptType.QUEST_START.function, - "noppes.npcs.api.event.IQuestEvent$QuestStartEvent", questImport); - registerBuiltInDefinition(ctx, EnumScriptType.QUEST_COMPLETED.function, - "noppes.npcs.api.event.IQuestEvent$QuestCompletedEvent", questImport); - registerBuiltInDefinition(ctx, EnumScriptType.QUEST_TURNIN.function, - "noppes.npcs.api.event.IQuestEvent$QuestTurnedInEvent", questImport); + registerHookDef(ctx, EnumScriptType.QUEST_START.function, "noppes.npcs.api.event.IQuestEvent$QuestStartEvent", questImport); + registerHookDef(ctx, EnumScriptType.QUEST_COMPLETED.function, "noppes.npcs.api.event.IQuestEvent$QuestCompletedEvent", questImport); + registerHookDef(ctx, EnumScriptType.QUEST_TURNIN.function, "noppes.npcs.api.event.IQuestEvent$QuestTurnedInEvent", questImport); // Dialog events - registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_OPEN.function, - "noppes.npcs.api.event.IDialogEvent$DialogOpen", dialogImport); - registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_OPTION.function, - "noppes.npcs.api.event.IDialogEvent$DialogOption", dialogImport); - registerBuiltInDefinition(ctx, EnumScriptType.DIALOG_CLOSE.function, - "noppes.npcs.api.event.IDialogEvent$DialogClosed", dialogImport); + registerHookDef(ctx, EnumScriptType.DIALOG_OPEN.function, "noppes.npcs.api.event.IDialogEvent$DialogOpen", dialogImport); + registerHookDef(ctx, EnumScriptType.DIALOG_OPTION.function, "noppes.npcs.api.event.IDialogEvent$DialogOption", dialogImport); + registerHookDef(ctx, EnumScriptType.DIALOG_CLOSE.function, "noppes.npcs.api.event.IDialogEvent$DialogClosed", dialogImport); // Faction events - registerBuiltInDefinition(ctx, EnumScriptType.FACTION_POINTS.function, - "noppes.npcs.api.event.IFactionEvent$FactionPoints", factionImport); + registerHookDef(ctx, EnumScriptType.FACTION_POINTS.function, "noppes.npcs.api.event.IFactionEvent$FactionPoints", factionImport); // Custom GUI events - registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_CLOSED.function, - "noppes.npcs.api.event.ICustomGuiEvent$CloseEvent", guiImport); - registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_BUTTON.function, - "noppes.npcs.api.event.ICustomGuiEvent$ButtonEvent", guiImport); - registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_SLOT.function, - "noppes.npcs.api.event.ICustomGuiEvent$SlotEvent", guiImport); - registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_SLOT_CLICKED.function, - "noppes.npcs.api.event.ICustomGuiEvent$SlotClickEvent", guiImport); - registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_SCROLL.function, - "noppes.npcs.api.event.ICustomGuiEvent$ScrollEvent", guiImport); - registerBuiltInDefinition(ctx, EnumScriptType.CUSTOM_GUI_TEXTFIELD.function, - "noppes.npcs.api.event.ICustomGuiEvent$UnfocusedEvent", guiImport); + registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_CLOSED.function, "noppes.npcs.api.event.ICustomGuiEvent$CloseEvent", guiImport); + registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_BUTTON.function, "noppes.npcs.api.event.ICustomGuiEvent$ButtonEvent", guiImport); + registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_SLOT.function, "noppes.npcs.api.event.ICustomGuiEvent$SlotEvent", guiImport); + registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_SLOT_CLICKED.function, "noppes.npcs.api.event.ICustomGuiEvent$SlotClickEvent", guiImport); + registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_SCROLL.function, "noppes.npcs.api.event.ICustomGuiEvent$ScrollEvent", guiImport); + registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_TEXTFIELD.function, "noppes.npcs.api.event.ICustomGuiEvent$UnfocusedEvent", guiImport); // Party events - registerBuiltInDefinition(ctx, EnumScriptType.PARTY_QUEST_COMPLETED.function, - "noppes.npcs.api.event.IPartyEvent$PartyQuestCompletedEvent", partyImport); - registerBuiltInDefinition(ctx, EnumScriptType.PARTY_QUEST_SET.function, - "noppes.npcs.api.event.IPartyEvent$PartyQuestSetEvent", partyImport); - registerBuiltInDefinition(ctx, EnumScriptType.PARTY_QUEST_TURNED_IN.function, - "noppes.npcs.api.event.IPartyEvent$PartyQuestTurnedInEvent", partyImport); - registerBuiltInDefinition(ctx, EnumScriptType.PARTY_INVITE.function, - "noppes.npcs.api.event.IPartyEvent$PartyInviteEvent", partyImport); - registerBuiltInDefinition(ctx, EnumScriptType.PARTY_KICK.function, - "noppes.npcs.api.event.IPartyEvent$PartyKickEvent", partyImport); - registerBuiltInDefinition(ctx, EnumScriptType.PARTY_LEAVE.function, - "noppes.npcs.api.event.IPartyEvent$PartyLeaveEvent", partyImport); - registerBuiltInDefinition(ctx, EnumScriptType.PARTY_DISBAND.function, - "noppes.npcs.api.event.IPartyEvent$PartyDisbandEvent", partyImport); + registerHookDef(ctx, EnumScriptType.PARTY_QUEST_COMPLETED.function, "noppes.npcs.api.event.IPartyEvent$PartyQuestCompletedEvent", partyImport); + registerHookDef(ctx, EnumScriptType.PARTY_QUEST_SET.function, "noppes.npcs.api.event.IPartyEvent$PartyQuestSetEvent", partyImport); + registerHookDef(ctx, EnumScriptType.PARTY_QUEST_TURNED_IN.function, "noppes.npcs.api.event.IPartyEvent$PartyQuestTurnedInEvent", partyImport); + registerHookDef(ctx, EnumScriptType.PARTY_INVITE.function, "noppes.npcs.api.event.IPartyEvent$PartyInviteEvent", partyImport); + registerHookDef(ctx, EnumScriptType.PARTY_KICK.function, "noppes.npcs.api.event.IPartyEvent$PartyKickEvent", partyImport); + registerHookDef(ctx, EnumScriptType.PARTY_LEAVE.function, "noppes.npcs.api.event.IPartyEvent$PartyLeaveEvent", partyImport); + registerHookDef(ctx, EnumScriptType.PARTY_DISBAND.function, "noppes.npcs.api.event.IPartyEvent$PartyDisbandEvent", partyImport); // Animation events - registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_START.function, - "noppes.npcs.api.event.IAnimationEvent$Started", animImport); - registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_END.function, - "noppes.npcs.api.event.IAnimationEvent$Ended", animImport); - registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_FRAME_ENTER.function, - "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Entered", animImport); - registerBuiltInDefinition(ctx, EnumScriptType.ANIMATION_FRAME_EXIT.function, - "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Exited", animImport); + registerHookDef(ctx, EnumScriptType.ANIMATION_START.function, "noppes.npcs.api.event.IAnimationEvent$Started", animImport); + registerHookDef(ctx, EnumScriptType.ANIMATION_END.function, "noppes.npcs.api.event.IAnimationEvent$Ended", animImport); + registerHookDef(ctx, EnumScriptType.ANIMATION_FRAME_ENTER.function, "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Entered", animImport); + registerHookDef(ctx, EnumScriptType.ANIMATION_FRAME_EXIT.function, "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Exited", animImport); // Profile events - registerBuiltInDefinition(ctx, EnumScriptType.PROFILE_CHANGE.function, - "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Changed", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.PROFILE_REMOVE.function, - "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Removed", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.PROFILE_CREATE.function, - "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Create", playerImport); + registerHookDef(ctx, EnumScriptType.PROFILE_CHANGE.function, "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Changed", playerImport); + registerHookDef(ctx, EnumScriptType.PROFILE_REMOVE.function, "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Removed", playerImport); + registerHookDef(ctx, EnumScriptType.PROFILE_CREATE.function, "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Create", playerImport); // Effect events (for player context) - registerBuiltInDefinition(ctx, EnumScriptType.ON_EFFECT_ADD.function, - "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Added", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.ON_EFFECT_TICK.function, - "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Ticked", playerImport); - registerBuiltInDefinition(ctx, EnumScriptType.ON_EFFECT_REMOVE.function, - "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Removed", playerImport); + registerHookDef(ctx, EnumScriptType.ON_EFFECT_ADD.function, "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Added", playerImport); + registerHookDef(ctx, EnumScriptType.ON_EFFECT_TICK.function, "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Ticked", playerImport); + registerHookDef(ctx, EnumScriptType.ON_EFFECT_REMOVE.function, "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Removed", playerImport); } private void initializeBlockHooks() { String ctx = ScriptContext.BLOCK.hookContext; String blockImport = "noppes.npcs.api.event.IBlockEvent"; - registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, - "noppes.npcs.api.event.IBlockEvent$InitEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, - "noppes.npcs.api.event.IBlockEvent$UpdateEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, - "noppes.npcs.api.event.IBlockEvent$InteractEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.FALLEN_UPON.function, - "noppes.npcs.api.event.IBlockEvent$EntityFallenUponEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.REDSTONE.function, - "noppes.npcs.api.event.IBlockEvent$RedstoneEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.BROKEN.function, - "noppes.npcs.api.event.IBlockEvent$BreakEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.EXPLODED.function, - "noppes.npcs.api.event.IBlockEvent$ExplodedEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.RAIN_FILLED.function, - "noppes.npcs.api.event.IBlockEvent$RainFillEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.NEIGHBOR_CHANGED.function, - "noppes.npcs.api.event.IBlockEvent$NeighborChangedEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.CLICKED.function, - "noppes.npcs.api.event.IBlockEvent$ClickedEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.HARVESTED.function, - "noppes.npcs.api.event.IBlockEvent$HarvestedEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.COLLIDE.function, - "noppes.npcs.api.event.IBlockEvent$CollidedEvent", blockImport); - registerBuiltInDefinition(ctx, EnumScriptType.TIMER.function, - "noppes.npcs.api.event.IBlockEvent$TimerEvent", blockImport); + registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.IBlockEvent$InitEvent", blockImport); + registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.IBlockEvent$UpdateEvent", blockImport); + registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.IBlockEvent$InteractEvent", blockImport); + registerHookDef(ctx, EnumScriptType.FALLEN_UPON.function, "noppes.npcs.api.event.IBlockEvent$EntityFallenUponEvent", blockImport); + registerHookDef(ctx, EnumScriptType.REDSTONE.function, "noppes.npcs.api.event.IBlockEvent$RedstoneEvent", blockImport); + registerHookDef(ctx, EnumScriptType.BROKEN.function, "noppes.npcs.api.event.IBlockEvent$BreakEvent", blockImport); + registerHookDef(ctx, EnumScriptType.EXPLODED.function, "noppes.npcs.api.event.IBlockEvent$ExplodedEvent", blockImport); + registerHookDef(ctx, EnumScriptType.RAIN_FILLED.function, "noppes.npcs.api.event.IBlockEvent$RainFillEvent", blockImport); + registerHookDef(ctx, EnumScriptType.NEIGHBOR_CHANGED.function, "noppes.npcs.api.event.IBlockEvent$NeighborChangedEvent", blockImport); + registerHookDef(ctx, EnumScriptType.CLICKED.function, "noppes.npcs.api.event.IBlockEvent$ClickedEvent", blockImport); + registerHookDef(ctx, EnumScriptType.HARVESTED.function, "noppes.npcs.api.event.IBlockEvent$HarvestedEvent", blockImport); + registerHookDef(ctx, EnumScriptType.COLLIDE.function, "noppes.npcs.api.event.IBlockEvent$CollidedEvent", blockImport); + registerHookDef(ctx, EnumScriptType.TIMER.function, "noppes.npcs.api.event.IBlockEvent$TimerEvent", blockImport); } private void initializeItemHooks() { String ctx = ScriptContext.ITEM.hookContext; String itemImport = "noppes.npcs.api.event.IItemEvent"; - registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, - "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, - "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.TOSSED.function, - "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.PICKEDUP.function, - "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.SPAWN.function, - "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, - "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.RIGHT_CLICK.function, - "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.ATTACK.function, - "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.START_USING_ITEM.function, - "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.USING_ITEM.function, - "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.STOP_USING_ITEM.function, - "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.FINISH_USING_ITEM.function, - "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); + registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); + registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); + registerHookDef(ctx, EnumScriptType.TOSSED.function, "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); + registerHookDef(ctx, EnumScriptType.PICKEDUP.function, "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); + registerHookDef(ctx, EnumScriptType.SPAWN.function, "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); + registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); + registerHookDef(ctx, EnumScriptType.RIGHT_CLICK.function, "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); + registerHookDef(ctx, EnumScriptType.ATTACK.function, "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); + registerHookDef(ctx, EnumScriptType.START_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); + registerHookDef(ctx, EnumScriptType.USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); + registerHookDef(ctx, EnumScriptType.STOP_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); + registerHookDef(ctx, EnumScriptType.FINISH_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); } private void initializeLinkedItemHooks() { @@ -406,95 +270,64 @@ public class ScriptHookController implements IScriptHookHandler { String linkedImport = "noppes.npcs.api.event.ILinkedItemEvent"; // Linked item specific hooks - registerBuiltInDefinition(ctx, EnumScriptType.LINKED_ITEM_BUILD.function, - "noppes.npcs.api.event.ILinkedItemEvent$BuildEvent", linkedImport); - registerBuiltInDefinition(ctx, EnumScriptType.LINKED_ITEM_VERSION.function, - "noppes.npcs.api.event.ILinkedItemEvent$VersionChangeEvent", linkedImport); + registerHookDef(ctx, EnumScriptType.LINKED_ITEM_BUILD.function, "noppes.npcs.api.event.ILinkedItemEvent$BuildEvent", linkedImport); + registerHookDef(ctx, EnumScriptType.LINKED_ITEM_VERSION.function, "noppes.npcs.api.event.ILinkedItemEvent$VersionChangeEvent", linkedImport); // Standard item hooks - registerBuiltInDefinition(ctx, EnumScriptType.INIT.function, - "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.TICK.function, - "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.TOSSED.function, - "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.PICKEDUP.function, - "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.SPAWN.function, - "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.INTERACT.function, - "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.RIGHT_CLICK.function, - "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.ATTACK.function, - "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.START_USING_ITEM.function, - "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.USING_ITEM.function, - "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.STOP_USING_ITEM.function, - "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); - registerBuiltInDefinition(ctx, EnumScriptType.FINISH_USING_ITEM.function, - "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); + registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); + registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); + registerHookDef(ctx, EnumScriptType.TOSSED.function, "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); + registerHookDef(ctx, EnumScriptType.PICKEDUP.function, "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); + registerHookDef(ctx, EnumScriptType.SPAWN.function, "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); + registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); + registerHookDef(ctx, EnumScriptType.RIGHT_CLICK.function, "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); + registerHookDef(ctx, EnumScriptType.ATTACK.function, "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); + registerHookDef(ctx, EnumScriptType.START_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); + registerHookDef(ctx, EnumScriptType.USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); + registerHookDef(ctx, EnumScriptType.STOP_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); + registerHookDef(ctx, EnumScriptType.FINISH_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); } /** - * Register a built-in hook with full definition metadata. + * Internal helper to register a hook definition. */ - private void registerBuiltInDefinition(String context, String hookName, String eventClassName, String... imports) { - HookDefinition def = HookDefinition.builder(hookName) - .eventClass(eventClassName) - .requiredImports(imports) - .build(); - + private void registerHookDef(String context, String hookName, String eventClassName, String... imports) { Map contextDefs = hookDefinitions.get(context); if (contextDefs == null) { - contextDefs = new HashMap<>(); + contextDefs = new LinkedHashMap<>(); hookDefinitions.put(context, contextDefs); } - contextDefs.put(hookName, def); - // Also register in legacy string list - registerBuiltIn(context, hookName); - } - - /** - * Register built-in hooks for a ScriptContext. - */ - private void registerBuiltIn(ScriptContext context, String... hooks) { - registerBuiltIn(context.hookContext, hooks); - } - - private void registerBuiltIn(String context, String... hooks) { - List list = builtInHooks.get(context); - if (list != null) { - for (String hook : hooks) { - if (!list.contains(hook)) { - list.add(hook); - } - } + HookDefinition.Builder builder = HookDefinition.builder(hookName); + if (eventClassName != null) { + builder.eventClass(eventClassName); } + if (imports != null && imports.length > 0 && imports[0] != null) { + builder.requiredImports(imports); + } + contextDefs.put(hookName, builder.build()); } - // ==================== RICH HOOK REGISTRATION (NEW API) ==================== + private void registerHookDef(ScriptContext context, String hookName, String eventClassName, String... imports) { + registerHookDef(context.hookContext, hookName, eventClassName, imports); + } + + // ==================== PUBLIC API ==================== @Override public void registerHookDefinition(String context, IHookDefinition definition) { - if (context == null || definition == null || definition.hookName() == null) { + if (context == null || definition == null || definition.hookName() == null) return; - } Map contextDefs = hookDefinitions.get(context); if (contextDefs == null) { - contextDefs = new HashMap<>(); + contextDefs = new LinkedHashMap<>(); hookDefinitions.put(context, contextDefs); } - // Store as HookDefinition (implementation) if (definition instanceof HookDefinition) { contextDefs.put(definition.hookName(), (HookDefinition) definition); } else { - // Convert foreign IHookDefinition to HookDefinition HookDefinition def = HookDefinition.builder(definition.hookName()) .eventClass(definition.eventClassName()) .paramNames(definition.paramNames()) @@ -504,18 +337,13 @@ public class ScriptHookController implements IScriptHookHandler { contextDefs.put(definition.hookName(), def); } - // Also add to legacy addon hooks list for backward compatibility - registerHook(context, definition.hookName()); - - // Increment revision for cache invalidation hookRevision++; } @Override public IHookDefinition getHookDefinition(String context, String hookName) { - if (context == null || hookName == null) { + if (context == null || hookName == null) return null; - } Map contextDefs = hookDefinitions.get(context); return contextDefs != null ? contextDefs.get(hookName) : null; @@ -523,14 +351,12 @@ public class ScriptHookController implements IScriptHookHandler { @Override public List getAllHookDefinitions(String context) { - if (context == null) { + if (context == null) return Collections.emptyList(); - } Map contextDefs = hookDefinitions.get(context); - if (contextDefs == null || contextDefs.isEmpty()) { + if (contextDefs == null || contextDefs.isEmpty()) return Collections.emptyList(); - } return new ArrayList<>(contextDefs.values()); } @@ -542,60 +368,44 @@ public class ScriptHookController implements IScriptHookHandler { // ==================== CONVENIENCE METHODS ==================== - /** - * Convenience method to register a hook using ScriptContext. - * @param context The script context - * @param functionName The hook function name - */ public void registerHook(ScriptContext context, String functionName) { registerHook(context.hookContext, functionName); } - /** - * Convenience method to register multiple hooks using ScriptContext. - * @param context The script context - * @param functionNames The hook function names - */ public void registerHooks(ScriptContext context, String... functionNames) { for (String fn : functionNames) { registerHook(context.hookContext, fn); } } - /** - * Convenience method to register a hook definition using ScriptContext. - * @param context The script context - * @param definition The hook definition - */ public void registerHookDefinition(ScriptContext context, IHookDefinition definition) { registerHookDefinition(context.hookContext, definition); } - // ==================== LEGACY HOOK REGISTRATION ==================== + // ==================== LEGACY API (IScriptHookHandler) ==================== @Override public void registerHook(String context, String functionName) { - if (context == null || functionName == null || functionName.isEmpty()) { + if (context == null || functionName == null || functionName.isEmpty()) return; + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new LinkedHashMap<>(); + hookDefinitions.put(context, contextDefs); } - List hooks = addonHooks.get(context); - if (hooks == null) { - // Custom context - create it - hooks = new ArrayList<>(); - addonHooks.put(context, hooks); - } - - if (!hooks.contains(functionName) && !isBuiltInHook(context, functionName)) { - hooks.add(functionName); + // Only add if not already present + if (!contextDefs.containsKey(functionName)) { + contextDefs.put(functionName, HookDefinition.builder(functionName).build()); + hookRevision++; } } @Override public void registerHooks(String functionName, String... contexts) { - if (functionName == null || functionName.isEmpty() || contexts == null) { + if (functionName == null || functionName.isEmpty() || contexts == null) return; - } for (String context : contexts) { registerHook(context, functionName); @@ -604,85 +414,57 @@ public class ScriptHookController implements IScriptHookHandler { @Override public void unregisterHook(String context, String functionName) { - if (context == null || functionName == null) { + if (context == null || functionName == null) return; - } - List hooks = addonHooks.get(context); - if (hooks != null) { - hooks.remove(functionName); + Map contextDefs = hookDefinitions.get(context); + if (contextDefs != null && contextDefs.remove(functionName) != null) { + hookRevision++; } } @Override public void unregisterHookFromAll(String functionName) { - if (functionName == null) { + if (functionName == null) return; - } - for (List hooks : addonHooks.values()) { - hooks.remove(functionName); + for (Map contextDefs : hookDefinitions.values()) { + if (contextDefs.remove(functionName) != null) { + hookRevision++; + } } } @Override public List getAddonHooks(String context) { - List hooks = addonHooks.get(context); - return hooks != null ? Collections.unmodifiableList(hooks) : Collections.emptyList(); + // With unified storage, we can't distinguish addon vs built-in + // Return empty for backwards compatibility + return Collections.emptyList(); } @Override public List getAllHooks(String context) { - List result = new ArrayList<>(); + if (context == null) + return Collections.emptyList(); - // Add built-in hooks - List builtIn = builtInHooks.get(context); - if (builtIn != null) { - result.addAll(builtIn); - } + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null || contextDefs.isEmpty()) + return Collections.emptyList(); - // Add addon hooks - List addon = addonHooks.get(context); - if (addon != null) { - for (String hook : addon) { - if (!result.contains(hook)) { - result.add(hook); - } - } - } - - return result; + return new ArrayList<>(contextDefs.keySet()); } @Override public boolean hasHook(String context, String functionName) { - if (context == null || functionName == null) { + if (context == null || functionName == null) return false; - } - return isBuiltInHook(context, functionName) || isAddonHook(context, functionName); + Map contextDefs = hookDefinitions.get(context); + return contextDefs != null && contextDefs.containsKey(functionName); } @Override public String[] getContexts() { - return builtInHooks.keySet().toArray(new String[0]); - } - - private boolean isBuiltInHook(String context, String functionName) { - List hooks = builtInHooks.get(context); - return hooks != null && hooks.contains(functionName); - } - - private boolean isAddonHook(String context, String functionName) { - List hooks = addonHooks.get(context); - return hooks != null && hooks.contains(functionName); - } - - /** - * Get built-in hooks for a context (used by GUIs for backwards compatibility). - */ - public List getBuiltInHooks(String context) { - List hooks = builtInHooks.get(context); - return hooks != null ? Collections.unmodifiableList(hooks) : Collections.emptyList(); + return hookDefinitions.keySet().toArray(new String[0]); } } diff --git a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java index 28bd3af4f..fb354e590 100644 --- a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java @@ -48,16 +48,9 @@ public class JaninoScriptHandler> extends ScriptHandle } public void deleteScript() { - unloadScript(); script = null; } - public void unloadScript() { - if (script != null) { - script.unload(); - } - } - // -------------------- NBT helpers -------------------- public void readFromNBT(NBTTagCompound compound) { diff --git a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java index 87b969e25..2ff8b4afb 100644 --- a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java +++ b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java @@ -7,20 +7,16 @@ import java.lang.reflect.Modifier; import java.util.*; /** - * Resolves script hook methods for compiled script classes using runtime - * argument values. Resolution favors exact matches and then the "closest" - * assignable parameter types (measured by inheritance distance). - * - * Methods are resolved dynamically from the compiled class - no interface - * definitions required. + * Resolves script hook methods for compiled script classes. + * Optimized for high-frequency hooks like TICK with fast O(1) lookups. */ public final class JaninoHookResolver { - private final Class interfaceType; /** - * Cache for methods found on compiled classes (keyed by signature). + * Primary cache: hookName -> CachedHandle + * Fast O(1) lookup without string building for non-overloaded hooks. */ - private final Map compiledClassCache = new HashMap<>(); + private final Map handleCache = new HashMap<>(); /** * Track which compiled class we've cached methods for. @@ -28,164 +24,78 @@ public final class JaninoHookResolver { */ private Class lastCompiledClass; - // MethodHandle caches - private final Map compiledHandleCache = new HashMap<>(); - - // Method -> MethodHandle for identity-based deduplication - private final Map methodToHandle = new IdentityHashMap<>(); - private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); /** - * Create a resolver for the provided script interface type. - * @param interfaceType non-null interface class that scripts implement + * Cached handle with expected parameter count for quick validation. */ - public JaninoHookResolver(Class interfaceType) { - this.interfaceType = Objects.requireNonNull(interfaceType, "interfaceType"); + private static final class CachedHandle { + final MethodHandle handle; + final int paramCount; + + CachedHandle(MethodHandle handle, int paramCount) { + this.handle = handle; + this.paramCount = paramCount; + } + } + + public JaninoHookResolver() { } /** - * Clears internal resolution caches so future calls perform fresh - * method resolution. Call when scripts or available methods change. + * Clears internal resolution caches. */ public void clearResolutionCaches() { - compiledClassCache.clear(); - compiledHandleCache.clear(); - methodToHandle.clear(); + handleCache.clear(); lastCompiledClass = null; } /** - * Resolve a hook method by name, searching the compiled class. - * - *

Resolution order:

- *
    - *
  1. Compiled class methods (user-defined)
  2. - *
  3. Fallback to no-arg versions
  4. - *
- * - * @param hookName hook method name - * @param args runtime arguments to use for overload resolution - * @param compiledInstance the compiled script instance - * @return the resolved {@link Method} or {@code null} if none matched - */ - public Method resolveHookMethod(String hookName, Object[] args, Object compiledInstance) { - if (hookName == null || hookName.isEmpty() || compiledInstance == null) - return null; - - Object[] safeArgs = args == null ? new Object[0] : args; - String signatureKey = buildSignatureKey(hookName, safeArgs); - - Class compiledClass = compiledInstance.getClass(); - - // Clear cache if compiled class changed (recompilation) - if (lastCompiledClass != compiledClass) { - compiledClassCache.clear(); - compiledHandleCache.clear(); - methodToHandle.clear(); - lastCompiledClass = compiledClass; - } - - // Check compiled class cache - Method method = compiledClassCache.get(signatureKey); - if (method != null) - return method; - - // Search compiled class for matching method - method = resolveHookMethodInternal(compiledClass, hookName, safeArgs); - if (method != null) { - compiledClassCache.put(signatureKey, method); - return method; - } - - // Try no-arg version on compiled class - String noArgKey = buildSignatureKey(hookName, new Object[0]); - method = compiledClassCache.get(noArgKey); - if (method != null) - return method; - - method = resolveHookMethodInternal(compiledClass, hookName, new Object[0]); - if (method != null) { - compiledClassCache.put(noArgKey, method); - return method; - } - - return null; - } - - /** - * Resolve a MethodHandle for the given hook. Faster than Method.invoke(). - * Uses existing Method resolution, then converts to cached MethodHandle. + * Resolve a MethodHandle for the given hook. + * Optimized for repeated calls with same hook name - O(1) after first resolution. */ public MethodHandle resolveHookHandle(String hookName, Object[] args, Object compiledInstance) { if (hookName == null || hookName.isEmpty() || compiledInstance == null) return null; - Object[] safeArgs = args == null ? new Object[0] : args; - String signatureKey = buildSignatureKey(hookName, safeArgs); - Class compiledClass = compiledInstance.getClass(); // Clear cache if compiled class changed (recompilation) if (lastCompiledClass != compiledClass) { - compiledClassCache.clear(); - compiledHandleCache.clear(); - methodToHandle.clear(); + handleCache.clear(); lastCompiledClass = compiledClass; } - // Check handle cache - MethodHandle handle = compiledHandleCache.get(signatureKey); - if (handle != null) - return handle; + int argCount = args == null ? 0 : args.length; - // Resolve Method first - Method method = resolveHookMethod(hookName, args, compiledInstance); + // Fast path: check primary cache by hookName only + CachedHandle cached = handleCache.get(hookName); + if (cached != null) { + // Verify arg count matches (handles most cases without full signature check) + if (cached.paramCount == argCount) { + return cached.handle; + } + // Arg count mismatch - need full resolution for potential overload + } + + // Slow path: resolve method and cache + Method method = resolveMethod(compiledClass, hookName, args); if (method == null) return null; - // Convert Method to MethodHandle (deduplicated by Method identity) - handle = methodToHandle.get(method); - if (handle == null) { - try { - handle = LOOKUP.unreflect(method); - methodToHandle.put(method, handle); - } catch (IllegalAccessException e) { - return null; - } + try { + MethodHandle handle = LOOKUP.unreflect(method); + handleCache.put(hookName, new CachedHandle(handle, method.getParameterCount())); + return handle; + } catch (IllegalAccessException e) { + return null; } - - compiledHandleCache.put(signatureKey, handle); - return handle; } /** - * Build a cache key for a hook name and runtime argument list. + * Find the best matching method on the compiled class. */ - private static String buildSignatureKey(String hookName, Object[] args) { - StringBuilder sb = new StringBuilder(); - sb.append(hookName).append('#'); - - if (args == null || args.length == 0) - return sb.append('0').toString(); - - sb.append(args.length).append(':'); - for (int i = 0; i < args.length; i++) { - Object arg = args[i]; - if (i > 0) - sb.append(','); - - sb.append(arg == null ? "null" : arg.getClass().getName()); - } - return sb.toString(); - } - - /** - * Internal resolution routine. Attempts to find the best matching method - * on the provided class by comparing parameter counts and a simple - * match scoring function. - */ - private static Method resolveHookMethodInternal(Class targetClass, String hookName, Object[] args) { + private Method resolveMethod(Class targetClass, String hookName, Object[] args) { Object[] safeArgs = args == null ? new Object[0] : args; Method best = null; @@ -202,18 +112,31 @@ public final class JaninoHookResolver { continue; Class[] paramTypes = method.getParameterTypes(); - if (paramTypes.length != safeArgs.length) - continue; - int score = scoreMethodMatch(paramTypes, safeArgs); - if (score == Integer.MAX_VALUE) - continue; + // Try exact arg count match first + if (paramTypes.length == safeArgs.length) { + int score = scoreMethodMatch(paramTypes, safeArgs); + if (score != Integer.MAX_VALUE && score < bestScore) { + bestScore = score; + best = method; + if (score == 0) + return best; // Perfect match + } + } + } - if (score < bestScore) { - bestScore = score; - best = method; - if (score == 0) - return best; + // If no match with args, try no-arg version + if (best == null && safeArgs.length > 0) { + for (Method method : targetClass.getMethods()) { + if (!method.getName().equals(hookName)) + continue; + if (Modifier.isFinal(method.getModifiers())) + continue; + if (method.getDeclaringClass() == Object.class) + continue; + if (method.getParameterCount() == 0) { + return method; + } } } @@ -221,8 +144,8 @@ public final class JaninoHookResolver { } /** - * Score how well a method's parameter types match the provided runtime arguments. - * Lower scores are better; Integer.MAX_VALUE indicates an incompatible parameter. + * Score how well parameter types match runtime arguments. + * Lower is better; MAX_VALUE means incompatible. */ private static int scoreMethodMatch(Class[] paramTypes, Object[] args) { int score = 0; @@ -233,7 +156,6 @@ public final class JaninoHookResolver { if (arg == null) { if (paramType.isPrimitive()) return Integer.MAX_VALUE; - score += 1; continue; } @@ -251,36 +173,26 @@ public final class JaninoHookResolver { return Integer.MAX_VALUE; } - return score; } private static Class boxType(Class type) { if (!type.isPrimitive()) return type; - if (type == boolean.class) - return Boolean.class; - if (type == byte.class) - return Byte.class; - if (type == short.class) - return Short.class; - if (type == int.class) - return Integer.class; - if (type == long.class) - return Long.class; - if (type == float.class) - return Float.class; - if (type == double.class) - return Double.class; - if (type == char.class) - return Character.class; + if (type == boolean.class) return Boolean.class; + if (type == byte.class) return Byte.class; + if (type == short.class) return Short.class; + if (type == int.class) return Integer.class; + if (type == long.class) return Long.class; + if (type == float.class) return Float.class; + if (type == double.class) return Double.class; + if (type == char.class) return Character.class; return type; } private static int inheritanceDistance(Class child, Class parent) { if (child.equals(parent)) return 0; - if (!parent.isAssignableFrom(child)) return 1000; diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 8093b8224..3ce614632 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -16,10 +16,9 @@ import org.codehaus.commons.compiler.InternalCompilerException; import org.codehaus.commons.compiler.Sandbox; import java.lang.invoke.MethodHandle; -import java.security.*; +import java.security.Permissions; +import java.security.PrivilegedAction; import java.util.*; -import java.util.function.Consumer; -import java.util.function.Function; import java.util.function.Supplier; public abstract class JaninoScript implements IScriptUnit { @@ -67,7 +66,7 @@ public abstract class JaninoScript implements IScriptUnit { this.scriptBody = builder.build(); - this.hookResolver = new JaninoHookResolver(type); + this.hookResolver = new JaninoHookResolver(); } protected JaninoScript(Class type, String[] defaultImports) { @@ -151,49 +150,6 @@ public abstract class JaninoScript implements IScriptUnit { return scriptBody.get(); } - public R call(Function fn) { - ensureCompiled(); - - T t = getUnsafe(); - if (t == null) - return null; - -// CodeSource cs = t.getClass().getProtectionDomain().getCodeSource(); -// ProtectionDomain pd = new ProtectionDomain(cs, new Permissions()); - - try { - return sandbox.confine((PrivilegedAction) () -> fn.apply(t)); -// return AccessController.doPrivileged((PrivilegedAction) () -> fn.apply(t), new AccessControlContext( -// new ProtectionDomain[] { -// pd -// } -// )); - } catch (Exception e) { - appendConsole("Runtime Error: " + e.getMessage()); - return null; - } - } - - public void run(Consumer fn) { - ensureCompiled(); - - T t = getUnsafe(); - if (t == null) - return; - - try { - sandbox.confine((PrivilegedAction) () -> { - fn.accept(t); - return null; - }); - } catch (Exception e) { - appendConsole("Runtime Error: " + e.getMessage()); - } - } - - public void unload() { - } - /** * Feed the code into the engine and compile it. * Rebuilds imports from hook definitions before compilation. @@ -205,6 +161,8 @@ public abstract class JaninoScript implements IScriptUnit { String[] allImports = collectAllImports(); builder.setDefaultImports(allImports); + // Rebuild scriptBody to apply new imports + this.scriptBody = builder.build(); scriptBody.setScript(code); } catch (InternalCompilerException e ) { Throwable parentCause = null; From a7d58bfb4ec6b019204dfc786684964e66329e60 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 19:38:04 -0500 Subject: [PATCH 258/358] Re-Add Call Run --- .../java/noppes/npcs/janino/JaninoScript.java | 50 ++++++++++++++++++- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 3ce614632..6de7b7a5a 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -19,6 +19,8 @@ import java.lang.invoke.MethodHandle; import java.security.Permissions; import java.security.PrivilegedAction; import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; public abstract class JaninoScript implements IScriptUnit { @@ -58,7 +60,7 @@ public abstract class JaninoScript implements IScriptUnit { this.type = type; this.defaultImports = defaultImports != null ? defaultImports : new String[0]; this.builder = IScriptBodyBuilder.getBuilder(type, isClient ? CustomNpcs.getClientCompiler() : CustomNpcs.getDynamicCompiler()) - .setDefaultImports(defaultImports); + .setDefaultImports(this.defaultImports); Permissions permissions = new Permissions(); permissions.setReadOnly(); @@ -135,7 +137,8 @@ public abstract class JaninoScript implements IScriptUnit { // Add imports from all hook definitions for this context String context = getHookContext(); if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { - for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { + java.util.List defs = ScriptHookController.Instance.getAllHookDefinitions(context); + for (IHookDefinition def : defs) { String[] imports = def.requiredImports(); if (imports != null) { Collections.addAll(allImports, imports); @@ -272,6 +275,49 @@ public abstract class JaninoScript implements IScriptUnit { } } + public R call(Function fn) { + ensureCompiled(); + + T t = getUnsafe(); + if (t == null) + return null; + +// CodeSource cs = t.getClass().getProtectionDomain().getCodeSource(); +// ProtectionDomain pd = new ProtectionDomain(cs, new Permissions()); + + try { + return sandbox.confine((PrivilegedAction) () -> fn.apply(t)); +// return AccessController.doPrivileged((PrivilegedAction) () -> fn.apply(t), new AccessControlContext( +// new ProtectionDomain[] { +// pd +// } +// )); + } catch (Exception e) { + appendConsole("Runtime Error: " + e.getMessage()); + return null; + } + } + + public void run(Consumer fn) { + ensureCompiled(); + + T t = getUnsafe(); + if (t == null) + return; + + try { + sandbox.confine((PrivilegedAction) () -> { + fn.accept(t); + return null; + }); + } catch (Exception e) { + appendConsole("Runtime Error: " + e.getMessage()); + } + } + + public void unload() { + } + // ==================== IScriptUnit IMPLEMENTATION ==================== From f8dc9ec1e4e8c648b529d9baafc6866cc162a6d3 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 19:43:05 -0500 Subject: [PATCH 259/358] Update JaninoScript.java --- src/main/java/noppes/npcs/janino/JaninoScript.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 6de7b7a5a..a97aed889 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -137,8 +137,7 @@ public abstract class JaninoScript implements IScriptUnit { // Add imports from all hook definitions for this context String context = getHookContext(); if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { - java.util.List defs = ScriptHookController.Instance.getAllHookDefinitions(context); - for (IHookDefinition def : defs) { + for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { String[] imports = def.requiredImports(); if (imports != null) { Collections.addAll(allImports, imports); From 7ba2dc72d7ecf4fe610da906a82ed9ffa02984ca Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 19:55:57 -0500 Subject: [PATCH 260/358] Goatee loves this --- src/api | 2 +- .../npcs/controllers/HookDefinition.java | 147 ++--- .../controllers/ScriptHookController.java | 582 ++++++++---------- 3 files changed, 308 insertions(+), 423 deletions(-) diff --git a/src/api b/src/api index 7a337a570..64fe8794e 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 7a337a5704c442fb98a0fd4e0ba26210cb54af98 +Subproject commit 64fe8794e71d82f09449e6c916c7902fe890cab7 diff --git a/src/main/java/noppes/npcs/controllers/HookDefinition.java b/src/main/java/noppes/npcs/controllers/HookDefinition.java index b8b78fe70..b5a61ceab 100644 --- a/src/main/java/noppes/npcs/controllers/HookDefinition.java +++ b/src/main/java/noppes/npcs/controllers/HookDefinition.java @@ -2,32 +2,23 @@ package noppes.npcs.controllers; import cpw.mods.fml.common.eventhandler.Cancelable; import noppes.npcs.api.handler.IHookDefinition; -import noppes.npcs.janino.annotations.ParamName; - -import java.lang.reflect.Method; -import java.lang.reflect.Parameter; /** * Implementation of {@link IHookDefinition} with a fluent builder API. * *

Usage Examples

*
{@code
- * // Full definition with class reference
- * HookDefinition.builder("onDBCTransform")
- *     .eventClass(IDBCEvent.TransformEvent.class)
- *     .paramNames("event")
- *     .requiredImports("com.dbc.api.event.IDBCEvent")
+ * // Recommended: auto-derives imports from event class
+ * HookDefinition.of("onDBCTransform", IDBCEvent.TransformEvent.class);
+ *
+ * // Full control with builder
+ * HookDefinition.builder("onDBCPowerUp")
+ *     .eventClass(IDBCEvent.PowerUpEvent.class)
  *     .cancelable(true)
  *     .build();
  *
- * // String-based (avoids classloading issues)
- * HookDefinition.builder("onDBCPowerUp")
- *     .eventClass("com.dbc.api.event.IDBCEvent$PowerUpEvent")
- *     .requiredImports("com.dbc.api.event.IDBCEvent")
- *     .build();
- *
- * // Simple hook (backward compat, no metadata)
- * HookDefinition.simple("legacyHook");
+ * // Simple hook (no event metadata)
+ * HookDefinition.simple("customHook");
  * }
*/ public class HookDefinition implements IHookDefinition { @@ -38,7 +29,6 @@ public class HookDefinition implements IHookDefinition { private final String[] requiredImports; private final boolean cancelable; - // Cached resolved class (transient - not serialized) private transient Class cachedEventClass; private transient boolean classResolutionAttempted; @@ -100,18 +90,19 @@ public class HookDefinition implements IHookDefinition { // ==================== Factory Methods ==================== /** - * Create a new builder for a hook definition. + * Create a hook definition from a hook name and event class. + * Automatically derives imports from the event class. * * @param hookName The hook function name - * @return A new builder instance + * @param eventClass The event class (e.g., INpcEvent.InitEvent.class) + * @return A hook definition with auto-derived imports */ - public static Builder builder(String hookName) { - return new Builder(hookName); + public static HookDefinition of(String hookName, Class eventClass) { + return builder(hookName).eventClass(eventClass).build(); } /** - * Create a simple hook definition with just a name (no metadata). - * For backward compatibility with legacy hook registration. + * Create a simple hook definition with just a name (no event metadata). * * @param hookName The hook function name * @return A minimal hook definition @@ -121,49 +112,13 @@ public class HookDefinition implements IHookDefinition { } /** - * Create a hook definition from an interface method. - * Automatically extracts metadata from the method signature and annotations. + * Create a new builder for a hook definition. * - * @param hookName The hook name (may differ from method name) - * @param method The method to extract metadata from - * @return A hook definition with extracted metadata + * @param hookName The hook function name + * @return A new builder instance */ - public static HookDefinition fromMethod(String hookName, Method method) { - Builder builder = builder(hookName); - - // Extract event type from first parameter - if (method.getParameterCount() > 0) { - Class eventType = method.getParameterTypes()[0]; - builder.eventClass(eventType); - - // Extract @Cancelable annotation - if (eventType.isAnnotationPresent(Cancelable.class)) { - builder.cancelable(true); - } - - // Build required imports from event type - String importName = getImportForClass(eventType); - if (importName != null) { - builder.requiredImports(importName); - } - } - - // Extract parameter names from @ParamName annotations - Parameter[] params = method.getParameters(); - if (params.length > 0) { - String[] names = new String[params.length]; - for (int i = 0; i < params.length; i++) { - ParamName annotation = params[i].getAnnotation(ParamName.class); - if (annotation != null) { - names[i] = annotation.value(); - } else { - names[i] = params[i].getType().getSimpleName().toLowerCase(); - } - } - builder.paramNames(names); - } - - return builder.build(); + public static Builder builder(String hookName) { + return new Builder(hookName); } /** @@ -182,7 +137,6 @@ public class HookDefinition implements IHookDefinition { } String name = enclosing.getName(); - // Skip java.lang classes if (name.startsWith("java.lang.")) { return null; } @@ -192,9 +146,6 @@ public class HookDefinition implements IHookDefinition { // ==================== Builder ==================== - /** - * Builder for creating HookDefinition instances with a fluent API. - */ public static class Builder { private final String hookName; private String eventClassName; @@ -210,27 +161,18 @@ public class HookDefinition implements IHookDefinition { } /** - * Set the event class by name (avoids classloading issues). - * - * @param className Full qualified class name (use $ for nested classes) - * @return this builder - */ - public Builder eventClass(String className) { - this.eventClassName = className; - return this; - } - - /** - * Set the event class directly (convenience method). - * The class name will be extracted automatically. - * - * @param clazz The event class - * @return this builder + * Set the event class directly. + * Automatically derives: class name, required imports, and cancelable status. */ public Builder eventClass(Class clazz) { if (clazz != null) { this.eventClassName = clazz.getName(); - // Auto-detect cancelable + + String importName = getImportForClass(clazz); + if (importName != null) { + this.requiredImports = new String[]{importName}; + } + if (clazz.isAnnotationPresent(Cancelable.class)) { this.cancelable = true; } @@ -238,11 +180,16 @@ public class HookDefinition implements IHookDefinition { return this; } + /** + * Set the event class by name (for addon classes that may not be loaded). + */ + public Builder eventClass(String className) { + this.eventClassName = className; + return this; + } + /** * Set parameter names for stub generation. - * - * @param names Parameter names - * @return this builder */ public Builder paramNames(String... names) { this.paramNames = names; @@ -250,10 +197,7 @@ public class HookDefinition implements IHookDefinition { } /** - * Set required imports for this hook's event type. - * - * @param imports Fully qualified class names to import - * @return this builder + * Set required imports (only needed if using eventClass(String)). */ public Builder requiredImports(String... imports) { this.requiredImports = imports; @@ -262,20 +206,12 @@ public class HookDefinition implements IHookDefinition { /** * Set whether the event is cancelable. - * - * @param cancelable true if the event can be canceled - * @return this builder */ public Builder cancelable(boolean cancelable) { this.cancelable = cancelable; return this; } - /** - * Build the hook definition. - * - * @return A new HookDefinition instance - */ public HookDefinition build() { return new HookDefinition(this); } @@ -285,19 +221,14 @@ public class HookDefinition implements IHookDefinition { @Override public String toString() { - return "HookDefinition{" + - "hookName='" + hookName + '\'' + - ", eventClassName='" + eventClassName + '\'' + - ", cancelable=" + cancelable + - '}'; + return "HookDefinition{hookName='" + hookName + "', eventClass='" + eventClassName + "'}"; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (!(obj instanceof HookDefinition)) return false; - HookDefinition other = (HookDefinition) obj; - return hookName.equals(other.hookName); + return hookName.equals(((HookDefinition) obj).hookName); } @Override diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index 8ecc8d43f..f89177c9e 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -1,5 +1,6 @@ package noppes.npcs.controllers; +import noppes.npcs.api.event.*; import noppes.npcs.api.handler.IHookDefinition; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; @@ -8,28 +9,24 @@ import noppes.npcs.controllers.data.RecipeScript; import java.util.*; +import static noppes.npcs.constants.EnumScriptType.*; +import static noppes.npcs.constants.ScriptContext.*; + /** * Controller for managing script hooks. * Allows addon mods to register custom hooks that appear in script editor GUIs. * - *

Rich Hook Registration

- * Supports full hook definitions with metadata for proper stub generation: + *

Addon Hook Registration

*
{@code
- * ScriptHookController.Instance.registerHookDefinition("player",
- *     HookDefinition.builder("onDBCTransform")
- *         .eventClass(IDBCEvent.TransformEvent.class)
- *         .requiredImports("com.dbc.api.event.IDBCEvent")
- *         .build());
+ * ScriptHookController.Instance.registerHook("player",
+ *     HookDefinition.of("onDBCTransform", IDBCEvent.TransformEvent.class));
  * }
*/ public class ScriptHookController implements IScriptHookHandler { public static ScriptHookController Instance; - // Hook definitions per context - single source of truth private final Map> hookDefinitions = new HashMap<>(); - - // Revision counter for cache invalidation private int hookRevision = 0; public ScriptHookController() { @@ -37,286 +34,268 @@ public class ScriptHookController implements IScriptHookHandler { initializeBuiltInHooks(); } + // ==================== INITIALIZATION ==================== + private void initializeBuiltInHooks() { - // Initialize empty maps for all contexts from ScriptContext for (ScriptContext context : ScriptContext.values()) { if (!context.hookContext.isEmpty()) { hookDefinitions.put(context.hookContext, new LinkedHashMap<>()); } } - // NPC hooks initializeNpcHooks(); - - // Player hooks initializePlayerHooks(); - - // Block hooks initializeBlockHooks(); - - // Item hooks initializeItemHooks(); - - // Linked item hooks initializeLinkedItemHooks(); - - // Recipe hooks - registerHookDef(ScriptContext.RECIPE, RecipeScript.ScriptType.PRE.function, null, null); - registerHookDef(ScriptContext.RECIPE, RecipeScript.ScriptType.POST.function, null, null); - - // Effect hooks - registerHookDef(ScriptContext.EFFECT, EnumScriptType.ON_EFFECT_ADD.function, null, null); - registerHookDef(ScriptContext.EFFECT, EnumScriptType.ON_EFFECT_TICK.function, null, null); - registerHookDef(ScriptContext.EFFECT, EnumScriptType.ON_EFFECT_REMOVE.function, null, null); - - // Forge hooks - registerHookDef(ScriptContext.FORGE, EnumScriptType.INIT.function, null, null); - registerHookDef(ScriptContext.FORGE, EnumScriptType.CNPC_NATURAL_SPAWN.function, null, null); + initializeRecipeHooks(); + initializeEffectHooks(); + initializeForgeHooks(); } private void initializeNpcHooks() { - String ctx = ScriptContext.NPC.hookContext; - String npcImport = "noppes.npcs.api.event.INpcEvent"; - String projImport = "noppes.npcs.api.event.IProjectileEvent"; - String abilityImport = "noppes.npcs.api.event.IAbilityEvent"; - // Core lifecycle - registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.INpcEvent$InitEvent", npcImport); - registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.INpcEvent$UpdateEvent", npcImport); - registerHookDef(ctx, EnumScriptType.TIMER.function, "noppes.npcs.api.event.INpcEvent$TimerEvent", npcImport); + hook(NPC, INIT, INpcEvent.InitEvent.class); + hook(NPC, TICK, INpcEvent.UpdateEvent.class); + hook(NPC, TIMER, INpcEvent.TimerEvent.class); // Interaction - registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.INpcEvent$InteractEvent", npcImport); - registerHookDef(ctx, EnumScriptType.DIALOG.function, "noppes.npcs.api.event.INpcEvent$DialogEvent", npcImport); - registerHookDef(ctx, EnumScriptType.DIALOG_CLOSE.function, "noppes.npcs.api.event.INpcEvent$DialogClosedEvent", npcImport); - registerHookDef(ctx, EnumScriptType.COLLIDE.function, "noppes.npcs.api.event.INpcEvent$CollideEvent", npcImport); + hook(NPC, INTERACT, INpcEvent.InteractEvent.class); + hook(NPC, DIALOG, INpcEvent.DialogEvent.class); + hook(NPC, DIALOG_CLOSE, INpcEvent.DialogClosedEvent.class); + hook(NPC, COLLIDE, INpcEvent.CollideEvent.class); // Combat - registerHookDef(ctx, EnumScriptType.DAMAGED.function, "noppes.npcs.api.event.INpcEvent$DamagedEvent", npcImport); - registerHookDef(ctx, EnumScriptType.KILLED.function, "noppes.npcs.api.event.INpcEvent$DiedEvent", npcImport); - registerHookDef(ctx, EnumScriptType.KILLS.function, "noppes.npcs.api.event.INpcEvent$KilledEntityEvent", npcImport); - registerHookDef(ctx, EnumScriptType.ATTACK_MELEE.function, "noppes.npcs.api.event.INpcEvent$MeleeAttackEvent", npcImport); - registerHookDef(ctx, EnumScriptType.ATTACK_SWING.function, "noppes.npcs.api.event.INpcEvent$SwingEvent", npcImport); - registerHookDef(ctx, EnumScriptType.RANGED_LAUNCHED.function, "noppes.npcs.api.event.INpcEvent$RangedLaunchedEvent", npcImport); - registerHookDef(ctx, EnumScriptType.TARGET.function, "noppes.npcs.api.event.INpcEvent$TargetEvent", npcImport); - registerHookDef(ctx, EnumScriptType.TARGET_LOST.function, "noppes.npcs.api.event.INpcEvent$TargetLostEvent", npcImport); + hook(NPC, DAMAGED, INpcEvent.DamagedEvent.class); + hook(NPC, KILLED, INpcEvent.DiedEvent.class); + hook(NPC, KILLS, INpcEvent.KilledEntityEvent.class); + hook(NPC, ATTACK_MELEE, INpcEvent.MeleeAttackEvent.class); + hook(NPC, ATTACK_SWING, INpcEvent.SwingEvent.class); + hook(NPC, RANGED_LAUNCHED, INpcEvent.RangedLaunchedEvent.class); + hook(NPC, TARGET, INpcEvent.TargetEvent.class); + hook(NPC, TARGET_LOST, INpcEvent.TargetLostEvent.class); - // Projectile events - registerHookDef(ctx, EnumScriptType.PROJECTILE_TICK.function, "noppes.npcs.api.event.IProjectileEvent$UpdateEvent", projImport); - registerHookDef(ctx, EnumScriptType.PROJECTILE_IMPACT.function, "noppes.npcs.api.event.IProjectileEvent$ImpactEvent", projImport); + // Projectile + hook(NPC, PROJECTILE_TICK, IProjectileEvent.UpdateEvent.class); + hook(NPC, PROJECTILE_IMPACT, IProjectileEvent.ImpactEvent.class); - // Ability events - registerHookDef(ctx, EnumScriptType.ABILITY_START.function, "noppes.npcs.api.event.IAbilityEvent$StartEvent", abilityImport); - registerHookDef(ctx, EnumScriptType.ABILITY_EXECUTE.function, "noppes.npcs.api.event.IAbilityEvent$ExecuteEvent", abilityImport); - registerHookDef(ctx, EnumScriptType.ABILITY_HIT.function, "noppes.npcs.api.event.IAbilityEvent$HitEvent", abilityImport); - registerHookDef(ctx, EnumScriptType.ABILITY_TICK.function, "noppes.npcs.api.event.IAbilityEvent$TickEvent", abilityImport); - registerHookDef(ctx, EnumScriptType.ABILITY_INTERRUPT.function, "noppes.npcs.api.event.IAbilityEvent$InterruptEvent", abilityImport); - registerHookDef(ctx, EnumScriptType.ABILITY_COMPLETE.function, "noppes.npcs.api.event.IAbilityEvent$CompleteEvent", abilityImport); + // Ability + hook(NPC, ABILITY_START, IAbilityEvent.StartEvent.class); + hook(NPC, ABILITY_EXECUTE, IAbilityEvent.ExecuteEvent.class); + hook(NPC, ABILITY_HIT, IAbilityEvent.HitEvent.class); + hook(NPC, ABILITY_TICK, IAbilityEvent.TickEvent.class); + hook(NPC, ABILITY_INTERRUPT, IAbilityEvent.InterruptEvent.class); + hook(NPC, ABILITY_COMPLETE, IAbilityEvent.CompleteEvent.class); } private void initializePlayerHooks() { - String ctx = ScriptContext.PLAYER.hookContext; - String playerImport = "noppes.npcs.api.event.IPlayerEvent"; - String questImport = "noppes.npcs.api.event.IQuestEvent"; - String dialogImport = "noppes.npcs.api.event.IDialogEvent"; - String factionImport = "noppes.npcs.api.event.IFactionEvent"; - String partyImport = "noppes.npcs.api.event.IPartyEvent"; - String guiImport = "noppes.npcs.api.event.ICustomGuiEvent"; - String animImport = "noppes.npcs.api.event.IAnimationEvent"; - // Core lifecycle - registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.IPlayerEvent$InitEvent", playerImport); - registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.IPlayerEvent$UpdateEvent", playerImport); - registerHookDef(ctx, EnumScriptType.TIMER.function, "noppes.npcs.api.event.IPlayerEvent$TimerEvent", playerImport); + hook(PLAYER, INIT, IPlayerEvent.InitEvent.class); + hook(PLAYER, TICK, IPlayerEvent.UpdateEvent.class); + hook(PLAYER, TIMER, IPlayerEvent.TimerEvent.class); // Combat - registerHookDef(ctx, EnumScriptType.ATTACK.function, "noppes.npcs.api.event.IPlayerEvent$AttackEvent", playerImport); - registerHookDef(ctx, EnumScriptType.ATTACKED.function, "noppes.npcs.api.event.IPlayerEvent$AttackedEvent", playerImport); - registerHookDef(ctx, EnumScriptType.DAMAGED.function, "noppes.npcs.api.event.IPlayerEvent$DamagedEvent", playerImport); - registerHookDef(ctx, EnumScriptType.DAMAGED_ENTITY.function, "noppes.npcs.api.event.IPlayerEvent$DamagedEntityEvent", playerImport); - registerHookDef(ctx, EnumScriptType.KILLS.function, "noppes.npcs.api.event.IPlayerEvent$KilledEntityEvent", playerImport); - registerHookDef(ctx, EnumScriptType.KILLED.function, "noppes.npcs.api.event.IPlayerEvent$DiedEvent", playerImport); + hook(PLAYER, ATTACK, IPlayerEvent.AttackEvent.class); + hook(PLAYER, ATTACKED, IPlayerEvent.AttackedEvent.class); + hook(PLAYER, DAMAGED, IPlayerEvent.DamagedEvent.class); + hook(PLAYER, DAMAGED_ENTITY, IPlayerEvent.DamagedEntityEvent.class); + hook(PLAYER, KILLS, IPlayerEvent.KilledEntityEvent.class); + hook(PLAYER, KILLED, IPlayerEvent.DiedEvent.class); // Interaction - registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.IPlayerEvent$InteractEvent", playerImport); - registerHookDef(ctx, EnumScriptType.RIGHT_CLICK.function, "noppes.npcs.api.event.IPlayerEvent$RightClickEvent", playerImport); - registerHookDef(ctx, EnumScriptType.BREAK_BLOCK.function, "noppes.npcs.api.event.IPlayerEvent$BreakEvent", playerImport); - registerHookDef(ctx, EnumScriptType.CHAT.function, "noppes.npcs.api.event.IPlayerEvent$ChatEvent", playerImport); + hook(PLAYER, INTERACT, IPlayerEvent.InteractEvent.class); + hook(PLAYER, RIGHT_CLICK, IPlayerEvent.RightClickEvent.class); + hook(PLAYER, BREAK_BLOCK, IPlayerEvent.BreakEvent.class); + hook(PLAYER, CHAT, IPlayerEvent.ChatEvent.class); // Connection - registerHookDef(ctx, EnumScriptType.LOGIN.function, "noppes.npcs.api.event.IPlayerEvent$LoginEvent", playerImport); - registerHookDef(ctx, EnumScriptType.LOGOUT.function, "noppes.npcs.api.event.IPlayerEvent$LogoutEvent", playerImport); - registerHookDef(ctx, EnumScriptType.RESPAWN.function, "noppes.npcs.api.event.IPlayerEvent$RespawnEvent", playerImport); + hook(PLAYER, LOGIN, IPlayerEvent.LoginEvent.class); + hook(PLAYER, LOGOUT, IPlayerEvent.LogoutEvent.class); + hook(PLAYER, RESPAWN, IPlayerEvent.RespawnEvent.class); // Input - registerHookDef(ctx, EnumScriptType.KEY_PRESSED.function, "noppes.npcs.api.event.IPlayerEvent$KeyPressedEvent", playerImport); - registerHookDef(ctx, EnumScriptType.MOUSE_CLICKED.function, "noppes.npcs.api.event.IPlayerEvent$MouseClickedEvent", playerImport); + hook(PLAYER, KEY_PRESSED, IPlayerEvent.KeyPressedEvent.class); + hook(PLAYER, MOUSE_CLICKED, IPlayerEvent.MouseClickedEvent.class); // Items - registerHookDef(ctx, EnumScriptType.PICKUP.function, "noppes.npcs.api.event.IPlayerEvent$PickUpEvent", playerImport); - registerHookDef(ctx, EnumScriptType.TOSS.function, "noppes.npcs.api.event.IPlayerEvent$TossEvent", playerImport); - registerHookDef(ctx, EnumScriptType.DROP.function, "noppes.npcs.api.event.IPlayerEvent$DropEvent", playerImport); - registerHookDef(ctx, EnumScriptType.PICKUP_XP.function, "noppes.npcs.api.event.IPlayerEvent$PickupXPEvent", playerImport); - registerHookDef(ctx, EnumScriptType.START_USING_ITEM.function, "noppes.npcs.api.event.IPlayerEvent$StartUsingItem", playerImport); - registerHookDef(ctx, EnumScriptType.USING_ITEM.function, "noppes.npcs.api.event.IPlayerEvent$UsingItem", playerImport); - registerHookDef(ctx, EnumScriptType.STOP_USING_ITEM.function, "noppes.npcs.api.event.IPlayerEvent$StopUsingItem", playerImport); - registerHookDef(ctx, EnumScriptType.FINISH_USING_ITEM.function, "noppes.npcs.api.event.IPlayerEvent$FinishUsingItem", playerImport); - registerHookDef(ctx, EnumScriptType.CONTAINER_OPEN.function, "noppes.npcs.api.event.IPlayerEvent$ContainerOpen", playerImport); + hook(PLAYER, PICKUP, IPlayerEvent.PickUpEvent.class); + hook(PLAYER, TOSS, IPlayerEvent.TossEvent.class); + hook(PLAYER, DROP, IPlayerEvent.DropEvent.class); + hook(PLAYER, PICKUP_XP, IPlayerEvent.PickupXPEvent.class); + hook(PLAYER, START_USING_ITEM, IPlayerEvent.StartUsingItem.class); + hook(PLAYER, USING_ITEM, IPlayerEvent.UsingItem.class); + hook(PLAYER, STOP_USING_ITEM, IPlayerEvent.StopUsingItem.class); + hook(PLAYER, FINISH_USING_ITEM, IPlayerEvent.FinishUsingItem.class); + hook(PLAYER, CONTAINER_OPEN, IPlayerEvent.ContainerOpen.class); // Movement - registerHookDef(ctx, EnumScriptType.JUMP.function, "noppes.npcs.api.event.IPlayerEvent$JumpEvent", playerImport); - registerHookDef(ctx, EnumScriptType.FALL.function, "noppes.npcs.api.event.IPlayerEvent$FallEvent", playerImport); - registerHookDef(ctx, EnumScriptType.CHANGED_DIM.function, "noppes.npcs.api.event.IPlayerEvent$ChangedDimension", playerImport); + hook(PLAYER, JUMP, IPlayerEvent.JumpEvent.class); + hook(PLAYER, FALL, IPlayerEvent.FallEvent.class); + hook(PLAYER, CHANGED_DIM, IPlayerEvent.ChangedDimension.class); // World - registerHookDef(ctx, EnumScriptType.RANGED_CHARGE.function, "noppes.npcs.api.event.IPlayerEvent$RangedChargeEvent", playerImport); - registerHookDef(ctx, EnumScriptType.RANGED_LAUNCHED.function, "noppes.npcs.api.event.IPlayerEvent$RangedLaunchedEvent", playerImport); - registerHookDef(ctx, EnumScriptType.USE_HOE.function, "noppes.npcs.api.event.IPlayerEvent$UseHoeEvent", playerImport); - registerHookDef(ctx, EnumScriptType.BONEMEAL.function, "noppes.npcs.api.event.IPlayerEvent$BonemealEvent", playerImport); - registerHookDef(ctx, EnumScriptType.FILL_BUCKET.function, "noppes.npcs.api.event.IPlayerEvent$FillBucketEvent", playerImport); - registerHookDef(ctx, EnumScriptType.WAKE_UP.function, "noppes.npcs.api.event.IPlayerEvent$WakeUpEvent", playerImport); - registerHookDef(ctx, EnumScriptType.SLEEP.function, "noppes.npcs.api.event.IPlayerEvent$SleepEvent", playerImport); - registerHookDef(ctx, EnumScriptType.PLAYSOUND.function, "noppes.npcs.api.event.IPlayerEvent$SoundEvent", playerImport); - registerHookDef(ctx, EnumScriptType.LIGHTNING.function, "noppes.npcs.api.event.IPlayerEvent$LightningEvent", playerImport); - registerHookDef(ctx, EnumScriptType.SCRIPT_COMMAND.function, "noppes.npcs.api.event.ICustomNPCsEvent$ScriptedCommandEvent", "noppes.npcs.api.event.ICustomNPCsEvent"); + hook(PLAYER, RANGED_CHARGE, IPlayerEvent.RangedChargeEvent.class); + hook(PLAYER, RANGED_LAUNCHED, IPlayerEvent.RangedLaunchedEvent.class); + hook(PLAYER, USE_HOE, IPlayerEvent.UseHoeEvent.class); + hook(PLAYER, BONEMEAL, IPlayerEvent.BonemealEvent.class); + hook(PLAYER, FILL_BUCKET, IPlayerEvent.FillBucketEvent.class); + hook(PLAYER, WAKE_UP, IPlayerEvent.WakeUpEvent.class); + hook(PLAYER, SLEEP, IPlayerEvent.SleepEvent.class); + hook(PLAYER, PLAYSOUND, IPlayerEvent.SoundEvent.class); + hook(PLAYER, LIGHTNING, IPlayerEvent.LightningEvent.class); + hook(PLAYER, SCRIPT_COMMAND, ICustomNPCsEvent.ScriptedCommandEvent.class); - // Quest events - registerHookDef(ctx, EnumScriptType.QUEST_START.function, "noppes.npcs.api.event.IQuestEvent$QuestStartEvent", questImport); - registerHookDef(ctx, EnumScriptType.QUEST_COMPLETED.function, "noppes.npcs.api.event.IQuestEvent$QuestCompletedEvent", questImport); - registerHookDef(ctx, EnumScriptType.QUEST_TURNIN.function, "noppes.npcs.api.event.IQuestEvent$QuestTurnedInEvent", questImport); + // Quest + hook(PLAYER, QUEST_START, IQuestEvent.QuestStartEvent.class); + hook(PLAYER, QUEST_COMPLETED, IQuestEvent.QuestCompletedEvent.class); + hook(PLAYER, QUEST_TURNIN, IQuestEvent.QuestTurnedInEvent.class); - // Dialog events - registerHookDef(ctx, EnumScriptType.DIALOG_OPEN.function, "noppes.npcs.api.event.IDialogEvent$DialogOpen", dialogImport); - registerHookDef(ctx, EnumScriptType.DIALOG_OPTION.function, "noppes.npcs.api.event.IDialogEvent$DialogOption", dialogImport); - registerHookDef(ctx, EnumScriptType.DIALOG_CLOSE.function, "noppes.npcs.api.event.IDialogEvent$DialogClosed", dialogImport); + // Dialog + hook(PLAYER, DIALOG_OPEN, IDialogEvent.DialogOpen.class); + hook(PLAYER, DIALOG_OPTION, IDialogEvent.DialogOption.class); + hook(PLAYER, DIALOG_CLOSE, IDialogEvent.DialogClosed.class); - // Faction events - registerHookDef(ctx, EnumScriptType.FACTION_POINTS.function, "noppes.npcs.api.event.IFactionEvent$FactionPoints", factionImport); + // Faction + hook(PLAYER, FACTION_POINTS, IFactionEvent.FactionPoints.class); - // Custom GUI events - registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_CLOSED.function, "noppes.npcs.api.event.ICustomGuiEvent$CloseEvent", guiImport); - registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_BUTTON.function, "noppes.npcs.api.event.ICustomGuiEvent$ButtonEvent", guiImport); - registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_SLOT.function, "noppes.npcs.api.event.ICustomGuiEvent$SlotEvent", guiImport); - registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_SLOT_CLICKED.function, "noppes.npcs.api.event.ICustomGuiEvent$SlotClickEvent", guiImport); - registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_SCROLL.function, "noppes.npcs.api.event.ICustomGuiEvent$ScrollEvent", guiImport); - registerHookDef(ctx, EnumScriptType.CUSTOM_GUI_TEXTFIELD.function, "noppes.npcs.api.event.ICustomGuiEvent$UnfocusedEvent", guiImport); + // Custom GUI + hook(PLAYER, CUSTOM_GUI_CLOSED, ICustomGuiEvent.CloseEvent.class); + hook(PLAYER, CUSTOM_GUI_BUTTON, ICustomGuiEvent.ButtonEvent.class); + hook(PLAYER, CUSTOM_GUI_SLOT, ICustomGuiEvent.SlotEvent.class); + hook(PLAYER, CUSTOM_GUI_SLOT_CLICKED, ICustomGuiEvent.SlotClickEvent.class); + hook(PLAYER, CUSTOM_GUI_SCROLL, ICustomGuiEvent.ScrollEvent.class); + hook(PLAYER, CUSTOM_GUI_TEXTFIELD, ICustomGuiEvent.UnfocusedEvent.class); - // Party events - registerHookDef(ctx, EnumScriptType.PARTY_QUEST_COMPLETED.function, "noppes.npcs.api.event.IPartyEvent$PartyQuestCompletedEvent", partyImport); - registerHookDef(ctx, EnumScriptType.PARTY_QUEST_SET.function, "noppes.npcs.api.event.IPartyEvent$PartyQuestSetEvent", partyImport); - registerHookDef(ctx, EnumScriptType.PARTY_QUEST_TURNED_IN.function, "noppes.npcs.api.event.IPartyEvent$PartyQuestTurnedInEvent", partyImport); - registerHookDef(ctx, EnumScriptType.PARTY_INVITE.function, "noppes.npcs.api.event.IPartyEvent$PartyInviteEvent", partyImport); - registerHookDef(ctx, EnumScriptType.PARTY_KICK.function, "noppes.npcs.api.event.IPartyEvent$PartyKickEvent", partyImport); - registerHookDef(ctx, EnumScriptType.PARTY_LEAVE.function, "noppes.npcs.api.event.IPartyEvent$PartyLeaveEvent", partyImport); - registerHookDef(ctx, EnumScriptType.PARTY_DISBAND.function, "noppes.npcs.api.event.IPartyEvent$PartyDisbandEvent", partyImport); + // Party + hook(PLAYER, PARTY_QUEST_COMPLETED, IPartyEvent.PartyQuestCompletedEvent.class); + hook(PLAYER, PARTY_QUEST_SET, IPartyEvent.PartyQuestSetEvent.class); + hook(PLAYER, PARTY_QUEST_TURNED_IN, IPartyEvent.PartyQuestTurnedInEvent.class); + hook(PLAYER, PARTY_INVITE, IPartyEvent.PartyInviteEvent.class); + hook(PLAYER, PARTY_KICK, IPartyEvent.PartyKickEvent.class); + hook(PLAYER, PARTY_LEAVE, IPartyEvent.PartyLeaveEvent.class); + hook(PLAYER, PARTY_DISBAND, IPartyEvent.PartyDisbandEvent.class); - // Animation events - registerHookDef(ctx, EnumScriptType.ANIMATION_START.function, "noppes.npcs.api.event.IAnimationEvent$Started", animImport); - registerHookDef(ctx, EnumScriptType.ANIMATION_END.function, "noppes.npcs.api.event.IAnimationEvent$Ended", animImport); - registerHookDef(ctx, EnumScriptType.ANIMATION_FRAME_ENTER.function, "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Entered", animImport); - registerHookDef(ctx, EnumScriptType.ANIMATION_FRAME_EXIT.function, "noppes.npcs.api.event.IAnimationEvent$IFrameEvent$Exited", animImport); + // Animation + hook(PLAYER, ANIMATION_START, IAnimationEvent.Started.class); + hook(PLAYER, ANIMATION_END, IAnimationEvent.Ended.class); + hook(PLAYER, ANIMATION_FRAME_ENTER, IAnimationEvent.IFrameEvent.Entered.class); + hook(PLAYER, ANIMATION_FRAME_EXIT, IAnimationEvent.IFrameEvent.Exited.class); - // Profile events - registerHookDef(ctx, EnumScriptType.PROFILE_CHANGE.function, "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Changed", playerImport); - registerHookDef(ctx, EnumScriptType.PROFILE_REMOVE.function, "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Removed", playerImport); - registerHookDef(ctx, EnumScriptType.PROFILE_CREATE.function, "noppes.npcs.api.event.IPlayerEvent$ProfileEvent$Create", playerImport); + // Profile + hook(PLAYER, PROFILE_CHANGE, IPlayerEvent.ProfileEvent.Changed.class); + hook(PLAYER, PROFILE_REMOVE, IPlayerEvent.ProfileEvent.Removed.class); + hook(PLAYER, PROFILE_CREATE, IPlayerEvent.ProfileEvent.Create.class); - // Effect events (for player context) - registerHookDef(ctx, EnumScriptType.ON_EFFECT_ADD.function, "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Added", playerImport); - registerHookDef(ctx, EnumScriptType.ON_EFFECT_TICK.function, "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Ticked", playerImport); - registerHookDef(ctx, EnumScriptType.ON_EFFECT_REMOVE.function, "noppes.npcs.api.event.IPlayerEvent$EffectEvent$Removed", playerImport); + // Effect (player context) + hook(PLAYER, ON_EFFECT_ADD, IPlayerEvent.EffectEvent.Added.class); + hook(PLAYER, ON_EFFECT_TICK, IPlayerEvent.EffectEvent.Ticked.class); + hook(PLAYER, ON_EFFECT_REMOVE, IPlayerEvent.EffectEvent.Removed.class); } private void initializeBlockHooks() { - String ctx = ScriptContext.BLOCK.hookContext; - String blockImport = "noppes.npcs.api.event.IBlockEvent"; - - registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.IBlockEvent$InitEvent", blockImport); - registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.IBlockEvent$UpdateEvent", blockImport); - registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.IBlockEvent$InteractEvent", blockImport); - registerHookDef(ctx, EnumScriptType.FALLEN_UPON.function, "noppes.npcs.api.event.IBlockEvent$EntityFallenUponEvent", blockImport); - registerHookDef(ctx, EnumScriptType.REDSTONE.function, "noppes.npcs.api.event.IBlockEvent$RedstoneEvent", blockImport); - registerHookDef(ctx, EnumScriptType.BROKEN.function, "noppes.npcs.api.event.IBlockEvent$BreakEvent", blockImport); - registerHookDef(ctx, EnumScriptType.EXPLODED.function, "noppes.npcs.api.event.IBlockEvent$ExplodedEvent", blockImport); - registerHookDef(ctx, EnumScriptType.RAIN_FILLED.function, "noppes.npcs.api.event.IBlockEvent$RainFillEvent", blockImport); - registerHookDef(ctx, EnumScriptType.NEIGHBOR_CHANGED.function, "noppes.npcs.api.event.IBlockEvent$NeighborChangedEvent", blockImport); - registerHookDef(ctx, EnumScriptType.CLICKED.function, "noppes.npcs.api.event.IBlockEvent$ClickedEvent", blockImport); - registerHookDef(ctx, EnumScriptType.HARVESTED.function, "noppes.npcs.api.event.IBlockEvent$HarvestedEvent", blockImport); - registerHookDef(ctx, EnumScriptType.COLLIDE.function, "noppes.npcs.api.event.IBlockEvent$CollidedEvent", blockImport); - registerHookDef(ctx, EnumScriptType.TIMER.function, "noppes.npcs.api.event.IBlockEvent$TimerEvent", blockImport); + hook(BLOCK, INIT, IBlockEvent.InitEvent.class); + hook(BLOCK, TICK, IBlockEvent.UpdateEvent.class); + hook(BLOCK, INTERACT, IBlockEvent.InteractEvent.class); + hook(BLOCK, FALLEN_UPON, IBlockEvent.EntityFallenUponEvent.class); + hook(BLOCK, REDSTONE, IBlockEvent.RedstoneEvent.class); + hook(BLOCK, BROKEN, IBlockEvent.BreakEvent.class); + hook(BLOCK, EXPLODED, IBlockEvent.ExplodedEvent.class); + hook(BLOCK, RAIN_FILLED, IBlockEvent.RainFillEvent.class); + hook(BLOCK, NEIGHBOR_CHANGED, IBlockEvent.NeighborChangedEvent.class); + hook(BLOCK, CLICKED, IBlockEvent.ClickedEvent.class); + hook(BLOCK, HARVESTED, IBlockEvent.HarvestedEvent.class); + hook(BLOCK, COLLIDE, IBlockEvent.CollidedEvent.class); + hook(BLOCK, TIMER, IBlockEvent.TimerEvent.class); } private void initializeItemHooks() { - String ctx = ScriptContext.ITEM.hookContext; - String itemImport = "noppes.npcs.api.event.IItemEvent"; - - registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); - registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); - registerHookDef(ctx, EnumScriptType.TOSSED.function, "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); - registerHookDef(ctx, EnumScriptType.PICKEDUP.function, "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); - registerHookDef(ctx, EnumScriptType.SPAWN.function, "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); - registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); - registerHookDef(ctx, EnumScriptType.RIGHT_CLICK.function, "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); - registerHookDef(ctx, EnumScriptType.ATTACK.function, "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); - registerHookDef(ctx, EnumScriptType.START_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); - registerHookDef(ctx, EnumScriptType.USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); - registerHookDef(ctx, EnumScriptType.STOP_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); - registerHookDef(ctx, EnumScriptType.FINISH_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); + hook(ITEM, INIT, IItemEvent.InitEvent.class); + hook(ITEM, TICK, IItemEvent.UpdateEvent.class); + hook(ITEM, TOSSED, IItemEvent.TossedEvent.class); + hook(ITEM, PICKEDUP, IItemEvent.PickedUpEvent.class); + hook(ITEM, SPAWN, IItemEvent.SpawnEvent.class); + hook(ITEM, INTERACT, IItemEvent.InteractEvent.class); + hook(ITEM, RIGHT_CLICK, IItemEvent.RightClickEvent.class); + hook(ITEM, ATTACK, IItemEvent.AttackEvent.class); + hook(ITEM, START_USING_ITEM, IItemEvent.StartUsingItem.class); + hook(ITEM, USING_ITEM, IItemEvent.UsingItem.class); + hook(ITEM, STOP_USING_ITEM, IItemEvent.StopUsingItem.class); + hook(ITEM, FINISH_USING_ITEM, IItemEvent.FinishUsingItem.class); } private void initializeLinkedItemHooks() { - String ctx = ScriptContext.LINKED_ITEM.hookContext; - String itemImport = "noppes.npcs.api.event.IItemEvent"; - String linkedImport = "noppes.npcs.api.event.ILinkedItemEvent"; - - // Linked item specific hooks - registerHookDef(ctx, EnumScriptType.LINKED_ITEM_BUILD.function, "noppes.npcs.api.event.ILinkedItemEvent$BuildEvent", linkedImport); - registerHookDef(ctx, EnumScriptType.LINKED_ITEM_VERSION.function, "noppes.npcs.api.event.ILinkedItemEvent$VersionChangeEvent", linkedImport); + // Linked item specific + hook(LINKED_ITEM, LINKED_ITEM_BUILD, ILinkedItemEvent.BuildEvent.class); + hook(LINKED_ITEM, LINKED_ITEM_VERSION, ILinkedItemEvent.VersionChangeEvent.class); // Standard item hooks - registerHookDef(ctx, EnumScriptType.INIT.function, "noppes.npcs.api.event.IItemEvent$InitEvent", itemImport); - registerHookDef(ctx, EnumScriptType.TICK.function, "noppes.npcs.api.event.IItemEvent$UpdateEvent", itemImport); - registerHookDef(ctx, EnumScriptType.TOSSED.function, "noppes.npcs.api.event.IItemEvent$TossedEvent", itemImport); - registerHookDef(ctx, EnumScriptType.PICKEDUP.function, "noppes.npcs.api.event.IItemEvent$PickedUpEvent", itemImport); - registerHookDef(ctx, EnumScriptType.SPAWN.function, "noppes.npcs.api.event.IItemEvent$SpawnEvent", itemImport); - registerHookDef(ctx, EnumScriptType.INTERACT.function, "noppes.npcs.api.event.IItemEvent$InteractEvent", itemImport); - registerHookDef(ctx, EnumScriptType.RIGHT_CLICK.function, "noppes.npcs.api.event.IItemEvent$RightClickEvent", itemImport); - registerHookDef(ctx, EnumScriptType.ATTACK.function, "noppes.npcs.api.event.IItemEvent$AttackEvent", itemImport); - registerHookDef(ctx, EnumScriptType.START_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$StartUsingItem", itemImport); - registerHookDef(ctx, EnumScriptType.USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$UsingItem", itemImport); - registerHookDef(ctx, EnumScriptType.STOP_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$StopUsingItem", itemImport); - registerHookDef(ctx, EnumScriptType.FINISH_USING_ITEM.function, "noppes.npcs.api.event.IItemEvent$FinishUsingItem", itemImport); + hook(LINKED_ITEM, INIT, IItemEvent.InitEvent.class); + hook(LINKED_ITEM, TICK, IItemEvent.UpdateEvent.class); + hook(LINKED_ITEM, TOSSED, IItemEvent.TossedEvent.class); + hook(LINKED_ITEM, PICKEDUP, IItemEvent.PickedUpEvent.class); + hook(LINKED_ITEM, SPAWN, IItemEvent.SpawnEvent.class); + hook(LINKED_ITEM, INTERACT, IItemEvent.InteractEvent.class); + hook(LINKED_ITEM, RIGHT_CLICK, IItemEvent.RightClickEvent.class); + hook(LINKED_ITEM, ATTACK, IItemEvent.AttackEvent.class); + hook(LINKED_ITEM, START_USING_ITEM, IItemEvent.StartUsingItem.class); + hook(LINKED_ITEM, USING_ITEM, IItemEvent.UsingItem.class); + hook(LINKED_ITEM, STOP_USING_ITEM, IItemEvent.StopUsingItem.class); + hook(LINKED_ITEM, FINISH_USING_ITEM, IItemEvent.FinishUsingItem.class); } - /** - * Internal helper to register a hook definition. - */ - private void registerHookDef(String context, String hookName, String eventClassName, String... imports) { - Map contextDefs = hookDefinitions.get(context); - if (contextDefs == null) { - contextDefs = new LinkedHashMap<>(); - hookDefinitions.put(context, contextDefs); - } - - HookDefinition.Builder builder = HookDefinition.builder(hookName); - if (eventClassName != null) { - builder.eventClass(eventClassName); - } - if (imports != null && imports.length > 0 && imports[0] != null) { - builder.requiredImports(imports); - } - contextDefs.put(hookName, builder.build()); + private void initializeRecipeHooks() { + hook(RECIPE, RecipeScript.ScriptType.PRE.function, IRecipeEvent.Pre.class); + hook(RECIPE, RecipeScript.ScriptType.POST.function, IRecipeEvent.Post.class); } - private void registerHookDef(ScriptContext context, String hookName, String eventClassName, String... imports) { - registerHookDef(context.hookContext, hookName, eventClassName, imports); + private void initializeEffectHooks() { + // Effect context uses IPlayerEvent.EffectEvent (same as player context) + hook(EFFECT, ON_EFFECT_ADD, IPlayerEvent.EffectEvent.Added.class); + hook(EFFECT, ON_EFFECT_TICK, IPlayerEvent.EffectEvent.Ticked.class); + hook(EFFECT, ON_EFFECT_REMOVE, IPlayerEvent.EffectEvent.Removed.class); + } + + private void initializeForgeHooks() { + hook(FORGE, INIT, IForgeEvent.InitEvent.class); + hook(FORGE, FORGE_WORLD, IForgeEvent.WorldEvent.class); + hook(FORGE, FORGE_ENTITY, IForgeEvent.EntityEvent.class); + hook(FORGE, CNPC_NATURAL_SPAWN, ICustomNPCsEvent.CNPCNaturalSpawnEvent.class); + } + + // ==================== INTERNAL REGISTRATION ==================== + + private void hook(ScriptContext context, EnumScriptType type, Class eventClass) { + hookDefinitions.get(context.hookContext) + .put(type.function, HookDefinition.of(type.function, eventClass)); + } + + private void hook(ScriptContext context, EnumScriptType type) { + hookDefinitions.get(context.hookContext) + .put(type.function, HookDefinition.simple(type.function)); + } + + private void hook(ScriptContext context, String hookName) { + hookDefinitions.get(context.hookContext) + .put(hookName, HookDefinition.simple(hookName)); + } + + private void hook(ScriptContext context, String hookName, Class eventClass) { + hookDefinitions.get(context.hookContext) + .put(hookName, HookDefinition.of(hookName, eventClass)); } // ==================== PUBLIC API ==================== - @Override - public void registerHookDefinition(String context, IHookDefinition definition) { - if (context == null || definition == null || definition.hookName() == null) + /** + * Register a hook with event class (auto-derives imports). + * For addon mods to register custom hooks. + * + * @param context Hook context (e.g., "npc", "player") + * @param hookName Function name in scripts + * @param eventClass The event class (imports auto-derived from enclosing class) + */ + public void registerHook(String context, String hookName, Class eventClass) { + if (context == null || hookName == null) return; Map contextDefs = hookDefinitions.get(context); @@ -325,19 +304,70 @@ public class ScriptHookController implements IScriptHookHandler { hookDefinitions.put(context, contextDefs); } + contextDefs.put(hookName, HookDefinition.of(hookName, eventClass)); + hookRevision++; + } + + public void registerHook(ScriptContext context, String hookName, Class eventClass) { + registerHook(context.hookContext, hookName, eventClass); + } + + /** + * Register a simple hook with no event class metadata. + */ + public void registerHook(String context, String hookName) { + if (context == null || hookName == null) + return; + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new LinkedHashMap<>(); + hookDefinitions.put(context, contextDefs); + } + + if (!contextDefs.containsKey(hookName)) { + contextDefs.put(hookName, HookDefinition.simple(hookName)); + hookRevision++; + } + } + + public void registerHook(ScriptContext context, String hookName) { + registerHook(context.hookContext, hookName); + } + + /** + * Register a hook definition for addon mods. + */ + public void registerHook(String context, HookDefinition definition) { + if (context == null || definition == null) + return; + + Map contextDefs = hookDefinitions.get(context); + if (contextDefs == null) { + contextDefs = new LinkedHashMap<>(); + hookDefinitions.put(context, contextDefs); + } + + contextDefs.put(definition.hookName(), definition); + hookRevision++; + } + + public void registerHook(ScriptContext context, HookDefinition definition) { + registerHook(context.hookContext, definition); + } + + @Override + public void registerHookDefinition(String context, IHookDefinition definition) { if (definition instanceof HookDefinition) { - contextDefs.put(definition.hookName(), (HookDefinition) definition); - } else { - HookDefinition def = HookDefinition.builder(definition.hookName()) + registerHook(context, (HookDefinition) definition); + } else if (definition != null) { + registerHook(context, HookDefinition.builder(definition.hookName()) .eventClass(definition.eventClassName()) .paramNames(definition.paramNames()) .requiredImports(definition.requiredImports()) .cancelable(definition.isCancelable()) - .build(); - contextDefs.put(definition.hookName(), def); + .build()); } - - hookRevision++; } @Override @@ -361,87 +391,6 @@ public class ScriptHookController implements IScriptHookHandler { return new ArrayList<>(contextDefs.values()); } - @Override - public int getHookRevision() { - return hookRevision; - } - - // ==================== CONVENIENCE METHODS ==================== - - public void registerHook(ScriptContext context, String functionName) { - registerHook(context.hookContext, functionName); - } - - public void registerHooks(ScriptContext context, String... functionNames) { - for (String fn : functionNames) { - registerHook(context.hookContext, fn); - } - } - - public void registerHookDefinition(ScriptContext context, IHookDefinition definition) { - registerHookDefinition(context.hookContext, definition); - } - - // ==================== LEGACY API (IScriptHookHandler) ==================== - - @Override - public void registerHook(String context, String functionName) { - if (context == null || functionName == null || functionName.isEmpty()) - return; - - Map contextDefs = hookDefinitions.get(context); - if (contextDefs == null) { - contextDefs = new LinkedHashMap<>(); - hookDefinitions.put(context, contextDefs); - } - - // Only add if not already present - if (!contextDefs.containsKey(functionName)) { - contextDefs.put(functionName, HookDefinition.builder(functionName).build()); - hookRevision++; - } - } - - @Override - public void registerHooks(String functionName, String... contexts) { - if (functionName == null || functionName.isEmpty() || contexts == null) - return; - - for (String context : contexts) { - registerHook(context, functionName); - } - } - - @Override - public void unregisterHook(String context, String functionName) { - if (context == null || functionName == null) - return; - - Map contextDefs = hookDefinitions.get(context); - if (contextDefs != null && contextDefs.remove(functionName) != null) { - hookRevision++; - } - } - - @Override - public void unregisterHookFromAll(String functionName) { - if (functionName == null) - return; - - for (Map contextDefs : hookDefinitions.values()) { - if (contextDefs.remove(functionName) != null) { - hookRevision++; - } - } - } - - @Override - public List getAddonHooks(String context) { - // With unified storage, we can't distinguish addon vs built-in - // Return empty for backwards compatibility - return Collections.emptyList(); - } - @Override public List getAllHooks(String context) { if (context == null) @@ -455,12 +404,17 @@ public class ScriptHookController implements IScriptHookHandler { } @Override - public boolean hasHook(String context, String functionName) { - if (context == null || functionName == null) + public boolean hasHook(String context, String hookName) { + if (context == null || hookName == null) return false; Map contextDefs = hookDefinitions.get(context); - return contextDefs != null && contextDefs.containsKey(functionName); + return contextDefs != null && contextDefs.containsKey(hookName); + } + + @Override + public int getHookRevision() { + return hookRevision; } @Override From 9c9bcc822859b8bb96fc04c2ede1292cf9b9267d Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 20:17:37 -0500 Subject: [PATCH 261/358] Update JaninoScript.java --- .../java/noppes/npcs/janino/JaninoScript.java | 452 ++++++------------ 1 file changed, 133 insertions(+), 319 deletions(-) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index a97aed889..b04031973 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -19,172 +19,64 @@ import java.lang.invoke.MethodHandle; import java.security.Permissions; import java.security.PrivilegedAction; import java.util.*; -import java.util.function.Consumer; -import java.util.function.Function; import java.util.function.Supplier; public abstract class JaninoScript implements IScriptUnit { - public boolean errored = false; - private String language = "Java"; - public TreeMap console = new TreeMap(); - /** - * Text written in the script GUI - */ + public boolean errored = false; public String script = ""; - /** - * This script's selected external files - * stored in "world/customnpcs/scripts/java" - */ public List externalScripts = new ArrayList<>(); - /** - * To evaluate if script/externalScripts were changed or not, - * and compile the fullScript freshly if so. - */ + public TreeMap console = new TreeMap<>(); public boolean evaluated; public final Class type; - protected final Sandbox sandbox; public final IScriptBodyBuilder builder; + protected final Sandbox sandbox; protected IScriptClassBody scriptBody; - private final JaninoHookResolver hookResolver; - + private final JaninoHookResolver hookResolver = new JaninoHookResolver(); private final String[] defaultImports; - // Hook definition cache for fast lookup (NEW) - private Map hookDefinitionCache; + private Map hookDefCache; private int lastHookRevision = -1; + private int lastSeenGlobalRevision; + protected JaninoScript(Class type, String[] defaultImports, boolean isClient) { this.type = type; this.defaultImports = defaultImports != null ? defaultImports : new String[0]; - this.builder = IScriptBodyBuilder.getBuilder(type, isClient ? CustomNpcs.getClientCompiler() : CustomNpcs.getDynamicCompiler()) + this.builder = IScriptBodyBuilder.getBuilder(type, + isClient ? CustomNpcs.getClientCompiler() : CustomNpcs.getDynamicCompiler()) .setDefaultImports(this.defaultImports); Permissions permissions = new Permissions(); permissions.setReadOnly(); - sandbox = new Sandbox(permissions); - + this.sandbox = new Sandbox(permissions); this.scriptBody = builder.build(); - - this.hookResolver = new JaninoHookResolver(); } protected JaninoScript(Class type, String[] defaultImports) { this(type, defaultImports, false); } - // ==================== HOOK CONTEXT (Abstract) ==================== - - /** - * Get the hook context identifier for this script type. - * Used to look up hook definitions from the registry. - * - * @return Context string, e.g., "npc", "player", "block" - */ protected abstract String getHookContext(); - // ==================== HOOK DEFINITION CACHE ==================== + // ==================== COMPILATION ==================== - /** - * Get hook definition from cache, rebuilding cache if revision changed. - * - * @param hookName The hook name to look up - * @return The hook definition, or null if not found - */ - protected IHookDefinition getHookDefinition(String hookName) { - if (ScriptHookController.Instance == null) { - return null; - } - - int currentRevision = ScriptHookController.Instance.getHookRevision(); - if (hookDefinitionCache == null || currentRevision != lastHookRevision) { - rebuildHookDefinitionCache(); - lastHookRevision = currentRevision; - } - - return hookDefinitionCache.get(hookName); - } - - /** - * Rebuild the hook definition cache from the registry. - */ - private void rebuildHookDefinitionCache() { - hookDefinitionCache = new HashMap<>(); - String context = getHookContext(); - - if (context == null || context.isEmpty() || ScriptHookController.Instance == null) { - return; - } - - for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { - hookDefinitionCache.put(def.hookName(), def); - } - } - - /** - * Get all required imports from hook definitions plus default imports. - * - * @return Array of all import statements needed - */ - protected String[] collectAllImports() { - Set allImports = new LinkedHashSet<>(); - - // Add default imports first - Collections.addAll(allImports, defaultImports); - - // Add imports from all hook definitions for this context - String context = getHookContext(); - if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { - for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { - String[] imports = def.requiredImports(); - if (imports != null) { - Collections.addAll(allImports, imports); - } - } - } - - return allImports.toArray(new String[0]); - } - - protected T getUnsafe() { - return scriptBody.get(); - } - - /** - * Feed the code into the engine and compile it. - * Rebuilds imports from hook definitions before compilation. - */ public void compileScript(String code) { - try { - // Rebuild imports from current hook definitions + defaults - String[] allImports = collectAllImports(); - builder.setDefaultImports(allImports); - - // Rebuild scriptBody to apply new imports + String[] imports = collectImportsForCode(code); + builder.setDefaultImports(imports); this.scriptBody = builder.build(); scriptBody.setScript(code); - } catch (InternalCompilerException e ) { - Throwable parentCause = null; - Throwable cause = e; - + } catch (InternalCompilerException e) { appendConsole("Compilation error: " + e.getMessage()); - - while (cause.getCause() != null) { parentCause = cause; cause = cause.getCause(); } - Throwable rootCause = cause; - - if (e != cause) { - appendConsole("\n"); - - if (parentCause != null) - appendConsole(parentCause.getMessage()); - appendConsole(rootCause.getMessage()); - } + Throwable cause = e; + while (cause.getCause() != null) cause = cause.getCause(); + if (e != cause) appendConsole(cause.getMessage()); hookResolver.clearResolutionCaches(); } catch (Exception e) { - appendConsole("Unknown error: " + e.getMessage()); + appendConsole("Error: " + e.getMessage()); hookResolver.clearResolutionCaches(); } } @@ -195,7 +87,6 @@ public abstract class JaninoScript implements IScriptUnit { public void ensureCompiled() { int global = ScriptController.Instance.globalRevision; - if (!evaluated || global != lastSeenGlobalRevision) { compileScript(); lastSeenGlobalRevision = global; @@ -203,221 +94,123 @@ public abstract class JaninoScript implements IScriptUnit { } } - /** - * globalRevision is incremented on server - * each time externalScripts are updated - * then sent to client. - */ - private int lastSeenGlobalRevision; - private String getFullCode() { StringBuilder sb = new StringBuilder(); - - if (ConfigScript.RunLoadedScriptsFirst) - this.appendExternalScripts(sb); - - // Main script - if (this.script != null && !this.script.isEmpty()) - sb.append(this.script).append("\n"); - - if (!ConfigScript.RunLoadedScriptsFirst) - this.appendExternalScripts(sb); - + if (ConfigScript.RunLoadedScriptsFirst) appendExternalScripts(sb); + if (script != null && !script.isEmpty()) sb.append(script).append("\n"); + if (!ConfigScript.RunLoadedScriptsFirst) appendExternalScripts(sb); return sb.toString(); } private void appendExternalScripts(StringBuilder sb) { - for (String scriptName : externalScripts) { - String code = ScriptController.Instance.scripts.get(scriptName); - if (code != null && !code.isEmpty()) - sb.append(code).append("\n"); + for (String name : externalScripts) { + String code = ScriptController.Instance.scripts.get(name); + if (code != null && !code.isEmpty()) sb.append(code).append("\n"); } } - public void callByHookName(String hookName, Object arg) { - callFunction(hookName, arg); + // ==================== SMART IMPORTS ==================== + + private String[] collectImportsForCode(String code) { + Set imports = new LinkedHashSet<>(); + Collections.addAll(imports, defaultImports); + + if (ScriptHookController.Instance == null) { + return imports.toArray(new String[0]); + } + + String context = getHookContext(); + if (context == null || context.isEmpty()) { + return imports.toArray(new String[0]); + } + + for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { + if (code.contains(def.hookName() + "(")) { + String[] hookImports = def.requiredImports(); + if (hookImports != null) { + Collections.addAll(imports, hookImports); + } + } + } + + return imports.toArray(new String[0]); } + // ==================== HOOK EXECUTION ==================== + @Override public Object callFunction(String hookName, Object... args) { - if (hookName == null || hookName.isEmpty()) - return null; + if (hookName == null || hookName.isEmpty()) return null; ensureCompiled(); - T t = getUnsafe(); - if (t == null) - return null; + T instance = scriptBody.get(); + if (instance == null) return null; Object[] invokeArgs = args == null ? new Object[0] : args; + MethodHandle handle = hookResolver.resolveHookHandle(hookName, invokeArgs, instance); + if (handle == null) return null; - MethodHandle handle = hookResolver.resolveHookHandle(hookName, invokeArgs, t); - if (handle == null) - return null; - - final MethodHandle finalHandle = handle; try { return sandbox.confine((PrivilegedAction) () -> { try { - // Prepend receiver to args for virtual method call Object[] fullArgs = new Object[invokeArgs.length + 1]; - fullArgs[0] = t; + fullArgs[0] = instance; System.arraycopy(invokeArgs, 0, fullArgs, 1, invokeArgs.length); - return finalHandle.invokeWithArguments(fullArgs); + return handle.invokeWithArguments(fullArgs); } catch (Throwable e) { - appendConsole("Error calling hook " + hookName + ": " + e.getMessage()); + appendConsole("Error in " + hookName + ": " + e.getMessage()); return null; } }); } catch (Exception e) { - appendConsole("Runtime Error in hook " + hookName + ": " + e.getMessage()); + appendConsole("Runtime error in " + hookName + ": " + e.getMessage()); return null; } } - public R call(Function fn) { - ensureCompiled(); - - T t = getUnsafe(); - if (t == null) - return null; - -// CodeSource cs = t.getClass().getProtectionDomain().getCodeSource(); -// ProtectionDomain pd = new ProtectionDomain(cs, new Permissions()); - - try { - return sandbox.confine((PrivilegedAction) () -> fn.apply(t)); -// return AccessController.doPrivileged((PrivilegedAction) () -> fn.apply(t), new AccessControlContext( -// new ProtectionDomain[] { -// pd -// } -// )); - } catch (Exception e) { - appendConsole("Runtime Error: " + e.getMessage()); - return null; - } - } - - public void run(Consumer fn) { - ensureCompiled(); - - T t = getUnsafe(); - if (t == null) - return; - - try { - sandbox.confine((PrivilegedAction) () -> { - fn.accept(t); - return null; - }); - } catch (Exception e) { - appendConsole("Runtime Error: " + e.getMessage()); - } - } - - public void unload() { - } - - - // ==================== IScriptUnit IMPLEMENTATION ==================== - - /** - * Execute this script unit for the given hook type and event. - * Implementation of IScriptUnit.run(EnumScriptType, Event) - * - * @param type The script hook type - * @param event The event object - */ @Override public void run(EnumScriptType type, Event event) { - if (type == null) - return; - - callFunction(type.function, event); + if (type != null) callFunction(type.function, event); } - /** - * Execute this script unit for the given hook name and event. - * Implementation of IScriptUnit.run(String, Event) - * - * @param hookName The hook name (e.g., "init", "interact") - * @param event The event object - */ @Override public void run(String hookName, Object event) { callFunction(hookName, event); } - @Override - public String getScript() { - return this.script; + // ==================== HOOK DEFINITIONS ==================== + + protected IHookDefinition getHookDefinition(String hookName) { + if (ScriptHookController.Instance == null) return null; + + int rev = ScriptHookController.Instance.getHookRevision(); + if (hookDefCache == null || rev != lastHookRevision) { + rebuildHookDefCache(); + lastHookRevision = rev; + } + return hookDefCache.get(hookName); } - @Override - public void setScript(String script) { - this.script = script; - this.evaluated = false; - hookResolver.clearResolutionCaches(); - } + private void rebuildHookDefCache() { + hookDefCache = new HashMap<>(); + String context = getHookContext(); + if (context == null || context.isEmpty() || ScriptHookController.Instance == null) return; - @Override - public List getExternalScripts() { - return this.externalScripts; - } - - @Override - public void setExternalScripts(List externalScripts) { - this.externalScripts = externalScripts; - this.evaluated = false; - hookResolver.clearResolutionCaches(); - } - - @Override - public TreeMap getConsole() { - return this.console; - } - - @Override - public void clearConsole() { - console.clear(); - } - - @Override - public void appendConsole(String message) { - if (message != null && !message.isEmpty()) { - long time = System.currentTimeMillis(); - if (this.console.containsKey(time)) { - message = (String) this.console.get(time) + "\n" + message; - } - - this.console.put(time, message); - - while (this.console.size() > 40) { - this.console.remove(this.console.firstKey()); - } + for (IHookDefinition def : ScriptHookController.Instance.getAllHookDefinitions(context)) { + hookDefCache.put(def.hookName(), def); } } - @Override - public String getLanguage() { - return this.language; - } - - @Override - public void setLanguage(String language) { - // JaninoScript is always Java, ignore attempts to change - // This method exists for IScriptUnit interface compatibility - } - - @Override - public boolean hasCode() { - if (!externalScripts.isEmpty()) - return true; - return script != null && !script.isEmpty(); + public List getHookList() { + String context = getHookContext(); + if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { + return ScriptHookController.Instance.getAllHooks(context); + } + return Collections.emptyList(); } @Override public String generateHookStub(String hookName, Object hookData) { - // Look up in hook definition registry IHookDefinition def = getHookDefinition(hookName); if (def != null) { String typeName = def.getUsableTypeName(); @@ -429,34 +222,69 @@ public abstract class JaninoScript implements IScriptUnit { def.hookName(), typeName, paramName); } } - - // Fallback: generic Object parameter return String.format("public void %s(Object event) {\n \n}\n", hookName); } + // ==================== IScriptUnit ==================== - public List getHookList() { - // Get hooks from registry based on context - String context = getHookContext(); - if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { - return ScriptHookController.Instance.getAllHooks(context); - } - return Collections.emptyList(); + @Override public String getScript() { return script; } + + @Override + public void setScript(String script) { + this.script = script; + this.evaluated = false; + hookResolver.clearResolutionCaches(); } + @Override public List getExternalScripts() { return externalScripts; } + + @Override + public void setExternalScripts(List scripts) { + this.externalScripts = scripts; + this.evaluated = false; + hookResolver.clearResolutionCaches(); + } + + @Override public TreeMap getConsole() { return console; } + @Override public void clearConsole() { console.clear(); } + + @Override + public void appendConsole(String message) { + if (message == null || message.isEmpty()) return; + long time = System.currentTimeMillis(); + if (console.containsKey(time)) { + message = console.get(time) + "\n" + message; + } + console.put(time, message); + while (console.size() > 40) { + console.remove(console.firstKey()); + } + } + + @Override public String getLanguage() { return "Java"; } + @Override public void setLanguage(String language) { } + + @Override + public boolean hasCode() { + return !externalScripts.isEmpty() || (script != null && !script.isEmpty()); + } + + @Override public boolean hasErrored() { return errored; } + @Override public void setErrored(boolean errored) { this.errored = errored; } + + // ==================== NBT ==================== @Override public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setString(IScriptUnit.NBT_TYPE_KEY, IScriptUnit.TYPE_JANINO); - compound.setTag("console", NBTTags.NBTLongStringMap(this.console)); - compound.setTag("externalScripts", NBTTags.nbtStringList(this.externalScripts)); + compound.setTag("console", NBTTags.NBTLongStringMap(console)); + compound.setTag("externalScripts", NBTTags.nbtStringList(externalScripts)); compound.setString("script", script); return compound; } @Override public void readFromNBT(NBTTagCompound compound) { - // Read legacy enabled field but ignore it (handler owns enabled now) this.console = NBTTags.GetLongStringMap(compound.getTagList("console", 10)); setExternalScripts(NBTTags.getStringList(compound.getTagList("externalScripts", 10))); setScript(compound.getString("script")); @@ -464,28 +292,14 @@ public abstract class JaninoScript implements IScriptUnit { public static > S readFromNBT(NBTTagCompound compound, S script, Supplier factory) { if (compound.hasKey("Script")) { - if (script == null) - script = factory.get(); - + if (script == null) script = factory.get(); script.readFromNBT(compound.getCompoundTag("Script")); } return script; } public static NBTTagCompound writeToNBT(NBTTagCompound compound, JaninoScript script) { - if (script != null) - compound.setTag("Script", script.writeToNBT(new NBTTagCompound())); - + if (script != null) compound.setTag("Script", script.writeToNBT(new NBTTagCompound())); return compound; } - - @Override - public boolean hasErrored() { - return this.errored; - } - - @Override - public void setErrored(boolean errored) { - this.errored = errored; - } } From 587d435a6fab35d346355bdd7e3f7fd305b7c261 Mon Sep 17 00:00:00 2001 From: bigguy345 Date: Fri, 23 Jan 2026 03:27:40 +0200 Subject: [PATCH 262/358] Added removed methods --- .../java/noppes/npcs/janino/JaninoScript.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index b04031973..7cae0bdbb 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -19,6 +19,8 @@ import java.lang.invoke.MethodHandle; import java.security.Permissions; import java.security.PrivilegedAction; import java.util.*; +import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Supplier; public abstract class JaninoScript implements IScriptUnit { @@ -61,6 +63,54 @@ public abstract class JaninoScript implements IScriptUnit { protected abstract String getHookContext(); + protected T getUnsafe() { + return scriptBody.get(); + } + + public R call(Function fn) { + ensureCompiled(); + + T t = getUnsafe(); + if (t == null) + return null; + + // CodeSource cs = t.getClass().getProtectionDomain().getCodeSource(); + // ProtectionDomain pd = new ProtectionDomain(cs, new Permissions()); + + try { + return sandbox.confine((PrivilegedAction) () -> fn.apply(t)); + // return AccessController.doPrivileged((PrivilegedAction) () -> fn.apply(t), new AccessControlContext( + // new ProtectionDomain[] { + // pd + // } + // )); + } catch (Exception e) { + appendConsole("Runtime Error: " + e.getMessage()); + return null; + } + } + + public void run(Consumer fn) { + ensureCompiled(); + + T t = getUnsafe(); + if (t == null) + return; + + try { + sandbox.confine((PrivilegedAction) () -> { + fn.accept(t); + return null; + }); + } catch (Exception e) { + appendConsole("Runtime Error: " + e.getMessage()); + } + } + + public void unload() { + } + + // ==================== COMPILATION ==================== public void compileScript(String code) { From 0f1a6028f3284c888d3aaa5d05a3a700fbd55c67 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 20:53:48 -0500 Subject: [PATCH 263/358] Event Janino Script --- .../npcs/controllers/data/DataScript.java | 4 ++-- .../controllers/data/PlayerDataScript.java | 4 ++-- .../noppes/npcs/janino/EventJaninoScript.java | 11 ++++++++++ .../java/noppes/npcs/janino/JaninoScript.java | 6 +++++- .../npcs/janino/impl/JaninoNpcScript.java | 21 ------------------- .../npcs/janino/impl/JaninoPlayerScript.java | 21 ------------------- 6 files changed, 20 insertions(+), 47 deletions(-) create mode 100644 src/main/java/noppes/npcs/janino/EventJaninoScript.java delete mode 100644 src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java delete mode 100644 src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index 677b3efde..ede4a4c96 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -21,7 +21,7 @@ import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.entity.EntityNPCInterface; -import noppes.npcs.janino.impl.JaninoNpcScript; +import noppes.npcs.janino.EventJaninoScript; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.ScriptWorld; import noppes.npcs.scripted.constants.EntityType; @@ -314,6 +314,6 @@ public class DataScript implements IScriptHandlerPacket { } @Override public IScriptUnit createJaninoScriptUnit() { - return new JaninoNpcScript(); + return new EventJaninoScript(ScriptContext.NPC); } } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java index 6549ed1e5..38d9e8447 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerDataScript.java @@ -13,7 +13,7 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; -import noppes.npcs.janino.impl.JaninoPlayerScript; +import noppes.npcs.janino.EventJaninoScript; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.player.PlayerEvent; import noppes.npcs.util.ScriptToStringHelper; @@ -201,6 +201,6 @@ public class PlayerDataScript extends MultiScriptHandler { @Override public IScriptUnit createJaninoScriptUnit() { - return new JaninoPlayerScript(); + return new EventJaninoScript(ScriptContext.PLAYER); } } diff --git a/src/main/java/noppes/npcs/janino/EventJaninoScript.java b/src/main/java/noppes/npcs/janino/EventJaninoScript.java new file mode 100644 index 000000000..2ac7dd218 --- /dev/null +++ b/src/main/java/noppes/npcs/janino/EventJaninoScript.java @@ -0,0 +1,11 @@ +package noppes.npcs.janino; + +import noppes.npcs.constants.ScriptContext; + +public class EventJaninoScript extends JaninoScript { + + public EventJaninoScript(ScriptContext context) { + super(Object.class, null, false); + this.context = context; + } +} diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index b04031973..d8dfb4007 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -9,6 +9,7 @@ import noppes.npcs.NBTTags; import noppes.npcs.api.handler.IHookDefinition; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.IScriptUnit; @@ -23,6 +24,7 @@ import java.util.function.Supplier; public abstract class JaninoScript implements IScriptUnit { + public ScriptContext context = ScriptContext.GLOBAL; public boolean errored = false; public String script = ""; public List externalScripts = new ArrayList<>(); @@ -59,7 +61,9 @@ public abstract class JaninoScript implements IScriptUnit { this(type, defaultImports, false); } - protected abstract String getHookContext(); + protected String getHookContext(){ + return this.context.hookContext; + } // ==================== COMPILATION ==================== diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java deleted file mode 100644 index 4445ed8c6..000000000 --- a/src/main/java/noppes/npcs/janino/impl/JaninoNpcScript.java +++ /dev/null @@ -1,21 +0,0 @@ -package noppes.npcs.janino.impl; - -import noppes.npcs.janino.JaninoScript; - -/** - * Janino (Java) script implementation for NPC events. - * Hook methods are resolved dynamically by name - no interface definition required. - */ -public class JaninoNpcScript extends JaninoScript { - - public JaninoNpcScript() { - super(Functions.class, null); - } - - @Override - protected String getHookContext() { - return "npc"; - } - - public interface Functions {} -} diff --git a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java b/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java deleted file mode 100644 index 79c7718b6..000000000 --- a/src/main/java/noppes/npcs/janino/impl/JaninoPlayerScript.java +++ /dev/null @@ -1,21 +0,0 @@ -package noppes.npcs.janino.impl; - -import noppes.npcs.janino.JaninoScript; - -/** - * Janino (Java) script implementation for player events. - * Hook methods are resolved dynamically by name - no interface definition required. - */ -public class JaninoPlayerScript extends JaninoScript { - - public JaninoPlayerScript() { - super(Functions.class, null, false); - } - - @Override - protected String getHookContext() { - return "player"; - } - - public interface Functions {} -} From d560c23241c9ae35e17e8fec9edb351d3af86449 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 22:01:13 -0500 Subject: [PATCH 264/358] @ParamName support for Hook Definitions in Overlay Scripts --- .../npcs/controllers/HookDefinition.java | 63 ++++++++++ .../java/noppes/npcs/janino/JaninoScript.java | 116 +++++++++++++++++- 2 files changed, 177 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/controllers/HookDefinition.java b/src/main/java/noppes/npcs/controllers/HookDefinition.java index b5a61ceab..9d8c34fa6 100644 --- a/src/main/java/noppes/npcs/controllers/HookDefinition.java +++ b/src/main/java/noppes/npcs/controllers/HookDefinition.java @@ -2,6 +2,10 @@ package noppes.npcs.controllers; import cpw.mods.fml.common.eventhandler.Cancelable; import noppes.npcs.api.handler.IHookDefinition; +import noppes.npcs.janino.annotations.ParamName; + +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; /** * Implementation of {@link IHookDefinition} with a fluent builder API. @@ -121,6 +125,65 @@ public class HookDefinition implements IHookDefinition { return new Builder(hookName); } + /** + * Create a hook definition from an interface method. + * Automatically extracts metadata from the method signature and @ParamName annotations. + * This provides backward compatibility with the annotation-based hook definition approach. + * + * @param hookName The hook name (may differ from method name) + * @param method The method to extract metadata from + * @return A hook definition with extracted metadata + */ + public static HookDefinition fromMethod(String hookName, Method method) { + Builder builder = builder(hookName); + + // Extract event type from first parameter + if (method.getParameterCount() > 0) { + Class eventType = method.getParameterTypes()[0]; + builder.eventClass(eventType); + + // Auto-detect cancelable from event class + if (eventType.isAnnotationPresent(Cancelable.class)) { + builder.cancelable(true); + } + + // Build required imports from event type + String importName = getImportForClass(eventType); + if (importName != null) { + builder.requiredImports(importName); + } + } + + // Extract parameter names from @ParamName annotations + Parameter[] params = method.getParameters(); + if (params.length > 0) { + String[] names = new String[params.length]; + for (int i = 0; i < params.length; i++) { + ParamName annotation = params[i].getAnnotation(ParamName.class); + if (annotation != null) { + names[i] = annotation.value(); + } else { + // Fallback: use lowercase simple type name + names[i] = Character.toLowerCase(params[i].getType().getSimpleName().charAt(0)) + + params[i].getType().getSimpleName().substring(1); + } + } + builder.paramNames(names); + } + + return builder.build(); + } + + /** + * Create a hook definition from a method using its name as the hook name. + * + * @param method The method to extract metadata from + * @return A hook definition with extracted metadata + */ + public static HookDefinition fromMethod(Method method) { + return fromMethod(method.getName(), method); + } + /** * Get the import statement needed for a class. * For nested classes, returns the enclosing class. diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 1deda1a94..46d0bf164 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -8,6 +8,8 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.NBTTags; import noppes.npcs.api.handler.IHookDefinition; import noppes.npcs.config.ConfigScript; +import noppes.npcs.controllers.HookDefinition; +import noppes.npcs.janino.annotations.ParamName; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptController; @@ -17,6 +19,8 @@ import org.codehaus.commons.compiler.InternalCompilerException; import org.codehaus.commons.compiler.Sandbox; import java.lang.invoke.MethodHandle; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.security.Permissions; import java.security.PrivilegedAction; import java.util.*; @@ -256,15 +260,29 @@ public abstract class JaninoScript implements IScriptUnit { } public List getHookList() { + Set hooks = new LinkedHashSet<>(); + + // 1. Add hooks from ScriptHookController (new system) String context = getHookContext(); if (context != null && !context.isEmpty() && ScriptHookController.Instance != null) { - return ScriptHookController.Instance.getAllHooks(context); + hooks.addAll(ScriptHookController.Instance.getAllHooks(context)); } - return Collections.emptyList(); + + // 2. Add methods from interface type (backward compatibility with @ParamName) + if (type != null) { + for (Method m : type.getDeclaredMethods()) { + if (!Modifier.isFinal(m.getModifiers()) && !hooks.contains(m.getName())) { + hooks.add(m.getName()); + } + } + } + + return new ArrayList<>(hooks); } @Override public String generateHookStub(String hookName, Object hookData) { + // 1. Try HookDefinition from registry (new system) IHookDefinition def = getHookDefinition(hookName); if (def != null) { String typeName = def.getUsableTypeName(); @@ -276,9 +294,103 @@ public abstract class JaninoScript implements IScriptUnit { def.hookName(), typeName, paramName); } } + + // 2. Fallback: Try interface method with @ParamName (backward compatibility) + Method method = findInterfaceMethod(hookName); + if (method != null) { + return generateMethodStub(method); + } + + // 3. Final fallback: generic stub return String.format("public void %s(Object event) {\n \n}\n", hookName); } + /** + * Find a method in the type interface by name. + */ + private Method findInterfaceMethod(String methodName) { + if (type == null || methodName == null) return null; + + for (Method m : type.getDeclaredMethods()) { + if (m.getName().equals(methodName) && !Modifier.isFinal(m.getModifiers())) { + return m; + } + } + return null; + } + + /** + * Generate a method stub from a Method, supporting @ParamName annotations. + * Provides backward compatibility with interface-based hook definitions. + */ + public static String generateMethodStub(Method method) { + String mods = Modifier.toString(method.getModifiers()); + mods = mods.replace("abstract ", "").replace("abstract", "") + .replace("default ", "").replace("default", "").trim(); + if (!mods.isEmpty()) mods += " "; + + String returnTypeStr = getUsableTypeName(method.getReturnType()); + String name = method.getName(); + + Map typeCount = new HashMap<>(); + StringBuilder params = new StringBuilder(); + for (int i = 0; i < method.getParameters().length; i++) { + java.lang.reflect.Parameter p = method.getParameters()[i]; + String typeName = getUsableTypeName(p.getType()); + + // Check for @ParamName annotation first + ParamName annotation = p.getAnnotation(ParamName.class); + String paramName; + if (annotation != null) { + paramName = annotation.value(); + } else { + // Fallback to generated name + String baseName = Character.toLowerCase(p.getType().getSimpleName().charAt(0)) + + p.getType().getSimpleName().substring(1); + int count = typeCount.getOrDefault(baseName, 0) + 1; + typeCount.put(baseName, count); + paramName = count == 1 ? baseName : baseName + (count - 1); + } + + if (i > 0) params.append(", "); + params.append(typeName).append(" ").append(paramName); + } + + String body; + Class returnType = method.getReturnType(); + if (returnType == void.class) { + body = ""; + } else if (returnType.isPrimitive()) { + if (returnType == boolean.class) body = " return false;"; + else if (returnType == char.class) body = " return '\\0';"; + else body = " return 0;"; + } else { + body = " return null;"; + } + + return String.format("%s%s %s(%s) {\n%s\n}\n", mods, returnTypeStr, name, params, body); + } + + /** + * Gets a usable type name for stub generation. + * For nested classes, returns "OuterClass.InnerClass" format. + */ + private static String getUsableTypeName(Class type) { + if (type.isPrimitive() || type.getEnclosingClass() == null) { + return type.getSimpleName(); + } + + // Build the nested class chain + StringBuilder sb = new StringBuilder(); + Class current = type; + while (current != null) { + if (sb.length() > 0) sb.insert(0, "."); + sb.insert(0, current.getSimpleName()); + current = current.getEnclosingClass(); + } + return sb.toString(); + } + // ==================== IScriptUnit ==================== @Override public String getScript() { return script; } From 31aac0d6b3e9fa475f1bffc162a89e275df66c58 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 22:52:04 -0500 Subject: [PATCH 265/358] GUI fixes and small bug fixes to Script GUIs, Overlay GUI, etc --- .../noppes/npcs/client/gui/GuiScript.java | 53 +++++++++++++------ .../client/gui/script/GuiScriptInterface.java | 6 ++- .../npcs/client/gui/util/GuiNPCInterface.java | 10 ++-- .../client/gui/util/GuiScriptTextArea.java | 14 +++-- 4 files changed, 55 insertions(+), 28 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index b98e94ac4..137ccf5a1 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -80,16 +80,18 @@ public class GuiScript extends GuiScriptInterface { public void initGui() { super.initGui(); this.guiTop += 10; - - // ==================== TOP BUTTONS ==================== - GuiMenuTopButton top; - addTopButton(top = new GuiMenuTopButton(13, guiLeft + 4, guiTop - 17, "script.scripts")); - addTopButton(new GuiMenuTopButton(16, guiLeft + (xSize - 102), guiTop - 17, "eventscript.eventScripts")); - addTopButton(new GuiMenuTopButton(17, guiLeft + (xSize - 22), guiTop - 17, "X")); - top.active = showScript; - addTopButton(top = new GuiMenuTopButton(14, top, "gui.settings")); - top.active = !showScript; - addTopButton(new GuiMenuTopButton(15, top, "gui.website")); + + // ==================== TOP BUTTONS (hidden in fullscreen) ==================== + if (!isFullscreen) { + GuiMenuTopButton top; + addTopButton(top = new GuiMenuTopButton(13, guiLeft + 4, guiTop - 17, "script.scripts")); + addTopButton(new GuiMenuTopButton(16, guiLeft + (xSize - 102), guiTop - 17, "eventscript.eventScripts")); + addTopButton(new GuiMenuTopButton(17, guiLeft + (xSize - 22), guiTop - 17, "X")); + top.active = showScript; + addTopButton(top = new GuiMenuTopButton(14, top, "gui.settings")); + top.active = !showScript; + addTopButton(new GuiMenuTopButton(15, top, "gui.website")); + } if (showScript) { initScriptView(); @@ -166,10 +168,8 @@ public class GuiScript extends GuiScriptInterface { int scrollbarOffset = activeArea.hasVerticalScrollbar() ? -8 : -2; fullscreenButton.initGui(editorX + editorWidth, editorY, scrollbarOffset); - // Set language and script context for proper syntax highlighting and autocomplete - activeArea.setLanguage(script.getLanguage()); - activeArea.setScriptContext(ScriptContext.NPC); - + // ==================== RIGHT PANEL (hidden in fullscreen) ==================== + if (!isFullscreen) { addButton(new GuiNpcButton(102, guiLeft + 315, guiTop + 4, 50, 20, "gui.clear")); addButton(new GuiNpcButton(101, guiLeft + 366, guiTop + 4, 50, 20, "gui.paste")); addButton(new GuiNpcButton(100, guiLeft + 315, guiTop + 25, 50, 20, "gui.copy")); @@ -183,11 +183,13 @@ public class GuiScript extends GuiScriptInterface { scroll.setList(container.scripts); addScroll(scroll); } + } private void initSettingsView() { addLabel(new GuiNpcLabel(0, "script.console", guiLeft + 4, guiTop + 16)); - getTopButton(14).active = true; + if (getTopButton(14) != null) + getTopButton(14).active = true; GuiNpcTextArea consoleArea = new GuiNpcTextArea(2, this, guiLeft + 4, guiTop + 26, 226, 186, getConsoleText()); consoleArea.canEdit = false; @@ -269,7 +271,7 @@ public class GuiScript extends GuiScriptInterface { } if (guibutton.id == 16) { close(); - GuiScriptInterface.open(this, new DataScript(npc)); + GuiScriptInterface.open(this, this.script); } if (guibutton.id == 17) { close(); @@ -342,5 +344,24 @@ public class GuiScript extends GuiScriptInterface { initGui(); } } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + super.drawScreen(mouseX, mouseY, partialTicks); + + // Draw fullscreen button when in script view (GuiScript uses 0-based activeTab) + if (showScript) { + fullscreenButton.draw(mouseX, mouseY); + } + } + + @Override + public void mouseClicked(int mouseX, int mouseY, int mouseButton) { + // Check fullscreen button first when in script view + if (showScript && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { + return; + } + super.mouseClicked(mouseX, mouseY, mouseButton); + } } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 5c3ee2596..15d3e8e76 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -354,7 +354,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb super.drawScreen(mouseX, mouseY, partialTicks); // Draw fullscreen button on top of everything when on script editor tab - if (this.activeTab > 0) { + // Skip for useSettingsToggle GUIs (like GuiScript) - they handle this themselves + if (this.activeTab > 0 && !useSettingsToggle && !handler.isSingleContainer()) { fullscreenButton.draw(mouseX, mouseY); } } @@ -364,7 +365,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb @Override public void mouseClicked(int mouseX, int mouseY, int mouseButton) { // Check fullscreen button first when on script editor tab - if (this.activeTab > 0 && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { + // Skip for useSettingsToggle GUIs (like GuiScript) - they handle this themselves + if (this.activeTab > 0 && !useSettingsToggle && !handler.isSingleContainer() && fullscreenButton.mouseClicked(mouseX, mouseY, mouseButton)) { return; } diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 823f6a3be..585ee8515 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -108,7 +108,7 @@ public abstract class GuiNPCInterface extends GuiScreen { } //Important for GuiScriptTextArea - for (GuiNpcTextField tf : textfields.values()) + for (GuiNpcTextField tf : textfields.values()) tf.initGui(); } @@ -264,8 +264,8 @@ public abstract class GuiNPCInterface extends GuiScreen { else close(); } - - + + if (subgui != null) subgui.keyTyped(c, i); for (GuiNpcTextField tf : textfields.values()) @@ -274,7 +274,7 @@ public abstract class GuiNPCInterface extends GuiScreen { for (GuiScrollWindow guiScrollableComponent : scrollWindows.values()) { guiScrollableComponent.keyTyped(c, i); } - + } public void onGuiClosed() { @@ -284,7 +284,7 @@ public abstract class GuiNPCInterface extends GuiScreen { public void closeOnEsc(Supplier close) { this.closeOnEscSupplier = close; } - + public void close() { if(GuiNpcTextField.activeTextfield != null) GuiNpcTextField.unfocus(); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 99e6ce480..c73f2bebf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -493,12 +493,16 @@ public class GuiScriptTextArea extends GuiNpcTextField { int maxScroll = Math.max(0, getPaddedLineCount() - container.visibleLines); - // Handle mouse wheel scroll - int wheelDelta = ((GuiNPCInterface) listener).mouseScroll = Mouse.getDWheel(); - if (listener instanceof GuiNPCInterface) { - ((GuiNPCInterface) listener).mouseScroll = wheelDelta; + // Handle mouse wheel scroll - only consume when mouse is over this text area + boolean isMouseOverTextArea = xMouse >= x && xMouse < x + width && yMouse >= y && yMouse < y + height; + int wheelDelta = 0; + if (isMouseOverTextArea) { + wheelDelta = Mouse.getDWheel(); + if (listener instanceof GuiNPCInterface) { + ((GuiNPCInterface) listener).mouseScroll = wheelDelta; + } boolean canScroll = !KEYS_OVERLAY.isVisible() || KEYS_OVERLAY.isVisible() && !KEYS_OVERLAY.aboveOverlay; - if (wheelDelta != 0 && canScroll) + if (wheelDelta != 0 && canScroll) scroll.applyWheelScroll(wheelDelta, maxScroll); } From a9b602713c064bb6e50a75543416cb857966572a Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 22 Jan 2026 23:07:38 -0500 Subject: [PATCH 266/358] Large Cleanup to Code Imports and Formatting --- src/api | 2 +- .../npcs/controllers/ProfileController.java | 1 - .../npcs/controllers/SyncController.java | 2 +- .../controllers/data/ability/Ability.java | 421 ++++++++++++----- .../data/ability/AbilityController.java | 27 +- .../controllers/data/ability/Condition.java | 55 ++- .../data/ability/TargetingMode.java | 28 +- .../data/ability/telegraph/Telegraph.java | 156 +++++-- .../ability/telegraph/TelegraphInstance.java | 110 +++-- .../ability/telegraph/TelegraphManager.java | 7 +- .../data/ability/telegraph/TelegraphType.java | 24 +- .../data/ability/type/AbilityBeam.java | 105 +++-- .../data/ability/type/AbilityCharge.java | 51 ++- .../data/ability/type/AbilityCutter.java | 135 ++++-- .../data/ability/type/AbilityDash.java | 52 ++- .../data/ability/type/AbilityGuard.java | 84 +++- .../data/ability/type/AbilityHazard.java | 223 ++++++--- .../data/ability/type/AbilityHeal.java | 66 ++- .../data/ability/type/AbilityHeavyHit.java | 64 ++- .../data/ability/type/AbilityOrb.java | 189 ++++++-- .../data/ability/type/AbilityProjectile.java | 85 +++- .../data/ability/type/AbilityShockwave.java | 69 ++- .../data/ability/type/AbilitySlam.java | 58 ++- .../data/ability/type/AbilityTeleport.java | 118 +++-- .../data/ability/type/AbilityTrap.java | 212 +++++++-- .../data/ability/type/AbilityVortex.java | 119 +++-- .../kamkeel/npcs/network/PacketHandler.java | 23 +- .../data/ability/TelegraphSpawnPacket.java | 1 - .../request/script/ScriptFilesPacket.java | 4 +- .../java/kamkeel/npcs/util/BukkitUtil.java | 4 + .../java/kamkeel/npcs/util/VaultUtil.java | 46 +- src/main/java/noppes/npcs/CustomNpcs.java | 10 +- src/main/java/noppes/npcs/DataAbilities.java | 47 +- src/main/java/noppes/npcs/EventHooks.java | 33 +- src/main/java/noppes/npcs/NBTTags.java | 1 - .../java/noppes/npcs/NoppesStringUtils.java | 2 +- .../noppes/npcs/ScriptForgeEventHandler.java | 2 +- .../noppes/npcs/ScriptPlayerEventHandler.java | 3 +- .../java/noppes/npcs/ServerEventsHandler.java | 2 - .../java/noppes/npcs/ServerTickHandler.java | 2 +- .../npcs/blocks/tiles/TileScripted.java | 6 +- .../npcs/client/ClientCacheHandler.java | 2 +- .../java/noppes/npcs/client/ClientProxy.java | 8 +- .../noppes/npcs/client/ClientTickHandler.java | 4 +- .../client/CustomNpcResourceListener.java | 2 +- .../client/controllers/MusicController.java | 1 + .../noppes/npcs/client/gui/GuiScript.java | 79 ++-- .../noppes/npcs/client/gui/GuiScriptList.java | 2 +- .../npcs/client/gui/SubGuiEffectGeneral.java | 2 +- .../npcs/client/gui/SubGuiPlayerData.java | 29 +- .../client/gui/advanced/GuiNPCAbilities.java | 13 +- .../gui/advanced/SubGuiAbilityConfig.java | 42 +- .../gui/advanced/SubGuiAbilityLoad.java | 6 +- .../gui/advanced/SubGuiConditionEdit.java | 8 +- .../advanced/ability/SubGuiAbilityBeam.java | 38 +- .../advanced/ability/SubGuiAbilityCharge.java | 18 +- .../advanced/ability/SubGuiAbilityCutter.java | 46 +- .../advanced/ability/SubGuiAbilityDash.java | 10 +- .../advanced/ability/SubGuiAbilityGuard.java | 22 +- .../advanced/ability/SubGuiAbilityHazard.java | 42 +- .../advanced/ability/SubGuiAbilityHeal.java | 26 +- .../ability/SubGuiAbilityHeavyHit.java | 22 +- .../advanced/ability/SubGuiAbilityOrb.java | 42 +- .../ability/SubGuiAbilityProjectile.java | 30 +- .../ability/SubGuiAbilityShockwave.java | 22 +- .../advanced/ability/SubGuiAbilitySlam.java | 22 +- .../ability/SubGuiAbilityTeleport.java | 38 +- .../advanced/ability/SubGuiAbilityTrap.java | 34 +- .../advanced/ability/SubGuiAbilityVortex.java | 42 +- .../gui/global/GuiNpcManageAbilities.java | 16 +- .../gui/global/GuiNpcManageRecipes.java | 2 +- .../client/gui/item/SubGuiLinkedItem.java | 6 +- .../client/gui/mainmenu/GuiNpcAdvanced.java | 4 +- .../client/gui/script/EventGuiScriptList.java | 2 +- .../client/gui/script/GuiJaninoScript.java | 36 +- .../client/gui/script/GuiNPCEventScripts.java | 2 +- .../client/gui/script/GuiScriptInterface.java | 114 +++-- .../client/gui/script/SubGuiSelectList.java | 2 +- .../gui/select/GuiTextureSelection.java | 1 + .../npcs/client/gui/util/GuiDiagram.java | 2 +- .../npcs/client/gui/util/GuiNPCInterface.java | 6 +- .../npcs/client/gui/util/GuiNpcButton.java | 1 + .../npcs/client/gui/util/GuiNpcTextField.java | 4 +- .../client/gui/util/GuiScriptTextArea.java | 185 ++++---- .../npcs/client/gui/util/GuiScrollWindow.java | 2 + .../noppes/npcs/client/gui/util/GuiUtil.java | 2 +- .../gui/util/IAbilityConfigCallback.java | 1 + .../gui/util/key/OverlayKeyPresetViewer.java | 10 +- .../gui/util/script/BracketMatcher.java | 98 ++-- .../gui/util/script/ClassPathFinder.java | 49 +- .../gui/util/script/CommentHandler.java | 73 +-- .../gui/util/script/CursorNavigation.java | 95 ++-- .../client/gui/util/script/FormatHelper.java | 428 +++++++++--------- .../gui/util/script/GoToLineDialog.java | 38 +- .../client/gui/util/script/IndentHelper.java | 145 +++--- .../gui/util/script/JavaTextContainer.java | 190 +++++--- .../client/gui/util/script/MethodBlock.java | 46 +- .../client/gui/util/script/PackageFinder.java | 1 - .../util/script/RenameRefactorHandler.java | 54 +-- .../client/gui/util/script/ScrollState.java | 102 +++-- .../gui/util/script/SearchReplaceBar.java | 396 ++++++++-------- .../gui/util/script/SelectionState.java | 92 ++-- .../client/gui/util/script/UndoManager.java | 55 ++- .../java/noppes/npcs/client/key/KeyEvent.java | 29 +- .../noppes/npcs/client/key/KeyPreset.java | 14 +- .../npcs/client/key/KeyPresetManager.java | 2 +- .../client/key/impl/ScriptEditorKeys.java | 14 +- .../noppes/npcs/client/model/ModelPony.java | 2 +- .../renderer/ImageBufferDownloadAlt.java | 2 +- .../npcs/client/renderer/RenderNPCPony.java | 2 +- .../blocks/BlockScriptedRenderer.java | 2 +- .../java/noppes/npcs/config/GlyphCache.java | 8 +- .../java/noppes/npcs/config/StringCache.java | 3 +- .../noppes/npcs/constants/ScriptContext.java | 92 ++-- .../npcs/controllers/AnimationController.java | 5 +- .../npcs/controllers/HookDefinition.java | 6 +- .../controllers/LinkedItemController.java | 9 +- .../npcs/controllers/MarketRegistry.java | 2 +- .../npcs/controllers/ScriptContainer.java | 25 +- .../controllers/ScriptHookController.java | 37 +- .../npcs/controllers/data/Animation.java | 32 +- .../npcs/controllers/data/CustomEffect.java | 6 +- .../npcs/controllers/data/DataScript.java | 1 + .../npcs/controllers/data/EffectScript.java | 7 +- .../npcs/controllers/data/IScriptHandler.java | 26 +- .../data/IScriptHandlerPacket.java | 4 +- .../npcs/controllers/data/IScriptUnit.java | 74 +-- .../controllers/data/JaninoScriptHandler.java | 14 +- .../npcs/controllers/data/LinkedItem.java | 4 +- .../controllers/data/LinkedItemScript.java | 25 +- .../controllers/data/MultiScriptHandler.java | 6 +- .../controllers/data/PlayerCurrencyData.java | 2 + .../npcs/controllers/data/RecipeScript.java | 33 +- .../npcs/controllers/data/ScriptHandler.java | 1 + .../controllers/data/SingleScriptHandler.java | 8 +- .../npcs/controllers/data/TraderStock.java | 14 +- .../npcs/controllers/data/action/Action.java | 26 +- .../data/action/ActionManager.java | 32 +- .../controllers/data/action/ActionQueue.java | 15 +- .../npcs/entity/EntityNPCInterface.java | 9 +- .../noppes/npcs/items/ItemElementalStaff.java | 2 +- .../noppes/npcs/items/ItemNpcScripter.java | 2 +- src/main/java/noppes/npcs/items/ItemOrb.java | 2 +- .../npcs/janino/JaninoHookResolver.java | 3 +- .../java/noppes/npcs/janino/JaninoScript.java | 62 ++- .../npcs/janino/annotations/ParamName.java | 10 +- .../java/noppes/npcs/roles/RoleTrader.java | 5 +- .../java/noppes/npcs/scripted/NpcAPI.java | 21 +- .../npcs/scripted/entity/ScriptPlayer.java | 7 +- .../npcs/scripted/event/AbilityEvent.java | 2 +- .../npcs/scripted/item/ScriptCustomItem.java | 4 +- .../java/noppes/npcs/util/NPCMountUtil.java | 2 +- src/main/java/noppes/npcs/util/ValueUtil.java | 2 +- .../janino/AdvancedClassFilter.java | 3 + .../janino/EntityUnloadListener.java | 2 +- 155 files changed, 4089 insertions(+), 2194 deletions(-) diff --git a/src/api b/src/api index 64fe8794e..a6b4a131c 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 64fe8794e71d82f09449e6c916c7902fe890cab7 +Subproject commit a6b4a131c843f27bbd3c561168f18498cb9d59ec diff --git a/src/main/java/kamkeel/npcs/controllers/ProfileController.java b/src/main/java/kamkeel/npcs/controllers/ProfileController.java index 73f16ebf6..f7ac06bb1 100644 --- a/src/main/java/kamkeel/npcs/controllers/ProfileController.java +++ b/src/main/java/kamkeel/npcs/controllers/ProfileController.java @@ -1,6 +1,5 @@ package kamkeel.npcs.controllers; -import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.data.profile.CNPCData; import kamkeel.npcs.controllers.data.profile.EnumProfileOperation; import kamkeel.npcs.controllers.data.profile.IProfileData; diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index 9617f4d12..3247498b5 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -2,9 +2,9 @@ package kamkeel.npcs.controllers; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; -import kamkeel.npcs.addon.DBCAddon; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import kamkeel.npcs.addon.DBCAddon; import kamkeel.npcs.network.LargeAbstractPacket; import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumSyncAction; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 4d62a934f..48cf46cb3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -15,8 +15,8 @@ import noppes.npcs.DataAbilities; import noppes.npcs.NpcDamageSource; import noppes.npcs.api.INbt; import noppes.npcs.api.ability.IAbility; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; @@ -89,33 +89,46 @@ public abstract class Ability implements IAbility { // ABSTRACT METHODS // ═══════════════════════════════════════════════════════════════════ - /** Called first tick of ACTIVE phase */ + /** + * Called first tick of ACTIVE phase + */ public abstract void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world); - /** Called every tick of ACTIVE phase */ + /** + * Called every tick of ACTIVE phase + */ public abstract void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick); - /** Write type-specific config to NBT */ + /** + * Write type-specific config to NBT + */ public abstract void writeTypeNBT(NBTTagCompound nbt); - /** Read type-specific config from NBT */ + /** + * Read type-specific config from NBT + */ public abstract void readTypeNBT(NBTTagCompound nbt); // ═══════════════════════════════════════════════════════════════════ // OPTIONAL OVERRIDES // ═══════════════════════════════════════════════════════════════════ - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) {} - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) {} - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) {} + public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + } + + public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + } + + public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + } /** * Apply damage to an entity with ability hit event support. * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. * - * @param npc The NPC executing the ability + * @param npc The NPC executing the ability * @param hitEntity The entity being hit - * @param damage The damage amount + * @param damage The damage amount * @param knockback The horizontal knockback * @return true if damage was applied (not cancelled), false if cancelled */ @@ -160,17 +173,17 @@ public abstract class Ability implements IAbility { * Apply damage to an entity with ability hit event support and custom knockback direction. * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. * - * @param npc The NPC executing the ability - * @param hitEntity The entity being hit - * @param damage The damage amount - * @param knockback The horizontal knockback + * @param npc The NPC executing the ability + * @param hitEntity The entity being hit + * @param damage The damage amount + * @param knockback The horizontal knockback * @param knockbackDirX The X component of knockback direction (normalized) * @param knockbackDirZ The Z component of knockback direction (normalized) * @return true if damage was applied (not cancelled), false if cancelled */ protected boolean applyAbilityDamageWithDirection(EntityNPCInterface npc, EntityLivingBase hitEntity, - float damage, float knockback, - double knockbackDirX, double knockbackDirZ) { + float damage, float knockback, + double knockbackDirX, double knockbackDirZ) { DataAbilities dataAbilities = npc.abilities; AbilityEvent.HitEvent event = dataAbilities.fireHitEvent( this, currentTarget, hitEntity, damage, knockback, 0.0f); @@ -206,53 +219,77 @@ public abstract class Ability implements IAbility { hitEntity.addVelocity(x, 0.1D, z); hitEntity.velocityChanged = true; } - public float getTelegraphRadius() { return 5.0f; } - public float getTelegraphLength() { return 5.0f; } - public float getTelegraphWidth() { return 2.0f; } - public float getTelegraphAngle() { return 45.0f; } + + public float getTelegraphRadius() { + return 5.0f; + } + + public float getTelegraphLength() { + return 5.0f; + } + + public float getTelegraphWidth() { + return 2.0f; + } + + public float getTelegraphAngle() { + return 45.0f; + } /** * Returns true if this ability controls NPC movement during ACTIVE phase. * When true, AI pathfinding will be blocked during ability execution. * Override in abilities that move the NPC (Charge, Dash, Slam, etc.) */ - public boolean hasAbilityMovement() { return false; } + public boolean hasAbilityMovement() { + return false; + } /** * Returns true if the targeting mode for this ability type is locked and cannot be changed. * Most abilities have fixed targeting (e.g., Heal is always SELF, Charge is always AGGRO_TARGET). * Override and return false for abilities with flexible targeting. */ - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } /** * Returns the allowed targeting modes for this ability, or null if all modes are allowed. * Override in abilities that only support specific targeting modes. */ - public TargetingMode[] getAllowedTargetingModes() { return null; } + public TargetingMode[] getAllowedTargetingModes() { + return null; + } /** * Returns true if the telegraph type for this ability is locked and cannot be changed. * Telegraph type is inherent to how the ability works (e.g., Charge uses LINE, Slam uses CIRCLE). */ - public boolean isTelegraphTypeLocked() { return true; } + public boolean isTelegraphTypeLocked() { + return true; + } /** * Returns true if this ability type has custom settings that need a Type-specific tab. * Override to return true if the ability has settings beyond the base Ability fields. */ - public boolean hasTypeSettings() { return false; } + public boolean hasTypeSettings() { + return false; + } /** * Get the number of rows needed for type-specific settings in the GUI. * Each row is approximately 24 pixels. Used for GUI layout. */ - public int getTypeSettingsRowCount() { return 0; } + public int getTypeSettingsRowCount() { + return 0; + } /** * Creates the GUI for configuring this ability. * Override this method to provide a custom GUI subclass for type-specific settings. - * + *

* Third-party mods can extend SubGuiAbilityConfig and override initTypeTab(), * handleTypeButton(), and handleTypeTextField() to provide custom type settings. * @@ -268,7 +305,7 @@ public abstract class Ability implements IAbility { * Create a telegraph instance for this ability. * Override for custom telegraph shapes. * - * @param npc The caster + * @param npc The caster * @param target The target (for position calculation) * @return The telegraph instance, or null if no telegraph */ @@ -283,9 +320,9 @@ public abstract class Ability implements IAbility { // Determine position based on targeting mode boolean positionAtNpc = targetingMode == TargetingMode.AOE_SELF || - targetingMode == TargetingMode.SELF || - telegraphType == TelegraphType.LINE || - telegraphType == TelegraphType.CONE; + targetingMode == TargetingMode.SELF || + telegraphType == TelegraphType.LINE || + telegraphType == TelegraphType.CONE; if (positionAtNpc) { x = npc.posX; @@ -347,10 +384,10 @@ public abstract class Ability implements IAbility { * Find the ground level at a given position. * Searches downward from the given Y to find a solid block. * - * @param world The world - * @param x X coordinate + * @param world The world + * @param x X coordinate * @param startY Starting Y coordinate (entity feet position) - * @param z Z coordinate + * @param z Z coordinate * @return The Y coordinate of the ground surface */ protected double findGroundLevel(World world, double x, double startY, double z) { @@ -391,7 +428,9 @@ public abstract class Ability implements IAbility { // EXECUTION LOGIC (called by CombatHandler) // ═══════════════════════════════════════════════════════════════════ - /** Start executing this ability */ + /** + * Start executing this ability + */ public void start(EntityLivingBase target) { this.phase = AbilityPhase.WINDUP; this.currentTick = 0; @@ -399,7 +438,9 @@ public abstract class Ability implements IAbility { this.executionStartTime = System.currentTimeMillis(); } - /** Tick the ability. Returns true if phase changed. */ + /** + * Tick the ability. Returns true if phase changed. + */ public boolean tick() { if (phase == AbilityPhase.IDLE) return false; @@ -432,7 +473,9 @@ public abstract class Ability implements IAbility { return false; } - /** Interrupt this ability */ + /** + * Interrupt this ability + */ public void interrupt() { phase = AbilityPhase.IDLE; currentTick = 0; @@ -440,17 +483,23 @@ public abstract class Ability implements IAbility { telegraphInstance = null; } - /** Start cooldown timer */ + /** + * Start cooldown timer + */ public void startCooldown() { cooldownEndTime = System.currentTimeMillis() + (cooldownTicks * 50L); } - /** Check if on cooldown */ + /** + * Check if on cooldown + */ public boolean isOnCooldown() { return System.currentTimeMillis() < cooldownEndTime; } - /** Check if executing */ + /** + * Check if executing + */ public boolean isExecuting() { return phase != AbilityPhase.IDLE; } @@ -487,7 +536,9 @@ public abstract class Ability implements IAbility { return true; } - /** Reset all execution state (called on combat end) */ + /** + * Reset all execution state (called on combat end) + */ public void reset() { phase = AbilityPhase.IDLE; currentTick = 0; @@ -507,7 +558,9 @@ public abstract class Ability implements IAbility { return true; } - /** Full eligibility check */ + /** + * Full eligibility check + */ public boolean canUse(EntityNPCInterface npc, EntityLivingBase target) { if (!enabled) return false; if (isOnCooldown()) return false; @@ -634,110 +687,266 @@ public abstract class Ability implements IAbility { // GETTERS & SETTERS // ═══════════════════════════════════════════════════════════════════ - public String getId() { return id; } - public void setId(String id) { this.id = id; } + public String getId() { + return id; + } - public String getName() { return name; } - public void setName(String name) { this.name = name; } + public void setId(String id) { + this.id = id; + } - public String getTypeId() { return typeId; } + public String getName() { + return name; + } - public int getWeight() { return weight; } - public void setWeight(int weight) { this.weight = weight; } + public void setName(String name) { + this.name = name; + } - public boolean isEnabled() { return enabled; } - public void setEnabled(boolean enabled) { this.enabled = enabled; } + public String getTypeId() { + return typeId; + } - public TargetingMode getTargetingMode() { return targetingMode; } - public void setTargetingMode(TargetingMode targetingMode) { this.targetingMode = targetingMode; } + public int getWeight() { + return weight; + } - public float getMinRange() { return minRange; } - public void setMinRange(float minRange) { this.minRange = minRange; } + public void setWeight(int weight) { + this.weight = weight; + } - public float getMaxRange() { return maxRange; } - public void setMaxRange(float maxRange) { this.maxRange = maxRange; } + public boolean isEnabled() { + return enabled; + } - public int getCooldownTicks() { return cooldownTicks; } - public void setCooldownTicks(int cooldownTicks) { this.cooldownTicks = cooldownTicks; } + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } - public int getWindUpTicks() { return windUpTicks; } - public void setWindUpTicks(int windUpTicks) { this.windUpTicks = windUpTicks; } + public TargetingMode getTargetingMode() { + return targetingMode; + } - public int getActiveTicks() { return activeTicks; } - public void setActiveTicks(int activeTicks) { this.activeTicks = activeTicks; } + public void setTargetingMode(TargetingMode targetingMode) { + this.targetingMode = targetingMode; + } - public int getRecoveryTicks() { return recoveryTicks; } - public void setRecoveryTicks(int recoveryTicks) { this.recoveryTicks = recoveryTicks; } + public float getMinRange() { + return minRange; + } - public boolean isInterruptible() { return interruptible; } - public void setInterruptible(boolean interruptible) { this.interruptible = interruptible; } + public void setMinRange(float minRange) { + this.minRange = minRange; + } - public boolean isLockMovement() { return lockMovement; } - public void setLockMovement(boolean lockMovement) { this.lockMovement = lockMovement; } + public float getMaxRange() { + return maxRange; + } - public int getWindUpColor() { return windUpColor; } - public void setWindUpColor(int windUpColor) { this.windUpColor = windUpColor; } + public void setMaxRange(float maxRange) { + this.maxRange = maxRange; + } - public int getActiveColor() { return activeColor; } - public void setActiveColor(int activeColor) { this.activeColor = activeColor; } + public int getCooldownTicks() { + return cooldownTicks; + } - /** @deprecated Use getWindUpColor() instead */ + public void setCooldownTicks(int cooldownTicks) { + this.cooldownTicks = cooldownTicks; + } + + public int getWindUpTicks() { + return windUpTicks; + } + + public void setWindUpTicks(int windUpTicks) { + this.windUpTicks = windUpTicks; + } + + public int getActiveTicks() { + return activeTicks; + } + + public void setActiveTicks(int activeTicks) { + this.activeTicks = activeTicks; + } + + public int getRecoveryTicks() { + return recoveryTicks; + } + + public void setRecoveryTicks(int recoveryTicks) { + this.recoveryTicks = recoveryTicks; + } + + public boolean isInterruptible() { + return interruptible; + } + + public void setInterruptible(boolean interruptible) { + this.interruptible = interruptible; + } + + public boolean isLockMovement() { + return lockMovement; + } + + public void setLockMovement(boolean lockMovement) { + this.lockMovement = lockMovement; + } + + public int getWindUpColor() { + return windUpColor; + } + + public void setWindUpColor(int windUpColor) { + this.windUpColor = windUpColor; + } + + public int getActiveColor() { + return activeColor; + } + + public void setActiveColor(int activeColor) { + this.activeColor = activeColor; + } + + /** + * @deprecated Use getWindUpColor() instead + */ @Deprecated - public int getTelegraphColor() { return windUpColor; } - /** @deprecated Use setWindUpColor() instead */ + public int getTelegraphColor() { + return windUpColor; + } + + /** + * @deprecated Use setWindUpColor() instead + */ @Deprecated - public void setTelegraphColor(int telegraphColor) { this.windUpColor = telegraphColor; } + public void setTelegraphColor(int telegraphColor) { + this.windUpColor = telegraphColor; + } - public String getWindUpSound() { return windUpSound; } - public void setWindUpSound(String windUpSound) { this.windUpSound = windUpSound; } + public String getWindUpSound() { + return windUpSound; + } - public String getActiveSound() { return activeSound; } - public void setActiveSound(String activeSound) { this.activeSound = activeSound; } + public void setWindUpSound(String windUpSound) { + this.windUpSound = windUpSound; + } - public int getWindUpAnimationId() { return windUpAnimationId; } - public void setWindUpAnimationId(int windUpAnimationId) { this.windUpAnimationId = windUpAnimationId; } + public String getActiveSound() { + return activeSound; + } - public int getActiveAnimationId() { return activeAnimationId; } - public void setActiveAnimationId(int activeAnimationId) { this.activeAnimationId = activeAnimationId; } + public void setActiveSound(String activeSound) { + this.activeSound = activeSound; + } - /** @deprecated Use getWindUpSound() instead */ + public int getWindUpAnimationId() { + return windUpAnimationId; + } + + public void setWindUpAnimationId(int windUpAnimationId) { + this.windUpAnimationId = windUpAnimationId; + } + + public int getActiveAnimationId() { + return activeAnimationId; + } + + public void setActiveAnimationId(int activeAnimationId) { + this.activeAnimationId = activeAnimationId; + } + + /** + * @deprecated Use getWindUpSound() instead + */ @Deprecated - public String getCastSound() { return windUpSound; } - /** @deprecated Use setWindUpSound() instead */ + public String getCastSound() { + return windUpSound; + } + + /** + * @deprecated Use setWindUpSound() instead + */ @Deprecated - public void setCastSound(String castSound) { this.windUpSound = castSound; } + public void setCastSound(String castSound) { + this.windUpSound = castSound; + } - /** @deprecated Use getWindUpAnimationId() instead */ + /** + * @deprecated Use getWindUpAnimationId() instead + */ @Deprecated - public int getAnimationId() { return windUpAnimationId; } - /** @deprecated Use setWindUpAnimationId() instead */ + public int getAnimationId() { + return windUpAnimationId; + } + + /** + * @deprecated Use setWindUpAnimationId() instead + */ @Deprecated - public void setAnimationId(int animationId) { this.windUpAnimationId = animationId; } + public void setAnimationId(int animationId) { + this.windUpAnimationId = animationId; + } - public boolean isShowTelegraph() { return showTelegraph; } - public void setShowTelegraph(boolean showTelegraph) { this.showTelegraph = showTelegraph; } + public boolean isShowTelegraph() { + return showTelegraph; + } - public TelegraphType getTelegraphType() { return telegraphType; } - public void setTelegraphType(TelegraphType telegraphType) { this.telegraphType = telegraphType; } + public void setShowTelegraph(boolean showTelegraph) { + this.showTelegraph = showTelegraph; + } - public float getTelegraphHeightOffset() { return telegraphHeightOffset; } - public void setTelegraphHeightOffset(float telegraphHeightOffset) { this.telegraphHeightOffset = telegraphHeightOffset; } + public TelegraphType getTelegraphType() { + return telegraphType; + } - public AbilityPhase getPhase() { return phase; } + public void setTelegraphType(TelegraphType telegraphType) { + this.telegraphType = telegraphType; + } + + public float getTelegraphHeightOffset() { + return telegraphHeightOffset; + } + + public void setTelegraphHeightOffset(float telegraphHeightOffset) { + this.telegraphHeightOffset = telegraphHeightOffset; + } + + public AbilityPhase getPhase() { + return phase; + } /** * Get the current execution phase as int (0=IDLE, 1=WINDUP, 2=ACTIVE, 3=RECOVERY). * Required for IAbility interface. */ @Override - public int getPhaseInt() { return phase.ordinal(); } + public int getPhaseInt() { + return phase.ordinal(); + } - public int getCurrentTick() { return currentTick; } - public EntityLivingBase getCurrentTarget() { return currentTarget; } - public NBTTagCompound getCustomData() { return customData; } + public int getCurrentTick() { + return currentTick; + } - public List getConditions() { return conditions; } - public void addCondition(Condition c) { conditions.add(c); } + public EntityLivingBase getCurrentTarget() { + return currentTarget; + } + + public NBTTagCompound getCustomData() { + return customData; + } + + public List getConditions() { + return conditions; + } + + public void addCondition(Condition c) { + conditions.add(c); + } // ═══════════════════════════════════════════════════════════════════ // IAbility API METHODS diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 685e5403d..1a9bbd522 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -1,6 +1,20 @@ package kamkeel.npcs.controllers.data.ability; -import kamkeel.npcs.controllers.data.ability.type.*; +import kamkeel.npcs.controllers.data.ability.type.AbilityBeam; +import kamkeel.npcs.controllers.data.ability.type.AbilityCharge; +import kamkeel.npcs.controllers.data.ability.type.AbilityCutter; +import kamkeel.npcs.controllers.data.ability.type.AbilityDash; +import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; +import kamkeel.npcs.controllers.data.ability.type.AbilityHazard; +import kamkeel.npcs.controllers.data.ability.type.AbilityHeal; +import kamkeel.npcs.controllers.data.ability.type.AbilityHeavyHit; +import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; +import kamkeel.npcs.controllers.data.ability.type.AbilityProjectile; +import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave; +import kamkeel.npcs.controllers.data.ability.type.AbilitySlam; +import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport; +import kamkeel.npcs.controllers.data.ability.type.AbilityTrap; +import kamkeel.npcs.controllers.data.ability.type.AbilityVortex; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; @@ -8,13 +22,17 @@ import noppes.npcs.api.handler.IAbilityHandler; import noppes.npcs.util.NBTJsonUtil; import java.io.File; -import java.util.*; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; import java.util.function.Supplier; /** * Controller for ability types. Allows external mods to register custom ability types. * Initialize during FMLInitializationEvent. - * + *

* Also manages saved ability presets that can be reused across NPCs. */ public class AbilityController implements IAbilityHandler { @@ -211,7 +229,7 @@ public class AbilityController implements IAbilityHandler { * Call during FMLInitializationEvent. * * @param factoryKey The factory key (e.g., "cnpc:slam", "mymod:custom_ability") - * @param factory Factory lambda that creates a new instance + * @param factory Factory lambda that creates a new instance */ public void registerType(String factoryKey, Supplier factory) { if (factories.containsKey(factoryKey)) { @@ -252,6 +270,7 @@ public class AbilityController implements IAbilityHandler { /** * Create a new empty ability of the given type. + * * @param factoryKey The factory key (e.g., "cnpc:slam") or the ability typeId (e.g., "ability.cnpc.slam") */ public Ability create(String factoryKey) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java index 70b3b290f..8d078421a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java @@ -13,7 +13,7 @@ public interface Condition { /** * Check if this condition is met. * - * @param npc The NPC that would use the ability + * @param npc The NPC that would use the ability * @param target The current target (may be null for self-targeting abilities) * @return true if the condition is satisfied */ @@ -80,7 +80,8 @@ public interface Condition { class ConditionHPAbove implements Condition { private float threshold = 0.5f; // 50% - public ConditionHPAbove() {} + public ConditionHPAbove() { + } public ConditionHPAbove(float threshold) { this.threshold = threshold; @@ -92,7 +93,9 @@ public interface Condition { } @Override - public String getTypeId() { return "hp_above"; } + public String getTypeId() { + return "hp_above"; + } @Override public NBTTagCompound writeNBT() { @@ -114,7 +117,8 @@ public interface Condition { class ConditionHPBelow implements Condition { private float threshold = 0.5f; - public ConditionHPBelow() {} + public ConditionHPBelow() { + } public ConditionHPBelow(float threshold) { this.threshold = threshold; @@ -126,7 +130,9 @@ public interface Condition { } @Override - public String getTypeId() { return "hp_below"; } + public String getTypeId() { + return "hp_below"; + } @Override public NBTTagCompound writeNBT() { @@ -148,7 +154,8 @@ public interface Condition { class ConditionTargetHPAbove implements Condition { private float threshold = 0.5f; - public ConditionTargetHPAbove() {} + public ConditionTargetHPAbove() { + } public ConditionTargetHPAbove(float threshold) { this.threshold = threshold; @@ -161,7 +168,9 @@ public interface Condition { } @Override - public String getTypeId() { return "target_hp_above"; } + public String getTypeId() { + return "target_hp_above"; + } @Override public NBTTagCompound writeNBT() { @@ -183,7 +192,8 @@ public interface Condition { class ConditionTargetHPBelow implements Condition { private float threshold = 0.5f; - public ConditionTargetHPBelow() {} + public ConditionTargetHPBelow() { + } public ConditionTargetHPBelow(float threshold) { this.threshold = threshold; @@ -196,7 +206,9 @@ public interface Condition { } @Override - public String getTypeId() { return "target_hp_below"; } + public String getTypeId() { + return "target_hp_below"; + } @Override public NBTTagCompound writeNBT() { @@ -220,7 +232,8 @@ public interface Condition { private int requiredHits = 3; private int withinTicks = 60; - public ConditionHitCount() {} + public ConditionHitCount() { + } public ConditionHitCount(int requiredHits, int withinTicks) { this.requiredHits = requiredHits; @@ -237,7 +250,9 @@ public interface Condition { } @Override - public String getTypeId() { return "hit_count"; } + public String getTypeId() { + return "hit_count"; + } @Override public NBTTagCompound writeNBT() { @@ -254,10 +269,20 @@ public interface Condition { withinTicks = nbt.getInteger("withinTicks"); } - public int getRequiredHits() { return requiredHits; } - public void setRequiredHits(int hits) { this.requiredHits = hits; } + public int getRequiredHits() { + return requiredHits; + } - public int getWithinTicks() { return withinTicks; } - public void setWithinTicks(int ticks) { this.withinTicks = ticks; } + public void setRequiredHits(int hits) { + this.requiredHits = hits; + } + + public int getWithinTicks() { + return withinTicks; + } + + public void setWithinTicks(int ticks) { + this.withinTicks = ticks; + } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java b/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java index a639702e1..7a2e43b64 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java @@ -4,24 +4,38 @@ package kamkeel.npcs.controllers.data.ability; * Defines how an ability selects its target(s). */ public enum TargetingMode { - /** Target the current aggro target */ + /** + * Target the current aggro target + */ AGGRO_TARGET, - /** Target self (for buffs, heals, guards) */ + /** + * Target self (for buffs, heals, guards) + */ SELF, - /** AOE centered on self */ + /** + * AOE centered on self + */ AOE_SELF, - /** AOE centered on target */ + /** + * AOE centered on target + */ AOE_TARGET, - /** Random nearby enemy */ + /** + * Random nearby enemy + */ RANDOM_ENEMY, - /** Lowest HP enemy in range */ + /** + * Lowest HP enemy in range + */ LOWEST_HP, - /** Highest threat enemy */ + /** + * Highest threat enemy + */ HIGHEST_THREAT } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java index 1b42b55e1..b8f92de17 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java @@ -35,7 +35,8 @@ public class Telegraph { private boolean followsCaster = false; private float offsetForward = 0.0f; - public Telegraph() {} + public Telegraph() { + } public Telegraph(String id, TelegraphType type) { this.id = id; @@ -149,54 +150,139 @@ public class Telegraph { // GETTERS & SETTERS // ═══════════════════════════════════════════════════════════════════ - public String getId() { return id; } - public void setId(String id) { this.id = id; } + public String getId() { + return id; + } - public TelegraphType getType() { return type; } - public void setType(TelegraphType type) { this.type = type; } + public void setId(String id) { + this.id = id; + } - public float getRadius() { return radius; } - public void setRadius(float radius) { this.radius = radius; } + public TelegraphType getType() { + return type; + } - public float getInnerRadius() { return innerRadius; } - public void setInnerRadius(float innerRadius) { this.innerRadius = innerRadius; } + public void setType(TelegraphType type) { + this.type = type; + } - public float getLength() { return length; } - public void setLength(float length) { this.length = length; } + public float getRadius() { + return radius; + } - public float getWidth() { return width; } - public void setWidth(float width) { this.width = width; } + public void setRadius(float radius) { + this.radius = radius; + } - public float getAngle() { return angle; } - public void setAngle(float angle) { this.angle = angle; } + public float getInnerRadius() { + return innerRadius; + } - public int getDurationTicks() { return durationTicks; } - public void setDurationTicks(int durationTicks) { this.durationTicks = durationTicks; } + public void setInnerRadius(float innerRadius) { + this.innerRadius = innerRadius; + } - public int getColor() { return color; } - public void setColor(int color) { this.color = color; } + public float getLength() { + return length; + } - public int getWarningColor() { return warningColor; } - public void setWarningColor(int warningColor) { this.warningColor = warningColor; } + public void setLength(float length) { + this.length = length; + } - public int getWarningStartTick() { return warningStartTick; } - public void setWarningStartTick(int warningStartTick) { this.warningStartTick = warningStartTick; } + public float getWidth() { + return width; + } - public boolean isAnimated() { return animated; } - public void setAnimated(boolean animated) { this.animated = animated; } + public void setWidth(float width) { + this.width = width; + } - public float getHeightOffset() { return heightOffset; } - public void setHeightOffset(float heightOffset) { this.heightOffset = heightOffset; } + public float getAngle() { + return angle; + } - public boolean isAtTarget() { return atTarget; } - public void setAtTarget(boolean atTarget) { this.atTarget = atTarget; } + public void setAngle(float angle) { + this.angle = angle; + } - public boolean isFollowsTarget() { return followsTarget; } - public void setFollowsTarget(boolean followsTarget) { this.followsTarget = followsTarget; } + public int getDurationTicks() { + return durationTicks; + } - public boolean isFollowsCaster() { return followsCaster; } - public void setFollowsCaster(boolean followsCaster) { this.followsCaster = followsCaster; } + public void setDurationTicks(int durationTicks) { + this.durationTicks = durationTicks; + } - public float getOffsetForward() { return offsetForward; } - public void setOffsetForward(float offsetForward) { this.offsetForward = offsetForward; } + public int getColor() { + return color; + } + + public void setColor(int color) { + this.color = color; + } + + public int getWarningColor() { + return warningColor; + } + + public void setWarningColor(int warningColor) { + this.warningColor = warningColor; + } + + public int getWarningStartTick() { + return warningStartTick; + } + + public void setWarningStartTick(int warningStartTick) { + this.warningStartTick = warningStartTick; + } + + public boolean isAnimated() { + return animated; + } + + public void setAnimated(boolean animated) { + this.animated = animated; + } + + public float getHeightOffset() { + return heightOffset; + } + + public void setHeightOffset(float heightOffset) { + this.heightOffset = heightOffset; + } + + public boolean isAtTarget() { + return atTarget; + } + + public void setAtTarget(boolean atTarget) { + this.atTarget = atTarget; + } + + public boolean isFollowsTarget() { + return followsTarget; + } + + public void setFollowsTarget(boolean followsTarget) { + this.followsTarget = followsTarget; + } + + public boolean isFollowsCaster() { + return followsCaster; + } + + public void setFollowsCaster(boolean followsCaster) { + this.followsCaster = followsCaster; + } + + public float getOffsetForward() { + return offsetForward; + } + + public void setOffsetForward(float offsetForward) { + this.offsetForward = offsetForward; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java index b16a9f0f2..2d705fb15 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java @@ -55,6 +55,7 @@ public class TelegraphInstance { /** * Called every tick to update telegraph state. + * * @param world The world (for entity lookup) * @return false if telegraph should be removed */ @@ -90,10 +91,10 @@ public class TelegraphInstance { * Find the ground level at a given position. * Searches downward from the given Y to find a solid block. * - * @param world The world - * @param x X coordinate + * @param world The world + * @param x X coordinate * @param startY Starting Y coordinate (entity feet position) - * @param z Z coordinate + * @param z Z coordinate * @return The Y coordinate of the ground surface */ private double findGroundLevel(World world, double x, double startY, double z) { @@ -207,42 +208,99 @@ public class TelegraphInstance { // GETTERS & SETTERS // ═══════════════════════════════════════════════════════════════════ - public String getInstanceId() { return instanceId; } - public void setInstanceId(String instanceId) { this.instanceId = instanceId; } + public String getInstanceId() { + return instanceId; + } - public Telegraph getTelegraph() { return telegraph; } - public void setTelegraph(Telegraph telegraph) { this.telegraph = telegraph; } + public void setInstanceId(String instanceId) { + this.instanceId = instanceId; + } - public double getX() { return x; } - public void setX(double x) { this.x = x; } + public Telegraph getTelegraph() { + return telegraph; + } - public double getY() { return y; } - public void setY(double y) { this.y = y; } + public void setTelegraph(Telegraph telegraph) { + this.telegraph = telegraph; + } - public double getZ() { return z; } - public void setZ(double z) { this.z = z; } + public double getX() { + return x; + } - public float getYaw() { return yaw; } - public void setYaw(float yaw) { this.yaw = yaw; } + public void setX(double x) { + this.x = x; + } - public int getEntityIdToFollow() { return entityIdToFollow; } - public void setEntityIdToFollow(int entityIdToFollow) { this.entityIdToFollow = entityIdToFollow; } + public double getY() { + return y; + } + + public void setY(double y) { + this.y = y; + } + + public double getZ() { + return z; + } + + public void setZ(double z) { + this.z = z; + } + + public float getYaw() { + return yaw; + } + + public void setYaw(float yaw) { + this.yaw = yaw; + } + + public int getEntityIdToFollow() { + return entityIdToFollow; + } + + public void setEntityIdToFollow(int entityIdToFollow) { + this.entityIdToFollow = entityIdToFollow; + } /** * Locks the telegraph at its current position. * Stops following any entity - used when ability commits to action. */ - public void lockPosition() { this.entityIdToFollow = -1; } + public void lockPosition() { + this.entityIdToFollow = -1; + } - public int getCasterEntityId() { return casterEntityId; } - public void setCasterEntityId(int casterEntityId) { this.casterEntityId = casterEntityId; } + public int getCasterEntityId() { + return casterEntityId; + } - public int getRemainingTicks() { return remainingTicks; } - public void setRemainingTicks(int remainingTicks) { this.remainingTicks = remainingTicks; } + public void setCasterEntityId(int casterEntityId) { + this.casterEntityId = casterEntityId; + } - public int getTotalTicks() { return totalTicks; } - public void setTotalTicks(int totalTicks) { this.totalTicks = totalTicks; } + public int getRemainingTicks() { + return remainingTicks; + } - public boolean isWarning() { return isWarning; } - public void setWarning(boolean warning) { isWarning = warning; } + public void setRemainingTicks(int remainingTicks) { + this.remainingTicks = remainingTicks; + } + + public int getTotalTicks() { + return totalTicks; + } + + public void setTotalTicks(int totalTicks) { + this.totalTicks = totalTicks; + } + + public boolean isWarning() { + return isWarning; + } + + public void setWarning(boolean warning) { + isWarning = warning; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java index b82afffd2..3dc4f9e0f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java @@ -12,12 +12,15 @@ import java.util.concurrent.ConcurrentHashMap; */ public class TelegraphManager { - /** Client-side instance for rendering */ + /** + * Client-side instance for rendering + */ public static TelegraphManager ClientInstance; private final ConcurrentHashMap telegraphs = new ConcurrentHashMap<>(); - public TelegraphManager() {} + public TelegraphManager() { + } /** * Initialize the client-side manager. diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java index dee9a249a..c7e4978b6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java @@ -4,21 +4,33 @@ package kamkeel.npcs.controllers.data.ability.telegraph; * Types of telegraph shapes for visual attack warnings. */ public enum TelegraphType { - /** Filled circle (uses radius) */ + /** + * Filled circle (uses radius) + */ CIRCLE, - /** Donut shape (uses radius and innerRadius) */ + /** + * Donut shape (uses radius and innerRadius) + */ RING, - /** Rectangle (uses length, width) */ + /** + * Rectangle (uses length, width) + */ LINE, - /** Fan/wedge shape (uses length, angle) */ + /** + * Fan/wedge shape (uses length, angle) + */ CONE, - /** Single point indicator */ + /** + * Single point indicator + */ POINT, - /** No telegraph */ + /** + * No telegraph + */ NONE } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java index f094a4f0d..a071fcb30 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -2,19 +2,17 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityBeam; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.DamageSource; import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityBeam; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashSet; @@ -61,21 +59,25 @@ public class AbilityBeam extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + IAbilityConfigCallback callback) { return new SubGuiAbilityBeam(this, callback); } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override @@ -184,7 +186,7 @@ public class AbilityBeam extends Ability { } private boolean isInBeamPath(EntityLivingBase entity, double startX, double startY, double startZ, - double dirX, double dirZ) { + double dirX, double dirZ) { // Project entity position onto beam line double entityX = entity.posX - startX; double entityZ = entity.posZ - startZ; @@ -209,7 +211,7 @@ public class AbilityBeam extends Ability { } private void spawnBeamParticles(World world, double startX, double startY, double startZ, - double dirX, double dirZ) { + double dirX, double dirZ) { // Spawn particles along beam line for (int i = 0; i < beamLength; i++) { double px = startX + dirX * i; @@ -259,30 +261,75 @@ public class AbilityBeam extends Ability { } // Getters & Setters - public float getBeamLength() { return beamLength; } - public void setBeamLength(float beamLength) { this.beamLength = beamLength; } + public float getBeamLength() { + return beamLength; + } - public float getBeamWidth() { return beamWidth; } - public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; } + public void setBeamLength(float beamLength) { + this.beamLength = beamLength; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getBeamWidth() { + return beamWidth; + } - public int getDamageInterval() { return damageInterval; } - public void setDamageInterval(int damageInterval) { this.damageInterval = damageInterval; } + public void setBeamWidth(float beamWidth) { + this.beamWidth = beamWidth; + } - public boolean isPiercing() { return piercing; } - public void setPiercing(boolean piercing) { this.piercing = piercing; } + public float getDamage() { + return damage; + } - public float getSweepAngle() { return sweepAngle; } - public void setSweepAngle(float sweepAngle) { this.sweepAngle = sweepAngle; } + public void setDamage(float damage) { + this.damage = damage; + } - public float getSweepSpeed() { return sweepSpeed; } - public void setSweepSpeed(float sweepSpeed) { this.sweepSpeed = sweepSpeed; } + public int getDamageInterval() { + return damageInterval; + } - public boolean isSweepBackAndForth() { return sweepBackAndForth; } - public void setSweepBackAndForth(boolean sweepBackAndForth) { this.sweepBackAndForth = sweepBackAndForth; } + public void setDamageInterval(int damageInterval) { + this.damageInterval = damageInterval; + } - public boolean isLockOnTarget() { return lockOnTarget; } - public void setLockOnTarget(boolean lockOnTarget) { this.lockOnTarget = lockOnTarget; } + public boolean isPiercing() { + return piercing; + } + + public void setPiercing(boolean piercing) { + this.piercing = piercing; + } + + public float getSweepAngle() { + return sweepAngle; + } + + public void setSweepAngle(float sweepAngle) { + this.sweepAngle = sweepAngle; + } + + public float getSweepSpeed() { + return sweepSpeed; + } + + public void setSweepSpeed(float sweepSpeed) { + this.sweepSpeed = sweepSpeed; + } + + public boolean isSweepBackAndForth() { + return sweepBackAndForth; + } + + public void setSweepBackAndForth(boolean sweepBackAndForth) { + this.sweepBackAndForth = sweepBackAndForth; + } + + public boolean isLockOnTarget() { + return lockOnTarget; + } + + public void setLockOnTarget(boolean lockOnTarget) { + this.lockOnTarget = lockOnTarget; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 41e288c5b..abe05c643 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -11,12 +11,11 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.DamageSource; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityCharge; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashSet; @@ -59,7 +58,9 @@ public class AbilityCharge extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) @@ -68,11 +69,13 @@ public class AbilityCharge extends Ability { } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override @@ -150,7 +153,7 @@ public class AbilityCharge extends Ability { // Calculate distance traveled double distanceTraveled = Math.sqrt( Math.pow(npc.posX - startX, 2) + - Math.pow(npc.posZ - startZ, 2) + Math.pow(npc.posZ - startZ, 2) ); // Check if reached max distance @@ -305,15 +308,35 @@ public class AbilityCharge extends Ability { } // Getters & Setters - public float getChargeSpeed() { return chargeSpeed; } - public void setChargeSpeed(float chargeSpeed) { this.chargeSpeed = chargeSpeed; } + public float getChargeSpeed() { + return chargeSpeed; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public void setChargeSpeed(float chargeSpeed) { + this.chargeSpeed = chargeSpeed; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public float getDamage() { + return damage; + } - public float getHitWidth() { return hitWidth; } - public void setHitWidth(float hitWidth) { this.hitWidth = hitWidth; } + public void setDamage(float damage) { + this.damage = damage; + } + + public float getKnockback() { + return knockback; + } + + public void setKnockback(float knockback) { + this.knockback = knockback; + } + + public float getHitWidth() { + return hitWidth; + } + + public void setHitWidth(float hitWidth) { + this.hitWidth = hitWidth; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index b258e6127..89ff0e3f0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -12,9 +12,9 @@ import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityCutter; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashSet; @@ -67,7 +67,9 @@ public class AbilityCutter extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) @@ -76,21 +78,29 @@ public class AbilityCutter extends Ability { } @Override - public boolean isTargetingModeLocked() { return true; } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AOE_SELF }; + public boolean isTargetingModeLocked() { + return true; } @Override - public float getTelegraphRadius() { return range; } + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AOE_SELF}; + } @Override - public float getTelegraphLength() { return range; } + public float getTelegraphRadius() { + return range; + } @Override - public float getTelegraphAngle() { return arcAngle; } + public float getTelegraphLength() { + return range; + } + + @Override + public float getTelegraphAngle() { + return arcAngle; + } @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { @@ -145,7 +155,7 @@ public class AbilityCutter extends Ability { hitEntities.add(entity.getEntityId()); - float distFactor = 1.0f - ((float)dist / maxDist) * 0.3f; + float distFactor = 1.0f - ((float) dist / maxDist) * 0.3f; float actualDamage = damage * distFactor; // Apply damage with scripted event support @@ -223,38 +233,95 @@ public class AbilityCutter extends Ability { } // Getters & Setters - public float getArcAngle() { return arcAngle; } - public void setArcAngle(float arcAngle) { this.arcAngle = arcAngle; } + public float getArcAngle() { + return arcAngle; + } - public float getRange() { return range; } - public void setRange(float range) { this.range = range; } + public void setArcAngle(float arcAngle) { + this.arcAngle = arcAngle; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getRange() { + return range; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public void setRange(float range) { + this.range = range; + } - public SweepMode getSweepMode() { return sweepMode; } - public void setSweepMode(SweepMode sweepMode) { this.sweepMode = sweepMode; } + public float getDamage() { + return damage; + } - public float getSweepSpeed() { return sweepSpeed; } - public void setSweepSpeed(float sweepSpeed) { this.sweepSpeed = sweepSpeed; } + public void setDamage(float damage) { + this.damage = damage; + } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public float getKnockback() { + return knockback; + } - public int getPoisonDurationSeconds() { return poisonDurationSeconds; } - public void setPoisonDurationSeconds(int poisonDurationSeconds) { this.poisonDurationSeconds = poisonDurationSeconds; } + public void setKnockback(float knockback) { + this.knockback = knockback; + } - public int getPoisonLevel() { return poisonLevel; } - public void setPoisonLevel(int poisonLevel) { this.poisonLevel = poisonLevel; } + public SweepMode getSweepMode() { + return sweepMode; + } - public boolean isPiercing() { return piercing; } - public void setPiercing(boolean piercing) { this.piercing = piercing; } + public void setSweepMode(SweepMode sweepMode) { + this.sweepMode = sweepMode; + } - public float getInnerRadius() { return innerRadius; } - public void setInnerRadius(float innerRadius) { this.innerRadius = innerRadius; } + public float getSweepSpeed() { + return sweepSpeed; + } - public float getCurrentRotation() { return currentRotation; } + public void setSweepSpeed(float sweepSpeed) { + this.sweepSpeed = sweepSpeed; + } + + public int getStunDuration() { + return stunDuration; + } + + public void setStunDuration(int stunDuration) { + this.stunDuration = stunDuration; + } + + public int getPoisonDurationSeconds() { + return poisonDurationSeconds; + } + + public void setPoisonDurationSeconds(int poisonDurationSeconds) { + this.poisonDurationSeconds = poisonDurationSeconds; + } + + public int getPoisonLevel() { + return poisonLevel; + } + + public void setPoisonLevel(int poisonLevel) { + this.poisonLevel = poisonLevel; + } + + public boolean isPiercing() { + return piercing; + } + + public void setPiercing(boolean piercing) { + this.piercing = piercing; + } + + public float getInnerRadius() { + return innerRadius; + } + + public void setInnerRadius(float innerRadius) { + this.innerRadius = innerRadius; + } + + public float getCurrentRotation() { + return currentRotation; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index db34b0bb8..0c0f9cf2c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -2,17 +2,16 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDash; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDash; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.Random; @@ -100,21 +99,25 @@ public class AbilityDash extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + IAbilityConfigCallback callback) { return new SubGuiAbilityDash(this, callback); } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override @@ -163,7 +166,7 @@ public class AbilityDash extends Ability { // Calculate distance traveled double distanceTraveled = Math.sqrt( Math.pow(npc.posX - startX, 2) + - Math.pow(npc.posZ - startZ, 2) + Math.pow(npc.posZ - startZ, 2) ); // Check if reached max distance @@ -246,14 +249,31 @@ public class AbilityDash extends Ability { } // Getters & Setters - public DashMode getDashMode() { return dashMode; } - public void setDashMode(DashMode dashMode) { this.dashMode = dashMode; } + public DashMode getDashMode() { + return dashMode; + } - public float getDashDistance() { return dashDistance; } - public void setDashDistance(float dashDistance) { this.dashDistance = dashDistance; } + public void setDashMode(DashMode dashMode) { + this.dashMode = dashMode; + } - public float getDashSpeed() { return dashSpeed; } - public void setDashSpeed(float dashSpeed) { this.dashSpeed = dashSpeed; } + public float getDashDistance() { + return dashDistance; + } - public DashDirection getChosenDirection() { return chosenDirection; } + public void setDashDistance(float dashDistance) { + this.dashDistance = dashDistance; + } + + public float getDashSpeed() { + return dashSpeed; + } + + public void setDashSpeed(float dashSpeed) { + this.dashSpeed = dashSpeed; + } + + public DashDirection getChosenDirection() { + return chosenDirection; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index 2ef0a446c..2427255fe 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -2,17 +2,16 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityGuard; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityGuard; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.Random; @@ -62,21 +61,25 @@ public class AbilityGuard extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + IAbilityConfigCallback callback) { return new SubGuiAbilityGuard(this, callback); } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.SELF }; + return new TargetingMode[]{TargetingMode.SELF}; } @Override @@ -99,7 +102,7 @@ public class AbilityGuard extends Ability { * This should be called from the damage handling code. * * @param attacker The entity that attacked - * @param damage The damage amount (after reduction) + * @param damage The damage amount (after reduction) */ public void onDamageTaken(EntityNPCInterface npc, EntityLivingBase attacker, DamageSource source, float damage) { if (!canCounter || attacker == null) return; @@ -212,24 +215,59 @@ public class AbilityGuard extends Ability { } // Getters & Setters - public float getDamageReduction() { return damageReduction; } - public void setDamageReduction(float damageReduction) { this.damageReduction = damageReduction; } + public float getDamageReduction() { + return damageReduction; + } - public boolean isCanCounter() { return canCounter; } - public void setCanCounter(boolean canCounter) { this.canCounter = canCounter; } + public void setDamageReduction(float damageReduction) { + this.damageReduction = damageReduction; + } - public CounterType getCounterType() { return counterType; } - public void setCounterType(CounterType counterType) { this.counterType = counterType; } + public boolean isCanCounter() { + return canCounter; + } - public float getCounterValue() { return counterValue; } - public void setCounterValue(float counterValue) { this.counterValue = counterValue; } + public void setCanCounter(boolean canCounter) { + this.canCounter = canCounter; + } - public float getCounterChance() { return counterChance; } - public void setCounterChance(float counterChance) { this.counterChance = counterChance; } + public CounterType getCounterType() { + return counterType; + } - public String getCounterSound() { return counterSound; } - public void setCounterSound(String counterSound) { this.counterSound = counterSound; } + public void setCounterType(CounterType counterType) { + this.counterType = counterType; + } - public int getCounterAnimationId() { return counterAnimationId; } - public void setCounterAnimationId(int counterAnimationId) { this.counterAnimationId = counterAnimationId; } + public float getCounterValue() { + return counterValue; + } + + public void setCounterValue(float counterValue) { + this.counterValue = counterValue; + } + + public float getCounterChance() { + return counterChance; + } + + public void setCounterChance(float counterChance) { + this.counterChance = counterChance; + } + + public String getCounterSound() { + return counterSound; + } + + public void setCounterSound(String counterSound) { + this.counterSound = counterSound; + } + + public int getCounterAnimationId() { + return counterAnimationId; + } + + public void setCounterAnimationId(int counterAnimationId) { + this.counterAnimationId = counterAnimationId; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index d211e7516..b8ce70929 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -2,11 +2,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHazard; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; @@ -17,6 +13,9 @@ import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHazard; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashSet; @@ -90,25 +89,31 @@ public class AbilityHazard extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + IAbilityConfigCallback callback) { return new SubGuiAbilityHazard(this, callback); } @Override - public boolean isTargetingModeLocked() { return true; } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + public boolean isTargetingModeLocked() { + return true; } @Override - public float getTelegraphRadius() { return radius; } + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public float getTelegraphRadius() { + return radius; + } @Override public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { @@ -414,68 +419,176 @@ public class AbilityHazard extends Ability { } // Getters & Setters - public float getRadius() { return radius; } - public void setRadius(float radius) { this.radius = radius; } + public float getRadius() { + return radius; + } - public float getInnerRadius() { return innerRadius; } - public void setInnerRadius(float innerRadius) { this.innerRadius = innerRadius; } + public void setRadius(float radius) { + this.radius = radius; + } - public float getConeAngle() { return coneAngle; } - public void setConeAngle(float coneAngle) { this.coneAngle = coneAngle; } + public float getInnerRadius() { + return innerRadius; + } - public HazardShape getShape() { return shape; } - public void setShape(HazardShape shape) { this.shape = shape; } + public void setInnerRadius(float innerRadius) { + this.innerRadius = innerRadius; + } - public PlacementMode getPlacement() { return placement; } - public void setPlacement(PlacementMode placement) { this.placement = placement; } + public float getConeAngle() { + return coneAngle; + } - public float getDamagePerTick() { return damagePerTick; } - public void setDamagePerTick(float damagePerTick) { this.damagePerTick = damagePerTick; } + public void setConeAngle(float coneAngle) { + this.coneAngle = coneAngle; + } - public int getDamageInterval() { return damageInterval; } - public void setDamageInterval(int damageInterval) { this.damageInterval = damageInterval; } + public HazardShape getShape() { + return shape; + } - public boolean isIgnoreInvulnFrames() { return ignoreInvulnFrames; } - public void setIgnoreInvulnFrames(boolean ignoreInvulnFrames) { this.ignoreInvulnFrames = ignoreInvulnFrames; } + public void setShape(HazardShape shape) { + this.shape = shape; + } - public int getSlownessLevel() { return slownessLevel; } - public void setSlownessLevel(int slownessLevel) { this.slownessLevel = slownessLevel; } + public PlacementMode getPlacement() { + return placement; + } - public int getWeaknessLevel() { return weaknessLevel; } - public void setWeaknessLevel(int weaknessLevel) { this.weaknessLevel = weaknessLevel; } + public void setPlacement(PlacementMode placement) { + this.placement = placement; + } - public int getPoisonLevel() { return poisonLevel; } - public void setPoisonLevel(int poisonLevel) { this.poisonLevel = poisonLevel; } + public float getDamagePerTick() { + return damagePerTick; + } - public int getWitherLevel() { return witherLevel; } - public void setWitherLevel(int witherLevel) { this.witherLevel = witherLevel; } + public void setDamagePerTick(float damagePerTick) { + this.damagePerTick = damagePerTick; + } - public int getBlindnessLevel() { return blindnessLevel; } - public void setBlindnessLevel(int blindnessLevel) { this.blindnessLevel = blindnessLevel; } + public int getDamageInterval() { + return damageInterval; + } - public int getDebuffDuration() { return debuffDuration; } - public void setDebuffDuration(int debuffDuration) { this.debuffDuration = debuffDuration; } + public void setDamageInterval(int damageInterval) { + this.damageInterval = damageInterval; + } - public boolean isAffectsCaster() { return affectsCaster; } - public void setAffectsCaster(boolean affectsCaster) { this.affectsCaster = affectsCaster; } + public boolean isIgnoreInvulnFrames() { + return ignoreInvulnFrames; + } - public float getHeightAbove() { return heightAbove; } - public void setHeightAbove(float heightAbove) { this.heightAbove = heightAbove; } + public void setIgnoreInvulnFrames(boolean ignoreInvulnFrames) { + this.ignoreInvulnFrames = ignoreInvulnFrames; + } - public float getHeightBelow() { return heightBelow; } - public void setHeightBelow(float heightBelow) { this.heightBelow = heightBelow; } + public int getSlownessLevel() { + return slownessLevel; + } - public float getMinOffset() { return minOffset; } - public void setMinOffset(float minOffset) { this.minOffset = minOffset; } + public void setSlownessLevel(int slownessLevel) { + this.slownessLevel = slownessLevel; + } - public float getMaxOffset() { return maxOffset; } - public void setMaxOffset(float maxOffset) { this.maxOffset = maxOffset; } + public int getWeaknessLevel() { + return weaknessLevel; + } - public boolean isRandomOffset() { return randomOffset; } - public void setRandomOffset(boolean randomOffset) { this.randomOffset = randomOffset; } + public void setWeaknessLevel(int weaknessLevel) { + this.weaknessLevel = weaknessLevel; + } + + public int getPoisonLevel() { + return poisonLevel; + } + + public void setPoisonLevel(int poisonLevel) { + this.poisonLevel = poisonLevel; + } + + public int getWitherLevel() { + return witherLevel; + } + + public void setWitherLevel(int witherLevel) { + this.witherLevel = witherLevel; + } + + public int getBlindnessLevel() { + return blindnessLevel; + } + + public void setBlindnessLevel(int blindnessLevel) { + this.blindnessLevel = blindnessLevel; + } + + public int getDebuffDuration() { + return debuffDuration; + } + + public void setDebuffDuration(int debuffDuration) { + this.debuffDuration = debuffDuration; + } + + public boolean isAffectsCaster() { + return affectsCaster; + } + + public void setAffectsCaster(boolean affectsCaster) { + this.affectsCaster = affectsCaster; + } + + public float getHeightAbove() { + return heightAbove; + } + + public void setHeightAbove(float heightAbove) { + this.heightAbove = heightAbove; + } + + public float getHeightBelow() { + return heightBelow; + } + + public void setHeightBelow(float heightBelow) { + this.heightBelow = heightBelow; + } + + public float getMinOffset() { + return minOffset; + } + + public void setMinOffset(float minOffset) { + this.minOffset = minOffset; + } + + public float getMaxOffset() { + return maxOffset; + } + + public void setMaxOffset(float maxOffset) { + this.maxOffset = maxOffset; + } + + public boolean isRandomOffset() { + return randomOffset; + } + + public void setRandomOffset(boolean randomOffset) { + this.randomOffset = randomOffset; + } // Runtime getters - public double getZoneX() { return zoneX; } - public double getZoneY() { return zoneY; } - public double getZoneZ() { return zoneZ; } + public double getZoneX() { + return zoneX; + } + + public double getZoneY() { + return zoneY; + } + + public double getZoneZ() { + return zoneZ; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index 4a393682b..cae5f6815 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -10,9 +10,9 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeal; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.ArrayList; @@ -50,7 +50,9 @@ public class AbilityHeal extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) @@ -59,11 +61,13 @@ public class AbilityHeal extends Ability { } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.SELF }; + return new TargetingMode[]{TargetingMode.SELF}; } @Override @@ -197,21 +201,51 @@ public class AbilityHeal extends Ability { } // Getters & Setters - public float getHealAmount() { return healAmount; } - public void setHealAmount(float healAmount) { this.healAmount = healAmount; } + public float getHealAmount() { + return healAmount; + } - public float getHealPercent() { return healPercent; } - public void setHealPercent(float healPercent) { this.healPercent = healPercent; } + public void setHealAmount(float healAmount) { + this.healAmount = healAmount; + } - public boolean isHealSelf() { return healSelf; } - public void setHealSelf(boolean healSelf) { this.healSelf = healSelf; } + public float getHealPercent() { + return healPercent; + } - public boolean isHealAllies() { return healAllies; } - public void setHealAllies(boolean healAllies) { this.healAllies = healAllies; } + public void setHealPercent(float healPercent) { + this.healPercent = healPercent; + } - public float getHealRadius() { return healRadius; } - public void setHealRadius(float healRadius) { this.healRadius = healRadius; } + public boolean isHealSelf() { + return healSelf; + } - public boolean isInstantHeal() { return instantHeal; } - public void setInstantHeal(boolean instantHeal) { this.instantHeal = instantHeal; } + public void setHealSelf(boolean healSelf) { + this.healSelf = healSelf; + } + + public boolean isHealAllies() { + return healAllies; + } + + public void setHealAllies(boolean healAllies) { + this.healAllies = healAllies; + } + + public float getHealRadius() { + return healRadius; + } + + public void setHealRadius(float healRadius) { + this.healRadius = healRadius; + } + + public boolean isInstantHeal() { + return instantHeal; + } + + public void setInstantHeal(boolean instantHeal) { + this.instantHeal = instantHeal; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index 288af63d7..caafc9d5d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -2,11 +2,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeavyHit; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; @@ -14,6 +10,9 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; import net.minecraft.potion.PotionEffect; import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeavyHit; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; /** @@ -46,21 +45,25 @@ public class AbilityHeavyHit extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + IAbilityConfigCallback callback) { return new SubGuiAbilityHeavyHit(this, callback); } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override @@ -105,18 +108,43 @@ public class AbilityHeavyHit extends Ability { } // Getters & Setters - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getDamage() { + return damage; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public void setDamage(float damage) { + this.damage = damage; + } - public int getSlownessLevel() { return slownessLevel; } - public void setSlownessLevel(int slownessLevel) { this.slownessLevel = slownessLevel; } + public float getKnockback() { + return knockback; + } - public int getWeaknessLevel() { return weaknessLevel; } - public void setWeaknessLevel(int weaknessLevel) { this.weaknessLevel = weaknessLevel; } + public void setKnockback(float knockback) { + this.knockback = knockback; + } - public int getPotionDurationSeconds() { return potionDurationSeconds; } - public void setPotionDurationSeconds(int potionDurationSeconds) { this.potionDurationSeconds = potionDurationSeconds; } + public int getSlownessLevel() { + return slownessLevel; + } + + public void setSlownessLevel(int slownessLevel) { + this.slownessLevel = slownessLevel; + } + + public int getWeaknessLevel() { + return weaknessLevel; + } + + public void setWeaknessLevel(int weaknessLevel) { + this.weaknessLevel = weaknessLevel; + } + + public int getPotionDurationSeconds() { + return potionDurationSeconds; + } + + public void setPotionDurationSeconds(int potionDurationSeconds) { + this.potionDurationSeconds = potionDurationSeconds; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index cf1555898..a3677a9db 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -2,11 +2,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityOrb; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; @@ -16,6 +12,9 @@ import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityOrb; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.List; @@ -67,21 +66,25 @@ public class AbilityOrb extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + IAbilityConfigCallback callback) { return new SubGuiAbilityOrb(this, callback); } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override @@ -124,8 +127,8 @@ public class AbilityOrb extends Ability { double distTraveled = Math.sqrt( Math.pow(orbX - startX, 2) + - Math.pow(orbY - startY, 2) + - Math.pow(orbZ - startZ, 2) + Math.pow(orbY - startY, 2) + + Math.pow(orbZ - startZ, 2) ); if (distTraveled >= maxDistance) { hasHit = true; @@ -139,8 +142,8 @@ public class AbilityOrb extends Ability { double distToTarget = Math.sqrt( Math.pow(targetX - orbX, 2) + - Math.pow(targetY - orbY, 2) + - Math.pow(targetZ - orbZ, 2) + Math.pow(targetY - orbY, 2) + + Math.pow(targetZ - orbZ, 2) ); if (distToTarget <= homingRange) { @@ -202,7 +205,7 @@ public class AbilityOrb extends Ability { break; } - if (!hasHit && world.getBlock((int)orbX, (int)orbY, (int)orbZ).getMaterial().isSolid()) { + if (!hasHit && world.getBlock((int) orbX, (int) orbY, (int) orbZ).getMaterial().isSolid()) { hasHit = true; if (explosive) { doExplosion(npc, world); @@ -224,12 +227,12 @@ public class AbilityOrb extends Ability { double dist = Math.sqrt( Math.pow(blastTarget.posX - orbX, 2) + - Math.pow(blastTarget.posY - orbY, 2) + - Math.pow(blastTarget.posZ - orbZ, 2) + Math.pow(blastTarget.posY - orbY, 2) + + Math.pow(blastTarget.posZ - orbZ, 2) ); if (dist <= explosionRadius) { - float falloff = 1.0f - (float)(dist / explosionRadius) * explosionDamageFalloff; + float falloff = 1.0f - (float) (dist / explosionRadius) * explosionDamageFalloff; // Apply damage with scripted event support boolean wasHit = applyAbilityDamage(npc, blastTarget, damage * falloff, knockback * falloff); if (wasHit) { @@ -314,54 +317,140 @@ public class AbilityOrb extends Ability { } // Getters & Setters - public float getOrbSpeed() { return orbSpeed; } - public void setOrbSpeed(float orbSpeed) { this.orbSpeed = orbSpeed; } + public float getOrbSpeed() { + return orbSpeed; + } - public float getOrbSize() { return orbSize; } - public void setOrbSize(float orbSize) { this.orbSize = orbSize; } + public void setOrbSpeed(float orbSpeed) { + this.orbSpeed = orbSpeed; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getOrbSize() { + return orbSize; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public void setOrbSize(float orbSize) { + this.orbSize = orbSize; + } - public float getMaxDistance() { return maxDistance; } - public void setMaxDistance(float maxDistance) { this.maxDistance = maxDistance; } + public float getDamage() { + return damage; + } - public int getMaxLifetime() { return maxLifetime; } - public void setMaxLifetime(int maxLifetime) { this.maxLifetime = maxLifetime; } + public void setDamage(float damage) { + this.damage = damage; + } - public boolean isHoming() { return homing; } - public void setHoming(boolean homing) { this.homing = homing; } + public float getKnockback() { + return knockback; + } - public float getHomingStrength() { return homingStrength; } - public void setHomingStrength(float homingStrength) { this.homingStrength = homingStrength; } + public void setKnockback(float knockback) { + this.knockback = knockback; + } - public float getHomingRange() { return homingRange; } - public void setHomingRange(float homingRange) { this.homingRange = homingRange; } + public float getMaxDistance() { + return maxDistance; + } - public boolean isExplosive() { return explosive; } - public void setExplosive(boolean explosive) { this.explosive = explosive; } + public void setMaxDistance(float maxDistance) { + this.maxDistance = maxDistance; + } - public float getExplosionRadius() { return explosionRadius; } - public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; } + public int getMaxLifetime() { + return maxLifetime; + } - public float getExplosionDamageFalloff() { return explosionDamageFalloff; } - public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.explosionDamageFalloff = explosionDamageFalloff; } + public void setMaxLifetime(int maxLifetime) { + this.maxLifetime = maxLifetime; + } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public boolean isHoming() { + return homing; + } - public int getSlowDuration() { return slowDuration; } - public void setSlowDuration(int slowDuration) { this.slowDuration = slowDuration; } + public void setHoming(boolean homing) { + this.homing = homing; + } - public int getSlowLevel() { return slowLevel; } - public void setSlowLevel(int slowLevel) { this.slowLevel = slowLevel; } + public float getHomingStrength() { + return homingStrength; + } + + public void setHomingStrength(float homingStrength) { + this.homingStrength = homingStrength; + } + + public float getHomingRange() { + return homingRange; + } + + public void setHomingRange(float homingRange) { + this.homingRange = homingRange; + } + + public boolean isExplosive() { + return explosive; + } + + public void setExplosive(boolean explosive) { + this.explosive = explosive; + } + + public float getExplosionRadius() { + return explosionRadius; + } + + public void setExplosionRadius(float explosionRadius) { + this.explosionRadius = explosionRadius; + } + + public float getExplosionDamageFalloff() { + return explosionDamageFalloff; + } + + public void setExplosionDamageFalloff(float explosionDamageFalloff) { + this.explosionDamageFalloff = explosionDamageFalloff; + } + + public int getStunDuration() { + return stunDuration; + } + + public void setStunDuration(int stunDuration) { + this.stunDuration = stunDuration; + } + + public int getSlowDuration() { + return slowDuration; + } + + public void setSlowDuration(int slowDuration) { + this.slowDuration = slowDuration; + } + + public int getSlowLevel() { + return slowLevel; + } + + public void setSlowLevel(int slowLevel) { + this.slowLevel = slowLevel; + } // Runtime getters for rendering - public double getOrbX() { return orbX; } - public double getOrbY() { return orbY; } - public double getOrbZ() { return orbZ; } - public boolean hasHit() { return hasHit; } + public double getOrbX() { + return orbX; + } + + public double getOrbY() { + return orbY; + } + + public double getOrbZ() { + return orbZ; + } + + public boolean hasHit() { + return hasHit; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index 4d2143b36..b47cd244a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -8,11 +8,10 @@ import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityProjectile; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.List; @@ -49,7 +48,9 @@ public class AbilityProjectile extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) @@ -58,11 +59,13 @@ public class AbilityProjectile extends Ability { } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override @@ -187,27 +190,67 @@ public class AbilityProjectile extends Ability { } // Getters & Setters - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getDamage() { + return damage; + } - public float getSpeed() { return speed; } - public void setSpeed(float speed) { this.speed = speed; } + public void setDamage(float damage) { + this.damage = damage; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public float getSpeed() { + return speed; + } - public String getProjectileType() { return projectileType; } - public void setProjectileType(String projectileType) { this.projectileType = projectileType; } + public void setSpeed(float speed) { + this.speed = speed; + } - public boolean isExplosive() { return explosive; } - public void setExplosive(boolean explosive) { this.explosive = explosive; } + public float getKnockback() { + return knockback; + } - public float getExplosionRadius() { return explosionRadius; } - public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; } + public void setKnockback(float knockback) { + this.knockback = knockback; + } - public boolean isHoming() { return homing; } - public void setHoming(boolean homing) { this.homing = homing; } + public String getProjectileType() { + return projectileType; + } - public float getHomingStrength() { return homingStrength; } - public void setHomingStrength(float homingStrength) { this.homingStrength = homingStrength; } + public void setProjectileType(String projectileType) { + this.projectileType = projectileType; + } + + public boolean isExplosive() { + return explosive; + } + + public void setExplosive(boolean explosive) { + this.explosive = explosive; + } + + public float getExplosionRadius() { + return explosionRadius; + } + + public void setExplosionRadius(float explosionRadius) { + this.explosionRadius = explosionRadius; + } + + public boolean isHoming() { + return homing; + } + + public void setHoming(boolean homing) { + this.homing = homing; + } + + public float getHomingStrength() { + return homingStrength; + } + + public void setHomingStrength(float homingStrength) { + this.homingStrength = homingStrength; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index 340b92826..e15cf683a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -12,9 +12,9 @@ import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityShockwave; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.List; @@ -50,7 +50,9 @@ public class AbilityShockwave extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) @@ -59,15 +61,19 @@ public class AbilityShockwave extends Ability { } @Override - public boolean isTargetingModeLocked() { return true; } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AOE_SELF }; + public boolean isTargetingModeLocked() { + return true; } @Override - public float getTelegraphRadius() { return pushRadius; } + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AOE_SELF}; + } + + @Override + public float getTelegraphRadius() { + return pushRadius; + } @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { @@ -111,7 +117,7 @@ public class AbilityShockwave extends Ability { } // Scale knockback by distance (closer = stronger) - float distFactor = 1.0f - (float)(dist / pushRadius) * 0.5f; + float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f; float finalPush = pushStrength * distFactor; // Apply damage with custom knockback direction boolean wasHit = applyAbilityDamageWithDirection(npc, entity, damage * distFactor, finalPush, dx, dz); @@ -153,19 +159,44 @@ public class AbilityShockwave extends Ability { } // Getters & Setters - public float getPushRadius() { return pushRadius; } - public void setPushRadius(float pushRadius) { this.pushRadius = pushRadius; } + public float getPushRadius() { + return pushRadius; + } - public float getPushStrength() { return pushStrength; } - public void setPushStrength(float pushStrength) { this.pushStrength = pushStrength; } + public void setPushRadius(float pushRadius) { + this.pushRadius = pushRadius; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getPushStrength() { + return pushStrength; + } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public void setPushStrength(float pushStrength) { + this.pushStrength = pushStrength; + } - public int getMaxTargets() { return maxTargets; } - public void setMaxTargets(int maxTargets) { this.maxTargets = maxTargets; } + public float getDamage() { + return damage; + } + + public void setDamage(float damage) { + this.damage = damage; + } + + public int getStunDuration() { + return stunDuration; + } + + public void setStunDuration(int stunDuration) { + this.stunDuration = stunDuration; + } + + public int getMaxTargets() { + return maxTargets; + } + + public void setMaxTargets(int maxTargets) { + this.maxTargets = maxTargets; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 321b9058f..1b0eb246f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -11,18 +11,17 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; -import net.minecraft.util.MathHelper; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilitySlam; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.List; /** * Slam ability: NPC leaps toward target in an arc and slams down, dealing AOE damage on landing. - * + *

* Phases: * - WINDUP: NPC prepares to leap, telegraph shows landing zone * - ACTIVE: NPC is in the air, traveling in arc toward destination @@ -62,7 +61,9 @@ public class AbilitySlam extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) @@ -80,7 +81,7 @@ public class AbilitySlam extends Ability { @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AOE_SELF, TargetingMode.AOE_TARGET }; + return new TargetingMode[]{TargetingMode.AOE_SELF, TargetingMode.AOE_TARGET}; } @Override @@ -317,7 +318,7 @@ public class AbilitySlam extends Ability { } // Ground crack particles - check if block is valid - net.minecraft.block.Block groundBlock = world.getBlock((int)x, (int)y - 1, (int)z); + net.minecraft.block.Block groundBlock = world.getBlock((int) x, (int) y - 1, (int) z); if (groundBlock != null && groundBlock.getMaterial().isSolid()) { int blockId = net.minecraft.block.Block.getIdFromBlock(groundBlock); if (blockId > 0) { @@ -427,18 +428,43 @@ public class AbilitySlam extends Ability { } // Getters & Setters - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getDamage() { + return damage; + } - public float getRadius() { return radius; } - public void setRadius(float radius) { this.radius = radius; } + public void setDamage(float damage) { + this.damage = damage; + } - public float getKnockbackStrength() { return knockbackStrength; } - public void setKnockbackStrength(float knockbackStrength) { this.knockbackStrength = knockbackStrength; } + public float getRadius() { + return radius; + } - public float getLeapSpeed() { return leapSpeed; } - public void setLeapSpeed(float leapSpeed) { this.leapSpeed = leapSpeed; } + public void setRadius(float radius) { + this.radius = radius; + } - public float getLeapHeight() { return leapHeight; } - public void setLeapHeight(float leapHeight) { this.leapHeight = leapHeight; } + public float getKnockbackStrength() { + return knockbackStrength; + } + + public void setKnockbackStrength(float knockbackStrength) { + this.knockbackStrength = knockbackStrength; + } + + public float getLeapSpeed() { + return leapSpeed; + } + + public void setLeapSpeed(float leapSpeed) { + this.leapSpeed = leapSpeed; + } + + public float getLeapHeight() { + return leapHeight; + } + + public void setLeapHeight(float leapHeight) { + this.leapHeight = leapHeight; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index 72971c572..50d59e729 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -2,11 +2,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityTeleport; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.block.Block; @@ -14,10 +10,12 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.DamageSource; import net.minecraft.util.MathHelper; import net.minecraft.util.Vec3; import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityTeleport; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.List; @@ -73,21 +71,25 @@ public class AbilityTeleport extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + IAbilityConfigCallback callback) { return new SubGuiAbilityTeleport(this, callback); } @Override - public boolean isTargetingModeLocked() { return true; } + public boolean isTargetingModeLocked() { + return true; + } @Override public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; } @Override @@ -202,7 +204,7 @@ public class AbilityTeleport extends Ability { } private Vec3 findSafeDestination(World world, double oldX, double oldY, double oldZ, - double destX, double destY, double destZ) { + double destX, double destY, double destZ) { int oldBlockX = (int) Math.floor(oldX); int oldBlockY = (int) Math.floor(oldY); int oldBlockZ = (int) Math.floor(oldZ); @@ -291,7 +293,7 @@ public class AbilityTeleport extends Ability { double checkZ = z1 + dz * d; if (hasLineOfSight(world, x1, y1 + npc.getEyeHeight(), z1, - checkX, checkY + npc.getEyeHeight(), checkZ)) { + checkX, checkY + npc.getEyeHeight(), checkZ)) { int blockX = MathHelper.floor_double(checkX); int blockY = MathHelper.floor_double(checkY); int blockZ = MathHelper.floor_double(checkZ); @@ -330,8 +332,8 @@ public class AbilityTeleport extends Ability { Block headBlock = world.getBlock(x, y + 1, z); return groundBlock.getMaterial().isSolid() && - !feetBlock.getMaterial().isSolid() && - !headBlock.getMaterial().isSolid(); + !feetBlock.getMaterial().isSolid() && + !headBlock.getMaterial().isSolid(); } private void dealDamageAt(EntityNPCInterface npc, World world, double x, double y, double z) { @@ -414,33 +416,83 @@ public class AbilityTeleport extends Ability { } // Getters & Setters - public TeleportMode getMode() { return mode; } - public void setMode(TeleportMode mode) { this.mode = mode; } + public TeleportMode getMode() { + return mode; + } - public int getBlinkCount() { return blinkCount; } - public void setBlinkCount(int blinkCount) { this.blinkCount = blinkCount; } + public void setMode(TeleportMode mode) { + this.mode = mode; + } - public int getBlinkDelayTicks() { return blinkDelayTicks; } - public void setBlinkDelayTicks(int blinkDelayTicks) { this.blinkDelayTicks = blinkDelayTicks; } + public int getBlinkCount() { + return blinkCount; + } - public float getBlinkRadius() { return blinkRadius; } - public void setBlinkRadius(float blinkRadius) { this.blinkRadius = blinkRadius; } + public void setBlinkCount(int blinkCount) { + this.blinkCount = blinkCount; + } - public float getBehindDistance() { return behindDistance; } - public void setBehindDistance(float behindDistance) { this.behindDistance = behindDistance; } + public int getBlinkDelayTicks() { + return blinkDelayTicks; + } - public boolean isRequireLineOfSight() { return requireLineOfSight; } - public void setRequireLineOfSight(boolean requireLineOfSight) { this.requireLineOfSight = requireLineOfSight; } + public void setBlinkDelayTicks(int blinkDelayTicks) { + this.blinkDelayTicks = blinkDelayTicks; + } - public boolean isDamageAtStart() { return damageAtStart; } - public void setDamageAtStart(boolean damageAtStart) { this.damageAtStart = damageAtStart; } + public float getBlinkRadius() { + return blinkRadius; + } - public boolean isDamageAtEnd() { return damageAtEnd; } - public void setDamageAtEnd(boolean damageAtEnd) { this.damageAtEnd = damageAtEnd; } + public void setBlinkRadius(float blinkRadius) { + this.blinkRadius = blinkRadius; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getBehindDistance() { + return behindDistance; + } - public float getDamageRadius() { return damageRadius; } - public void setDamageRadius(float damageRadius) { this.damageRadius = damageRadius; } + public void setBehindDistance(float behindDistance) { + this.behindDistance = behindDistance; + } + + public boolean isRequireLineOfSight() { + return requireLineOfSight; + } + + public void setRequireLineOfSight(boolean requireLineOfSight) { + this.requireLineOfSight = requireLineOfSight; + } + + public boolean isDamageAtStart() { + return damageAtStart; + } + + public void setDamageAtStart(boolean damageAtStart) { + this.damageAtStart = damageAtStart; + } + + public boolean isDamageAtEnd() { + return damageAtEnd; + } + + public void setDamageAtEnd(boolean damageAtEnd) { + this.damageAtEnd = damageAtEnd; + } + + public float getDamage() { + return damage; + } + + public void setDamage(float damage) { + this.damage = damage; + } + + public float getDamageRadius() { + return damageRadius; + } + + public void setDamageRadius(float damageRadius) { + this.damageRadius = damageRadius; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 9d1090da6..021b1f7d6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -13,9 +13,9 @@ import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityTrap; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashSet; @@ -80,7 +80,9 @@ public class AbilityTrap extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) @@ -89,15 +91,19 @@ public class AbilityTrap extends Ability { } @Override - public boolean isTargetingModeLocked() { return true; } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AGGRO_TARGET }; + public boolean isTargetingModeLocked() { + return true; } @Override - public float getTelegraphRadius() { return triggerRadius; } + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public float getTelegraphRadius() { + return triggerRadius; + } @Override public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { @@ -362,66 +368,172 @@ public class AbilityTrap extends Ability { } // Getters & Setters - public TrapPlacement getPlacement() { return placement; } - public void setPlacement(TrapPlacement placement) { this.placement = placement; } + public TrapPlacement getPlacement() { + return placement; + } - public float getPlacementDistance() { return placementDistance; } - public void setPlacementDistance(float placementDistance) { this.placementDistance = placementDistance; } + public void setPlacement(TrapPlacement placement) { + this.placement = placement; + } - public float getTriggerRadius() { return triggerRadius; } - public void setTriggerRadius(float triggerRadius) { this.triggerRadius = triggerRadius; } + public float getPlacementDistance() { + return placementDistance; + } - public int getArmTime() { return armTime; } - public void setArmTime(int armTime) { this.armTime = armTime; } + public void setPlacementDistance(float placementDistance) { + this.placementDistance = placementDistance; + } - public int getMaxTriggers() { return maxTriggers; } - public void setMaxTriggers(int maxTriggers) { this.maxTriggers = maxTriggers; } + public float getTriggerRadius() { + return triggerRadius; + } - public int getTriggerCooldown() { return triggerCooldown; } - public void setTriggerCooldown(int triggerCooldown) { this.triggerCooldown = triggerCooldown; } + public void setTriggerRadius(float triggerRadius) { + this.triggerRadius = triggerRadius; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public int getArmTime() { + return armTime; + } - public float getDamageRadius() { return damageRadius; } - public void setDamageRadius(float damageRadius) { this.damageRadius = damageRadius; } + public void setArmTime(int armTime) { + this.armTime = armTime; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public int getMaxTriggers() { + return maxTriggers; + } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public void setMaxTriggers(int maxTriggers) { + this.maxTriggers = maxTriggers; + } - public int getRootDuration() { return rootDuration; } - public void setRootDuration(int rootDuration) { this.rootDuration = rootDuration; } + public int getTriggerCooldown() { + return triggerCooldown; + } - public int getSlowDuration() { return slowDuration; } - public void setSlowDuration(int slowDuration) { this.slowDuration = slowDuration; } + public void setTriggerCooldown(int triggerCooldown) { + this.triggerCooldown = triggerCooldown; + } - public int getSlowLevel() { return slowLevel; } - public void setSlowLevel(int slowLevel) { this.slowLevel = slowLevel; } + public float getDamage() { + return damage; + } - public int getPoisonDuration() { return poisonDuration; } - public void setPoisonDuration(int poisonDuration) { this.poisonDuration = poisonDuration; } + public void setDamage(float damage) { + this.damage = damage; + } - public int getPoisonLevel() { return poisonLevel; } - public void setPoisonLevel(int poisonLevel) { this.poisonLevel = poisonLevel; } + public float getDamageRadius() { + return damageRadius; + } - public boolean isVisible() { return visible; } - public void setVisible(boolean visible) { this.visible = visible; } + public void setDamageRadius(float damageRadius) { + this.damageRadius = damageRadius; + } - public float getMinOffset() { return minOffset; } - public void setMinOffset(float minOffset) { this.minOffset = minOffset; } + public float getKnockback() { + return knockback; + } - public float getMaxOffset() { return maxOffset; } - public void setMaxOffset(float maxOffset) { this.maxOffset = maxOffset; } + public void setKnockback(float knockback) { + this.knockback = knockback; + } - public boolean isRandomOffset() { return randomOffset; } - public void setRandomOffset(boolean randomOffset) { this.randomOffset = randomOffset; } + public int getStunDuration() { + return stunDuration; + } + + public void setStunDuration(int stunDuration) { + this.stunDuration = stunDuration; + } + + public int getRootDuration() { + return rootDuration; + } + + public void setRootDuration(int rootDuration) { + this.rootDuration = rootDuration; + } + + public int getSlowDuration() { + return slowDuration; + } + + public void setSlowDuration(int slowDuration) { + this.slowDuration = slowDuration; + } + + public int getSlowLevel() { + return slowLevel; + } + + public void setSlowLevel(int slowLevel) { + this.slowLevel = slowLevel; + } + + public int getPoisonDuration() { + return poisonDuration; + } + + public void setPoisonDuration(int poisonDuration) { + this.poisonDuration = poisonDuration; + } + + public int getPoisonLevel() { + return poisonLevel; + } + + public void setPoisonLevel(int poisonLevel) { + this.poisonLevel = poisonLevel; + } + + public boolean isVisible() { + return visible; + } + + public void setVisible(boolean visible) { + this.visible = visible; + } + + public float getMinOffset() { + return minOffset; + } + + public void setMinOffset(float minOffset) { + this.minOffset = minOffset; + } + + public float getMaxOffset() { + return maxOffset; + } + + public void setMaxOffset(float maxOffset) { + this.maxOffset = maxOffset; + } + + public boolean isRandomOffset() { + return randomOffset; + } + + public void setRandomOffset(boolean randomOffset) { + this.randomOffset = randomOffset; + } // Runtime getters - public double getTrapX() { return trapX; } - public double getTrapY() { return trapY; } - public double getTrapZ() { return trapZ; } - public boolean isArmed() { return armed; } + public double getTrapX() { + return trapX; + } + + public double getTrapY() { + return trapY; + } + + public double getTrapZ() { + return trapZ; + } + + public boolean isArmed() { + return armed; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index ccd8d4a19..ab7171461 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -2,11 +2,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; - import kamkeel.npcs.controllers.data.ability.Ability; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityVortex; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; @@ -16,6 +12,9 @@ import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityVortex; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import java.util.HashSet; @@ -61,25 +60,31 @@ public class AbilityVortex extends Ability { } @Override - public boolean hasTypeSettings() { return true; } + public boolean hasTypeSettings() { + return true; + } @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + IAbilityConfigCallback callback) { return new SubGuiAbilityVortex(this, callback); } @Override - public boolean isTargetingModeLocked() { return true; } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[] { TargetingMode.AOE_SELF }; + public boolean isTargetingModeLocked() { + return true; } @Override - public float getTelegraphRadius() { return pullRadius; } + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AOE_SELF}; + } + + @Override + public float getTelegraphRadius() { + return pullRadius; + } @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { @@ -249,33 +254,83 @@ public class AbilityVortex extends Ability { } // Getters & Setters - public float getPullRadius() { return pullRadius; } - public void setPullRadius(float pullRadius) { this.pullRadius = pullRadius; } + public float getPullRadius() { + return pullRadius; + } - public float getPullStrength() { return pullStrength; } - public void setPullStrength(float pullStrength) { this.pullStrength = pullStrength; } + public void setPullRadius(float pullRadius) { + this.pullRadius = pullRadius; + } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } + public float getPullStrength() { + return pullStrength; + } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } + public void setPullStrength(float pullStrength) { + this.pullStrength = pullStrength; + } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public float getDamage() { + return damage; + } - public int getRootDuration() { return rootDuration; } - public void setRootDuration(int rootDuration) { this.rootDuration = rootDuration; } + public void setDamage(float damage) { + this.damage = damage; + } - public boolean isAoe() { return aoe; } - public void setAoe(boolean aoe) { this.aoe = aoe; } + public float getKnockback() { + return knockback; + } - public int getMaxTargets() { return maxTargets; } - public void setMaxTargets(int maxTargets) { this.maxTargets = maxTargets; } + public void setKnockback(float knockback) { + this.knockback = knockback; + } - public boolean isDamageOnPull() { return damageOnPull; } - public void setDamageOnPull(boolean damageOnPull) { this.damageOnPull = damageOnPull; } + public int getStunDuration() { + return stunDuration; + } - public float getPullDamage() { return pullDamage; } - public void setPullDamage(float pullDamage) { this.pullDamage = pullDamage; } + public void setStunDuration(int stunDuration) { + this.stunDuration = stunDuration; + } + + public int getRootDuration() { + return rootDuration; + } + + public void setRootDuration(int rootDuration) { + this.rootDuration = rootDuration; + } + + public boolean isAoe() { + return aoe; + } + + public void setAoe(boolean aoe) { + this.aoe = aoe; + } + + public int getMaxTargets() { + return maxTargets; + } + + public void setMaxTargets(int maxTargets) { + this.maxTargets = maxTargets; + } + + public boolean isDamageOnPull() { + return damageOnPull; + } + + public void setDamageOnPull(boolean damageOnPull) { + this.damageOnPull = damageOnPull; + } + + public float getPullDamage() { + return pullDamage; + } + + public void setPullDamage(float pullDamage) { + this.pullDamage = pullDamage; + } } diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index c87a5aa2d..1405b44c9 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -26,6 +26,8 @@ import kamkeel.npcs.network.packets.data.SoundManagementPacket; import kamkeel.npcs.network.packets.data.SwingPlayerArmPacket; import kamkeel.npcs.network.packets.data.UpdateAnimationsPacket; import kamkeel.npcs.network.packets.data.VillagerListPacket; +import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket; +import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket; import kamkeel.npcs.network.packets.data.gui.GuiClosePacket; import kamkeel.npcs.network.packets.data.gui.GuiErrorPacket; import kamkeel.npcs.network.packets.data.gui.GuiOpenBookPacket; @@ -49,8 +51,6 @@ import kamkeel.npcs.network.packets.data.npc.WeaponNpcPacket; import kamkeel.npcs.network.packets.data.script.ScriptOverlayClosePacket; import kamkeel.npcs.network.packets.data.script.ScriptOverlayDataPacket; import kamkeel.npcs.network.packets.data.script.ScriptedParticlePacket; -import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket; -import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket; import kamkeel.npcs.network.packets.player.BankActionPacket; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.CompanionActionPacket; @@ -64,6 +64,7 @@ import kamkeel.npcs.network.packets.player.SaveBookPacket; import kamkeel.npcs.network.packets.player.SaveSignPacket; import kamkeel.npcs.network.packets.player.ScreenSizePacket; import kamkeel.npcs.network.packets.player.SpecialKeyStatePacket; +import kamkeel.npcs.network.packets.player.SyncRevisionInfoPacket; import kamkeel.npcs.network.packets.player.TransportPacket; import kamkeel.npcs.network.packets.player.customgui.CustomGuiButtonPacket; import kamkeel.npcs.network.packets.player.customgui.CustomGuiClosePacket; @@ -75,7 +76,6 @@ import kamkeel.npcs.network.packets.player.item.MagicCyclesPacket; import kamkeel.npcs.network.packets.player.profile.ProfileChangePacket; import kamkeel.npcs.network.packets.player.profile.ProfileCreatePacket; import kamkeel.npcs.network.packets.player.profile.ProfileGetInfoPacket; -import kamkeel.npcs.network.packets.player.SyncRevisionInfoPacket; import kamkeel.npcs.network.packets.player.profile.ProfileGetPacket; import kamkeel.npcs.network.packets.player.profile.ProfileRemovePacket; import kamkeel.npcs.network.packets.player.profile.ProfileRenamePacket; @@ -88,6 +88,9 @@ import kamkeel.npcs.network.packets.request.MountPacket; import kamkeel.npcs.network.packets.request.TileEntityGetPacket; import kamkeel.npcs.network.packets.request.TileEntitySavePacket; import kamkeel.npcs.network.packets.request.TraderMarketSavePacket; +import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket; +import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket; +import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcSavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket; @@ -141,9 +144,6 @@ import kamkeel.npcs.network.packets.request.linked.LinkedItemSavePacket; import kamkeel.npcs.network.packets.request.linked.LinkedNPCAddPacket; import kamkeel.npcs.network.packets.request.linked.LinkedNPCRemovePacket; import kamkeel.npcs.network.packets.request.linked.LinkedSetPacket; -import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket; -import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket; -import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcSavePacket; import kamkeel.npcs.network.packets.request.magic.MagicCycleRemovePacket; import kamkeel.npcs.network.packets.request.magic.MagicCycleSavePacket; import kamkeel.npcs.network.packets.request.magic.MagicGetAllPacket; @@ -216,7 +216,16 @@ import kamkeel.npcs.network.packets.request.recipe.RecipesGetPacket; import kamkeel.npcs.network.packets.request.role.RoleCompanionUpdatePacket; import kamkeel.npcs.network.packets.request.role.RoleGetPacket; import kamkeel.npcs.network.packets.request.role.RoleSavePacket; -import kamkeel.npcs.network.packets.request.script.*; +import kamkeel.npcs.network.packets.request.script.BlockScriptPacket; +import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; +import kamkeel.npcs.network.packets.request.script.EventScriptPacket; +import kamkeel.npcs.network.packets.request.script.ForgeScriptPacket; +import kamkeel.npcs.network.packets.request.script.GlobalNPCScriptPacket; +import kamkeel.npcs.network.packets.request.script.NPCScriptPacket; +import kamkeel.npcs.network.packets.request.script.PlayerScriptPacket; +import kamkeel.npcs.network.packets.request.script.RecipeScriptPacket; +import kamkeel.npcs.network.packets.request.script.ScriptFilesPacket; +import kamkeel.npcs.network.packets.request.script.ScriptInfoPacket; import kamkeel.npcs.network.packets.request.script.item.ItemScriptErrorPacket; import kamkeel.npcs.network.packets.request.script.item.ItemScriptPacket; import kamkeel.npcs.network.packets.request.script.item.LinkedItemScriptPacket; diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java index bbf342ade..e9c846764 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java @@ -3,7 +3,6 @@ package kamkeel.npcs.network.packets.data.ability; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; -import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; import kamkeel.npcs.network.AbstractPacket; diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java index 74bf8bd5e..37383ea6c 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java @@ -3,7 +3,9 @@ package kamkeel.npcs.network.packets.request.script; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; -import kamkeel.npcs.network.*; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; import kamkeel.npcs.network.enums.EnumRequestPacket; import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; diff --git a/src/main/java/kamkeel/npcs/util/BukkitUtil.java b/src/main/java/kamkeel/npcs/util/BukkitUtil.java index 78cd49c82..bc35cae6c 100644 --- a/src/main/java/kamkeel/npcs/util/BukkitUtil.java +++ b/src/main/java/kamkeel/npcs/util/BukkitUtil.java @@ -95,6 +95,7 @@ public class BukkitUtil { /** * Checks if a specific Bukkit plugin is enabled. + * * @param pluginName the plugin name to check * @return true if the plugin is enabled */ @@ -112,6 +113,7 @@ public class BukkitUtil { /** * Gets a Bukkit OfflinePlayer by name. + * * @param playerName the player name * @return the OfflinePlayer object, or null if not available */ @@ -128,6 +130,7 @@ public class BukkitUtil { /** * Gets a Bukkit Player by name (online players only). + * * @param playerName the player name * @return the Player object, or null if not online or not available */ @@ -144,6 +147,7 @@ public class BukkitUtil { /** * Gets a service provider from Bukkit's ServicesManager. + * * @param serviceClass the service class to get * @return the service provider, or null if not available */ diff --git a/src/main/java/kamkeel/npcs/util/VaultUtil.java b/src/main/java/kamkeel/npcs/util/VaultUtil.java index c7ef92e97..64743ffba 100644 --- a/src/main/java/kamkeel/npcs/util/VaultUtil.java +++ b/src/main/java/kamkeel/npcs/util/VaultUtil.java @@ -174,6 +174,7 @@ public class VaultUtil { /** * Checks if a player has an account in the economy system. + * * @param player the player to check * @return true if the player has an account */ @@ -183,6 +184,7 @@ public class VaultUtil { /** * Checks if a player has an account in the economy system. + * * @param playerName the player name to check * @return true if the player has an account */ @@ -203,6 +205,7 @@ public class VaultUtil { /** * Gets the balance of a player. + * * @param player the player * @return the player's balance, or 0 if not available */ @@ -212,6 +215,7 @@ public class VaultUtil { /** * Gets the balance of a player by name. + * * @param playerName the player name * @return the player's balance, or 0 if not available */ @@ -232,6 +236,7 @@ public class VaultUtil { /** * Checks if a player has at least the specified amount. + * * @param player the player * @param amount the amount to check * @return true if the player has at least the specified amount @@ -242,8 +247,9 @@ public class VaultUtil { /** * Checks if a player has at least the specified amount. + * * @param playerName the player name - * @param amount the amount to check + * @param amount the amount to check * @return true if the player has at least the specified amount */ public static boolean has(String playerName, double amount) { @@ -263,6 +269,7 @@ public class VaultUtil { /** * Withdraws money from a player's account. + * * @param player the player * @param amount the amount to withdraw * @return true if the transaction was successful @@ -273,8 +280,9 @@ public class VaultUtil { /** * Withdraws money from a player's account. + * * @param playerName the player name - * @param amount the amount to withdraw + * @param amount the amount to withdraw * @return true if the transaction was successful */ public static boolean withdrawMoney(String playerName, double amount) { @@ -297,6 +305,7 @@ public class VaultUtil { /** * Adds money to a player's account (deposit). + * * @param player the player * @param amount the amount to add * @return true if the transaction was successful @@ -307,8 +316,9 @@ public class VaultUtil { /** * Adds money to a player's account (deposit). + * * @param playerName the player name - * @param amount the amount to add + * @param amount the amount to add * @return true if the transaction was successful */ public static boolean addMoney(String playerName, double amount) { @@ -332,6 +342,7 @@ public class VaultUtil { /** * Sets a player's balance to the specified amount. * This withdraws all money first, then deposits the new amount. + * * @param player the player * @param amount the new balance * @return true if the transaction was successful @@ -343,8 +354,9 @@ public class VaultUtil { /** * Sets a player's balance to the specified amount. * This withdraws all money first, then deposits the new amount. + * * @param playerName the player name - * @param amount the new balance + * @param amount the new balance * @return true if the transaction was successful */ public static boolean setBalance(String playerName, double amount) { @@ -364,6 +376,7 @@ public class VaultUtil { /** * Formats an amount according to the economy plugin's settings. + * * @param amount the amount to format * @return the formatted string, or the raw number as string if not available */ @@ -380,6 +393,7 @@ public class VaultUtil { /** * Gets the plural currency name (e.g., "Dollars"). + * * @return the plural currency name, or "coins" if not available */ public static String getCurrencyNamePlural() { @@ -395,6 +409,7 @@ public class VaultUtil { /** * Gets the singular currency name (e.g., "Dollar"). + * * @return the singular currency name, or "coin" if not available */ public static String getCurrencyNameSingular() { @@ -410,6 +425,7 @@ public class VaultUtil { /** * Creates an account for the player if one doesn't exist. + * * @param player the player * @return true if the account was created or already exists */ @@ -419,6 +435,7 @@ public class VaultUtil { /** * Creates an account for the player if one doesn't exist. + * * @param playerName the player name * @return true if the account was created or already exists */ @@ -455,6 +472,7 @@ public class VaultUtil { /** * Gets a list of all bank names. + * * @return list of bank names, or empty list if not available */ @SuppressWarnings("unchecked") @@ -471,6 +489,7 @@ public class VaultUtil { /** * Gets the balance of a bank. + * * @param bankName the bank name * @return the bank balance, or 0 if not available */ @@ -488,8 +507,9 @@ public class VaultUtil { /** * Checks if a bank has at least the specified amount. + * * @param bankName the bank name - * @param amount the amount to check + * @param amount the amount to check * @return true if the bank has at least the specified amount */ public static boolean bankHas(String bankName, double amount) { @@ -506,8 +526,9 @@ public class VaultUtil { /** * Withdraws money from a bank. + * * @param bankName the bank name - * @param amount the amount to withdraw + * @param amount the amount to withdraw * @return true if the transaction was successful */ public static boolean bankWithdraw(String bankName, double amount) { @@ -524,8 +545,9 @@ public class VaultUtil { /** * Deposits money into a bank. + * * @param bankName the bank name - * @param amount the amount to deposit + * @param amount the amount to deposit * @return true if the transaction was successful */ public static boolean bankDeposit(String bankName, double amount) { @@ -542,8 +564,9 @@ public class VaultUtil { /** * Transfers money from one player to another. - * @param from the player to take money from - * @param to the player to give money to + * + * @param from the player to take money from + * @param to the player to give money to * @param amount the amount to transfer * @return true if the transfer was successful */ @@ -553,9 +576,10 @@ public class VaultUtil { /** * Transfers money from one player to another. + * * @param fromPlayerName the player name to take money from - * @param toPlayerName the player name to give money to - * @param amount the amount to transfer + * @param toPlayerName the player name to give money to + * @param amount the amount to transfer * @return true if the transfer was successful */ public static boolean transfer(String fromPlayerName, String toPlayerName, double amount) { diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 604ba26a0..c5b82d72a 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -4,7 +4,13 @@ import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.Mod; import cpw.mods.fml.common.Mod.EventHandler; import cpw.mods.fml.common.SidedProxy; -import cpw.mods.fml.common.event.*; +import cpw.mods.fml.common.event.FMLInitializationEvent; +import cpw.mods.fml.common.event.FMLLoadCompleteEvent; +import cpw.mods.fml.common.event.FMLPreInitializationEvent; +import cpw.mods.fml.common.event.FMLServerAboutToStartEvent; +import cpw.mods.fml.common.event.FMLServerStartedEvent; +import cpw.mods.fml.common.event.FMLServerStartingEvent; +import cpw.mods.fml.common.event.FMLServerStoppedEvent; import cpw.mods.fml.common.network.NetworkRegistry; import cpw.mods.fml.common.registry.EntityRegistry; import cpw.mods.fml.relauncher.Side; @@ -18,6 +24,7 @@ import kamkeel.npcs.command.profile.CommandProfile; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.profile.CNPCData; import kamkeel.npcs.developer.Developer; import kamkeel.npcs.network.PacketHandler; @@ -68,7 +75,6 @@ import noppes.npcs.controllers.SpawnController; import noppes.npcs.controllers.TagController; import noppes.npcs.controllers.TransportController; import noppes.npcs.enchants.EnchantInterface; -import kamkeel.npcs.controllers.data.ability.AbilityController; import noppes.npcs.entity.EntityChairMount; import noppes.npcs.entity.EntityCustomNpc; import noppes.npcs.entity.EntityMagicProjectile; diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index d9655b20a..3bed6075f 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -1,8 +1,8 @@ package noppes.npcs; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityPhase; import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.ability.AbilityPhase; import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket; import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket; @@ -35,32 +35,48 @@ public class DataAbilities { // CONFIGURATION (saved to NBT) // ═══════════════════════════════════════════════════════════════════ - /** List of abilities this NPC can use */ + /** + * List of abilities this NPC can use + */ private List abilities = new ArrayList<>(); - /** Whether the ability system is enabled for this NPC */ + /** + * Whether the ability system is enabled for this NPC + */ public boolean enabled = false; - /** Minimum ticks between ability selections (global cooldown) */ + /** + * Minimum ticks between ability selections (global cooldown) + */ public int globalCooldown = 20; // ═══════════════════════════════════════════════════════════════════ // RUNTIME STATE (not saved) // ═══════════════════════════════════════════════════════════════════ - /** Currently executing ability (null if none) */ + /** + * Currently executing ability (null if none) + */ private transient Ability currentAbility; - /** Per-ability cooldown timers (ability ID -> world time when cooldown ends) */ + /** + * Per-ability cooldown timers (ability ID -> world time when cooldown ends) + */ private transient Map cooldowns = new HashMap<>(); - /** Ticks until next ability can be selected (global cooldown timer) */ + /** + * Ticks until next ability can be selected (global cooldown timer) + */ private transient int globalCooldownTimer = 0; - /** Last target used for ability execution */ + /** + * Last target used for ability execution + */ private transient EntityLivingBase lastTarget; - /** Recent hit timestamps for hit count condition */ + /** + * Recent hit timestamps for hit count condition + */ private transient List recentHitTimes = new ArrayList<>(); // ═══════════════════════════════════════════════════════════════════ @@ -532,11 +548,11 @@ public class DataAbilities { * Fire an ability hit event. Called by abilities when they hit an entity. * Returns null if the event was cancelled, otherwise returns the (possibly modified) event. * - * @param ability The ability doing the hit - * @param target The original target of the ability - * @param hitEntity The entity being hit - * @param damage The damage amount - * @param knockback The horizontal knockback + * @param ability The ability doing the hit + * @param target The original target of the ability + * @param hitEntity The entity being hit + * @param damage The damage amount + * @param knockback The horizontal knockback * @param knockbackUp The vertical knockback (deprecated, ignored) * @return The event (with possibly modified values), or null if cancelled */ @@ -636,7 +652,7 @@ public class DataAbilities { /** * Check if ability is controlling NPC movement (AI pathfinding should be blocked). * Called by AI tasks to determine if they should try to path. - * + *

* Rules: * - WINDUP phase: Block AI if lockMovement is true * - ACTIVE phase: Block AI if lockMovement OR hasAbilityMovement is true @@ -672,6 +688,7 @@ public class DataAbilities { /** * Check if NPC movement should be blocked due to ability execution. + * * @deprecated Use isAbilityControllingMovement() instead */ @Deprecated diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index f72bbdab7..5e086abce 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -38,7 +38,6 @@ import noppes.npcs.controllers.CustomGuiController; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.Dialog; -import noppes.npcs.controllers.data.EffectScript; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptBlockHandler; import noppes.npcs.controllers.data.IScriptHandler; @@ -265,7 +264,7 @@ public class EventHooks { noppes.npcs.scripted.event.NpcEvent.InitEvent event = new noppes.npcs.scripted.event.NpcEvent.InitEvent(npc.wrappedNPC); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.INIT, event); npc.script.callScript(EnumScriptType.INIT, event); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); npc.advanced.soulStoneInit = false; } @@ -276,7 +275,7 @@ public class EventHooks { noppes.npcs.scripted.event.NpcEvent.UpdateEvent event = new noppes.npcs.scripted.event.NpcEvent.UpdateEvent(npc.wrappedNPC); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.TICK, event); npc.script.callScript(EnumScriptType.TICK, event); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCDialog(EntityNPCInterface npc, EntityPlayer player, int dialogId, int optionId, Dialog dialog) { @@ -297,7 +296,7 @@ public class EventHooks { NpcEvent.DialogClosedEvent event = new NpcEvent.DialogClosedEvent(npc.wrappedNPC, player, dialogId, optionId, dialog); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.DIALOG_CLOSE, event); npc.script.callScript(EnumScriptType.DIALOG_CLOSE, event, "player", event.getPlayer(), "dialog", event.getDialogId(), "option", event.getOptionId(), "dialogObj", event.getDialog()); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCInteract(EntityNPCInterface npc, EntityPlayer player) { @@ -307,7 +306,7 @@ public class EventHooks { NpcEvent.InteractEvent event = new NpcEvent.InteractEvent(npc.wrappedNPC, player); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.INTERACT, event); boolean result = npc.script.callScript(EnumScriptType.INTERACT, event, "player", player); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); return result; } @@ -318,7 +317,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ATTACK_MELEE, event); npc.script.callScript(EnumScriptType.ATTACK_MELEE, event, "target", event.target); npc.script.callScript(EnumScriptType.ATTACK, event, "target", event.target); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCMeleeSwing(EntityNPCInterface npc, NpcEvent.SwingEvent event) { @@ -327,7 +326,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.ATTACK_SWING, event); npc.script.callScript(EnumScriptType.ATTACK_SWING, event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCRangedAttack(EntityNPCInterface npc, NpcEvent.RangedLaunchedEvent event) { @@ -337,7 +336,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.RANGED_LAUNCHED, event); npc.script.callScript(EnumScriptType.RANGED_LAUNCHED, event, "target", event.target); npc.script.callScript(EnumScriptType.ATTACK, event, "target", event.target); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static void onNPCKilledEntity(EntityNPCInterface npc, EntityLivingBase entity) { @@ -347,7 +346,7 @@ public class EventHooks { NpcEvent.KilledEntityEvent event = new NpcEvent.KilledEntityEvent(npc.wrappedNPC, entity); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.KILLS, event); npc.script.callScript(EnumScriptType.KILLS, event, "target", entity); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCTarget(EntityNPCInterface npc, NpcEvent.TargetEvent event) { @@ -356,7 +355,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.TARGET, event); npc.script.callScript(EnumScriptType.TARGET, event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCTargetLost(EntityNPCInterface npc, EntityLivingBase prevtarget, EntityLivingBase newTarget) { @@ -366,7 +365,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.TARGET_LOST, event); npc.script.callScript(EnumScriptType.TARGET_LOST, event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static void onNPCCollide(EntityNPCInterface npc, Entity entity) { @@ -376,7 +375,7 @@ public class EventHooks { NpcEvent.CollideEvent event = new NpcEvent.CollideEvent(npc.wrappedNPC, entity); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.COLLIDE, event); npc.script.callScript(EnumScriptType.COLLIDE, event, "entity", entity); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCDamaged(EntityNPCInterface npc, NpcEvent.DamagedEvent event) { @@ -385,7 +384,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.DAMAGED, event); npc.script.callScript(EnumScriptType.DAMAGED, event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static boolean onNPCKilled(EntityNPCInterface npc, NpcEvent.DiedEvent event) { @@ -394,7 +393,7 @@ public class EventHooks { ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.KILLED, event); npc.script.callScript(EnumScriptType.KILLED, event); - return NpcAPI.EVENT_BUS.post(event); + return NpcAPI.EVENT_BUS.post(event); } public static void onNPCTimer(EntityNPCInterface npc, int id) { @@ -404,7 +403,7 @@ public class EventHooks { NpcEvent.TimerEvent event = new NpcEvent.TimerEvent(npc.wrappedNPC, id); ScriptController.Instance.globalNpcScripts.callScript(EnumScriptType.TIMER, event); npc.script.callScript(EnumScriptType.TIMER, event); - NpcAPI.EVENT_BUS.post(event); + NpcAPI.EVENT_BUS.post(event); } public static void onProjectileTick(EntityProjectile projectile) { @@ -422,7 +421,7 @@ public class EventHooks { EntityNPCInterface npc = (EntityNPCInterface) thrower; npc.script.callScript(EnumScriptType.PROJECTILE_TICK, event); - } + } } NpcAPI.EVENT_BUS.post(event); @@ -442,7 +441,7 @@ public class EventHooks { EntityNPCInterface npc = (EntityNPCInterface) thrower; npc.script.callScript(EnumScriptType.PROJECTILE_IMPACT, event); - } + } } NpcAPI.EVENT_BUS.post(event); diff --git a/src/main/java/noppes/npcs/NBTTags.java b/src/main/java/noppes/npcs/NBTTags.java index fff8d1c5d..6eec4c14d 100644 --- a/src/main/java/noppes/npcs/NBTTags.java +++ b/src/main/java/noppes/npcs/NBTTags.java @@ -6,7 +6,6 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagDouble; import net.minecraft.nbt.NBTTagInt; import net.minecraft.nbt.NBTTagList; -import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.IScriptUnit; diff --git a/src/main/java/noppes/npcs/NoppesStringUtils.java b/src/main/java/noppes/npcs/NoppesStringUtils.java index cd7031ef0..ddc11d5c6 100644 --- a/src/main/java/noppes/npcs/NoppesStringUtils.java +++ b/src/main/java/noppes/npcs/NoppesStringUtils.java @@ -4,7 +4,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.util.StatCollector; import noppes.npcs.entity.EntityNPCInterface; -import java.awt.*; +import java.awt.Toolkit; import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.ClipboardOwner; import java.awt.datatransfer.DataFlavor; diff --git a/src/main/java/noppes/npcs/ScriptForgeEventHandler.java b/src/main/java/noppes/npcs/ScriptForgeEventHandler.java index 4294a6d9e..abc5a80ee 100644 --- a/src/main/java/noppes/npcs/ScriptForgeEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptForgeEventHandler.java @@ -36,7 +36,7 @@ public class ScriptForgeEventHandler { @SubscribeEvent public void forgeEntity(Event event) { if (CustomNpcs.getServer() != null && ScriptController.Instance.forgeScripts.isEnabled()) { - if(FMLCommonHandler.instance().getEffectiveSide() == Side.CLIENT){ + if (FMLCommonHandler.instance().getEffectiveSide() == Side.CLIENT) { return; } diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index e9531296a..a37e6ba08 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -62,7 +62,6 @@ import noppes.npcs.items.ItemNpcTool; import noppes.npcs.quests.QuestItem; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.item.ScriptCustomizableItem; -import noppes.npcs.scripted.item.ScriptItemStack; import static noppes.npcs.config.ConfigMain.TrackedQuestUpdateFrequency; @@ -549,7 +548,7 @@ public class ScriptPlayerEventHandler { if (event.source.getEntity() instanceof EntityPlayerMP) { EntityPlayerMP playerMP = (EntityPlayerMP) event.source.getEntity(); - if(playerMP.getHeldItem() != null) { + if (playerMP.getHeldItem() != null) { IItemStack itemStack = NpcAPI.Instance().getIItemStack(playerMP.getHeldItem()); if (itemStack instanceof ScriptCustomizableItem) { ScriptCustomizableItem sci = (ScriptCustomizableItem) itemStack; diff --git a/src/main/java/noppes/npcs/ServerEventsHandler.java b/src/main/java/noppes/npcs/ServerEventsHandler.java index 47a1148df..b6671b72a 100644 --- a/src/main/java/noppes/npcs/ServerEventsHandler.java +++ b/src/main/java/noppes/npcs/ServerEventsHandler.java @@ -51,7 +51,6 @@ import noppes.npcs.controllers.ServerCloneController; import noppes.npcs.controllers.data.Animation; import noppes.npcs.controllers.data.AnimationData; import noppes.npcs.controllers.data.Line; -import noppes.npcs.controllers.data.LinkedItem; import noppes.npcs.controllers.data.MarkData; import noppes.npcs.controllers.data.Party; import noppes.npcs.controllers.data.PlayerData; @@ -60,7 +59,6 @@ import noppes.npcs.controllers.data.Quest; import noppes.npcs.controllers.data.QuestData; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.items.ItemExcalibur; -import noppes.npcs.items.ItemLinked; import noppes.npcs.items.ItemShield; import noppes.npcs.items.ItemSoulstoneEmpty; import noppes.npcs.quests.QuestKill; diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index eb5018871..93acda2ea 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -10,8 +10,8 @@ import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.world.WorldServer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.PlayerData; -import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.controllers.data.action.ActionManager; +import noppes.npcs.entity.EntityNPCInterface; public class ServerTickHandler { diff --git a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java index b9558dd93..64dbb767b 100644 --- a/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java +++ b/src/main/java/noppes/npcs/blocks/tiles/TileScripted.java @@ -28,8 +28,8 @@ import noppes.npcs.client.renderer.blocks.BlockScriptedRenderer; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptController; -import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.controllers.data.IScriptBlockHandler; +import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.entity.data.DataTimers; import noppes.npcs.scripted.BlockScriptedWrapper; @@ -39,8 +39,6 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; public class TileScripted extends TileEntity implements IScriptBlockHandler, IScriptHandlerPacket { public List scripts = new ArrayList<>(); @@ -449,7 +447,7 @@ public class TileScripted extends TileEntity implements IScriptBlockHandler, ISc public List getScripts() { return scripts; } - + @SideOnly(Side.CLIENT) @Override diff --git a/src/main/java/noppes/npcs/client/ClientCacheHandler.java b/src/main/java/noppes/npcs/client/ClientCacheHandler.java index 2c6a368c2..30b14be83 100644 --- a/src/main/java/noppes/npcs/client/ClientCacheHandler.java +++ b/src/main/java/noppes/npcs/client/ClientCacheHandler.java @@ -1,5 +1,6 @@ package noppes.npcs.client; +import kamkeel.npcs.network.enums.EnumSyncType; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.ResourceLocation; import noppes.npcs.client.controllers.MusicController; @@ -15,7 +16,6 @@ import noppes.npcs.controllers.data.AnimationData; import noppes.npcs.controllers.data.Party; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.controllers.data.SkinOverlay; -import kamkeel.npcs.network.enums.EnumSyncType; import noppes.npcs.util.CacheHashMap; import java.util.EnumMap; diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 249836b80..656efc97b 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -6,6 +6,8 @@ import cpw.mods.fml.client.registry.RenderingRegistry; import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.ObfuscationReflectionHelper; import kamkeel.npcs.addon.client.DBCClient; +import kamkeel.npcs.client.renderer.TelegraphRenderer; +import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.model.ModelBiped; @@ -138,9 +140,6 @@ import noppes.npcs.client.renderer.RenderNpcCrystal; import noppes.npcs.client.renderer.RenderNpcDragon; import noppes.npcs.client.renderer.RenderNpcSlime; import noppes.npcs.client.renderer.RenderProjectile; -import kamkeel.npcs.client.renderer.TelegraphRenderer; -import noppes.npcs.scripted.item.ScriptCustomItem; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; import noppes.npcs.client.renderer.blocks.BlockBannerRenderer; import noppes.npcs.client.renderer.blocks.BlockBarrelRenderer; import noppes.npcs.client.renderer.blocks.BlockBeamRenderer; @@ -214,6 +213,7 @@ import noppes.npcs.entity.data.ModelPartData; import noppes.npcs.items.ItemLinked; import noppes.npcs.items.ItemNpcTool; import noppes.npcs.items.ItemScripted; +import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.input.Keyboard; import tconstruct.client.tabs.InventoryTabCustomNpc; import tconstruct.client.tabs.InventoryTabVanilla; @@ -635,7 +635,7 @@ public class ClientProxy extends CommonProxy { return GuiScriptInterface.create(null, (TileScripted) tile); } - + @Override public void spawnParticle(EntityLivingBase player, String string, Object... ob) { diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index 7c5ec98f2..78a473f41 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -5,11 +5,12 @@ import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.InputEvent; import cpw.mods.fml.common.gameevent.TickEvent; import cpw.mods.fml.common.gameevent.TickEvent.Phase; +import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.InputDevicePacket; -import kamkeel.npcs.network.packets.player.SpecialKeyStatePacket; import kamkeel.npcs.network.packets.player.ScreenSizePacket; +import kamkeel.npcs.network.packets.player.SpecialKeyStatePacket; import net.minecraft.client.Minecraft; import net.minecraft.entity.Entity; import net.minecraft.entity.player.EntityPlayer; @@ -18,7 +19,6 @@ import net.minecraft.world.World; import noppes.npcs.CustomNpcs; import noppes.npcs.client.controllers.MusicController; import noppes.npcs.client.controllers.ScriptSoundController; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; import noppes.npcs.client.gui.hud.ClientHudManager; import noppes.npcs.client.gui.hud.CompassHudComponent; import noppes.npcs.client.gui.hud.EnumHudComponent; diff --git a/src/main/java/noppes/npcs/client/CustomNpcResourceListener.java b/src/main/java/noppes/npcs/client/CustomNpcResourceListener.java index 132805af1..2f2bc3115 100644 --- a/src/main/java/noppes/npcs/client/CustomNpcResourceListener.java +++ b/src/main/java/noppes/npcs/client/CustomNpcResourceListener.java @@ -11,7 +11,7 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; import javax.imageio.ImageIO; -import java.awt.*; +import java.awt.Graphics; import java.awt.image.BufferedImage; import java.io.File; import java.io.InputStream; diff --git a/src/main/java/noppes/npcs/client/controllers/MusicController.java b/src/main/java/noppes/npcs/client/controllers/MusicController.java index 98c30f2ac..172152f51 100644 --- a/src/main/java/noppes/npcs/client/controllers/MusicController.java +++ b/src/main/java/noppes/npcs/client/controllers/MusicController.java @@ -199,6 +199,7 @@ public class MusicController { /** * Stops a specific sound by its resource key. * This is used to stop dialog sounds without affecting Bard music. + * * @param soundKey The sound resource key to stop */ public void stopSound(String soundKey) { diff --git a/src/main/java/noppes/npcs/client/gui/GuiScript.java b/src/main/java/noppes/npcs/client/gui/GuiScript.java index 137ccf5a1..8d960e142 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScript.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScript.java @@ -5,34 +5,41 @@ import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiConfirmOpenLink; import net.minecraft.client.gui.GuiYesNo; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.nbt.NBTTagList; import net.minecraft.server.MinecraftServer; import net.minecraft.util.StatCollector; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.NoppesUtil; import noppes.npcs.client.gui.script.GuiScriptInterface; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiMenuTopButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextArea; +import noppes.npcs.client.gui.util.GuiScriptTextArea; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.DataScript; import noppes.npcs.entity.EntityNPCInterface; -import java.util.*; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Map; /** * NPC Script GUI - extends GuiScriptInterface but with unique behavior: * - Uses scroll-based tab selection instead of top buttons - * - Shows settings/script toggle + * - Shows settings/script toggle * - Uses DataScript with getNPCScript(index) access pattern * - Has console filtering by hook type */ public class GuiScript extends GuiScriptInterface { public boolean showScript = false; public DataScript script; - + private static int activeConsole = 0; - + public GuiScript(EntityNPCInterface npc) { super(); this.npc = npc; @@ -40,7 +47,7 @@ public class GuiScript extends GuiScriptInterface { this.handler = script; this.useScrollTabs = true; this.useSettingsToggle = true; - + // Initialize hook list for NPC scripts hookList.add("script.init"); hookList.add("script.update"); @@ -57,20 +64,20 @@ public class GuiScript extends GuiScriptInterface { hookList.add("script.targetLost"); hookList.add("script.projectileTick"); hookList.add("script.projectileImpact"); - + NPCScriptPacket.Get(); } - + @Override protected ScriptContext getScriptContext() { return ScriptContext.NPC; } - + @Override protected int getActiveScriptIndex() { return this.activeTab; } - + @Override protected ScriptContainer getCurrentContainer() { return script.getNPCScript(activeTab); @@ -99,13 +106,13 @@ public class GuiScript extends GuiScriptInterface { initSettingsView(); } } - + private void initScriptView() { ScriptContainer container = getCurrentContainer(); - + // ==================== CALCULATE VIEWPORT BOUNDS ==================== int editorX, editorY, editorWidth, editorHeight; - + if (isFullscreen) { // Fullscreen: viewport fills screen with configured padding FullscreenConfig.paddingTop = 30; @@ -123,11 +130,11 @@ public class GuiScript extends GuiScriptInterface { editorWidth = 239; editorHeight = 208; } - + // ==================== HOOKS SCROLL (hidden in fullscreen) ==================== if (!isFullscreen) { addLabel(new GuiNpcLabel(0, "script.hooks", guiLeft + 4, guiTop + 5)); - + GuiCustomScroll hooks = new GuiCustomScroll(this, 1); hooks.setSize(68, 198); hooks.guiLeft = guiLeft + 4; @@ -142,28 +149,28 @@ public class GuiScript extends GuiScriptInterface { GuiScriptTextArea activeArea = getActiveScriptArea(); if (activeArea == null) { activeArea = new GuiScriptTextArea(this, 2, editorX, editorY, editorWidth, editorHeight, - container == null ? "" : container.script); + container == null ? "" : container.script); activeArea.setListener(this); this.closeOnEsc(activeArea::closeOnEsc); textAreas.put(idx, activeArea); } else { - activeArea.init(editorX, editorY, editorWidth, editorHeight, - container == null ? "" : container.script); + activeArea.init(editorX, editorY, editorWidth, editorHeight, + container == null ? "" : container.script); } activeArea.setLanguage(script.getLanguage()); activeArea.setScriptContext(getScriptContext()); - + // Setup fullscreen key binding GuiScriptTextArea.KEYS.FULLSCREEN.setTask(e -> { if (e.isPress()) { toggleFullscreen(); } }); - + activeArea.enableCodeHighlighting(); addTextField(activeArea); - + // Initialize fullscreen button int scrollbarOffset = activeArea.hasVerticalScrollbar() ? -8 : -2; fullscreenButton.initGui(editorX + editorWidth, editorY, scrollbarOffset); @@ -184,35 +191,35 @@ public class GuiScript extends GuiScriptInterface { addScroll(scroll); } } - - + + private void initSettingsView() { addLabel(new GuiNpcLabel(0, "script.console", guiLeft + 4, guiTop + 16)); if (getTopButton(14) != null) getTopButton(14).active = true; - + GuiNpcTextArea consoleArea = new GuiNpcTextArea(2, this, guiLeft + 4, guiTop + 26, 226, 186, getConsoleText()); consoleArea.canEdit = false; addTextField(consoleArea); - + addButton(new GuiNpcButton(100, guiLeft + 232, guiTop + 170, 56, 20, "gui.copy")); addButton(new GuiNpcButton(102, guiLeft + 232, guiTop + 192, 56, 20, "gui.clear")); List consoleOptions = new ArrayList<>(); consoleOptions.add("All"); consoleOptions.addAll(hookList); - addButton(new GuiNpcButton(105, guiLeft + 60, guiTop + 4, 80, 20, - consoleOptions.toArray(new String[0]), activeConsole)); + addButton(new GuiNpcButton(105, guiLeft + 60, guiTop + 4, 80, 20, + consoleOptions.toArray(new String[0]), activeConsole)); addLabel(new GuiNpcLabel(1, "script.language", guiLeft + 232, guiTop + 30)); List languageOptions = getLanguageOptions(); - addButton(new GuiNpcButton(103, guiLeft + 294, guiTop + 25, 80, 20, - languageOptions.toArray(new String[0]), getLanguageIndex(languageOptions))); + addButton(new GuiNpcButton(103, guiLeft + 294, guiTop + 25, 80, 20, + languageOptions.toArray(new String[0]), getLanguageIndex(languageOptions))); getButton(103).enabled = languageOptions.size() > 0; addLabel(new GuiNpcLabel(2, "gui.enabled", guiLeft + 232, guiTop + 53)); - addButton(new GuiNpcButton(104, guiLeft + 294, guiTop + 48, 50, 20, - new String[]{"gui.no", "gui.yes"}, script.enabled ? 1 : 0)); + addButton(new GuiNpcButton(104, guiLeft + 294, guiTop + 48, 50, 20, + new String[]{"gui.no", "gui.yes"}, script.enabled ? 1 : 0)); if (MinecraftServer.getServer() != null) addButton(new GuiNpcButton(106, guiLeft + 232, guiTop + 71, 150, 20, "script.openfolder")); @@ -280,12 +287,12 @@ public class GuiScript extends GuiScriptInterface { NoppesStringUtils.setClipboardContents(getTextField(2).getText()); } if (guibutton.id == 101) { - displayGuiScreen(new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), - StatCollector.translateToLocal("gui.sure"), 101)); + displayGuiScreen(new GuiYesNo(this, StatCollector.translateToLocal("gui.paste"), + StatCollector.translateToLocal("gui.sure"), 101)); } if (guibutton.id == 102) { - displayGuiScreen(new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), - StatCollector.translateToLocal("gui.sure"), 102)); + displayGuiScreen(new GuiYesNo(this, StatCollector.translateToLocal("gui.clear"), + StatCollector.translateToLocal("gui.sure"), 102)); } if (guibutton.id == 103) { script.scriptLanguage = ((GuiNpcButton) guibutton).displayString; diff --git a/src/main/java/noppes/npcs/client/gui/GuiScriptList.java b/src/main/java/noppes/npcs/client/gui/GuiScriptList.java index 547f75f83..a13f2a056 100644 --- a/src/main/java/noppes/npcs/client/gui/GuiScriptList.java +++ b/src/main/java/noppes/npcs/client/gui/GuiScriptList.java @@ -20,7 +20,7 @@ public class GuiScriptList extends SubGuiInterface { public GuiScriptList(List scripts, ScriptContainer container) { this(scripts, (IScriptUnit) container); } - + public GuiScriptList(List scripts, IScriptUnit scriptUnit) { this.scriptUnit = scriptUnit; setBackground("menubg.png"); diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java b/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java index db1b516ea..cfde598de 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiEffectGeneral.java @@ -8,7 +8,6 @@ import net.minecraft.util.ResourceLocation; import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.client.gui.global.GuiNPCManageEffects; import noppes.npcs.client.gui.script.GuiScriptInterface; -import noppes.npcs.controllers.data.EffectScript; import noppes.npcs.client.gui.util.GuiMenuTopButton; import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -18,6 +17,7 @@ import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.client.renderer.ImageData; import noppes.npcs.controllers.data.CustomEffect; +import noppes.npcs.controllers.data.EffectScript; import org.lwjgl.opengl.GL11; import java.util.ArrayList; diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java b/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java index 9025023b1..c3d85c8ad 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiPlayerData.java @@ -36,7 +36,7 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Fields - //////////////////////////////////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////////////////////////////// // Current tab (10=Quest, 11=Dialog, 12=Transport, 13=Bank, 14=Faction, 15=Magic) private int currentTab = 10; @@ -99,7 +99,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Constructor - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// public SubGuiPlayerData(String playerName) { this.playerName = playerName; xSize = 420; @@ -112,7 +113,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Layout Helpers - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// private int getPaddedLeft() { return guiLeft + leftPadding; } @@ -135,7 +137,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Component Setup Helpers - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// private void setupTopButtons() { // Show General Display Information GuiMenuTopButton playerTab = new GuiMenuTopButton(-10, guiLeft + 4, guiTop - 10, playerName); @@ -192,7 +195,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // initGui() & Tab Initialization - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void initGui() { super.initGui(); @@ -489,7 +493,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Drawing Helpers - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void drawScreen(int mouseX, int mouseY, float partialTicks) { super.drawScreen(mouseX, mouseY, partialTicks); @@ -525,7 +530,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Mouse & Resizing Handlers - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void mouseClicked(int mouseX, int mouseY, int mouseButton) { int hitMargin = 3; @@ -672,7 +678,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Key & Search Field Handling - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void keyTyped(char c, int i) { super.keyTyped(c, i); @@ -894,7 +901,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Action Handling - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void actionPerformed(GuiButton button) { if (button.id == -5) { @@ -1043,7 +1051,8 @@ public class SubGuiPlayerData extends SubGuiInterface implements IPlayerDataInfo //////////////////////////////////////////////////////////////////////////////// // Data Setting Methods (IPlayerDataInfo) - //////////////////////////////////////////////////////////////////////////////// + + /// ///////////////////////////////////////////////////////////////////////////// @Override public void setQuestData(Map questCategories, Map questActive, Map questFinished) { this.questCatData = new HashMap<>(questCategories); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java index 584bd8238..0f41347d4 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -10,7 +10,18 @@ import net.minecraft.client.gui.GuiButton; import net.minecraft.client.resources.I18n; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiNPCInterface2; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.client.gui.util.IScrollData; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.constants.EnumScrollData; import noppes.npcs.entity.EntityNPCInterface; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index c88c18095..1146ea4ae 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -10,7 +10,14 @@ import net.minecraft.util.StatCollector; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.select.GuiAnimationSelection; import noppes.npcs.client.gui.select.GuiSoundSelection; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiMenuTopButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; import java.util.ArrayList; import java.util.List; @@ -18,13 +25,13 @@ import java.util.List; /** * Base SubGui for editing ability configuration with tabbed interface. * Uses GuiMenuTopButton for navigation tabs. - * + *

* Tabs: * - General: Name, Weight, Lock Movement, Interruptible, Timing * - Type: Type-specific settings per Ability (override in subclasses) * - Target: Min/Max Range, Targeting Mode, Conditions (up to 3) * - Effects: Sounds, Animations, Telegraph settings, Colors - * + *

* Subclasses should override initTypeTab() and handleTypeButton()/handleTypeTextField() * to provide type-specific settings UI. */ @@ -376,13 +383,13 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi String typeId = cond.getTypeId(); switch (typeId) { case "hp_above": - return StatCollector.translateToLocal("condition.hp_above") + " " + (int)(getConditionThreshold(cond) * 100) + "%"; + return StatCollector.translateToLocal("condition.hp_above") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; case "hp_below": - return StatCollector.translateToLocal("condition.hp_below") + " " + (int)(getConditionThreshold(cond) * 100) + "%"; + return StatCollector.translateToLocal("condition.hp_below") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; case "target_hp_above": - return StatCollector.translateToLocal("condition.target_hp_above") + " " + (int)(getConditionThreshold(cond) * 100) + "%"; + return StatCollector.translateToLocal("condition.target_hp_above") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; case "target_hp_below": - return StatCollector.translateToLocal("condition.target_hp_below") + " " + (int)(getConditionThreshold(cond) * 100) + "%"; + return StatCollector.translateToLocal("condition.target_hp_below") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; case "hit_count": Condition.ConditionHitCount hitCount = (Condition.ConditionHitCount) cond; return StatCollector.translateToLocal("condition.hit_count") + ": " + hitCount.getRequiredHits() + "/" + hitCount.getWithinTicks() + "t"; @@ -566,8 +573,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // General tab buttons else if (id == 14) { interruptible = ((GuiNpcButton) guibutton).getValue() == 1; - } - else if (id == 16) { + } else if (id == 16) { lockMovement = ((GuiNpcButton) guibutton).getValue() == 1; } @@ -619,12 +625,10 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi else if (id == 20) { showTelegraph = ((GuiNpcButton) guibutton).getValue() == 1; initGui(); - } - else if (id == 22) { + } else if (id == 22) { editingColorId = 22; setSubGui(new SubGuiColorSelector(windUpColor)); - } - else if (id == 24) { + } else if (id == 24) { editingColorId = 24; setSubGui(new SubGuiColorSelector(activeColor)); } @@ -633,8 +637,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi else if (id == 30) { editingSoundId = 30; setSubGui(new GuiSoundSelection(windUpSound)); - } - else if (id == 31) { + } else if (id == 31) { editingSoundId = 31; setSubGui(new GuiSoundSelection(activeSound)); } @@ -642,8 +645,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi else if (id == 35) { windUpSound = ""; initGui(); - } - else if (id == 36) { + } else if (id == 36) { activeSound = ""; initGui(); } @@ -651,8 +653,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi else if (id == 32) { editingAnimationId = 32; setSubGui(new GuiAnimationSelection(windUpAnimationId)); - } - else if (id == 33) { + } else if (id == 33) { editingAnimationId = 33; setSubGui(new GuiAnimationSelection(activeAnimationId)); } @@ -661,8 +662,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi windUpAnimationId = -1; windUpAnimationName = null; initGui(); - } - else if (id == 38) { + } else if (id == 38) { activeAnimationId = -1; activeAnimationName = null; initGui(); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java index 069c9f1b4..c48683070 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java @@ -3,7 +3,11 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import net.minecraft.client.gui.GuiButton; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.SubGuiInterface; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java index f0d4f2734..3d2874dbc 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java @@ -2,7 +2,11 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Condition; import net.minecraft.client.gui.GuiButton; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; /** * SubGui for editing a single ability condition. @@ -106,7 +110,7 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi } else { // HP threshold field (as percentage) addLabel(new GuiNpcLabel(2, "condition.percent", labelX, y + 5)); - int percent = (int)(threshold * 100); + int percent = (int) (threshold * 100); GuiNpcTextField thresholdField = new GuiNpcTextField(4, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(percent)); thresholdField.setIntegersOnly(); thresholdField.setMinMaxDefault(1, 99, 50); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java index fb3e6b074..ce56f809e 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityBeam; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Beam ability type-specific settings. @@ -72,21 +72,39 @@ public class SubGuiAbilityBeam extends SubGuiAbilityConfig { protected void handleTypeButton(int id, GuiNpcButton button) { int value = button.getValue(); switch (id) { - case 106: beam.setPiercing(value == 1); break; - case 107: beam.setLockOnTarget(value == 1); break; - case 108: beam.setSweepBackAndForth(value == 1); break; + case 106: + beam.setPiercing(value == 1); + break; + case 107: + beam.setLockOnTarget(value == 1); + break; + case 108: + beam.setSweepBackAndForth(value == 1); + break; } } @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: beam.setDamage(parseFloat(field, beam.getDamage())); break; - case 101: beam.setDamageInterval(field.getInteger()); break; - case 102: beam.setBeamLength(parseFloat(field, beam.getBeamLength())); break; - case 103: beam.setBeamWidth(parseFloat(field, beam.getBeamWidth())); break; - case 104: beam.setSweepAngle(parseFloat(field, beam.getSweepAngle())); break; - case 105: beam.setSweepSpeed(parseFloat(field, beam.getSweepSpeed())); break; + case 100: + beam.setDamage(parseFloat(field, beam.getDamage())); + break; + case 101: + beam.setDamageInterval(field.getInteger()); + break; + case 102: + beam.setBeamLength(parseFloat(field, beam.getBeamLength())); + break; + case 103: + beam.setBeamWidth(parseFloat(field, beam.getBeamWidth())); + break; + case 104: + beam.setSweepAngle(parseFloat(field, beam.getSweepAngle())); + break; + case 105: + beam.setSweepSpeed(parseFloat(field, beam.getSweepSpeed())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java index d3ff5fd81..4d7486c5e 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java @@ -1,10 +1,10 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityCharge; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Charge ability type-specific settings. @@ -50,10 +50,18 @@ public class SubGuiAbilityCharge extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: charge.setDamage(parseFloat(field, charge.getDamage())); break; - case 101: charge.setChargeSpeed(parseFloat(field, charge.getChargeSpeed())); break; - case 102: charge.setKnockback(parseFloat(field, charge.getKnockback())); break; - case 104: charge.setHitWidth(parseFloat(field, charge.getHitWidth())); break; + case 100: + charge.setDamage(parseFloat(field, charge.getDamage())); + break; + case 101: + charge.setChargeSpeed(parseFloat(field, charge.getChargeSpeed())); + break; + case 102: + charge.setKnockback(parseFloat(field, charge.getKnockback())); + break; + case 104: + charge.setHitWidth(parseFloat(field, charge.getHitWidth())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java index d917282f2..514391f8a 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityCutter; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Cutter ability type-specific settings. @@ -82,23 +82,45 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { protected void handleTypeButton(int id, GuiNpcButton button) { int value = button.getValue(); switch (id) { - case 106: cutter.setSweepMode(AbilityCutter.SweepMode.values()[value]); break; - case 107: cutter.setPiercing(value == 1); break; + case 106: + cutter.setSweepMode(AbilityCutter.SweepMode.values()[value]); + break; + case 107: + cutter.setPiercing(value == 1); + break; } } @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: cutter.setDamage(parseFloat(field, cutter.getDamage())); break; - case 101: cutter.setRange(parseFloat(field, cutter.getRange())); break; - case 102: cutter.setArcAngle(parseFloat(field, cutter.getArcAngle())); break; - case 103: cutter.setInnerRadius(parseFloat(field, cutter.getInnerRadius())); break; - case 104: cutter.setKnockback(parseFloat(field, cutter.getKnockback())); break; - case 105: cutter.setSweepSpeed(parseFloat(field, cutter.getSweepSpeed())); break; - case 108: cutter.setStunDuration(field.getInteger()); break; - case 109: cutter.setPoisonDurationSeconds(field.getInteger()); break; - case 110: cutter.setPoisonLevel(field.getInteger()); break; + case 100: + cutter.setDamage(parseFloat(field, cutter.getDamage())); + break; + case 101: + cutter.setRange(parseFloat(field, cutter.getRange())); + break; + case 102: + cutter.setArcAngle(parseFloat(field, cutter.getArcAngle())); + break; + case 103: + cutter.setInnerRadius(parseFloat(field, cutter.getInnerRadius())); + break; + case 104: + cutter.setKnockback(parseFloat(field, cutter.getKnockback())); + break; + case 105: + cutter.setSweepSpeed(parseFloat(field, cutter.getSweepSpeed())); + break; + case 108: + cutter.setStunDuration(field.getInteger()); + break; + case 109: + cutter.setPoisonDurationSeconds(field.getInteger()); + break; + case 110: + cutter.setPoisonLevel(field.getInteger()); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java index 7ae0ea1f5..26bfd6610 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityDash; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Dash ability type-specific settings. @@ -52,8 +52,12 @@ public class SubGuiAbilityDash extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 101: dash.setDashDistance(parseFloat(field, dash.getDashDistance())); break; - case 102: dash.setDashSpeed(parseFloat(field, dash.getDashSpeed())); break; + case 101: + dash.setDashDistance(parseFloat(field, dash.getDashDistance())); + break; + case 102: + dash.setDashSpeed(parseFloat(field, dash.getDashSpeed())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java index 4858445fb..c5240d862 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Guard ability type-specific settings. @@ -86,11 +86,21 @@ public class SubGuiAbilityGuard extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: guard.setDamageReduction(parseFloat(field, guard.getDamageReduction())); break; - case 103: guard.setCounterValue(parseFloat(field, guard.getCounterValue())); break; - case 104: guard.setCounterChance(parseFloat(field, guard.getCounterChance())); break; - case 105: guard.setCounterSound(field.getText()); break; - case 106: guard.setCounterAnimationId(field.getInteger()); break; + case 100: + guard.setDamageReduction(parseFloat(field, guard.getDamageReduction())); + break; + case 103: + guard.setCounterValue(parseFloat(field, guard.getCounterValue())); + break; + case 104: + guard.setCounterChance(parseFloat(field, guard.getCounterChance())); + break; + case 105: + guard.setCounterSound(field.getText()); + break; + case 106: + guard.setCounterAnimationId(field.getInteger()); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java index def67e71d..e44eb6984 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityHazard; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Hazard ability type-specific settings. @@ -77,22 +77,42 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { protected void handleTypeButton(int id, GuiNpcButton button) { int value = button.getValue(); switch (id) { - case 100: hazard.setShape(AbilityHazard.HazardShape.values()[value]); break; - case 101: hazard.setPlacement(AbilityHazard.PlacementMode.values()[value]); break; - case 109: hazard.setAffectsCaster(value == 1); break; + case 100: + hazard.setShape(AbilityHazard.HazardShape.values()[value]); + break; + case 101: + hazard.setPlacement(AbilityHazard.PlacementMode.values()[value]); + break; + case 109: + hazard.setAffectsCaster(value == 1); + break; } } @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 102: hazard.setRadius(parseFloat(field, hazard.getRadius())); break; - case 103: hazard.setInnerRadius(parseFloat(field, hazard.getInnerRadius())); break; - case 104: hazard.setDamagePerTick(parseFloat(field, hazard.getDamagePerTick())); break; - case 105: hazard.setDamageInterval(field.getInteger()); break; - case 106: hazard.setSlownessLevel(field.getInteger()); break; - case 107: hazard.setDebuffDuration(field.getInteger()); break; - case 108: hazard.setPoisonLevel(field.getInteger()); break; + case 102: + hazard.setRadius(parseFloat(field, hazard.getRadius())); + break; + case 103: + hazard.setInnerRadius(parseFloat(field, hazard.getInnerRadius())); + break; + case 104: + hazard.setDamagePerTick(parseFloat(field, hazard.getDamagePerTick())); + break; + case 105: + hazard.setDamageInterval(field.getInteger()); + break; + case 106: + hazard.setSlownessLevel(field.getInteger()); + break; + case 107: + hazard.setDebuffDuration(field.getInteger()); + break; + case 108: + hazard.setPoisonLevel(field.getInteger()); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java index 30ccb739e..8b679fb25 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityHeal; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Heal ability type-specific settings. @@ -57,18 +57,30 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig { protected void handleTypeButton(int id, GuiNpcButton button) { int value = button.getValue(); switch (id) { - case 103: heal.setInstantHeal(value == 1); break; - case 104: heal.setHealSelf(value == 1); break; - case 105: heal.setHealAllies(value == 1); break; + case 103: + heal.setInstantHeal(value == 1); + break; + case 104: + heal.setHealSelf(value == 1); + break; + case 105: + heal.setHealAllies(value == 1); + break; } } @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: heal.setHealAmount(parseFloat(field, heal.getHealAmount())); break; - case 101: heal.setHealPercent(parseFloat(field, heal.getHealPercent())); break; - case 102: heal.setHealRadius(parseFloat(field, heal.getHealRadius())); break; + case 100: + heal.setHealAmount(parseFloat(field, heal.getHealAmount())); + break; + case 101: + heal.setHealPercent(parseFloat(field, heal.getHealPercent())); + break; + case 102: + heal.setHealRadius(parseFloat(field, heal.getHealRadius())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java index 5c0d99c48..ea21127dc 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java @@ -1,10 +1,10 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityHeavyHit; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Heavy Hit ability type-specific settings. @@ -52,11 +52,21 @@ public class SubGuiAbilityHeavyHit extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: heavyHit.setDamage(parseFloat(field, heavyHit.getDamage())); break; - case 101: heavyHit.setPotionDurationSeconds(field.getInteger()); break; - case 102: heavyHit.setKnockback(parseFloat(field, heavyHit.getKnockback())); break; - case 103: heavyHit.setSlownessLevel(field.getInteger()); break; - case 104: heavyHit.setWeaknessLevel(field.getInteger()); break; + case 100: + heavyHit.setDamage(parseFloat(field, heavyHit.getDamage())); + break; + case 101: + heavyHit.setPotionDurationSeconds(field.getInteger()); + break; + case 102: + heavyHit.setKnockback(parseFloat(field, heavyHit.getKnockback())); + break; + case 103: + heavyHit.setSlownessLevel(field.getInteger()); + break; + case 104: + heavyHit.setWeaknessLevel(field.getInteger()); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java index fd75700c0..f94e8d0cf 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Orb ability type-specific settings. @@ -75,22 +75,42 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { protected void handleTypeButton(int id, GuiNpcButton button) { int value = button.getValue(); switch (id) { - case 104: orb.setHoming(value == 1); break; - case 106: orb.setExplosive(value == 1); break; + case 104: + orb.setHoming(value == 1); + break; + case 106: + orb.setExplosive(value == 1); + break; } } @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: orb.setDamage(parseFloat(field, orb.getDamage())); break; - case 101: orb.setOrbSpeed(parseFloat(field, orb.getOrbSpeed())); break; - case 102: orb.setOrbSize(parseFloat(field, orb.getOrbSize())); break; - case 103: orb.setMaxDistance(parseFloat(field, orb.getMaxDistance())); break; - case 105: orb.setHomingStrength(parseFloat(field, orb.getHomingStrength())); break; - case 107: orb.setExplosionRadius(parseFloat(field, orb.getExplosionRadius())); break; - case 108: orb.setKnockback(parseFloat(field, orb.getKnockback())); break; - case 109: orb.setMaxLifetime(field.getInteger()); break; + case 100: + orb.setDamage(parseFloat(field, orb.getDamage())); + break; + case 101: + orb.setOrbSpeed(parseFloat(field, orb.getOrbSpeed())); + break; + case 102: + orb.setOrbSize(parseFloat(field, orb.getOrbSize())); + break; + case 103: + orb.setMaxDistance(parseFloat(field, orb.getMaxDistance())); + break; + case 105: + orb.setHomingStrength(parseFloat(field, orb.getHomingStrength())); + break; + case 107: + orb.setExplosionRadius(parseFloat(field, orb.getExplosionRadius())); + break; + case 108: + orb.setKnockback(parseFloat(field, orb.getKnockback())); + break; + case 109: + orb.setMaxLifetime(field.getInteger()); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java index 88d7bbf00..c9be00e38 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityProjectile; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Projectile ability type-specific settings. @@ -79,19 +79,33 @@ public class SubGuiAbilityProjectile extends SubGuiAbilityConfig { String[] projTypes = {"fireball", "arrow", "magic"}; projectile.setProjectileType(projTypes[value]); break; - case 104: projectile.setExplosive(value == 1); break; - case 106: projectile.setHoming(value == 1); break; + case 104: + projectile.setExplosive(value == 1); + break; + case 106: + projectile.setHoming(value == 1); + break; } } @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: projectile.setDamage(parseFloat(field, projectile.getDamage())); break; - case 101: projectile.setSpeed(parseFloat(field, projectile.getSpeed())); break; - case 102: projectile.setKnockback(parseFloat(field, projectile.getKnockback())); break; - case 105: projectile.setExplosionRadius(parseFloat(field, projectile.getExplosionRadius())); break; - case 107: projectile.setHomingStrength(parseFloat(field, projectile.getHomingStrength())); break; + case 100: + projectile.setDamage(parseFloat(field, projectile.getDamage())); + break; + case 101: + projectile.setSpeed(parseFloat(field, projectile.getSpeed())); + break; + case 102: + projectile.setKnockback(parseFloat(field, projectile.getKnockback())); + break; + case 105: + projectile.setExplosionRadius(parseFloat(field, projectile.getExplosionRadius())); + break; + case 107: + projectile.setHomingStrength(parseFloat(field, projectile.getHomingStrength())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java index 38ab22158..a03a85736 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java @@ -1,10 +1,10 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Shockwave ability type-specific settings. @@ -55,11 +55,21 @@ public class SubGuiAbilityShockwave extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: shockwave.setDamage(parseFloat(field, shockwave.getDamage())); break; - case 101: shockwave.setPushRadius(parseFloat(field, shockwave.getPushRadius())); break; - case 102: shockwave.setPushStrength(parseFloat(field, shockwave.getPushStrength())); break; - case 103: shockwave.setStunDuration(field.getInteger()); break; - case 104: shockwave.setMaxTargets(field.getInteger()); break; + case 100: + shockwave.setDamage(parseFloat(field, shockwave.getDamage())); + break; + case 101: + shockwave.setPushRadius(parseFloat(field, shockwave.getPushRadius())); + break; + case 102: + shockwave.setPushStrength(parseFloat(field, shockwave.getPushStrength())); + break; + case 103: + shockwave.setStunDuration(field.getInteger()); + break; + case 104: + shockwave.setMaxTargets(field.getInteger()); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java index 7f8c7194e..989202037 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java @@ -1,10 +1,10 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilitySlam; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Slam ability type-specific settings. @@ -52,11 +52,21 @@ public class SubGuiAbilitySlam extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: slam.setDamage(parseFloat(field, slam.getDamage())); break; - case 101: slam.setRadius(parseFloat(field, slam.getRadius())); break; - case 102: slam.setKnockbackStrength(parseFloat(field, slam.getKnockbackStrength())); break; - case 103: slam.setLeapSpeed(parseFloat(field, slam.getLeapSpeed())); break; - case 104: slam.setLeapHeight(parseFloat(field, slam.getLeapHeight())); break; + case 100: + slam.setDamage(parseFloat(field, slam.getDamage())); + break; + case 101: + slam.setRadius(parseFloat(field, slam.getRadius())); + break; + case 102: + slam.setKnockbackStrength(parseFloat(field, slam.getKnockbackStrength())); + break; + case 103: + slam.setLeapSpeed(parseFloat(field, slam.getLeapSpeed())); + break; + case 104: + slam.setLeapHeight(parseFloat(field, slam.getLeapHeight())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java index 5570d944f..6173f9ded 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Teleport ability type-specific settings. @@ -95,21 +95,39 @@ public class SubGuiAbilityTeleport extends SubGuiAbilityConfig { teleport.setMode(AbilityTeleport.TeleportMode.values()[value]); initGui(); break; - case 105: teleport.setRequireLineOfSight(value == 1); break; - case 108: teleport.setDamageAtStart(value == 1); break; - case 109: teleport.setDamageAtEnd(value == 1); break; + case 105: + teleport.setRequireLineOfSight(value == 1); + break; + case 108: + teleport.setDamageAtStart(value == 1); + break; + case 109: + teleport.setDamageAtEnd(value == 1); + break; } } @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 101: teleport.setBlinkRadius(parseFloat(field, teleport.getBlinkRadius())); break; - case 102: teleport.setBlinkCount(field.getInteger()); break; - case 103: teleport.setBlinkDelayTicks(field.getInteger()); break; - case 104: teleport.setBehindDistance(parseFloat(field, teleport.getBehindDistance())); break; - case 106: teleport.setDamage(parseFloat(field, teleport.getDamage())); break; - case 107: teleport.setDamageRadius(parseFloat(field, teleport.getDamageRadius())); break; + case 101: + teleport.setBlinkRadius(parseFloat(field, teleport.getBlinkRadius())); + break; + case 102: + teleport.setBlinkCount(field.getInteger()); + break; + case 103: + teleport.setBlinkDelayTicks(field.getInteger()); + break; + case 104: + teleport.setBehindDistance(parseFloat(field, teleport.getBehindDistance())); + break; + case 106: + teleport.setDamage(parseFloat(field, teleport.getDamage())); + break; + case 107: + teleport.setDamageRadius(parseFloat(field, teleport.getDamageRadius())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java index 88529fe68..781d0665a 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityTrap; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Trap ability type-specific settings. @@ -79,14 +79,30 @@ public class SubGuiAbilityTrap extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 101: trap.setTriggerRadius(parseFloat(field, trap.getTriggerRadius())); break; - case 102: trap.setDamage(parseFloat(field, trap.getDamage())); break; - case 103: trap.setDamageRadius(parseFloat(field, trap.getDamageRadius())); break; - case 104: trap.setArmTime(field.getInteger()); break; - case 105: trap.setMaxTriggers(field.getInteger()); break; - case 106: trap.setRootDuration(field.getInteger()); break; - case 107: trap.setStunDuration(field.getInteger()); break; - case 108: trap.setKnockback(parseFloat(field, trap.getKnockback())); break; + case 101: + trap.setTriggerRadius(parseFloat(field, trap.getTriggerRadius())); + break; + case 102: + trap.setDamage(parseFloat(field, trap.getDamage())); + break; + case 103: + trap.setDamageRadius(parseFloat(field, trap.getDamageRadius())); + break; + case 104: + trap.setArmTime(field.getInteger()); + break; + case 105: + trap.setMaxTriggers(field.getInteger()); + break; + case 106: + trap.setRootDuration(field.getInteger()); + break; + case 107: + trap.setStunDuration(field.getInteger()); + break; + case 108: + trap.setKnockback(parseFloat(field, trap.getKnockback())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java index a1abbd661..003cef5c6 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java @@ -1,11 +1,11 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityVortex; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; /** * GUI for configuring Vortex ability type-specific settings. @@ -75,22 +75,42 @@ public class SubGuiAbilityVortex extends SubGuiAbilityConfig { protected void handleTypeButton(int id, GuiNpcButton button) { int value = button.getValue(); switch (id) { - case 104: vortex.setAoe(value == 1); break; - case 108: vortex.setDamageOnPull(value == 1); break; + case 104: + vortex.setAoe(value == 1); + break; + case 108: + vortex.setDamageOnPull(value == 1); + break; } } @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { - case 100: vortex.setPullRadius(parseFloat(field, vortex.getPullRadius())); break; - case 101: vortex.setPullStrength(parseFloat(field, vortex.getPullStrength())); break; - case 102: vortex.setDamage(parseFloat(field, vortex.getDamage())); break; - case 103: vortex.setKnockback(parseFloat(field, vortex.getKnockback())); break; - case 105: vortex.setMaxTargets(field.getInteger()); break; - case 106: vortex.setStunDuration(field.getInteger()); break; - case 107: vortex.setRootDuration(field.getInteger()); break; - case 109: vortex.setPullDamage(parseFloat(field, vortex.getPullDamage())); break; + case 100: + vortex.setPullRadius(parseFloat(field, vortex.getPullRadius())); + break; + case 101: + vortex.setPullStrength(parseFloat(field, vortex.getPullStrength())); + break; + case 102: + vortex.setDamage(parseFloat(field, vortex.getDamage())); + break; + case 103: + vortex.setKnockback(parseFloat(field, vortex.getKnockback())); + break; + case 105: + vortex.setMaxTargets(field.getInteger()); + break; + case 106: + vortex.setStunDuration(field.getInteger()); + break; + case 107: + vortex.setRootDuration(field.getInteger()); + break; + case 109: + vortex.setPullDamage(parseFloat(field, vortex.getPullDamage())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java index e5ae8c436..22d0f3103 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -3,13 +3,21 @@ package noppes.npcs.client.gui.global; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import net.minecraft.client.gui.GuiButton; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.SubGuiAbilityDeleteConfirm; -import noppes.npcs.client.gui.advanced.SubGuiAbilityLoad; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiNPCInterface2; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.entity.EntityNPCInterface; -import java.util.*; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; /** * Global GUI for managing saved ability presets. diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java index f246cb417..40df2d463 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageRecipes.java @@ -13,7 +13,6 @@ import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.NoppesUtil; import noppes.npcs.client.gui.SubGuiNpcAvailability; import noppes.npcs.client.gui.script.GuiScriptInterface; -import noppes.npcs.controllers.data.RecipeScript; import noppes.npcs.client.gui.util.GuiButtonBiDirectional; import noppes.npcs.client.gui.util.GuiContainerNPCInterface2; import noppes.npcs.client.gui.util.GuiCustomScroll; @@ -32,6 +31,7 @@ import noppes.npcs.constants.EnumScrollData; import noppes.npcs.containers.ContainerManageRecipes; import noppes.npcs.controllers.data.RecipeAnvil; import noppes.npcs.controllers.data.RecipeCarpentry; +import noppes.npcs.controllers.data.RecipeScript; import noppes.npcs.entity.EntityNPCInterface; import org.lwjgl.opengl.GL11; diff --git a/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java b/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java index fd57c0171..b71f6642a 100644 --- a/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java +++ b/src/main/java/noppes/npcs/client/gui/item/SubGuiLinkedItem.java @@ -13,7 +13,6 @@ import noppes.npcs.client.CustomNpcResourceListener; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.global.GuiNPCManageLinked; import noppes.npcs.client.gui.script.GuiScriptInterface; -import noppes.npcs.controllers.data.LinkedItemScript; import noppes.npcs.client.gui.util.GuiButtonBiDirectional; import noppes.npcs.client.gui.util.GuiMenuTopButton; import noppes.npcs.client.gui.util.GuiNpcButton; @@ -26,6 +25,7 @@ import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.client.renderer.ImageData; import noppes.npcs.controllers.data.LinkedItem; +import noppes.npcs.controllers.data.LinkedItemScript; import org.lwjgl.opengl.GL11; public class SubGuiLinkedItem extends SubGuiInterface implements ITextfieldListener, GuiYesNoCallback, ISubGuiListener { @@ -120,8 +120,8 @@ public class SubGuiLinkedItem extends SubGuiInterface implements ITextfieldListe GuiNpcTextField digSpeedField = new GuiNpcTextField(12, this, x + 110, y, 50, 20, "" + linkedItem.digSpeed); digSpeedField.setIntegersOnly().setMinMaxDefault(0, 20, 1); addTextField(digSpeedField); - GuiNpcTextField attackSpeedField = new GuiNpcTextField(14, this, x +165, y, 50, 20, "" +linkedItem.attackSpeed); - attackSpeedField.setIntegersOnly().setMinMaxDefault(0,999999999,20); + GuiNpcTextField attackSpeedField = new GuiNpcTextField(14, this, x + 165, y, 50, 20, "" + linkedItem.attackSpeed); + attackSpeedField.setIntegersOnly().setMinMaxDefault(0, 999999999, 20); addTextField(attackSpeedField); y += spacing; diff --git a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java index 885c61d94..26942e57f 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java @@ -9,11 +9,11 @@ import net.minecraft.client.gui.GuiButton; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.advanced.GuiNPCAbilities; import noppes.npcs.client.gui.advanced.GuiNPCAdvancedLinkedNpc; import noppes.npcs.client.gui.advanced.GuiNPCDialogNpcOptions; import noppes.npcs.client.gui.advanced.GuiNPCFactionSetup; import noppes.npcs.client.gui.advanced.GuiNPCLinesMenu; -import noppes.npcs.client.gui.advanced.GuiNPCAbilities; import noppes.npcs.client.gui.advanced.GuiNPCMagic; import noppes.npcs.client.gui.advanced.GuiNPCMarks; import noppes.npcs.client.gui.advanced.GuiNPCNightSetup; @@ -25,8 +25,8 @@ import noppes.npcs.client.gui.roles.GuiNpcConversation; import noppes.npcs.client.gui.roles.GuiNpcFollowerJob; import noppes.npcs.client.gui.roles.GuiNpcGuard; import noppes.npcs.client.gui.roles.GuiNpcHealer; -import noppes.npcs.client.gui.roles.GuiNpcSpawner; import noppes.npcs.client.gui.roles.GuiNpcMount; +import noppes.npcs.client.gui.roles.GuiNpcSpawner; import noppes.npcs.client.gui.roles.GuiNpcTransporter; import noppes.npcs.client.gui.util.GuiNPCInterface2; import noppes.npcs.client.gui.util.GuiNpcButton; diff --git a/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java b/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java index cd47fed55..fe6daff23 100644 --- a/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java +++ b/src/main/java/noppes/npcs/client/gui/script/EventGuiScriptList.java @@ -105,7 +105,7 @@ public class EventGuiScriptList extends SubGuiInterface { public List getScripts() { return this.container.getExternalScripts(); } - + public void save() { } } diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java index acfac310a..9fb601162 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiJaninoScript.java @@ -1,17 +1,38 @@ package noppes.npcs.client.gui.script; -import noppes.npcs.janino.JaninoScript; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.*; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiConfirmOpenLink; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiYesNo; +import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.util.StatCollector; import noppes.npcs.NoppesStringUtils; -import noppes.npcs.client.gui.util.*; +import noppes.npcs.client.gui.util.GuiCustomScroll; +import noppes.npcs.client.gui.util.GuiMenuTopButton; +import noppes.npcs.client.gui.util.GuiNPCInterface; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextArea; +import noppes.npcs.client.gui.util.GuiScriptTextArea; +import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.ITextChangeListener; +import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.controllers.ScriptController; +import noppes.npcs.janino.JaninoScript; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback, ITextChangeListener, ICustomScrollListener, ISubGuiListener { @@ -21,7 +42,7 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback public List allExternalScripts = new ArrayList<>(); private final int scriptLimit = 1; public String previousHookClicked = ""; - + // GUI display list List hooklist = new ArrayList<>(); @@ -83,6 +104,7 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback } } } + public void initGui() { this.ySize = (int) ((double) this.xSize * 0.56); if ((double) this.ySize > (double) this.height * 0.95) { @@ -95,7 +117,7 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback this.guiTop += 10; int yoffset = (int) ((double) this.ySize * 0.02); GuiMenuTopButton top; - addTopButton(new GuiMenuTopButton(-1, guiLeft + xSize-2 , guiTop - 17, "X")); + addTopButton(new GuiMenuTopButton(-1, guiLeft + xSize - 2, guiTop - 17, "X")); this.addTopButton(top = new GuiMenuTopButton(0, this.guiLeft + 4, this.guiTop - 17, "gui.settings")); int topXoffset = 0; int topYoffset = 0; @@ -135,7 +157,7 @@ public class GuiJaninoScript extends GuiNPCInterface implements GuiYesNoCallback textArea.setListener(this); this.closeOnEsc(textArea::closeOnEsc); this.addTextField(textArea); - + int left1 = this.guiLeft + this.xSize - 104; this.addButton(new GuiNpcButton(102, left1, this.guiTop + yoffset, 60, 20, "gui.clear")); this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java index cff2c111e..db1338fff 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiNPCEventScripts.java @@ -1,7 +1,7 @@ package noppes.npcs.client.gui.script; -import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.controllers.data.DataScript; +import noppes.npcs.controllers.data.IScriptHandlerPacket; import noppes.npcs.entity.EntityNPCInterface; public class GuiNPCEventScripts extends GuiScriptInterface { diff --git a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java index 15d3e8e76..82012429f 100644 --- a/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java +++ b/src/main/java/noppes/npcs/client/gui/script/GuiScriptInterface.java @@ -1,7 +1,11 @@ package noppes.npcs.client.gui.script; import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.*; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiConfirmOpenLink; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.GuiYesNo; +import net.minecraft.client.gui.GuiYesNoCallback; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.StatCollector; @@ -21,13 +25,13 @@ import noppes.npcs.client.gui.util.IGuiData; import noppes.npcs.client.gui.util.IJTextAreaListener; import noppes.npcs.client.gui.util.ITextChangeListener; import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; -import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.ForgeDataScript; import noppes.npcs.controllers.data.IScriptHandler; -import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.controllers.data.IScriptHandlerPacket; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.item.ScriptCustomItem; import org.lwjgl.opengl.Display; @@ -50,15 +54,21 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected Map textAreas = new HashMap<>(); protected GuiScreen parent; - - /** If true, uses scroll-based tab selection instead of top buttons */ + + /** + * If true, uses scroll-based tab selection instead of top buttons + */ protected boolean useScrollTabs = false; - - /** If true, shows settings/script toggle instead of numbered tabs */ - protected boolean useSettingsToggle = false; + + /** + * If true, shows settings/script toggle instead of numbered tabs + */ + protected boolean useSettingsToggle = false; // ==================== FULLSCREEN MODE ==================== - /** Whether the editor viewport is currently in fullscreen mode */ + /** + * Whether the editor viewport is currently in fullscreen mode + */ public static boolean isFullscreen = false; /** @@ -67,19 +77,29 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb * All padding values are in scaled GUI pixels. */ public static class FullscreenConfig { - /** Padding from the top edge of the screen */ + /** + * Padding from the top edge of the screen + */ public static int paddingTop = 20; - /** Padding from the bottom edge of the screen */ + /** + * Padding from the bottom edge of the screen + */ public static int paddingBottom = 20; - /** Padding from the left edge of the screen */ + /** + * Padding from the left edge of the screen + */ public static int paddingLeft = 20; - /** Padding from the right edge of the screen */ + /** + * Padding from the right edge of the screen + */ public static int paddingRight = 20; } - /** Fullscreen toggle button drawn at top-right of viewport */ + /** + * Fullscreen toggle button drawn at top-right of viewport + */ public final FullscreenButton fullscreenButton = new FullscreenButton(); - + public GuiScriptInterface() { this.drawDefaultBackground = true; this.closeOnEsc = true; @@ -150,7 +170,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb topXoffset -= top.width + 20 * 22; } this.addTopButton(top = new GuiMenuTopButton(ta + 1, top.xPosition + top.width + topXoffset, - top.yPosition + topYoffset, ta + 1 + "")); + top.yPosition + topYoffset, ta + 1 + "")); topXoffset = 0; topYoffset = 0; scriptLimit = ta + 2; @@ -159,8 +179,8 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb if (isSingle && handler.getSingleScript() != null) this.addTopButton( - top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, - "Script")); + top = new GuiMenuTopButton(1, top.xPosition + top.width + topXoffset, top.yPosition + topYoffset, + "Script")); else if (this.handler.getScripts().size() < 100) this.addTopButton(new GuiMenuTopButton(scriptLimit, top.xPosition + top.width, top.yPosition, "+")); @@ -241,15 +261,15 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb GuiScriptTextArea activeArea = getActiveScriptArea(); if (activeArea == null) { activeArea = new GuiScriptTextArea(this, 2, editorX, editorY, editorWidth, editorHeight, - container == null ? "" : container.getScript()); + container == null ? "" : container.getScript()); activeArea.setListener(this); this.closeOnEsc(activeArea::closeOnEsc); textAreas.put(idx, activeArea); } else { activeArea.init(editorX, editorY, editorWidth, editorHeight, - container == null ? "" : container.getScript()); + container == null ? "" : container.getScript()); } - + // Set the scripting language for proper syntax highlighting // Use the container's language if available, otherwise fall back to handler's language String language = (container != null) ? container.getLanguage() : this.handler.getLanguage(); @@ -279,12 +299,12 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.addButton(new GuiNpcButton(101, left1 + 61, this.guiTop + yoffset, 60, 20, "gui.paste")); this.addButton(new GuiNpcButton(100, left1, this.guiTop + 21 + yoffset, 60, 20, "gui.copy")); this.addButton(new GuiNpcButton(105, left1 + 61, this.guiTop + 21 + yoffset, 60, 20, "gui.remove")); - + // Language toggle button (only if handler supports Janino and there is more than one option) if (handler.supportsJanino() && getLanguageOptions().size() > 1 && container != null) { this.addButton(new GuiNpcButton(113, left1, this.guiTop + 42 + yoffset, 121, 20, container.getLanguage())); } - + this.addButton(new GuiNpcButton(107, left1, this.guiTop + 66 + yoffset, 80, 20, "script.loadscript")); GuiCustomScroll scroll = (new GuiCustomScroll(this, 0)).setUnselectable(); @@ -303,7 +323,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb */ private void initSettingsTab(int yoffset) { GuiNpcTextArea var8 = new GuiNpcTextArea(2, this, this.guiLeft + 4 + yoffset, this.guiTop + 6 + yoffset, - this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); + this.xSize - 160 - yoffset, (int) ((float) this.ySize * 0.92F) - yoffset * 2, this.getConsoleText()); var8.canEdit = false; var8.setFocused(true); this.addTextField(var8); @@ -319,7 +339,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb this.getButton(103).enabled = languageOptions.size() > 0; this.addLabel(new GuiNpcLabel(2, "gui.enabled", var9, this.guiTop + 36)); this.addButton(new GuiNpcButton(104, var9 + 60, this.guiTop + 31, 50, 20, - new String[]{"gui.no", "gui.yes"}, this.handler.getEnabled() ? 1 : 0)); + new String[]{"gui.no", "gui.yes"}, this.handler.getEnabled() ? 1 : 0)); if (this.player.worldObj.isRemote) { this.addButton(new GuiNpcButton(106, var9, this.guiTop + 55, 150, 20, "script.openfolder")); @@ -382,11 +402,11 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb IScriptUnit container = getCurrentContainer(); if (container == null) return; - + String addString = ""; if (!this.getTextField(2).getText().isEmpty()) addString += "\n"; - + // Generate the appropriate stub based on language // hook is already the proper method name from EnumScriptType.function addString += container.generateHookStub(hook, null); @@ -525,7 +545,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return textAreas.get(idx); return null; } - + /** * Get the current script index for text area storage. * Override in subclasses with different tab logic. @@ -533,7 +553,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb protected int getActiveScriptIndex() { return this.activeTab - 1; } - + /** * Get the current script container. * Override in subclasses with different container access patterns. @@ -642,14 +662,14 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb String language = container != null ? container.getLanguage() : this.handler.getLanguage(); this.setSubGui(new EventGuiScriptList((List) this.languages.get(language), container)); } - + // Language toggle button - switch between ECMAScript and Java if (guibutton.id == 113) { int idx = getActiveScriptIndex(); if (idx >= 0 && idx < handler.getScripts().size()) { IScriptUnit currentUnit = handler.getScripts().get(idx); IScriptUnit newUnit; - + if (currentUnit.isJanino()) { // Switch from Java to ECMAScript newUnit = new ScriptContainer(handler); @@ -669,7 +689,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb handler.replaceScriptUnit(idx, newUnit); // Clear the cached text area so it recreates with new language textAreas.remove(idx); - + // Reinitialize the GUI initGui(); } @@ -712,7 +732,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb } return; } - + loadLanguagesData(compound); } @@ -759,7 +779,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb return; this.setScript(); - + if (handler instanceof IScriptHandlerPacket) ((IScriptHandlerPacket) handler).sync(); @@ -790,7 +810,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb IScriptUnit current = getCurrentContainer(); if (current != null) current.ensureCompiled(); - + if (parent != null) { this.save(); parent.setWorldAndResolution(mc, width, height); @@ -838,14 +858,18 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb public int size = 12; public boolean hovered = false; - /** Initialize button position relative to viewport bounds */ + /** + * Initialize button position relative to viewport bounds + */ public void initGui(int viewportEndX, int viewportY, int scrollbarOffset) { // Position at top-right, accounting for scrollbar this.x = viewportEndX + scrollbarOffset - size - 4; this.y = viewportY + 4; } - /** Draw the fullscreen toggle button */ + /** + * Draw the fullscreen toggle button + */ public void draw(int mouseX, int mouseY) { hovered = isMouseOver(mouseX, mouseY); int y = this.y + getYOffset(); @@ -877,7 +901,7 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb drawRect(right, bottom, right + cornerLen, bottom + 1, iconColor); drawRect(right, bottom, right + 1, bottom + cornerLen, iconColor); - + } else { // Draw expand icon (corners pointing outward) ⌐¬⌙⌞ // Top-left corner pointing outward @@ -891,23 +915,27 @@ public class GuiScriptInterface extends GuiNPCInterface implements GuiYesNoCallb drawRect(x + inset, y + size - inset - 1, x + inset + cornerLen, y + size - inset, iconColor); // Bottom-right corner pointing outward drawRect(x + size - inset - 1, y + size - inset - cornerLen, x + size - inset, y + size - inset, - iconColor); + iconColor); drawRect(x + size - inset - cornerLen, y + size - inset - 1, x + size - inset, y + size - inset, - iconColor); + iconColor); } } - + public void drawRect(int x1, int y1, int x2, int y2, int color) { GuiUtil.drawRectD(x1, y1, x2, y2, color); } - /** Check if mouse is over the button */ + /** + * Check if mouse is over the button + */ public boolean isMouseOver(int mouseX, int mouseY) { int y = this.y + getYOffset(); return mouseX >= x && mouseX < x + size && mouseY >= y && mouseY < y + size; } - /** Handle click - returns true if click was consumed */ + /** + * Handle click - returns true if click was consumed + */ public boolean mouseClicked(int mouseX, int mouseY, int button) { if (button == 0 && isMouseOver(mouseX, mouseY)) { toggleFullscreen(); diff --git a/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java b/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java index e2812ecb2..2e20a6ed5 100644 --- a/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java +++ b/src/main/java/noppes/npcs/client/gui/script/SubGuiSelectList.java @@ -42,7 +42,7 @@ public class SubGuiSelectList extends SubGuiInterface { scroll1.setSize(140, 180); } - addTopButton(new GuiMenuTopButton(-1, guiLeft + xSize-22 , guiTop - 17, "X")); + addTopButton(new GuiMenuTopButton(-1, guiLeft + xSize - 22, guiTop - 17, "X")); scroll1.guiLeft = guiLeft + 4; scroll1.guiTop = guiTop + 14; diff --git a/src/main/java/noppes/npcs/client/gui/select/GuiTextureSelection.java b/src/main/java/noppes/npcs/client/gui/select/GuiTextureSelection.java index 4f1bccfaf..3b8fda84d 100644 --- a/src/main/java/noppes/npcs/client/gui/select/GuiTextureSelection.java +++ b/src/main/java/noppes/npcs/client/gui/select/GuiTextureSelection.java @@ -371,6 +371,7 @@ public class GuiTextureSelection extends SubGuiInterface implements ICustomScrol selectedDomain = domain; this.location = location; } + static class TextureData { String domain; String absoluteName; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiDiagram.java b/src/main/java/noppes/npcs/client/gui/util/GuiDiagram.java index 07822888c..3624de03c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiDiagram.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiDiagram.java @@ -9,7 +9,7 @@ import noppes.npcs.constants.EnumDiagramLayout; import org.lwjgl.input.Mouse; import org.lwjgl.opengl.GL11; -import java.awt.*; +import java.awt.Point; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 585ee8515..8444c5c1d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -109,7 +109,7 @@ public abstract class GuiNPCInterface extends GuiScreen { //Important for GuiScriptTextArea for (GuiNpcTextField tf : textfields.values()) - tf.initGui(); + tf.initGui(); } @Override @@ -208,6 +208,7 @@ public abstract class GuiNPCInterface extends GuiScreen { } } } + @Override public void mouseClickMove(int mouseX, int mouseY, int clickedMouseButton, long timeSinceLastClick) { if (subgui != null) { @@ -286,7 +287,7 @@ public abstract class GuiNPCInterface extends GuiScreen { } public void close() { - if(GuiNpcTextField.activeTextfield != null) + if (GuiNpcTextField.activeTextfield != null) GuiNpcTextField.unfocus(); Keyboard.enableRepeatEvents(false); @@ -537,6 +538,7 @@ public abstract class GuiNPCInterface extends GuiScreen { public void drawNpcWithExtras(EntityLivingBase entity, int mouseX, int mouseY, float partialTicks) { drawNpc(entity, mouseX, mouseY, partialTicks); } + public void drawNpc(EntityLivingBase entity, int mouseX, int mouseY, float partialTicks) { if (hasSubGui() && !drawNPConSub) return; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java index 37ffdb807..c14d0da83 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java @@ -126,6 +126,7 @@ public class GuiNpcButton extends GuiButton { } public AtomicBoolean rightClicked = new AtomicBoolean(); + @Override public boolean mousePressed(Minecraft minecraft, int i, int j) { boolean bo = super.mousePressed(minecraft, i, j); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java index 1345a584f..91f3ca142 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java @@ -38,10 +38,10 @@ public class GuiNpcTextField extends GuiTextField { public GuiNpcTextField(int id, GuiScreen parent, int i, int j, int k, int l, String s) { this(id, parent, Minecraft.getMinecraft().fontRenderer, i, j, k, l, s); } - + public void initGui() { } - + protected boolean charAllowed(char c, int i) { if (!integersOnly || Character.isDigit(c)) return true; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index c73f2bebf..77be0dd7c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -8,8 +8,17 @@ import noppes.npcs.NoppesStringUtils; import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.script.GuiScriptInterface; import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; -import noppes.npcs.client.gui.util.script.*; +import noppes.npcs.client.gui.util.script.BracketMatcher; +import noppes.npcs.client.gui.util.script.CommentHandler; +import noppes.npcs.client.gui.util.script.CursorNavigation; +import noppes.npcs.client.gui.util.script.GoToLineDialog; +import noppes.npcs.client.gui.util.script.IndentHelper; +import noppes.npcs.client.gui.util.script.JavaTextContainer; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; +import noppes.npcs.client.gui.util.script.RenameRefactorHandler; +import noppes.npcs.client.gui.util.script.ScrollState; +import noppes.npcs.client.gui.util.script.SearchReplaceBar; +import noppes.npcs.client.gui.util.script.SelectionState; import noppes.npcs.client.key.impl.ScriptEditorKeys; import noppes.npcs.constants.ScriptContext; import noppes.npcs.util.ValueUtil; @@ -27,7 +36,7 @@ import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; /** * Script text editor component with syntax highlighting, bracket matching, * smooth scrolling, and IDE-like features. - * + *

* Helper classes used: * - ScrollState: smooth scroll animation and state management * - SelectionState: cursor position and text selection management @@ -37,11 +46,11 @@ import static net.minecraft.client.gui.GuiScreen.isCtrlKeyDown; * - CursorNavigation: cursor movement logic */ public class GuiScriptTextArea extends GuiNpcTextField { - + // ==================== DIMENSIONS & POSITION ==================== public int x; public int y; - + // ==================== STATE FLAGS ==================== public boolean active = false; public boolean enabled = true; @@ -51,7 +60,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { public boolean tripleClicked = false; private int clickCount = 0; private long lastClicked = 0L; - + // ==================== TEXT & CONTAINER ==================== public String text = null; public String highlightedWord; @@ -80,12 +89,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== HELPER CLASS INSTANCES ==================== private final ScrollState scroll = new ScrollState(); private final SelectionState selection = new SelectionState(); - + // ==================== UI COMPONENTS ==================== private int cursorCounter; private ITextChangeListener listener; private static int LINE_NUMBER_GUTTER_WIDTH = 25; - + // ==================== UNDO/REDO ==================== public List undoList = new ArrayList<>(); public List redoList = new ArrayList<>(); @@ -97,7 +106,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== SEARCH/REPLACE ==================== public static final SearchReplaceBar searchBar = new SearchReplaceBar(); - + // ==================== GO TO LINE ==================== private final GoToLineDialog goToLineDialog = new GoToLineDialog(); @@ -125,11 +134,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.searchBaseHeight = 0; this.searchAppliedOffset = 0; this.searchBaseInitialized = false; - + KEYS_OVERLAY.openOnClick = true; initGui(); initializeKeyBindings(); } + public void initGui() { int endX = x + width, endY = y + height; int xOffset = hasVerticalScrollbar() ? -8 : -2; @@ -137,11 +147,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { KEYS_OVERLAY.borderCol1 = KEYS_OVERLAY.borderCol2 = 0xFF3c3c3c; int overlayWidth = 160; KEYS_OVERLAY.initGui(x + (width - overlayWidth) / 2 + 5, y + height / 10, overlayWidth, - height - height / 5 - 10); + height - height / 5 - 10); KEYS_OVERLAY.viewButton.scale = 0.45f; KEYS_OVERLAY.viewButton.initGui(endX + xOffset, endY - 26); - + // Initialize search bar (preserves state across initGui calls) searchBar.initGui(x, y, width); if (searchBar.isVisible()) { // If open @@ -150,7 +160,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (!active) // Focus search if opening another script tab & bar is open searchBar.focus(false); } - + // Initialize Go To Line dialog goToLineDialog.initGui(x, y, width); } @@ -193,7 +203,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { int visibleLines = effectiveHeight / container.lineHeight; // Calculate how many lines the search bar covers int linesHiddenBySRB = searchBarOffset > 0 ? (int) Math.ceil( - (double) searchBarOffset / container.lineHeight) : 0; + (double) searchBarOffset / container.lineHeight) : 0; for (int i = 0; i < container.lines.size(); i++) { LineData ld = container.lines.get(i); @@ -457,12 +467,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } + // ==================== RENDERING ==================== public void drawTextBox(int xMouse, int yMouse) { if (!visible) return; clampSelectionBounds(); - + // Dynamically calculate gutter width based on line count digits if (container != null && container.linesCount > 0) { int maxLineNum = container.linesCount; @@ -473,7 +484,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Draw outer border around entire area int offset = fullscreen() ? 2 : 1; drawRect(x - offset, y - offset - searchBar.getTotalHeight(), x + width + offset, y + height + offset, - 0xffa0a0a0); + 0xffa0a0a0); int searchHeight = searchBar.getTotalHeight(); @@ -483,7 +494,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Draw text viewport background (starts after gutter) drawRect(x + LINE_NUMBER_GUTTER_WIDTH, y, x + width, y + height, 0xff000000); // Draw separator line between gutter and text area - drawRect(x + LINE_NUMBER_GUTTER_WIDTH-1, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff3c3f41); + drawRect(x + LINE_NUMBER_GUTTER_WIDTH - 1, y, x + LINE_NUMBER_GUTTER_WIDTH, y + height, 0xff3c3f41); // Enable scissor test to clip drawing to the TEXT viewport rectangle (excludes gutter) GL11.glEnable(GL11.GL_SCISSOR_TEST); @@ -509,7 +520,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Handle scrollbar dragging (delegated to ScrollState) if (scroll.isClickScrolling()) scroll.handleClickScrolling(yMouse, x, y, height, container.visibleLines, getPaddedLineCount(), maxScroll); - + // Update scroll animation scroll.initializeIfNeeded(scroll.getScrolledLine()); scroll.update(maxScroll); @@ -535,8 +546,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Calculate braces next to cursor to highlight int startBracket = 0, endBracket = 0; if (selection.getStartSelection() >= 0 && text != null && text.length() > 0 && - (selection.getEndSelection() - selection.getStartSelection() == 1 || !selection.hasSelection())) { - int[] span = BracketMatcher.findBracketSpanAt(text,selection.getStartSelection()); + (selection.getEndSelection() - selection.getStartSelection() == 1 || !selection.hasSelection())) { + int[] span = BracketMatcher.findBracketSpanAt(text, selection.getStartSelection()); if (span != null) { startBracket = span[0]; endBracket = span[1]; @@ -544,12 +555,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { } List list = new ArrayList<>(container.lines); - + // Build brace spans: {origDepth, open line, close line, adjustedDepth} List braceSpans = BracketMatcher.computeBraceSpans(text, list); // Always highlight unmatched braces (positions in text) List unmatchedBraces = BracketMatcher.findUnmatchedBracePositions(text); - + // Determine which exact brace span (openLine/closeLine) to highlight indent guides for int highlightedOpenLine = -1; int highlightedCloseLine = -1; @@ -618,19 +629,19 @@ public class GuiScriptTextArea extends GuiNpcTextField { float fracPixels = (float) (fracOffset * container.lineHeight); GL11.glPushMatrix(); GL11.glTranslatef(0.0f, -fracPixels, 0.0f); - - + + // Expand render range by one line above/below so partially-visible lines are drawn int renderStart = Math.max(0, scroll.getScrolledLine() - 1); // Compute the last line index to render, including the last partially-visible line if any. // Adds the fractional scroll offset (fracOffset * lineHeight) to ensure the bottom line is drawn // when only part of it is visible in the viewport. int renderEnd = (int) Math.min(list.size() - 1, - scroll.getScrolledLine() + container.visibleLines + fracPixels + 1); + scroll.getScrolledLine() + container.visibleLines + fracPixels + 1); // Strings start drawing vertically this much into the line. int stringYOffset = 2; - + // Render LINE GUTTER numbers for (int i = renderStart; i <= renderEnd; i++) { int posY = y + (i - scroll.getScrolledLine()) * container.lineHeight + stringYOffset; @@ -698,7 +709,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } - + // Highlight search matches if (searchBar.isVisible()) { List searchMatches = searchBar.getMatches(); @@ -747,8 +758,8 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (occStart <= occEnd) { // Changed from < to <= to handle empty case int s = ClientProxy.Font.width(line.substring(0, occStart)); int e = isEmpty ? s + 2 : ClientProxy.Font.width( - line.substring(0, occEnd)) + 1; // 2px wide for empty - int occX = x + LINE_NUMBER_GUTTER_WIDTH + s; + line.substring(0, occEnd)) + 1; // 2px wide for empty + int occX = x + LINE_NUMBER_GUTTER_WIDTH + s; int occEndX = x + LINE_NUMBER_GUTTER_WIDTH + 2 + e; boolean isPrimary = renameHandler.isPrimaryOccurrence(occ[0]); @@ -761,12 +772,12 @@ public class GuiScriptTextArea extends GuiNpcTextField { int borderColor = 0xDDFFFFFF; //RED BG drawRect(occX, posY, occEndX, posY + container.lineHeight, 0x33ff0000); - + // Top border drawRect(occX, posY, occEndX, posY + 1, borderColor); - // Bottom border + // Bottom border drawRect(occX, posY + container.lineHeight - 1, occEndX, - posY + container.lineHeight, borderColor); + posY + container.lineHeight, borderColor); // Left border drawRect(occX, posY, occX + 1, posY + container.lineHeight, borderColor); // Right border @@ -778,7 +789,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { String currentWord = renameHandler.getCurrentWord(); if (currentWord != null && cursorInWord >= 0 && cursorInWord <= currentWord.length()) { String beforeCursor = currentWord.substring(0, - Math.min(cursorInWord, currentWord.length())); + Math.min(cursorInWord, currentWord.length())); int cursorX = occX + ClientProxy.Font.width(beforeCursor); // drawRect(cursorX, posY + 1, cursorX + 1, posY + container.lineHeight - 1, // 0xFFFFFFFF); @@ -789,18 +800,18 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } - + // Highlight the current line (light gray) under any selection if (active && isEnabled() && (selection.getCursorPosition() >= data.start && selection.getCursorPosition() < data.end || (i == list.size() - 1 && selection.getCursorPosition() == text.length()))) { - drawRect(x , posY, x + width - 1, posY + container.lineHeight, 0x22e0e0e0); + drawRect(x, posY, x + width - 1, posY + container.lineHeight, 0x22e0e0e0); } // Highlight selection if (selection.hasSelection() && selection.getEndSelection() > data.start && selection.getStartSelection() <= data.end) { if (selection.getStartSelection() < data.end) { int s = ClientProxy.Font.width( - line.substring(0, Math.max(selection.getStartSelection() - data.start, 0))); + line.substring(0, Math.max(selection.getStartSelection() - data.start, 0))); int e = ClientProxy.Font.width( - line.substring(0, Math.min(selection.getEndSelection() - data.start, w))) + 1; + line.substring(0, Math.min(selection.getEndSelection() - data.start, w))) + 1; drawRect(x + LINE_NUMBER_GUTTER_WIDTH + 1 + s, posY, x + LINE_NUMBER_GUTTER_WIDTH + 1 + e, posY + container.lineHeight, 0x992172ff); } } @@ -837,11 +848,11 @@ public class GuiScriptTextArea extends GuiNpcTextField { boolean highlighted = (openLine == highlightedOpenLine && closeLine == highlightedCloseLine); int guideColor = highlighted ? 0x9933cc00 : 0x33FFFFFF; - + int topY = y + (drawStart - scroll.getScrolledLine()) * container.lineHeight; int bottomY = y + (endLine - scroll.getScrolledLine() + 1) * container.lineHeight; - if(highlighted) - bottomY-=2; + if (highlighted) + bottomY -= 2; drawRect(gx, topY, gx + 1, bottomY, guideColor); } } @@ -852,15 +863,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { boolean recentInput = selection.hadRecentInput(); if (active && isEnabled() && (recentInput || (cursorCounter / 10) % 2 == 0) && (selection.getCursorPosition() >= data.start && selection.getCursorPosition() < data.end || (i == list.size() - 1 && selection.getCursorPosition() == text.length()))) { int posX = x + LINE_NUMBER_GUTTER_WIDTH + ClientProxy.Font.width( - line.substring(0, Math.min(selection.getCursorPosition() - data.start, line.length()))); - drawRect(posX + 1, posY, posX + 2, posY + container.lineHeight, 0xffffffff); + line.substring(0, Math.min(selection.getCursorPosition() - data.start, line.length()))); + drawRect(posX + 1, posY, posX + 2, posY + container.lineHeight, 0xffffffff); } } } GL11.glPopMatrix(); GL11.glDisable(GL11.GL_SCISSOR_TEST); - + if (hasVerticalScrollbar()) { Minecraft.getMinecraft().renderEngine.bindTexture(GuiCustomScroll.resource); int effLines = Math.max(1, getPaddedLineCount()); @@ -875,7 +886,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Draw search/replace bar (overlays viewport) searchBar.draw(xMouse, yMouse); - + // Draw go to line dialog (overlays everything) goToLineDialog.draw(xMouse, yMouse); KEYS_OVERLAY.draw(xMouse, yMouse, wheelDelta); @@ -902,13 +913,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { // visible lines (fractional positions) correctly hit that line. double fracPixels = scroll.getFractionalOffset() * container.lineHeight; double yMouseD = yMouse + fracPixels; - + ArrayList list = new ArrayList(this.container.lines); for (int i = 0; i < list.size(); ++i) { LineData data = (LineData) list.get(i); //+1 to account for the fractional line - if (i >= scroll.getScrolledLine() && i <= scroll.getScrolledLine() + this.container.visibleLines +1) { + if (i >= scroll.getScrolledLine() && i <= scroll.getScrolledLine() + this.container.visibleLines + 1) { double yPos = (i - scroll.getScrolledLine()) * this.container.lineHeight; if (yMouseD >= yPos && yMouseD < yPos + this.container.lineHeight) { int lineWidth = 0; @@ -941,13 +952,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { private int getCursorLineIndex() { return selection.getCursorLineIndex(container.lines, text != null ? text.length() : 0); } - - + + // Scroll viewport to keep cursor visible (minimal adjustment, like IntelliJ) // Only scrolls if cursor is outside the visible area private void scrollToCursor() { if (container == null || container.lines == null || container.lines.isEmpty()) return; - + int lineIdx = getCursorLineIndex(); int visible = Math.max(1, container.visibleLines); int effectiveVisible = Math.max(1, visible - bottomPaddingLines); @@ -1162,27 +1173,27 @@ public class GuiScriptTextArea extends GuiNpcTextField { KEYS.SEARCH.setTask(e -> { if (!e.isPress() || !openBoxes.get()) return; - + unfocusAll(); searchBar.openSearch(); }); - + // SEARCH_REPLACE: Open search+replace bar (Ctrl+Shift+R) // Works in search bar. KEYS.SEARCH_REPLACE.setTask(e -> { if (!e.isPress() || !openBoxes.get()) return; - + unfocusAll(); searchBar.openSearchReplace(); }); - + // GO_TO_LINE: Open go to line dialog (Ctrl+G) // Works in search bar. KEYS.GO_TO_LINE.setTask(e -> { if (!e.isPress() || !openBoxes.get()) return; - + unfocusAll(); goToLineDialog.toggle(); }); @@ -1221,13 +1232,13 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Handle rename refactor input first if active if (renameHandler.isActive() && renameHandler.keyTyped(c, i)) return true; - + // Handle Go To Line dialog input first if it has focus - if (goToLineDialog.isVisible() &&goToLineDialog.keyTyped(c, i)) - return true; - + if (goToLineDialog.isVisible() && goToLineDialog.keyTyped(c, i)) + return true; + // Handle search bar input first if it has focus - if (searchBar.isVisible() && searchBar.keyTyped(c, i)) + if (searchBar.isVisible() && searchBar.keyTyped(c, i)) return true; if (!active) @@ -1339,7 +1350,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { int cursorPos = selection.getCursorPosition(); int prevNonWs = cursorPos - 1; while (prevNonWs >= 0 && prevNonWs < (text != null ? text.length() : 0) && Character.isWhitespace( - text.charAt(prevNonWs))) { + text.charAt(prevNonWs))) { prevNonWs--; } @@ -1473,7 +1484,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { s = s.substring(0, nearestCondition); // Adjust selection start to match removed characters selection.setStartSelection( - selection.getStartSelection() - (selection.getCursorPosition() - nearestCondition)); + selection.getStartSelection() - (selection.getCursorPosition() - nearestCondition)); } setText(s + getSelectionAfterText()); selection.reset(selection.getStartSelection()); @@ -1561,9 +1572,9 @@ public class GuiScriptTextArea extends GuiNpcTextField { char lastChar = before.charAt(before.length() - 1); char firstChar = content.charAt(0); // Avoid adding space when punctuation/brackets are adjacent - if (!Character.isWhitespace(lastChar) && + if (!Character.isWhitespace(lastChar) && lastChar != '{' && lastChar != '(' && lastChar != '[' && - firstChar != '}' && firstChar != ')' && firstChar != ']' && + firstChar != '}' && firstChar != ')' && firstChar != ']' && firstChar != ';' && firstChar != ',' && firstChar != '.' && firstChar != '\n') { spacer = " "; @@ -1584,14 +1595,14 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (selection.getStartSelection() > 0 && selection.getStartSelection() < text.length()) { char prev = text.charAt(selection.getStartSelection() - 1); char nextc = text.charAt(selection.getStartSelection()); - if ((prev == '(' && nextc == ')') || - (prev == '[' && nextc == ']') || - (prev == '{' && nextc == '}') || - (prev == '\'' && nextc == '\'') || + if ((prev == '(' && nextc == ')') || + (prev == '[' && nextc == ']') || + (prev == '{' && nextc == '}') || + (prev == '\'' && nextc == '\'') || (prev == '"' && nextc == '"')) { String before = text.substring(0, selection.getStartSelection() - 1); String after = selection.getStartSelection() + 1 < text.length() ? text.substring( - selection.getStartSelection() + 1) : ""; + selection.getStartSelection() + 1) : ""; setText(before + after); selection.setStartSelection(selection.getStartSelection() - 1); selection.reset(selection.getStartSelection()); @@ -1757,7 +1768,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // already immediately after the caret, move caret past it instead // of inserting another closer. This prevents duplicate closers // when the editor auto-inserts pairs. - if ((c == ')' || c == ']' || c == '"' || c == '\'' ) && after.length() > 0 && after.charAt(0) == c) { + if ((c == ')' || c == ']' || c == '"' || c == '\'') && after.length() > 0 && after.charAt(0) == c) { // Move caret forward by one (skip over existing closer) selection.reset(before.length() + 1); scrollToCursor(); @@ -1805,10 +1816,10 @@ public class GuiScriptTextArea extends GuiNpcTextField { // ==================== COMMENT TOGGLING ==================== // Uses CommentHandler helper for comment operations - + private void toggleCommentSelection() { CommentHandler.SelectionToggleResult result = CommentHandler.toggleCommentSelection( - text, container.lines, selection.getStartSelection(), selection.getEndSelection()); + text, container.lines, selection.getStartSelection(), selection.getEndSelection()); setText(result.newText); selection.setStartSelection(result.newStartSelection); selection.setEndSelection(result.newEndSelection); @@ -1816,15 +1827,15 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void toggleCommentLineAtCursor() { CommentHandler.SingleLineToggleResult result = CommentHandler.toggleCommentAtCursor( - text, container.lines, selection.getCursorPosition()); + text, container.lines, selection.getCursorPosition()); setText(result.newText); setCursor(result.newCursorPosition, false); } - - public boolean closeOnEsc(){ - return !KEYS_OVERLAY.isVisible() && !searchBar.isVisible() && !goToLineDialog.isVisible() && !renameHandler.isActive(); + + public boolean closeOnEsc() { + return !KEYS_OVERLAY.isVisible() && !searchBar.isVisible() && !goToLineDialog.isVisible() && !renameHandler.isActive(); } - + // ==================== KEYBOARD MODIFIERS ==================== private boolean isAltKeyDown() { @@ -1872,7 +1883,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { return IndentHelper.getAutoIndentForEnter(currentLine.text, selection.getCursorPosition() - currentLine.start); } // ==================== TEXT FORMATTING ==================== - + private int getTabSize() { return IndentHelper.TAB_SIZE; } @@ -1888,7 +1899,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { setText(result.text); selection.reset(Math.max(0, Math.min(result.cursorPosition, this.text.length()))); } - + // ==================== TAB HANDLING ==================== private void handleTab() { @@ -1986,7 +1997,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { } } } - + // ==================== CURSOR MANAGEMENT ==================== private void setCursor(int i, boolean select) { @@ -2014,7 +2025,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { public String getSelectionAfterText() { return selection.getTextAfter(text); } - + // ==================== MOUSE HANDLING ==================== public void mouseClicked(int xMouse, int yMouse, int mouseButton) { @@ -2022,21 +2033,21 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (goToLineDialog.isVisible() && goToLineDialog.mouseClicked(xMouse, yMouse, mouseButton)) { return; } - + // Check search bar clicks first if (searchBar.isVisible() && searchBar.mouseClicked(xMouse, yMouse, mouseButton)) { return; } - + // If search bar is visible but click was outside it, unfocus the search bar if (searchBar.isVisible()) { searchBar.unfocus(); } - + // Let the overlay consume clicks (it returns true when it handled the event) if (KEYS_OVERLAY.mouseClicked(xMouse, yMouse, mouseButton)) return; - + // Determine whether click occurred inside the text area bounds this.active = xMouse >= this.x && xMouse < this.x + this.width && yMouse >= this.y && yMouse < this.y + this.height; if (this.active) { @@ -2067,7 +2078,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { if (this.clicked && getPaddedLineCount() * this.container.lineHeight > this.height && xMouse > this.x + this.width - 8) { // We consumed the mouse-down as a scrollbar drag start this.clicked = false; - scroll.startScrollbarDrag(yMouse,this.y,this.height, getPaddedLineCount()); + scroll.startScrollbarDrag(yMouse, this.y, this.height, getPaddedLineCount()); } else { // Handle double/triple click selection counting if (time - this.lastClicked < 300L) { @@ -2111,7 +2122,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { renameHandler.updateCursor(); ++this.cursorCounter; } - + // ==================== TEXT MANAGEMENT ==================== public void setText(String text) { @@ -2140,7 +2151,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { this.container.init(text, this.width, this.height); - if (this.enableCodeHighlighting) + if (this.enableCodeHighlighting) this.container.formatCodeText(); // Ensure scroll state stays in bounds after text change @@ -2202,7 +2213,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { private void clampSelectionBounds() { selection.clamp(text != null ? text.length() : 0); } - + // ==================== INNER CLASSES ==================== public static class UndoData { diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java b/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java index 59afbea09..3273f1f6c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScrollWindow.java @@ -143,6 +143,7 @@ public class GuiScrollWindow extends GuiScreen implements ITextfieldListener, IC } } } + public void mouseEvent(int i, int j, int k) { } @@ -208,6 +209,7 @@ public class GuiScrollWindow extends GuiScreen implements ITextfieldListener, IC } public int backgroundColor = 0x66000000; + @Override public void drawScreen(int mouseX, int mouseY, float partialTicks) { scrollY = (float) lerp(scrollY, nextScrollY, partialTicks); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java b/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java index 9a9fc71ca..7350668b8 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiUtil.java @@ -112,7 +112,7 @@ public class GuiUtil { GL11.glEnable(GL11.GL_ALPHA_TEST); GL11.glEnable(GL11.GL_TEXTURE_2D); } - + public static void drawGradientRectHorizontal(int left, int top, int right, int bottom, int startColor, int endColor) { double zLevel = 0; diff --git a/src/main/java/noppes/npcs/client/gui/util/IAbilityConfigCallback.java b/src/main/java/noppes/npcs/client/gui/util/IAbilityConfigCallback.java index ec186d066..4012c9f73 100644 --- a/src/main/java/noppes/npcs/client/gui/util/IAbilityConfigCallback.java +++ b/src/main/java/noppes/npcs/client/gui/util/IAbilityConfigCallback.java @@ -9,6 +9,7 @@ import kamkeel.npcs.controllers.data.ability.Ability; public interface IAbilityConfigCallback { /** * Called when an ability has been edited and saved. + * * @param ability The edited ability */ void onAbilitySaved(Ability ability); diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java index e8fefd475..7ebc098be 100644 --- a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -79,7 +79,7 @@ public class OverlayKeyPresetViewer { public void drawOverlay(int wheel) { scroll.update(wheel); - + if (hasBorder) { // int borderCol2 = 0xffff00ff,borderCol2 = 0xff00ffff; NEON @@ -133,7 +133,7 @@ public class OverlayKeyPresetViewer { showOverlay = false; return true; } - + // If overlay is visible but no element consumed the key, consider it handled return true; } @@ -155,7 +155,7 @@ public class OverlayKeyPresetViewer { // If overlay is not visible, no other clicks matter if (!showOverlay) return false; - + boolean isAboveOverlay = isMouseAbove(mouseX, mouseY); // In click-to-open mode, clicking outside the overlay (and not the button) should close it if (openOnClick && !isAboveOverlay && !isAboveButton) { @@ -259,7 +259,7 @@ public class OverlayKeyPresetViewer { public void drawBar() { GL11.glPushMatrix(); GL11.glTranslatef(endX - barWidth - 2, startY + yStartSpacing + scrollY * scrollFactor, 1); - GuiUtil.drawRectD(0, 1, barWidth, scrollbarHeight-1, (isMouseAboveBar(mouseX, mouseY) || isMouseDragging) ? 0xffbababa : 0xff767676); + GuiUtil.drawRectD(0, 1, barWidth, scrollbarHeight - 1, (isMouseAboveBar(mouseX, mouseY) || isMouseDragging) ? 0xffbababa : 0xff767676); GL11.glPopMatrix(); } @@ -480,7 +480,7 @@ public class OverlayKeyPresetViewer { int oldMinFilter = GL11.glGetTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER); GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); GL11.glDisable(GL11.GL_BLEND); - + GL11.glPushMatrix(); float color = aboveButton ? 1f : 0.4f; GL11.glColor4f(color, color, color, 1); diff --git a/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java b/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java index 96fe2a7ba..b7b9ab63a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/BracketMatcher.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; + import java.util.ArrayList; import java.util.List; @@ -8,14 +9,15 @@ import java.util.List; * Handles bracket matching, brace span computation, and indent guide generation. */ public class BracketMatcher { - + /** * Find matching bracket at cursor position. + * * @return int[2] with {startPos, endPos} or null if no bracket found */ public static int[] findBracketSpanAt(String text, int pos) { if (text == null || text.isEmpty()) return null; - + // Try at current position if (pos >= 0 && pos < text.length()) { char c = text.charAt(pos); @@ -34,7 +36,10 @@ public class BracketMatcher { int scan = pos - 1; while (scan >= 0) { char sc = text.charAt(scan); - if (sc == ' ' || sc == '\t') { scan--; continue; } + if (sc == ' ' || sc == '\t') { + scan--; + continue; + } if (sc == '\n') break; // Don't cross line boundary int found2 = 0; @@ -73,25 +78,43 @@ public class BracketMatcher { char next = pos + 1 < text.length() ? text.charAt(pos + 1) : 0; if (inString) { - if (escape) { escape = false; } - else if (c == '\\') { escape = true; } - else if (c == stringDelimiter) { inString = false; } - else if (c == '\n') { inString = false; } + if (escape) { + escape = false; + } else if (c == '\\') { + escape = true; + } else if (c == stringDelimiter) { + inString = false; + } else if (c == '\n') { + inString = false; + } continue; } if (inBlockComment) { - if (c == '*' && next == '/') { inBlockComment = false; pos++; } + if (c == '*' && next == '/') { + inBlockComment = false; + pos++; + } continue; } if (inLineComment) { - if (c == '\n') { inLineComment = false; } + if (c == '\n') { + inLineComment = false; + } continue; } - if (c == '/' && next == '/') { inLineComment = true; pos++; continue; } - if (c == '/' && next == '*') { inBlockComment = true; pos++; continue; } + if (c == '/' && next == '/') { + inLineComment = true; + pos++; + continue; + } + if (c == '/' && next == '*') { + inBlockComment = true; + pos++; + continue; + } if (c == '"' || c == '\'') { inString = true; @@ -193,29 +216,47 @@ public class BracketMatcher { // String handling if (inString) { - if (escape) { escape = false; } - else if (c == '\\') { escape = true; } - else if (c == stringDelimiter) { inString = false; } - else if (c == '\n') { inString = false; } // Unterminated string + if (escape) { + escape = false; + } else if (c == '\\') { + escape = true; + } else if (c == stringDelimiter) { + inString = false; + } else if (c == '\n') { + inString = false; + } // Unterminated string continue; } // Block comment handling if (inBlockComment) { - if (c == '*' && next == '/') { inBlockComment = false; pos++; } + if (c == '*' && next == '/') { + inBlockComment = false; + pos++; + } continue; } // Line comment handling if (inLineComment) { - if (c == '\n') { inLineComment = false; } + if (c == '\n') { + inLineComment = false; + } continue; } // Start of comments - if (c == '/' && next == '/') { inLineComment = true; pos++; continue; } - if (c == '/' && next == '*') { inBlockComment = true; pos++; continue; } - + if (c == '/' && next == '/') { + inLineComment = true; + pos++; + continue; + } + if (c == '/' && next == '*') { + inBlockComment = true; + pos++; + continue; + } + // Start of string if (c == '"' || c == '\'') { inString = true; @@ -256,11 +297,12 @@ public class BracketMatcher { /** * Find which brace span should be highlighted based on bracket at position + * * @return int[2] {openLine, closeLine} or null */ public static int[] findHighlightedSpan(int bracketPos, List lines, List spans) { if (bracketPos < 0 || lines == null || spans == null) return null; - + // Find line containing bracket int bracketLineIdx = -1; for (int i = 0; i < lines.size(); i++) { @@ -270,7 +312,7 @@ public class BracketMatcher { break; } } - + if (bracketLineIdx >= 0) { for (int[] span : spans) { int openLine = span[1]; @@ -282,13 +324,13 @@ public class BracketMatcher { } return null; } - + /** * Check if a brace at given position has a matching close with same indent */ public static boolean hasMatchingCloseWithSameIndent(String text, int bracePos, List lines, int currentIndent) { if (text == null || lines == null || bracePos < 0) return false; - + // Find line containing the brace int openLineIdx = -1; for (int i = 0; i < lines.size(); i++) { @@ -298,9 +340,9 @@ public class BracketMatcher { break; } } - + if (openLineIdx < 0) return false; - + List spans = computeBraceSpans(text, lines); for (int[] span : spans) { if (span[1] == openLineIdx) { @@ -314,7 +356,7 @@ public class BracketMatcher { } return false; } - + /** * Get leading whitespace count of a line */ @@ -327,4 +369,4 @@ public class BracketMatcher { } return indent; } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java index e54f5d878..844da165b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ClassPathFinder.java @@ -1,6 +1,13 @@ package noppes.npcs.client.gui.util.script; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; /** * Helper class for resolving Java classpath imports and determining class types. @@ -21,14 +28,14 @@ public class ClassPathFinder { // Common java.lang classes (auto-imported in Java) public static final Set JAVA_LANG_CLASSES = new HashSet<>(Arrays.asList( - "Object", "String", "Class", "System", "Math", "Integer", "Double", "Float", "Long", "Short", "Byte", - "Character", "Boolean", "Number", "Void", "Thread", "Runnable", "Exception", "RuntimeException", - "Error", "Throwable", "StringBuilder", "StringBuffer", "Enum", "Comparable", "Iterable", - "CharSequence", "Cloneable", "Process", "ProcessBuilder", "Runtime", "SecurityManager", - "ClassLoader", "Package", "ArithmeticException", "ArrayIndexOutOfBoundsException", - "ClassCastException", "IllegalArgumentException", "IllegalStateException", - "IndexOutOfBoundsException", "NullPointerException", "NumberFormatException", - "UnsupportedOperationException", "AssertionError", "OutOfMemoryError", "StackOverflowError" + "Object", "String", "Class", "System", "Math", "Integer", "Double", "Float", "Long", "Short", "Byte", + "Character", "Boolean", "Number", "Void", "Thread", "Runnable", "Exception", "RuntimeException", + "Error", "Throwable", "StringBuilder", "StringBuffer", "Enum", "Comparable", "Iterable", + "CharSequence", "Cloneable", "Process", "ProcessBuilder", "Runtime", "SecurityManager", + "ClassLoader", "Package", "ArithmeticException", "ArrayIndexOutOfBoundsException", + "ClassCastException", "IllegalArgumentException", "IllegalStateException", + "IndexOutOfBoundsException", "NullPointerException", "NumberFormatException", + "UnsupportedOperationException", "AssertionError", "OutOfMemoryError", "StackOverflowError" )); /** @@ -193,10 +200,10 @@ public class ClassPathFinder { // Full resolution successful List classSegments = buildClassSegments(segments, classStartIdx, n); - return ResolveResult.found( - new ClassInfo(info.resolvedName, info.type, packagePortion.length()), - packagePortion, validClassPortion, classSegments, classStartIdx - ); + return ResolveResult.found( + new ClassInfo(info.resolvedName, info.type, packagePortion.length()), + packagePortion, validClassPortion, classSegments, classStartIdx + ); } } } @@ -243,7 +250,7 @@ public class ClassPathFinder { // Build the typed package path segment by segment and compare with cache StringBuilder sb = new StringBuilder(); int lastValidIndex = -1; - + for (int i = 0; i < classStartIdx; i++) { if (i > 0) sb.append("."); sb.append(segments[i]); @@ -272,7 +279,7 @@ public class ClassPathFinder { } } } - + // If we found some valid prefix but not all, return where it stopped being valid if (lastValidIndex > 0 && lastValidIndex < classStartIdx) { // Check if segment at lastValidIndex diverges from any known path @@ -282,15 +289,15 @@ public class ClassPathFinder { // There's a known continuation - check if typed segment matches String nextTyped = segments[lastValidIndex]; String cachedRemainder = cached.substring(validPrefix.length() + 1); - String nextCached = cachedRemainder.contains(".") ? - cachedRemainder.substring(0, cachedRemainder.indexOf('.')) : cachedRemainder; + String nextCached = cachedRemainder.contains(".") ? + cachedRemainder.substring(0, cachedRemainder.indexOf('.')) : cachedRemainder; if (!nextTyped.equals(nextCached)) { return lastValidIndex; // Divergence point } } } } - + return -1; // No typo detected } @@ -312,7 +319,7 @@ public class ClassPathFinder { registerValidPackage(packagePath); return true; } - + // Fallback: try to find any class under this package to validate it String[] testClasses = getTestClassesForPackage(packagePath); for (String testClass : testClasses) { @@ -580,7 +587,7 @@ public class ClassPathFinder { default: // For unknown packages, try common class suffixes return new String[]{ - packagePath + ".package-info", // Won't work but harmless + packagePath + ".package-info", // Won't work but harmless }; } } @@ -591,7 +598,7 @@ public class ClassPathFinder { * Parse generic type content and return type names with their positions. * Handles arbitrary nesting depth like "Map>>" * - * @param genericContent The content inside <...> (without the outer angle brackets) + * @param genericContent The content inside <...> (without the outer angle brackets) * @param importedClasses Map of simple name -> full class name for resolution * @return List of TypeOccurrence with position and type info */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/CommentHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/CommentHandler.java index b5c0f2205..ed586e304 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/CommentHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/CommentHandler.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; + import java.util.List; /** @@ -15,18 +16,18 @@ public class CommentHandler { public final String text; public final int delta; // Change in length (negative if comment removed) public final int nonWsIndex; // Index of first non-whitespace character - + public ToggleResult(String text, int delta, int nonWsIndex) { this.text = text; this.delta = delta; this.nonWsIndex = nonWsIndex; } - + public boolean wasCommented() { return delta < 0; } } - + /** * Toggle line comment on a single line */ @@ -36,11 +37,11 @@ public class CommentHandler { while (nonWs < lineText.length() && Character.isWhitespace(lineText.charAt(nonWs))) { nonWs++; } - + boolean hasContent = nonWs < lineText.length(); boolean hasComment = hasContent && lineText.startsWith("//", nonWs); String newText; - + if (hasComment) { // Remove comment int cut = Math.min(nonWs + 2, lineText.length()); @@ -52,10 +53,10 @@ public class CommentHandler { // Empty/whitespace line - no change newText = lineText; } - + return new ToggleResult(newText, newText.length() - lineText.length(), nonWs); } - + /** * Result of toggling comments on a selection */ @@ -63,33 +64,33 @@ public class CommentHandler { public final String newText; public final int newStartSelection; public final int newEndSelection; - + public SelectionToggleResult(String text, int start, int end) { this.newText = text; this.newStartSelection = start; this.newEndSelection = end; } } - + /** * Toggle comments on all lines in a selection */ public static SelectionToggleResult toggleCommentSelection( - String text, - List lines, - int startSelection, - int endSelection) { - + String text, + List lines, + int startSelection, + int endSelection) { + StringBuilder newText = new StringBuilder(text.length() + 100); int prevEnd = 0; int newStart = -1; int newEnd = -1; int newIndex = 0; - + for (LineData line : lines) { int safeLineStart = Math.max(0, Math.min(line.start, text.length())); int safeLineEnd = Math.max(0, Math.min(line.end, text.length())); - + // Handle gap between lines (shouldn't happen normally) if (safeLineStart > prevEnd) { if (newStart == -1 && startSelection >= prevEnd && startSelection <= safeLineStart) { @@ -101,7 +102,7 @@ public class CommentHandler { newText.append(text, prevEnd, safeLineStart); newIndex += safeLineStart - prevEnd; } - + // Check if this line is in selection if (safeLineEnd > startSelection && safeLineStart < endSelection) { String lineText = text.substring(safeLineStart, safeLineEnd); @@ -109,7 +110,7 @@ public class CommentHandler { String newLineText = tr.text; int nonWs = tr.nonWsIndex; int delta = tr.delta; - + // Update start selection position if (newStart == -1 && startSelection >= safeLineStart && startSelection <= safeLineEnd) { int offsetInOld = startSelection - safeLineStart; @@ -119,7 +120,7 @@ public class CommentHandler { } newStart = newIndex + Math.max(0, newOffset); } - + // Update end selection position if (newEnd == -1 && endSelection >= safeLineStart && endSelection <= safeLineEnd) { int offsetInOld = endSelection - safeLineStart; @@ -129,7 +130,7 @@ public class CommentHandler { } newEnd = newIndex + Math.max(0, newOffset); } - + newText.append(newLineText); newIndex += newLineText.length(); } else { @@ -145,7 +146,7 @@ public class CommentHandler { } prevEnd = safeLineEnd; } - + // Handle remaining text after last line if (prevEnd < text.length()) { if (newStart == -1 && startSelection >= prevEnd && startSelection <= text.length()) { @@ -157,35 +158,35 @@ public class CommentHandler { newText.append(text, prevEnd, text.length()); newIndex += text.length() - prevEnd; } - + // Fallback for positions not found if (newStart == -1) newStart = Math.max(0, Math.min(startSelection, newText.length())); if (newEnd == -1) newEnd = Math.max(0, Math.min(endSelection, newText.length())); - + return new SelectionToggleResult(newText.toString(), newStart, newEnd); } - + /** * Toggle comment on a single line at cursor position */ public static SingleLineToggleResult toggleCommentAtCursor( - String text, - List lines, - int cursorPosition) { - + String text, + List lines, + int cursorPosition) { + for (LineData line : lines) { int lineStart = Math.max(0, Math.min(line.start, text.length())); int lineEnd = Math.max(0, Math.min(line.end, text.length())); - + if (cursorPosition >= lineStart && cursorPosition <= lineEnd) { String lineText = text.substring(lineStart, lineEnd); ToggleResult tr = toggleLine(lineText); boolean hadComment = tr.delta < 0; - + String before = text.substring(0, lineStart); String after = lineEnd <= text.length() ? text.substring(lineEnd) : ""; String newText = before + tr.text + after; - + // Calculate new cursor position int cursorDelta = hadComment ? -2 : 2; int newCursor = cursorPosition + cursorDelta; @@ -193,25 +194,25 @@ public class CommentHandler { newCursor = cursorPosition; } newCursor = Math.max(lineStart, newCursor); - + return new SingleLineToggleResult(newText, newCursor); } } - + // No line found at cursor - return unchanged return new SingleLineToggleResult(text, cursorPosition); } - + /** * Result of toggling comment on single line */ public static class SingleLineToggleResult { public final String newText; public final int newCursorPosition; - + public SingleLineToggleResult(String text, int cursor) { this.newText = text; this.newCursorPosition = cursor; } } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/CursorNavigation.java b/src/main/java/noppes/npcs/client/gui/util/script/CursorNavigation.java index 2a39d5bbb..52faebe0c 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/CursorNavigation.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/CursorNavigation.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; + import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -9,25 +10,25 @@ import java.util.regex.Pattern; * Handles cursor navigation logic (up, down, left, right, word movement). */ public class CursorNavigation { - + /** * Move cursor up one line, maintaining column position */ public static int cursorUp(int cursorPosition, List lines, String text) { if (lines == null || lines.isEmpty()) return 0; - + for (int i = 0; i < lines.size(); i++) { LineData data = lines.get(i); boolean isLastLine = (i == lines.size() - 1); - boolean isOnLine = isLastLine - ? (cursorPosition >= data.start && cursorPosition <= data.end) - : (cursorPosition >= data.start && cursorPosition < data.end); - + boolean isOnLine = isLastLine + ? (cursorPosition >= data.start && cursorPosition <= data.end) + : (cursorPosition >= data.start && cursorPosition < data.end); + if (isOnLine) { if (i == 0) { return 0; // Already on first line } - + int column = Math.min(cursorPosition - data.start, data.text.length()); LineData target = lines.get(i - 1); int targetColumn = Math.min(column, target.text.length()); @@ -39,26 +40,26 @@ public class CursorNavigation { } return 0; } - + /** * Move cursor down one line, maintaining column position */ public static int cursorDown(int cursorPosition, List lines, String text) { if (lines == null || lines.isEmpty()) return text != null ? text.length() : 0; if (text != null && cursorPosition == text.length()) return cursorPosition; - + for (int i = 0; i < lines.size(); i++) { LineData data = lines.get(i); boolean isLastLine = (i == lines.size() - 1); - boolean isOnLine = isLastLine - ? (cursorPosition >= data.start && cursorPosition <= data.end) - : (cursorPosition >= data.start && cursorPosition < data.end); - + boolean isOnLine = isLastLine + ? (cursorPosition >= data.start && cursorPosition <= data.end) + : (cursorPosition >= data.start && cursorPosition < data.end); + if (isOnLine) { if (i >= lines.size() - 1) { return cursorPosition; // Already on last line } - + int column = Math.min(cursorPosition - data.start, data.text.length()); LineData target = lines.get(i + 1); int targetColumn = Math.min(column, target.text.length()); @@ -70,13 +71,13 @@ public class CursorNavigation { } return text != null ? text.length() : 0; } - + /** * Move cursor left, optionally by word */ public static int cursorLeft(int cursorPosition, String text, Pattern wordPattern, boolean byWord) { if (text == null || cursorPosition <= 0) return 0; - + int moveAmount = 1; if (byWord && wordPattern != null) { Matcher m = wordPattern.matcher(text.substring(0, cursorPosition)); @@ -88,7 +89,7 @@ public class CursorNavigation { } return Math.max(cursorPosition - moveAmount, 0); } - + /** * Move cursor right, optionally by word */ @@ -96,7 +97,7 @@ public class CursorNavigation { if (text == null) return 0; int textLen = text.length(); if (cursorPosition >= textLen) return textLen; - + int moveAmount = 1; if (byWord && wordPattern != null) { Matcher m = wordPattern.matcher(text.substring(cursorPosition)); @@ -108,45 +109,45 @@ public class CursorNavigation { } return Math.min(cursorPosition + moveAmount, textLen); } - + /** * Get cursor position from mouse click coordinates */ public static int getPositionFromMouse( - int mouseX, int mouseY, - int areaX, int areaY, - int gutterWidth, - int lineHeight, - int scrolledLine, - double scrollFracOffset, - List lines, - String text, - FontWidthProvider fontWidth) { - + int mouseX, int mouseY, + int areaX, int areaY, + int gutterWidth, + int lineHeight, + int scrolledLine, + double scrollFracOffset, + List lines, + String text, + FontWidthProvider fontWidth) { + int xMouse = mouseX - (areaX + gutterWidth + 1); int yMouse = mouseY - areaY - 1; - + // Account for fractional scroll offset double fracPixels = scrollFracOffset * lineHeight; yMouse = (int) Math.round(yMouse + fracPixels); - + if (lines == null || lines.isEmpty()) { return text != null ? text.length() : 0; } - + int visibleLines = Math.min(lines.size(), scrolledLine + 50); // Reasonable limit - + for (int i = 0; i < lines.size(); i++) { LineData data = lines.get(i); - + if (i >= scrolledLine && i <= visibleLines) { int yPos = (i - scrolledLine) * lineHeight; - + if (yMouse >= yPos && yMouse < yPos + lineHeight) { // Found the line, now find character position int lineWidth = 0; char[] chars = data.text.toCharArray(); - + for (int j = 1; j <= chars.length; j++) { int w = fontWidth.getWidth(data.text.substring(0, j)); if (xMouse < lineWidth + (w - lineWidth) / 2) { @@ -154,24 +155,24 @@ public class CursorNavigation { } lineWidth = w; } - + // Click past end of line - place cursor at end of text (not on newline) int posAfterChars = data.start + chars.length; return Math.min(posAfterChars, text.length()); } } } - + return text != null ? text.length() : 0; } - + /** * Interface for font width measurement */ public interface FontWidthProvider { int getWidth(String text); } - + /** * Get leading whitespace count of a line */ @@ -184,26 +185,26 @@ public class CursorNavigation { } return indent; } - + /** * Find line index containing given position */ public static int findLineIndex(int position, List lines) { if (lines == null || lines.isEmpty()) return 0; - + for (int i = 0; i < lines.size(); i++) { LineData line = lines.get(i); boolean isLastLine = (i == lines.size() - 1); - boolean isOnLine = isLastLine - ? (position >= line.start && position <= line.end) - : (position >= line.start && position < line.end); + boolean isOnLine = isLastLine + ? (position >= line.start && position <= line.end) + : (position >= line.start && position < line.end); if (isOnLine) { return i; } } return lines.size() - 1; } - + /** * Find line data containing given position */ @@ -214,4 +215,4 @@ public class CursorNavigation { } return null; } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java b/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java index 5c534cc94..a1eedce0b 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/FormatHelper.java @@ -6,7 +6,7 @@ import java.util.regex.Pattern; /** * Configurable code formatter with IntelliJ-like settings. - * + *

* Features: * - Operator spacing (a+b → a + b) * - Whitespace normalization (a b → a b) @@ -15,9 +15,9 @@ import java.util.regex.Pattern; * - Preserves string content and comments */ public class FormatHelper { - + // ==================== SETTINGS ==================== - + /** * Formatting configuration - similar to IntelliJ Code Style settings */ @@ -28,12 +28,12 @@ public class FormatHelper { public boolean spaceAroundComparison = true; // a == b, a != b, a <= b public boolean spaceAroundLogical = true; // a && b || c public boolean spaceAroundBitwise = true; // a & b | c ^ d - + // Spaces within brackets public boolean spaceWithinParens = false; // (a) vs ( a ) public boolean spaceWithinBrackets = false; // [a] vs [ a ] public boolean spaceWithinBraces = true; // {a} vs { a } (for initializers) - + // Spaces before/after public boolean spaceAfterComma = true; // a, b vs a,b public boolean spaceBeforeComma = false; // a, b vs a , b @@ -41,48 +41,48 @@ public class FormatHelper { public boolean spaceBeforeSemicolon = false; public boolean spaceAfterColon = true; // case a: b vs case a:b public boolean spaceBeforeColon = false; - + // Method calls public boolean spaceBeforeMethodParens = false; // foo() vs foo () public boolean spaceAfterTypeCast = true; // (int) x vs (int)x - + // Control statements public boolean spaceBeforeIfParens = true; // if (...) vs if(...) public boolean spaceBeforeWhileParens = true; public boolean spaceBeforeForParens = true; public boolean spaceBeforeSwitchParens = true; public boolean spaceBeforeCatchParens = true; - + // Braces public boolean spaceBeforeOpenBrace = true; // if () { vs if (){ - + // Line length public int maxLineLength = 120; public boolean wrapLongLines = false; // Auto-wrap lines longer than maxLineLength public int wrapIndentSpaces = 4; // Extra indent for wrapped lines public boolean wrapComments = true; // Also wrap long comments - + // Wrap break preferences (where to break long lines) public boolean wrapAfterComma = true; // Prefer breaking after commas public boolean wrapAfterOperator = true; // Prefer breaking after operators public boolean wrapBeforeDot = true; // Break before . for chained calls public boolean wrapMethodArguments = true; // Each arg on new line when wrapping method calls - + // Whitespace normalization public boolean normalizeWhitespace = true; // Multiple spaces → single space public boolean trimTrailingWhitespace = true; - + // Unary operators public boolean spaceAroundUnaryNot = false; // !a vs ! a public boolean spaceAroundUnaryIncDec = false; // ++i vs ++ i - + /** * Create default settings (similar to IntelliJ defaults) */ public static FormatSettings defaults() { return new FormatSettings(); } - + /** * Create compact settings (minimal spacing) */ @@ -95,53 +95,53 @@ public class FormatHelper { return s; } } - + private FormatSettings settings; - + public FormatHelper() { this.settings = FormatSettings.defaults(); } - + public FormatHelper(FormatSettings settings) { this.settings = settings != null ? settings : FormatSettings.defaults(); } - + public void setSettings(FormatSettings settings) { this.settings = settings; } - + public FormatSettings getSettings() { return settings; } - + // ==================== MAIN FORMAT METHOD ==================== - + /** * Format the entire text with all configured options */ public String format(String text) { if (text == null || text.isEmpty()) return text; - + // Process line by line to preserve structure String[] lines = text.split("\n", -1); StringBuilder result = new StringBuilder(); - + for (int i = 0; i < lines.length; i++) { String line = formatLine(lines[i], text, getLineStartOffset(lines, i)); - + if (settings.trimTrailingWhitespace) { line = trimTrailing(line); } - + result.append(line); if (i < lines.length - 1) { result.append("\n"); } } - + return result.toString(); } - + /** * Split multiple statements on one line into separate lines with proper indentation. * E.g., " this.a = 1; this.b = 2;" becomes two lines @@ -150,10 +150,10 @@ public class FormatHelper { private String splitStatementsOntoSeparateLines(String text) { String[] lines = text.split("\n", -1); StringBuilder result = new StringBuilder(); - + for (int lineIdx = 0; lineIdx < lines.length; lineIdx++) { String line = lines[lineIdx]; - + // Skip empty lines or comment-only lines String trimmed = line.trim(); if (trimmed.isEmpty() || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) { @@ -161,7 +161,7 @@ public class FormatHelper { if (lineIdx < lines.length - 1) result.append("\n"); continue; } - + // Extract leading indentation int indent = 0; while (indent < line.length() && (line.charAt(indent) == ' ' || line.charAt(indent) == '\t')) { @@ -169,14 +169,14 @@ public class FormatHelper { } String indentation = line.substring(0, indent); String content = line.substring(indent); - + // Split by semicolons (but preserve those in strings, for loops, etc.) List statements = splitBySemicolons(content); - + for (int si = 0; si < statements.size(); si++) { String stmt = statements.get(si).trim(); if (stmt.isEmpty()) continue; - + // Check if statement opens a scope { and has code after it int braceIdx = stmt.indexOf('{'); if (braceIdx >= 0 && braceIdx < stmt.length() - 1) { @@ -191,20 +191,20 @@ public class FormatHelper { continue; } } - + result.append(indentation).append(stmt); // Add semicolon back if it was removed if (si < statements.size() - 1) { result.append("\n"); } } - + if (lineIdx < lines.length - 1) result.append("\n"); } - + return result.toString(); } - + /** * Split content by semicolons, preserving those in strings and for-loops */ @@ -216,25 +216,25 @@ public class FormatHelper { boolean inChar = false; boolean escaped = false; int forLoopDepth = 0; // Track for(...) depth to preserve semicolons - + for (int i = 0; i < content.length(); i++) { char c = content.charAt(i); char prev = i > 0 ? content.charAt(i - 1) : ' '; - + // Track strings if (c == '\"' && !escaped && !inChar) { inString = !inString; } else if (c == '\'' && !escaped && !inString) { inChar = !inChar; } - + escaped = (c == '\\' && !escaped); - + if (inString || inChar) { current.append(c); continue; } - + // Track for loops if (c == '(' && i >= 3 && content.substring(Math.max(0, i - 3), i + 1).matches(".*\\bfor\\s*\\(")) { forLoopDepth++; @@ -247,7 +247,7 @@ public class FormatHelper { forLoopDepth = 0; } } - + // Semicolon: split if not in for-loop header if (c == ';' && forLoopDepth == 0) { current.append(c); @@ -255,18 +255,18 @@ public class FormatHelper { current = new StringBuilder(); continue; } - + current.append(c); } - + // Add remaining content if (current.length() > 0) { statements.add(current.toString()); } - + return statements; } - + private int getLineStartOffset(String[] lines, int lineIndex) { int offset = 0; for (int i = 0; i < lineIndex; i++) { @@ -274,13 +274,13 @@ public class FormatHelper { } return offset; } - + /** * Format a single line of code */ private String formatLine(String line, String fullText, int lineOffset) { if (line.trim().isEmpty()) return line; - + // Preserve leading indentation int indent = 0; while (indent < line.length() && (line.charAt(indent) == ' ' || line.charAt(indent) == '\t')) { @@ -288,27 +288,27 @@ public class FormatHelper { } String indentation = line.substring(0, indent); String content = line.substring(indent); - + if (content.isEmpty()) return line; - + // Skip formatting for lines that are entirely comments String trimmed = content.trim(); if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) { return line; } - + // Skip formatting for continuation lines (lines that start with operators, dots, etc.) // These are wrapped lines from a previous format and should be preserved as-is if (isContinuationLine(trimmed)) { return line; } - + // Format the content String formatted = formatContent(content, fullText, lineOffset + indent); - + return indentation + formatted; } - + /** * Check if a line appears to be a continuation of a previous line * (i.e., starts with an operator, dot, or other continuation character) @@ -317,7 +317,7 @@ public class FormatHelper { if (trimmedContent.isEmpty()) return false; char firstChar = trimmedContent.charAt(0); // Lines starting with these are likely continuations - if (firstChar == '.' || firstChar == '+' || firstChar == '-' || + if (firstChar == '.' || firstChar == '+' || firstChar == '-' || firstChar == '*' || firstChar == '/' || firstChar == '%' || firstChar == '&' || firstChar == '|' || firstChar == '^' || firstChar == '?' || firstChar == ':' || firstChar == ',') { @@ -329,53 +329,53 @@ public class FormatHelper { } return false; } - + /** * Format line content, preserving strings and comments */ private String formatContent(String content, String fullText, int offset) { // Find string/comment regions to preserve List preserveRegions = findPreserveRegions(content); - + if (preserveRegions.isEmpty()) { // No strings/comments, format everything return formatCodeSegment(content); } - + // Build result by formatting non-preserved regions StringBuilder result = new StringBuilder(); int lastEnd = 0; - + for (int[] region : preserveRegions) { // Format code before this preserved region if (region[0] > lastEnd) { String codeSegment = content.substring(lastEnd, region[0]); result.append(formatCodeSegment(codeSegment)); } - + // Append preserved region unchanged result.append(content.substring(region[0], region[1])); lastEnd = region[1]; } - + // Format remaining code after last preserved region if (lastEnd < content.length()) { result.append(formatCodeSegment(content.substring(lastEnd))); } - + return result.toString(); } - + /** * Find string literals and comments that should not be formatted */ private List findPreserveRegions(String content) { List regions = new ArrayList<>(); - + int i = 0; while (i < content.length()) { char c = content.charAt(i); - + // String literal if (c == '"') { int end = findStringEnd(content, i, '"'); @@ -383,7 +383,7 @@ public class FormatHelper { i = end; continue; } - + // Char literal if (c == '\'') { int end = findStringEnd(content, i, '\''); @@ -391,13 +391,13 @@ public class FormatHelper { i = end; continue; } - + // Line comment if (c == '/' && i + 1 < content.length() && content.charAt(i + 1) == '/') { regions.add(new int[]{i, content.length()}); break; } - + // Block comment (could span multiple lines but we process line-by-line) if (c == '/' && i + 1 < content.length() && content.charAt(i + 1) == '*') { int end = content.indexOf("*/", i + 2); @@ -410,13 +410,13 @@ public class FormatHelper { } continue; } - + i++; } - + return regions; } - + /** * Find end of string/char literal */ @@ -435,37 +435,37 @@ public class FormatHelper { } return content.length(); } - + // ==================== CODE SEGMENT FORMATTING ==================== - + /** * Format a code segment (no strings/comments) */ private String formatCodeSegment(String code) { if (code.isEmpty()) return code; - + String result = code; - + // Normalize whitespace first if (settings.normalizeWhitespace) { result = normalizeWhitespace(result); } - + // Apply operator spacing result = formatOperators(result); - + // Apply bracket spacing result = formatBrackets(result); - + // Apply keyword spacing result = formatKeywords(result); - + // Apply punctuation spacing result = formatPunctuation(result); - + return result; } - + /** * Normalize whitespace (multiple spaces → single space) */ @@ -473,13 +473,13 @@ public class FormatHelper { // Replace multiple spaces with single space, but preserve indentation return code.replaceAll(" +", " "); } - + /** * Format operator spacing */ private String formatOperators(String code) { String result = code; - + // Assignment operators if (settings.spaceAroundAssignment) { // Compound assignments first (longer patterns first) @@ -494,11 +494,11 @@ public class FormatHelper { result = formatBinaryOp(result, "<<=", " <<= "); result = formatBinaryOp(result, ">>=", " >>= "); result = formatBinaryOp(result, ">>>=", " >>>= "); - + // Simple assignment (but not ==, !=, <=, >=) result = formatSimpleAssignment(result); } - + // Comparison operators if (settings.spaceAroundComparison) { result = formatBinaryOp(result, "==", " == "); @@ -508,27 +508,27 @@ public class FormatHelper { // Simple < > (but not generics) result = formatComparisonOperators(result); } - + // Logical operators if (settings.spaceAroundLogical) { result = formatBinaryOp(result, "&&", " && "); result = formatBinaryOp(result, "||", " || "); } - + // Arithmetic operators if (settings.spaceAroundArithmetic) { // Handle ++ and -- first to avoid conflicts result = formatArithmeticOperators(result); } - + // Bitwise operators (but not in generics) if (settings.spaceAroundBitwise) { result = formatBitwiseOperators(result); } - + return result; } - + /** * Format a binary operator with proper spacing */ @@ -537,7 +537,7 @@ public class FormatHelper { String pattern = "\\s*" + Pattern.quote(op) + "\\s*"; return code.replaceAll(pattern, replacement); } - + /** * Format simple assignment = (not ==, !=, <=, >=, etc.) */ @@ -550,13 +550,13 @@ public class FormatHelper { // Check it's not part of ==, !=, <=, >=, +=, etc. char prev = i > 0 ? code.charAt(i - 1) : ' '; char next = i + 1 < code.length() ? code.charAt(i + 1) : ' '; - - boolean isCompound = prev == '!' || prev == '<' || prev == '>' || - prev == '+' || prev == '-' || prev == '*' || - prev == '/' || prev == '%' || prev == '&' || - prev == '|' || prev == '^'; + + boolean isCompound = prev == '!' || prev == '<' || prev == '>' || + prev == '+' || prev == '-' || prev == '*' || + prev == '/' || prev == '%' || prev == '&' || + prev == '|' || prev == '^'; boolean isEquality = next == '='; - + if (!isCompound && !isEquality) { // Ensure space before = if (result.length() > 0 && result.charAt(result.length() - 1) != ' ') { @@ -577,7 +577,7 @@ public class FormatHelper { } return result.toString(); } - + /** * Format < and > comparison operators (avoiding generics) */ @@ -586,11 +586,11 @@ public class FormatHelper { int i = 0; while (i < code.length()) { char c = code.charAt(i); - + if ((c == '<' || c == '>') && !isLikelyGeneric(code, i, c)) { char prev = i > 0 ? code.charAt(i - 1) : ' '; char next = i + 1 < code.length() ? code.charAt(i + 1) : ' '; - + // Skip if part of <<, >>, <=, >= if ((c == '<' && next == '<') || (c == '>' && next == '>') || next == '=' || prev == '<' || prev == '>') { @@ -612,7 +612,7 @@ public class FormatHelper { } return result.toString(); } - + /** * Check if < or > is likely part of a generic type declaration */ @@ -651,7 +651,7 @@ public class FormatHelper { return depth == 0; } } - + /** * Format arithmetic operators (+, -, *, /, %) */ @@ -662,9 +662,9 @@ public class FormatHelper { char c = code.charAt(i); char next = i + 1 < code.length() ? code.charAt(i + 1) : ' '; char prev = i > 0 ? code.charAt(i - 1) : ' '; - + boolean isArithOp = c == '+' || c == '-' || c == '*' || c == '/' || c == '%'; - + if (isArithOp) { // Skip ++, --, +=, -=, *=, /=, %=, and unary +/- if ((c == '+' && next == '+') || (c == '-' && next == '-') || @@ -691,41 +691,41 @@ public class FormatHelper { } return result.toString(); } - + /** * Check if position is in a unary context (operator is unary, not binary) */ private boolean isUnaryContext(String code, int pos) { if (pos == 0) return true; - + // Look back for context int i = pos - 1; while (i >= 0 && code.charAt(i) == ' ') { i--; } - + if (i < 0) return true; - + char prev = code.charAt(i); // Unary context after: ( [ { , ; = + - * / % < > & | ^ ? : - return prev == '(' || prev == '[' || prev == '{' || prev == ',' || - prev == ';' || prev == '=' || prev == '+' || prev == '-' || - prev == '*' || prev == '/' || prev == '%' || prev == '<' || - prev == '>' || prev == '&' || prev == '|' || prev == '^' || - prev == '?' || prev == ':' || prev == '!'; + return prev == '(' || prev == '[' || prev == '{' || prev == ',' || + prev == ';' || prev == '=' || prev == '+' || prev == '-' || + prev == '*' || prev == '/' || prev == '%' || prev == '<' || + prev == '>' || prev == '&' || prev == '|' || prev == '^' || + prev == '?' || prev == ':' || prev == '!'; } - + /** * Format bitwise operators */ private String formatBitwiseOperators(String code) { String result = code; - + // Shift operators first result = formatBinaryOp(result, ">>>", " >>> "); result = formatBinaryOp(result, "<<", " << "); result = formatBinaryOp(result, ">>", " >> "); - + // Single & | ^ (not && ||) StringBuilder sb = new StringBuilder(); int i = 0; @@ -733,7 +733,7 @@ public class FormatHelper { char c = result.charAt(i); char next = i + 1 < result.length() ? result.charAt(i + 1) : ' '; char prev = i > 0 ? result.charAt(i - 1) : ' '; - + if ((c == '&' && next != '&' && prev != '&' && next != '=') || (c == '|' && next != '|' && prev != '|' && next != '=') || (c == '^' && next != '=')) { @@ -749,18 +749,18 @@ public class FormatHelper { } i++; } - + return sb.toString(); } - + // ==================== BRACKET FORMATTING ==================== - + /** * Format bracket spacing */ private String formatBrackets(String code) { String result = code; - + // Parentheses if (settings.spaceWithinParens) { result = result.replaceAll("\\(\\s*", "( "); @@ -769,7 +769,7 @@ public class FormatHelper { result = result.replaceAll("\\(\\s+", "("); result = result.replaceAll("\\s+\\)", ")"); } - + // Square brackets if (settings.spaceWithinBrackets) { result = result.replaceAll("\\[\\s*", "[ "); @@ -778,69 +778,69 @@ public class FormatHelper { result = result.replaceAll("\\[\\s+", "["); result = result.replaceAll("\\s+\\]", "]"); } - + // Curly braces (for array initializers inline) // Note: standalone braces for blocks are handled by indentation logic - + return result; } - + // ==================== KEYWORD FORMATTING ==================== - + /** * Format keyword spacing (if, while, for, etc.) */ private String formatKeywords(String code) { String result = code; - + if (settings.spaceBeforeIfParens) { result = result.replaceAll("\\bif\\s*\\(", "if ("); } else { result = result.replaceAll("\\bif\\s+\\(", "if("); } - + if (settings.spaceBeforeWhileParens) { result = result.replaceAll("\\bwhile\\s*\\(", "while ("); } else { result = result.replaceAll("\\bwhile\\s+\\(", "while("); } - + if (settings.spaceBeforeForParens) { result = result.replaceAll("\\bfor\\s*\\(", "for ("); } else { result = result.replaceAll("\\bfor\\s+\\(", "for("); } - + if (settings.spaceBeforeSwitchParens) { result = result.replaceAll("\\bswitch\\s*\\(", "switch ("); } else { result = result.replaceAll("\\bswitch\\s+\\(", "switch("); } - + if (settings.spaceBeforeCatchParens) { result = result.replaceAll("\\bcatch\\s*\\(", "catch ("); } else { result = result.replaceAll("\\bcatch\\s+\\(", "catch("); } - + // Space before opening brace if (settings.spaceBeforeOpenBrace) { result = result.replaceAll("\\)\\s*\\{", ") {"); } else { result = result.replaceAll("\\)\\s+\\{", "){"); } - + return result; } - + // ==================== PUNCTUATION FORMATTING ==================== - + /** * Format punctuation spacing (commas, semicolons, colons) */ private String formatPunctuation(String code) { String result = code; - + // Commas if (settings.spaceAfterComma && !settings.spaceBeforeComma) { result = result.replaceAll("\\s*,\\s*", ", "); @@ -851,17 +851,17 @@ public class FormatHelper { } else { result = result.replaceAll("\\s*,\\s*", ","); } - + // Semicolons (but not in for loops - handle more carefully) // This is tricky because for(;;) should be handled differently // For now, just handle trailing semicolons if (settings.trimTrailingWhitespace) { result = result.replaceAll("\\s+;", ";"); } - + return result; } - + /** * Trim trailing whitespace from a line */ @@ -872,43 +872,43 @@ public class FormatHelper { } return line.substring(0, end); } - + // ==================== STATIC UTILITIES ==================== - + /** * Format code with default settings */ public static String formatCode(String code) { return new FormatHelper().format(code); } - + /** * Format code with custom settings */ public static String formatCode(String code, FormatSettings settings) { return new FormatHelper(settings).format(code); } - + // ==================== LINE WRAPPING ==================== - + /** * Wrap long lines according to settings. * Call this after format() if wrapLongLines is enabled. - * - * @param text The text to wrap + * + * @param text The text to wrap * @param maxWidth Maximum line width (use settings.maxLineLength if 0) * @return Text with wrapped lines */ public String wrapLines(String text, int maxWidth) { if (text == null || text.isEmpty()) return text; - + int width = maxWidth > 0 ? maxWidth : settings.maxLineLength; String[] lines = text.split("\n", -1); StringBuilder result = new StringBuilder(); - + for (int i = 0; i < lines.length; i++) { String line = lines[i]; - + if (getVisualLength(line) <= width) { result.append(line); } else { @@ -922,15 +922,15 @@ public class FormatHelper { result.append(wrapCodeLine(line, width)); } } - + if (i < lines.length - 1) { result.append("\n"); } } - + return result.toString(); } - + /** * Get visual length of a line (tabs count as 4 spaces) */ @@ -945,7 +945,7 @@ public class FormatHelper { } return len; } - + /** * Extract leading indentation from a line */ @@ -956,67 +956,67 @@ public class FormatHelper { } return line.substring(0, i); } - + /** * Wrap a single-line comment (// style) */ private String wrapCommentLine(String line, int maxWidth) { if (!settings.wrapComments) return line; - + String indent = extractIndent(line); String content = line.substring(indent.length()); - + // Must start with // if (!content.startsWith("//")) return line; - + String commentContent = content.substring(2).trim(); String prefix = indent + "// "; int prefixLen = getVisualLength(prefix); int contentWidth = maxWidth - prefixLen; - + if (contentWidth <= 10) return line; // Too narrow to wrap - + StringBuilder result = new StringBuilder(); result.append(indent).append("// "); - + String[] words = commentContent.split("\\s+"); int lineLen = prefixLen; - + for (int i = 0; i < words.length; i++) { String word = words[i]; int wordLen = word.length(); - + if (lineLen + wordLen > maxWidth && lineLen > prefixLen) { // Start new line result.append("\n").append(prefix); lineLen = prefixLen; } - + if (lineLen > prefixLen) { result.append(" "); lineLen++; } - + result.append(word); lineLen += wordLen; } - + return result.toString(); } - + /** * Wrap a block comment line (* style) */ private String wrapBlockCommentLine(String line, int maxWidth) { if (!settings.wrapComments) return line; - + String indent = extractIndent(line); String content = line.substring(indent.length()).trim(); - + // Determine prefix for wrapped lines String prefix; String textContent; - + if (content.startsWith("/**")) { prefix = indent + " * "; textContent = content.substring(3).trim(); @@ -1029,20 +1029,20 @@ public class FormatHelper { } else { return line; } - + // Handle closing */ boolean hasClose = textContent.endsWith("*/"); if (hasClose) { textContent = textContent.substring(0, textContent.length() - 2).trim(); } - + int prefixLen = getVisualLength(prefix); int contentWidth = maxWidth - prefixLen; - + if (contentWidth <= 10) return line; - + StringBuilder result = new StringBuilder(); - + // First line keeps original prefix if (content.startsWith("/**")) { result.append(indent).append("/** "); @@ -1051,72 +1051,72 @@ public class FormatHelper { } else { result.append(indent).append(" * "); } - + String[] words = textContent.split("\\s+"); int lineLen = prefixLen; - + for (int i = 0; i < words.length; i++) { String word = words[i]; int wordLen = word.length(); - + if (lineLen + wordLen > maxWidth && lineLen > prefixLen) { result.append("\n").append(prefix); lineLen = prefixLen; } - + if (lineLen > prefixLen) { result.append(" "); lineLen++; } - + result.append(word); lineLen += wordLen; } - + if (hasClose) { result.append(" */"); } - + return result.toString(); } - + /** * Wrap a code line at appropriate break points */ private String wrapCodeLine(String line, int maxWidth) { String indent = extractIndent(line); String content = line.substring(indent.length()); - + // Additional indent for wrapped lines String wrapIndent = indent + spaces(settings.wrapIndentSpaces); - + // Find break points - positions where we can break the line List breakPoints = findBreakPoints(content); - + if (breakPoints.isEmpty()) { // No good break points, just return as-is return line; } - + StringBuilder result = new StringBuilder(); result.append(indent); - + int currentLen = getVisualLength(indent); int lastBreak = 0; boolean isFirstLine = true; - + for (BreakPoint bp : breakPoints) { int segmentEnd = bp.position; String segment = content.substring(lastBreak, segmentEnd); int segmentLen = getVisualLength(segment); - + // Check if adding this segment would exceed max width if (currentLen + segmentLen > maxWidth && !isFirstLine) { // Break here result.append("\n").append(wrapIndent); currentLen = getVisualLength(wrapIndent); isFirstLine = false; - + // Trim leading space from continuation if any segment = trimLeading(segment); } else if (currentLen + segmentLen > maxWidth && isFirstLine && lastBreak > 0) { @@ -1125,29 +1125,29 @@ public class FormatHelper { currentLen = getVisualLength(wrapIndent); isFirstLine = false; } - + result.append(segment); currentLen += getVisualLength(segment); lastBreak = segmentEnd; isFirstLine = false; } - + // Append remaining content if (lastBreak < content.length()) { String remaining = content.substring(lastBreak); int remLen = getVisualLength(remaining); - + if (currentLen + remLen > maxWidth && !isFirstLine) { result.append("\n").append(wrapIndent); remaining = trimLeading(remaining); } - + result.append(remaining); } - + return result.toString(); } - + /** * Trim leading whitespace (Java 8 compatible) */ @@ -1158,7 +1158,7 @@ public class FormatHelper { } return s.substring(start); } - + /** * A potential line break point */ @@ -1166,29 +1166,29 @@ public class FormatHelper { int position; // Position in string (after this char) @SuppressWarnings("unused") int priority; // Lower = better place to break (for future sorting) - + BreakPoint(int pos, int prio) { this.position = pos; this.priority = prio; } } - + /** * Find good positions to break a line */ private List findBreakPoints(String content) { List points = new ArrayList<>(); - + int parenDepth = 0; int bracketDepth = 0; int braceDepth = 0; boolean inString = false; char stringChar = 0; - + for (int i = 0; i < content.length(); i++) { char c = content.charAt(i); char prev = i > 0 ? content.charAt(i - 1) : ' '; - + // Track string state if ((c == '"' || c == '\'') && prev != '\\') { if (!inString) { @@ -1198,9 +1198,9 @@ public class FormatHelper { inString = false; } } - + if (inString) continue; - + // Track bracket depth if (c == '(') parenDepth++; else if (c == ')') parenDepth--; @@ -1208,16 +1208,16 @@ public class FormatHelper { else if (c == ']') bracketDepth--; else if (c == '{') braceDepth++; else if (c == '}') braceDepth--; - + // Calculate total nesting depth (prefer breaking at lower depth) int depth = parenDepth + bracketDepth + braceDepth; - + // Find break points if (settings.wrapAfterComma && c == ',') { // After comma is a good break point (better at lower depth) points.add(new BreakPoint(i + 1, 1 + depth)); } - + if (settings.wrapAfterOperator) { // After binary operators if ((c == '+' || c == '-') && i > 0 && i < content.length() - 1) { @@ -1234,24 +1234,24 @@ public class FormatHelper { points.add(new BreakPoint(i + 2, 2 + depth)); } } - + if (settings.wrapBeforeDot && c == '.') { // Before dot (for method chaining) points.add(new BreakPoint(i, 2 + depth)); } - + // After opening brace if (c == '{' && i + 1 < content.length()) { points.add(new BreakPoint(i + 1, 1)); } } - + // Sort by position points.sort((a, b) -> Integer.compare(a.position, b.position)); - + return points; } - + /** * Create a string of n spaces */ @@ -1262,7 +1262,7 @@ public class FormatHelper { } return sb.toString(); } - + /** * Format and wrap code in one step */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java index 7a8217493..f054f32a7 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java @@ -5,7 +5,6 @@ import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.Gui; import net.minecraft.util.ChatAllowedCharacters; import net.minecraft.util.ResourceLocation; -import noppes.npcs.client.gui.util.GuiUtil; import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; import org.lwjgl.input.Keyboard; import org.lwjgl.opengl.GL11; @@ -13,7 +12,7 @@ import org.lwjgl.opengl.GL11; /** * Go To Line:Column dialog for the script editor. * Provides IntelliJ-like "Go to Line" functionality (Ctrl+G). - * + *

* Input format: "line" or "line:column" * Example: "42" goes to line 42, column 1 * Example: "42:15" goes to line 42, column 15 @@ -55,26 +54,35 @@ public class GoToLineDialog { * Callback interface for go-to-line operations */ public interface GoToLineCallback { - /** Get total line count */ + /** + * Get total line count + */ int getLineCount(); - /** Get the column count for a specific line (0-indexed) */ + /** + * Get the column count for a specific line (0-indexed) + */ int getColumnCount(int lineIndex); - /** Navigate to specific line and column (both 1-indexed for user display) */ + /** + * Navigate to specific line and column (both 1-indexed for user display) + */ void goToLineColumn(int line, int column); - /** Called when dialog closes - should restore focus to editor */ + /** + * Called when dialog closes - should restore focus to editor + */ void onDialogClose(); void unfocusMainEditor(); - + void focusMainEditor(); } - + public void setCallback(GoToLineCallback callback) { this.callback = callback; } + /** * Initialize/update position for the dialog */ @@ -163,7 +171,7 @@ public class GoToLineDialog { int fieldY = y + padding + 14 + 5; textFieldWidth = dialogWidth - padding * 2; drawTextField(fieldX, fieldY, textFieldWidth, textFieldHeight, inputText, cursor, - selectionStart, selectionEnd, scrollOffset, focused); + selectionStart, selectionEnd, scrollOffset, focused); if (inputText.isEmpty()) font.drawString("[Line][:Column]", x + padding + 6, y + padding + 23, 0xFF888888); @@ -228,7 +236,7 @@ public class GoToLineDialog { int maxSel = Math.max(adjustedSelStart, adjustedSelEnd); String beforeSel = visibleText.substring(0, Math.min(minSel, visibleText.length())); String inSel = visibleText.substring(Math.min(minSel, visibleText.length()), - Math.min(maxSel, visibleText.length())); + Math.min(maxSel, visibleText.length())); int selX = textX + font.getStringWidth(beforeSel); int selW = font.getStringWidth(inSel); Gui.drawRect(selX, textY - 1, selX + selW, textY + 9, 0xFF264f78); @@ -240,7 +248,7 @@ public class GoToLineDialog { // Draw cursor if (isFocused && shouldShowCursor()) { String beforeCursor = adjustedCursor > 0 && adjustedCursor <= visibleText.length() - ? visibleText.substring(0, adjustedCursor) : ""; + ? visibleText.substring(0, adjustedCursor) : ""; int cursorX = textX + font.getStringWidth(beforeCursor); Gui.drawRect(cursorX, textY - 1, cursorX + 1, textY + 9, 0xFFe0e0e0); } @@ -258,13 +266,13 @@ public class GoToLineDialog { int fieldX = x + padding; int fieldY = y + padding + 14; if (mouseX >= fieldX && mouseX < fieldX + textFieldWidth && - mouseY >= fieldY && mouseY < fieldY + textFieldHeight) { + mouseY >= fieldY && mouseY < fieldY + textFieldHeight) { focused = true; if (callback != null) callback.unfocusMainEditor(); // Position cursor based on click int clickX = mouseX - fieldX - 4; String visibleText = scrollOffset > 0 && scrollOffset < inputText.length() - ? inputText.substring(scrollOffset) : inputText; + ? inputText.substring(scrollOffset) : inputText; int newCursor = getCharIndexAtX(visibleText, clickX) + scrollOffset; cursor = Math.max(0, Math.min(newCursor, inputText.length())); selectionStart = cursor; @@ -297,7 +305,7 @@ public class GoToLineDialog { close(); return true; } - + if (!visible || !focused) return false; @@ -617,4 +625,4 @@ public class GoToLineDialog { public int getTotalHeight() { return visible ? dialogHeight + 10 : 0; } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java b/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java index a978dc724..12a5567ad 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/IndentHelper.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; + import java.util.Arrays; import java.util.List; @@ -8,9 +9,9 @@ import java.util.List; * Helper class for indentation-related operations. */ public class IndentHelper { - + public static final int TAB_SIZE = 4; - + /** * Get leading whitespace count of a line */ @@ -23,7 +24,7 @@ public class IndentHelper { } return leading; } - + /** * Extract leading whitespace as string */ @@ -31,7 +32,7 @@ public class IndentHelper { int indent = getLineIndent(lineText); return lineText.substring(0, indent); } - + /** * Create a string of n spaces */ @@ -41,16 +42,16 @@ public class IndentHelper { Arrays.fill(arr, ' '); return new String(arr); } - + /** * Calculate expected indent for a line based on context */ public static int getExpectedIndent(LineData currentLine, List allLines) { if (currentLine == null || allLines == null) return 0; - + int idx = allLines.indexOf(currentLine); if (idx <= 0) return 0; - + // Find previous non-empty line LineData prevLine = null; for (int i = idx - 1; i >= 0; i--) { @@ -60,40 +61,40 @@ public class IndentHelper { break; } } - + if (prevLine == null) return 0; - + int prevIndent = getLineIndent(prevLine.text); String prevTrimmed = prevLine.text.trim(); String currTrimmed = currentLine.text.trim(); - + // If current line starts with }, expected is one level back if (currTrimmed.startsWith("}")) { return Math.max(0, prevIndent - TAB_SIZE); } - + // If previous line ends with {, expected is one level forward if (prevTrimmed.endsWith("{")) { return prevIndent + TAB_SIZE; } - + return prevIndent; } - + /** * Calculate auto-indent for Enter key */ public static String getAutoIndentForEnter(String lineText, int cursorInLine) { int leading = 0; - while (leading < lineText.length() && - (lineText.charAt(leading) == ' ' || lineText.charAt(leading) == '\t')) { + while (leading < lineText.length() && + (lineText.charAt(leading) == ' ' || lineText.charAt(leading) == '\t')) { leading++; } - + String baseIndent = lineText.substring(0, leading); int relativeCursor = Math.max(0, Math.min(cursorInLine, lineText.length())); String beforeCursor = lineText.substring(0, relativeCursor); - + // Find last non-whitespace before cursor int lastNonWs = -1; for (int i = beforeCursor.length() - 1; i >= 0; i--) { @@ -102,7 +103,7 @@ public class IndentHelper { break; } } - + // Add extra indent if last char is { // Only add extra indent if cursor is actually after the opening brace (not just in trailing whitespace) // Check that the last non-whitespace is at or after the line's indent position @@ -110,10 +111,10 @@ public class IndentHelper { if (opensBlock) { return baseIndent + spaces(TAB_SIZE); } - + return baseIndent; } - + /** * Calculate target indent for Tab key */ @@ -136,62 +137,63 @@ public class IndentHelper { } } } - + /** * Calculate target indent for Shift+Tab */ public static int calculateShiftTabTargetIndent(int currentIndent) { return Math.max(0, ((currentIndent - 1) / TAB_SIZE) * TAB_SIZE); } - + /** * Format entire text with proper indentation and operator spacing */ public static FormatResult formatText(String text, int cursorPosition) { return formatText(text, cursorPosition, 0); } - + /** * Format entire text with proper indentation, operator spacing, and optional line wrapping - * @param text The text to format + * + * @param text The text to format * @param cursorPosition Current cursor position - * @param viewportWidth Viewport width for line wrapping (0 = no wrapping) + * @param viewportWidth Viewport width for line wrapping (0 = no wrapping) */ public static FormatResult formatText(String text, int cursorPosition, int viewportWidth) { // First pass: fix indentation String indented = formatIndentation(text, cursorPosition); - + // Second pass: apply operator spacing and whitespace normalization FormatHelper helper = new FormatHelper(); FormatHelper.FormatSettings settings = helper.getSettings(); - + // Enable wrapping if viewport width is provided if (viewportWidth > 0) { settings.wrapLongLines = true; settings.wrapComments = true; // Use 75% of viewport width as max line length for some margin int charWidth = 6; // Approximate character width - settings.maxLineLength = Math.max(60, (int)(viewportWidth * 0.8f / charWidth)); + settings.maxLineLength = Math.max(60, (int) (viewportWidth * 0.8f / charWidth)); } - + String formatted = helper.format(indented); - + // Apply wrapping if enabled if (viewportWidth > 0 && settings.wrapLongLines) { formatted = helper.wrapLines(formatted, settings.maxLineLength); } - + // Recalculate cursor position (try to maintain relative position) int newCursorPos = Math.min(cursorPosition, formatted.length()); - + // Try to find cursor on same line with same content context String[] origLines = text.split("\n", -1); String[] newLines = formatted.split("\n", -1); - + int lineStartPos = 0; int cursorLine = -1; int cursorColInLine = 0; - + for (int li = 0; li < origLines.length; li++) { int lineEndPos = lineStartPos + origLines[li].length(); if (cursorPosition >= lineStartPos && cursorPosition <= lineEndPos) { @@ -201,7 +203,7 @@ public class IndentHelper { } lineStartPos = lineEndPos + 1; } - + if (cursorLine >= 0 && cursorLine < newLines.length) { int newLineStart = 0; for (int li = 0; li < cursorLine; li++) { @@ -210,51 +212,66 @@ public class IndentHelper { // Clamp cursor to new line length newCursorPos = newLineStart + Math.min(cursorColInLine, newLines[cursorLine].length()); } - + return new FormatResult(formatted, newCursorPos); } - + /** * Format indentation only (first pass) */ private static String formatIndentation(String text, int cursorPosition) { String[] linesArr = text.split("\n", -1); StringBuilder out = new StringBuilder(text.length() + 32); - + boolean inString = false; boolean escape = false; boolean inBlockComment = false; int depth = 0; - + for (int li = 0; li < linesArr.length; li++) { String line = linesArr[li]; String trimmedLeading = line.replaceAll("^[ \\t]+", ""); int originalIndent = getLineIndent(line); - + int opens = 0, closes = 0; boolean startsWithClose = false; - + // Parse line for braces for (int idx = 0; idx < line.length(); idx++) { char c = line.charAt(idx); char next = idx + 1 < line.length() ? line.charAt(idx + 1) : 0; - + if (inString) { - if (escape) { escape = false; } - else if (c == '\\') { escape = true; } - else if (c == '"') { inString = false; } + if (escape) { + escape = false; + } else if (c == '\\') { + escape = true; + } else if (c == '"') { + inString = false; + } continue; } - + if (inBlockComment) { - if (c == '*' && next == '/') { inBlockComment = false; idx++; } + if (c == '*' && next == '/') { + inBlockComment = false; + idx++; + } continue; } - + if (c == '/' && next == '/') break; - if (c == '/' && next == '*') { inBlockComment = true; idx++; continue; } - if (c == '"') { inString = true; escape = false; continue; } - + if (c == '/' && next == '*') { + inBlockComment = true; + idx++; + continue; + } + if (c == '"') { + inString = true; + escape = false; + continue; + } + if (c == '{') { opens++; } else if (c == '}') { @@ -264,11 +281,11 @@ public class IndentHelper { } } } - + int indentLevel = depth; if (startsWithClose) indentLevel = Math.max(0, indentLevel - 1); int targetIndent = indentLevel * TAB_SIZE; - + // Check if this is a continuation line: // 1. Line starts with operator/dot (explicit continuation) // 2. Previous line ends with operator (implicit continuation - this line continues it) @@ -279,7 +296,7 @@ public class IndentHelper { String prevLine = linesArr[li - 1].trim(); isContinuation = lineEndsWithContinuationOperator(prevLine); } - + if (isContinuation && originalIndent > targetIndent) { // This is a continuation line - preserve its extra indent out.append(spaces(originalIndent)).append(trimmedLeading); @@ -287,13 +304,13 @@ public class IndentHelper { out.append(spaces(targetIndent)).append(trimmedLeading); } if (li < linesArr.length - 1) out.append('\n'); - + depth = Math.max(0, depth + opens - closes); } - + return out.toString(); } - + /** * Check if a line appears to be a continuation of a previous line * (i.e., starts with an operator, dot, or other continuation character) @@ -302,7 +319,7 @@ public class IndentHelper { if (trimmedContent.isEmpty()) return false; char firstChar = trimmedContent.charAt(0); // Lines starting with these are likely continuations - if (firstChar == '.' || firstChar == '+' || firstChar == '-' || + if (firstChar == '.' || firstChar == '+' || firstChar == '-' || firstChar == '*' || firstChar == '/' || firstChar == '%' || firstChar == '&' || firstChar == '|' || firstChar == '^' || firstChar == '?' || firstChar == ':' || firstChar == ',') { @@ -314,7 +331,7 @@ public class IndentHelper { } return false; } - + /** * Check if a line ends with an operator that indicates the next line is a continuation */ @@ -326,10 +343,10 @@ public class IndentHelper { trimmedLine = trimmedLine.substring(0, commentIdx).trim(); } if (trimmedLine.isEmpty()) return false; - + // Check if ends with operator char lastChar = trimmedLine.charAt(trimmedLine.length() - 1); - if (lastChar == '+' || lastChar == '-' || lastChar == '*' || + if (lastChar == '+' || lastChar == '-' || lastChar == '*' || lastChar == '/' || lastChar == '%' || lastChar == '=' || lastChar == '&' || lastChar == '|' || lastChar == '^' || lastChar == '?' || lastChar == ':' || lastChar == ',' || @@ -343,17 +360,17 @@ public class IndentHelper { } return false; } - + /** * Result of text formatting */ public static class FormatResult { public final String text; public final int cursorPosition; - + public FormatResult(String text, int cursorPosition) { this.text = text; this.cursorPosition = cursorPosition; } } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java index ccdd9cc44..fe09cb81a 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/JavaTextContainer.java @@ -11,37 +11,38 @@ import java.util.regex.Pattern; public class JavaTextContainer extends TextContainer { public static final Pattern MODIFIER = Pattern.compile( - "\\b(public|protected|private|static|final|abstract|synchronized|native|default)\\b"); + "\\b(public|protected|private|static|final|abstract|synchronized|native|default)\\b"); public static final Pattern KEYWORD = Pattern.compile( - "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof|import)\\b"); + "\\b(null|boolean|int|float|double|long|char|byte|short|void|if|else|switch|case|for|while|do|try|catch|finally|return|throw|var|let|const|function|continue|break|this|new|typeof|instanceof|import)\\b"); public static final Pattern CLASS_DECL = Pattern.compile("\\b(class|interface|enum)\\s+([A-Za-z_][a-zA-Z0-9_]*)"); - public static final Pattern NEW_TYPE = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)");; + public static final Pattern NEW_TYPE = Pattern.compile("\\bnew\\s+([A-Za-z_][a-zA-Z0-9_]*)"); + ; public static final Pattern METHOD_DECL = Pattern.compile("\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type - "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(" // method name + "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(" // method name ); public static final Pattern METHOD_CALL = Pattern.compile("([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\("); // Class-level global fields public static final Pattern GLOBAL_FIELD_DECL = Pattern.compile( - "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + "\\b([A-Za-z_][a-zA-Z0-9_<>\\[\\]]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); // Local fields inside methods (simplified) public static final Pattern LOCAL_FIELD_DECL = Pattern.compile( - "\\b([A-Z][a-zA-Z0-9_<>\\[\\]]*|[a-z][a-zA-Z0-9_]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + "\\b([A-Z][a-zA-Z0-9_<>\\[\\]]*|[a-z][a-zA-Z0-9_]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); public static final Pattern STRING = Pattern.compile("([\"'])(?:(?=(\\\\?))\\2.)*?\\1"); public static final Pattern COMMENT = Pattern.compile("/\\*[\\s\\S]*?(?:\\*/|$)|//.*|#.*"); public static final Pattern NUMBER = Pattern.compile( - "\\b-?(?:0[xX][\\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?(?:[fFbBdDlLsS])?|NaN|null|Infinity|true|false)\\b"); + "\\b-?(?:0[xX][\\dA-Fa-f]+|0[bB][01]+|0[oO][0-7]+|\\d*\\.?\\d+(?:[Ee][+-]?\\d+)?(?:[fFbBdDlLsS])?|NaN|null|Infinity|true|false)\\b"); - // Import statement: import [static] package.path.ClassName [.*]; (semicolon optional for live typing) - // Allows optional whitespace (including newlines) around the dots so wrapped imports match. - // Match imports while allowing optional whitespace/newlines around dots. - public static final Pattern IMPORT = Pattern.compile( - "(?m)\\bimport\\s+(?:static\\s+)?([A-Za-z_][A-Za-z0-9_]*(?:\\s*\\.\\s*[A-Za-z_][A-Za-z0-9_]*)*)(?:\\s*\\.\\s*\\*?)?\\s*(?:;|$)"); + // Import statement: import [static] package.path.ClassName [.*]; (semicolon optional for live typing) + // Allows optional whitespace (including newlines) around the dots so wrapped imports match. + // Match imports while allowing optional whitespace/newlines around dots. + public static final Pattern IMPORT = Pattern.compile( + "(?m)\\bimport\\s+(?:static\\s+)?([A-Za-z_][A-Za-z0-9_]*(?:\\s*\\.\\s*[A-Za-z_][A-Za-z0-9_]*)*)(?:\\s*\\.\\s*\\*?)?\\s*(?:;|$)"); // ClassPathFinder for resolving imports and determining class types private final ClassPathFinder classPathFinder = new ClassPathFinder(); @@ -73,10 +74,10 @@ public class JavaTextContainer extends TextContainer { return pathStart + orig.length(); } - public void init(String text,int width, int height) { + public void init(String text, int width, int height) { this.text = text == null ? "" : text.replaceAll("\\r?\\n|\\r", "\n"); lines.clear(); - String[] split = text.split("\n",-1); + String[] split = text.split("\n", -1); int totalChars = 0; for (String l : split) { @@ -196,32 +197,38 @@ public class JavaTextContainer extends TextContainer { } } } - + /** * Parse and highlight generic type parameters using ClassPathFinder. * Handles arbitrarily nested generics like "Map>>". */ private void highlightGenericTypes(String genericContent, int contentStart, List marks, List excluded) { if (genericContent == null || genericContent.isEmpty()) return; - + List occurrences = classPathFinder.parseGenericTypes(genericContent, importedClasses, importedPackages); - + for (ClassPathFinder.TypeOccurrence occ : occurrences) { int absStart = contentStart + occ.startOffset; int absEnd = contentStart + occ.endOffset; - + if (!isInExcludedRange(absStart, excluded)) { TokenType tokenType; switch (occ.type) { - case INTERFACE: tokenType = TokenType.INTERFACE_DECL; break; - case ENUM: tokenType = TokenType.ENUM_DECL; break; - default: tokenType = TokenType.IMPORTED_CLASS; break; + case INTERFACE: + tokenType = TokenType.INTERFACE_DECL; + break; + case ENUM: + tokenType = TokenType.ENUM_DECL; + break; + default: + tokenType = TokenType.IMPORTED_CLASS; + break; } marks.add(new Mark(absStart, absEnd, tokenType)); } } } - + private void collectFields() { globalFields.clear(); localFields.clear(); @@ -229,7 +236,7 @@ public class JavaTextContainer extends TextContainer { // Extract method blocks first methodBlocks = MethodBlock.collectMethodBlocks(text); - + // Get excluded ranges (strings and comments) for the entire text List excludedRanges = MethodBlock.getExcludedRanges(text); @@ -238,7 +245,7 @@ public class JavaTextContainer extends TextContainer { while (mGlobal.find()) { String varName = mGlobal.group(2); int varPosition = mGlobal.start(2); - + // Skip if inside a string or comment if (isInExcludedRange(varPosition, excludedRanges)) { continue; @@ -298,7 +305,7 @@ public class JavaTextContainer extends TextContainer { "String", "Object", "Array", "Math", "System", "Integer", "Double", "Float", "Boolean", "Long", "Byte", "Short", "Character", "List", "Map", "Set" )); - + // First pass: handle field accesses (this.field and obj.field patterns) Pattern thisFieldPattern = Pattern.compile("\\bthis\\s*\\.\\s*([a-zA-Z_][a-zA-Z0-9_]*)"); Matcher thisFieldMatcher = thisFieldPattern.matcher(text); @@ -312,7 +319,7 @@ public class JavaTextContainer extends TextContainer { marks.add(new Mark(fieldPos, thisFieldMatcher.end(1), TokenType.UNDEFINED_VAR)); } } - + // Handle identifier.field patterns (e.g., obj.field, global.field) // The field part should be highlighted as GLOBAL_FIELD (light blue) // Also, detect chained accesses like `a.b.c` and treat subsequent segments @@ -378,13 +385,13 @@ public class JavaTextContainer extends TextContainer { marks.add(new Mark(fieldPos, objFieldMatcher.end(2), TokenType.GLOBAL_FIELD)); } } - + Pattern identifier = Pattern.compile("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b"); Matcher m = identifier.matcher(text); while (m.find()) { String name = m.group(1); int position = m.start(1); - + // Skip known keywords if (knownIdentifiers.contains(name)) continue; @@ -462,24 +469,24 @@ public class JavaTextContainer extends TextContainer { } return false; } - + /** * Check if the identifier at this position is a field access (preceded by a dot) * e.g., in "obj.field", the "field" part is a field access */ private boolean isFieldAccess(int position) { if (position <= 0) return false; - + // Look backwards from position, skipping any whitespace int i = position - 1; while (i >= 0 && Character.isWhitespace(text.charAt(i))) { i--; } - + // Check if preceded by a dot return i >= 0 && text.charAt(i) == '.'; } - + /** * Check if the identifier at this position is a method call (followed by parenthesis) */ @@ -497,7 +504,7 @@ public class JavaTextContainer extends TextContainer { // Check for opening paren return i < text.length() && text.charAt(i) == '('; } - + /** * Check if this looks like a type reference (e.g., part of a declaration or generic) * But NOT a comparison like "i < container" which uses < as less-than operator @@ -510,7 +517,7 @@ public class JavaTextContainer extends TextContainer { return true; } } - + // Check if preceded by < but make sure it's a generic, not a comparison // Generic: Type tokenStarts = new ArrayList<>(); List tokenEnds = new ArrayList<>(); Matcher tokm = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*").matcher(orig); - while (tokm.find()) { tokens.add(tokm.group()); tokenStarts.add(tokm.start()); tokenEnds.add(tokm.end()); } + while (tokm.find()) { + tokens.add(tokm.group()); + tokenStarts.add(tokm.start()); + tokenEnds.add(tokm.end()); + } int tokenCount = tokenStarts.size(); if (tokenCount > 0) { @@ -713,9 +736,15 @@ public class JavaTextContainer extends TextContainer { TokenType segType = TokenType.IMPORTED_CLASS; if (outerInfo != null) { switch (outerInfo.type) { - case INTERFACE: segType = TokenType.INTERFACE_DECL; break; - case ENUM: segType = TokenType.ENUM_DECL; break; - default: segType = TokenType.IMPORTED_CLASS; break; + case INTERFACE: + segType = TokenType.INTERFACE_DECL; + break; + case ENUM: + segType = TokenType.ENUM_DECL; + break; + default: + segType = TokenType.IMPORTED_CLASS; + break; } } marks.add(new Mark(s, e, segType)); @@ -748,7 +777,6 @@ public class JavaTextContainer extends TextContainer { } } - private void collectImportedClassUsages(List marks) { List excluded = MethodBlock.getExcludedRanges(text); @@ -779,7 +807,7 @@ public class JavaTextContainer extends TextContainer { boolean isShadowed = globalFields.contains(className) || localFields.contains(className); if (block != null) { isShadowed = isShadowed || block.parameters.contains(className) || block.isLocalDeclaredAtPosition( - className, start); + className, start); } if (isImported && !isShadowed) { @@ -788,8 +816,12 @@ public class JavaTextContainer extends TextContainer { TokenType tokenType = TokenType.IMPORTED_CLASS; if (info != null) { switch (info.type) { - case INTERFACE: tokenType = TokenType.INTERFACE_DECL; break; - case ENUM: tokenType = TokenType.ENUM_DECL; break; + case INTERFACE: + tokenType = TokenType.INTERFACE_DECL; + break; + case ENUM: + tokenType = TokenType.ENUM_DECL; + break; } } marks.add(new Mark(start, end, tokenType)); @@ -819,7 +851,7 @@ public class JavaTextContainer extends TextContainer { } } } - + public void formatCodeText() { // Step 1: Tokenize the full text List marks = new ArrayList<>(); @@ -855,7 +887,7 @@ public class JavaTextContainer extends TextContainer { // Compute indent guides based on matched braces, ignoring strings/comments computeIndentGuides(marks); - + // Step 2: Clear existing tokens for (LineData line : lines) { line.tokens.clear(); @@ -880,7 +912,7 @@ public class JavaTextContainer extends TextContainer { if (cursor < tokenStart) { int end = Math.min(tokenStart, text.length()); line.tokens.add( - new Token(text.substring(cursor, end), TokenType.DEFAULT, cursor, end)); + new Token(text.substring(cursor, end), TokenType.DEFAULT, cursor, end)); } // token text @@ -910,50 +942,53 @@ public class JavaTextContainer extends TextContainer { private void collectTypeDeclarations(List marks) { List excluded = MethodBlock.getExcludedRanges(text); - + // Pattern to find type declarations: Type<...> variableName or Type variableName // We'll manually parse the generic portion to handle arbitrary nesting Pattern typeStart = Pattern.compile( "(?:(?:public|private|protected|static|final|transient|volatile)\\s+)*" + - "([A-Z][a-zA-Z0-9_]*)\\s*" // Group 1 → type name (must start with uppercase) + "([A-Z][a-zA-Z0-9_]*)\\s*" // Group 1 → type name (must start with uppercase) ); - + for (LineData ld : lines) { int lineStart = ld.start; int lineEnd = ld.end; if (lineStart >= lineEnd) continue; String s = ld.text; - + Matcher m = typeStart.matcher(s); int searchFrom = 0; - + while (m.find(searchFrom)) { int typeNameStart = lineStart + m.start(1); int typeNameEnd = lineStart + m.end(1); - + // Skip if in excluded range boolean skip = false; for (int[] r : excluded) { - if (typeNameStart < r[1] && typeNameEnd > r[0]) { skip = true; break; } + if (typeNameStart < r[1] && typeNameEnd > r[0]) { + skip = true; + break; + } } if (skip) { searchFrom = m.end(); continue; } - + String typeName = m.group(1); int posAfterType = m.end(1); - + // Skip whitespace after type name while (posAfterType < s.length() && Character.isWhitespace(s.charAt(posAfterType))) { posAfterType++; } - + // Check for generic parameters String genericContent = null; int genericStart = -1; int genericEnd = -1; - + if (posAfterType < s.length() && s.charAt(posAfterType) == '<') { genericStart = posAfterType; int depth = 1; @@ -970,12 +1005,12 @@ public class JavaTextContainer extends TextContainer { posAfterType = genericEnd; } } - + // Skip whitespace after generics while (posAfterType < s.length() && Character.isWhitespace(s.charAt(posAfterType))) { posAfterType++; } - + // Check if this looks like a type declaration: // 1. Has generic content (e.g., List) - always a type // 2. Followed by a variable name (lowercase start or underscore) @@ -983,38 +1018,43 @@ public class JavaTextContainer extends TextContainer { boolean hasGeneric = genericContent != null && !genericContent.isEmpty(); boolean atEndOfLine = posAfterType >= s.length(); boolean followedByVarName = false; - + if (!atEndOfLine) { char nextChar = s.charAt(posAfterType); followedByVarName = Character.isLetter(nextChar) || nextChar == '_'; } - + // Accept as type if: has generics OR followed by variable name // This handles both `List myList` and wrapped lines like `List` if (hasGeneric || followedByVarName) { // This looks like a type declaration - + // Resolve the main type ClassPathFinder.ClassInfo info = classPathFinder.resolveSimpleName(typeName, importedClasses, importedPackages); TokenType tokenType = TokenType.UNDEFINED_VAR; if (info != null) { switch (info.type) { - case INTERFACE: tokenType = TokenType.INTERFACE_DECL; break; - case ENUM: tokenType = TokenType.ENUM_DECL; break; - default: tokenType = TokenType.TYPE_DECL; + case INTERFACE: + tokenType = TokenType.INTERFACE_DECL; + break; + case ENUM: + tokenType = TokenType.ENUM_DECL; + break; + default: + tokenType = TokenType.TYPE_DECL; } } marks.add(new Mark(typeNameStart, typeNameEnd, tokenType)); - + // Handle generic content if (hasGeneric && genericStart >= 0) { int absGenericStart = lineStart + genericStart; int absGenericEnd = lineStart + genericEnd; - + // Mark < and > as default marks.add(new Mark(absGenericStart, absGenericStart + 1, TokenType.DEFAULT)); marks.add(new Mark(absGenericEnd - 1, absGenericEnd, TokenType.DEFAULT)); - + // Parse and highlight all types in generic content int contentStart = lineStart + genericStart + 1; highlightGenericTypes(genericContent, contentStart, marks, excluded); @@ -1046,7 +1086,7 @@ public class JavaTextContainer extends TextContainer { } } } - + searchFrom = m.end(); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java index ef9567779..6158c85ed 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/MethodBlock.java @@ -11,17 +11,17 @@ public class MethodBlock { // pattern matches returnType + methodName + parentheses public static final Pattern METHOD_PATTERN = Pattern.compile( - "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type - "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(" // method name + "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type + "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(" // method name ); // Pattern for variable declarations (same as in JavaTextContainer) public static final Pattern LOCAL_VAR_DECL = Pattern.compile( - "\\b([A-Z][a-zA-Z0-9_<>\\[\\]]*|[a-z][a-zA-Z0-9_]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); - + "\\b([A-Z][a-zA-Z0-9_<>\\[\\]]*|[a-z][a-zA-Z0-9_]*)\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*(=|;)"); + // Pattern for method parameters: Type name (with optional ... for varargs) public static final Pattern PARAMETER_PATTERN = Pattern.compile( - "([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*(?:\\.{3})?)\\s+([a-zA-Z_][a-zA-Z0-9_]*)"); + "([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*(?:\\.{3})?)\\s+([a-zA-Z_][a-zA-Z0-9_]*)"); public int startOffset; public int endOffset; @@ -37,22 +37,22 @@ public class MethodBlock { extractParameters(); extractLocalVariables(); } - + // Extract method parameters from the method signature private void extractParameters() { parameters.clear(); - + // Find the parameter list between ( and ) int parenStart = text.indexOf('('); int parenEnd = text.indexOf(')'); - + if (parenStart < 0 || parenEnd <= parenStart) return; - + String paramList = text.substring(parenStart + 1, parenEnd); - + // Skip if empty if (paramList.trim().isEmpty()) return; - + // Split by comma (but be careful of generic types like List) Matcher m = PARAMETER_PATTERN.matcher(paramList); while (m.find()) { @@ -67,20 +67,20 @@ public class MethodBlock { private void extractLocalVariables() { localVariables.clear(); localVarPositions.clear(); - + // Get excluded ranges (strings and comments) for this method's text List excludedRanges = getExcludedRanges(text); - + Matcher m = LOCAL_VAR_DECL.matcher(text); while (m.find()) { String varName = m.group(2); int declPosition = m.start(); - + // Skip if inside a string or comment if (isInExcludedRange(declPosition, excludedRanges)) { continue; } - + // Check if this is a "this.field" assignment - if so, skip it // Look backwards from the declaration position to see if preceded by "this." boolean isThisFieldAssignment = false; @@ -91,7 +91,7 @@ public class MethodBlock { isThisFieldAssignment = true; } } - + if (!isThisFieldAssignment && !localVariables.contains(varName)) { localVariables.add(varName); // Store the absolute position in the full text (startOffset + relative position) @@ -99,7 +99,7 @@ public class MethodBlock { } } } - + // Check if a reference position comes before the local variable declaration // This handles cases like: global.field on line 7, then var global = "x" on line 9 // At line 7, the local 'global' doesn't exist yet, so it should refer to the global 'global' @@ -129,10 +129,10 @@ public class MethodBlock { // Matches: returnType methodName(...) { (non-greedy capture until first { ) Pattern methodPattern = Pattern.compile( - "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type - "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\([^)]*\\)\\s*" + // method name + parameters - "\\{", // opening brace - Pattern.MULTILINE); + "\\b([a-zA-Z_][a-zA-Z0-9_<>\\[\\]]*)\\s+" + // return type + "([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\([^)]*\\)\\s*" + // method name + parameters + "\\{", // opening brace + Pattern.MULTILINE); Matcher m = methodPattern.matcher(text); while (m.find()) { @@ -160,14 +160,14 @@ public class MethodBlock { Pattern stringPattern = Pattern.compile("([\"'])(?:(?=(\\\\?))\\2.)*?\\1"); Matcher stringMatcher = stringPattern.matcher(text); while (stringMatcher.find()) { - ranges.add(new int[] { stringMatcher.start(), stringMatcher.end() }); + ranges.add(new int[]{stringMatcher.start(), stringMatcher.end()}); } // Find all comments (block and line comments) Pattern commentPattern = Pattern.compile("/\\*[\\s\\S]*?(?:\\*/|$)|//.*"); Matcher commentMatcher = commentPattern.matcher(text); while (commentMatcher.find()) { - ranges.add(new int[] { commentMatcher.start(), commentMatcher.end() }); + ranges.add(new int[]{commentMatcher.start(), commentMatcher.end()}); } if (ranges.isEmpty()) diff --git a/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java b/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java index 391ea0e14..5dd70e7ce 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/PackageFinder.java @@ -7,7 +7,6 @@ import java.net.URL; import java.net.URLClassLoader; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Enumeration; import java.util.HashSet; import java.util.Set; diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java index 4f36da54b..3d5c9909d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -1,7 +1,6 @@ package noppes.npcs.client.gui.util.script; import net.minecraft.client.gui.GuiScreen; -import noppes.npcs.client.ClientProxy; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; import org.lwjgl.input.Keyboard; @@ -12,7 +11,7 @@ import java.util.regex.Pattern; /** * Handles IntelliJ-like rename refactoring with scope-aware renaming. - * + *

* Features: * - Scope detection: local variables rename only within their block, global fields rename everywhere * - Visual feedback: primary occurrence has white border, others just highlighted @@ -33,7 +32,7 @@ public class RenameRefactorHandler { private int primaryOccurrenceEnd = -1; private List allOccurrences = new ArrayList<>(); private ScopeInfo scope = null; - + // For global scope, track positions that have local shadowing (to exclude them) private List localShadowedPositions = new ArrayList<>(); @@ -141,10 +140,10 @@ public class RenameRefactorHandler { initialWord = text.substring(wordBounds[0], wordBounds[1]); // Remember for undo originalWord = initialWord; currentWord = initialWord; - + // Determine scope for this identifier - MUST check if local shadows global scope = determineScope(text, wordBounds[0], originalWord, callback.getContainer()); - + // If global scope, find all positions where local variables shadow this name localShadowedPositions.clear(); if (scope != null && scope.isGlobal) { @@ -158,12 +157,12 @@ public class RenameRefactorHandler { return false; active = true; - + // Set selection to full word (uses GuiScriptTextArea's selection) primaryOccurrenceStart = wordBounds[0]; primaryOccurrenceEnd = wordBounds[1]; callback.getSelectionState().setSelection(wordBounds[0], wordBounds[1]); - // callback.setCursorPosition(wordBounds[1]); + // callback.setCursorPosition(wordBounds[1]); callback.getSelectionState().markActivity(); return true; @@ -207,7 +206,7 @@ public class RenameRefactorHandler { // During live editing we used setTextWithoutUndo to avoid creating intermediate // undo entries; pushing the original snapshot here gives a single predictable // undo step back to the pre-rename state. - if (originalText != null && !originalText.isEmpty()) + if (originalText != null && !originalText.isEmpty()) callback.pushUndoState(originalText, originalCursorPos); // Calculate cursor position relative to primary occurrence @@ -237,6 +236,7 @@ public class RenameRefactorHandler { /** * Handle keyboard input during rename + * * @return true if input was consumed */ public boolean keyTyped(char c, int keyCode) { @@ -264,7 +264,7 @@ public class RenameRefactorHandler { // Get selection state SelectionState sel = callback.getSelectionState(); boolean hasSelection = sel.hasSelection(); - + // Calculate cursor position within the word int cursorInWord = callback.getCursorPosition() - primaryOccurrenceStart; cursorInWord = Math.max(0, Math.min(cursorInWord, currentWord.length())); @@ -315,7 +315,7 @@ public class RenameRefactorHandler { sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart); callback.setCursorPosition(primaryOccurrenceStart); } - + // Left arrow if (keyCode == Keyboard.KEY_LEFT) { if (cursorInWord > 0) { @@ -354,7 +354,7 @@ public class RenameRefactorHandler { // Ctrl+A - select all (select the whole word) if (keyCode == Keyboard.KEY_A && - Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) { + Keyboard.isKeyDown(Keyboard.KEY_LCONTROL)) { sel.setSelection(primaryOccurrenceStart, primaryOccurrenceStart + currentWord.length()); callback.setCursorPosition(primaryOccurrenceStart + currentWord.length()); sel.markActivity(); @@ -537,22 +537,22 @@ public class RenameRefactorHandler { } } } - + /** * Find all positions where local variables with the same name shadow the global variable. * This populates localShadowedPositions with ranges where local declarations exist. */ private void findLocalShadowedPositions(String text, String varName) { localShadowedPositions.clear(); - + List methods = MethodBlock.collectMethodBlocks(text); List excluded = MethodBlock.getExcludedRanges(text); - + for (MethodBlock method : methods) { // Check if this method has a local variable or parameter with the same name boolean hasLocalDecl = method.localVariables.contains(varName); boolean hasParamDecl = isParameterInMethod(text, varName, method); - + if (hasLocalDecl || hasParamDecl) { // Find where the local/param scope starts int scopeStart; @@ -564,18 +564,18 @@ public class RenameRefactorHandler { scopeStart = findLocalDeclarationPosition(text, varName, method); if (scopeStart < 0) scopeStart = method.startOffset; } - + // Find all occurrences of varName within this method's shadowed range Pattern pattern = Pattern.compile("\\b" + Pattern.quote(varName) + "\\b"); Matcher m = pattern.matcher(text); - + while (m.find()) { int pos = m.start(); - + // Skip if in string or comment if (isInExcludedRange(pos, excluded)) continue; - + // Check if within the shadowed scope of this method if (pos >= scopeStart && pos < method.endOffset) { localShadowedPositions.add(new int[]{m.start(), m.end()}); @@ -584,7 +584,7 @@ public class RenameRefactorHandler { } } } - + /** * Check if a position is within a locally shadowed range */ @@ -595,13 +595,13 @@ public class RenameRefactorHandler { } return false; } - + /** * Check if varName is a parameter in the method */ private boolean isParameterInMethod(String text, String varName, MethodBlock method) { String methodHeader = text.substring(method.startOffset, - Math.min(method.startOffset + 500, method.endOffset)); + Math.min(method.startOffset + 500, method.endOffset)); int parenStart = methodHeader.indexOf('('); int parenEnd = methodHeader.indexOf(')'); if (parenStart >= 0 && parenEnd > parenStart) { @@ -687,7 +687,7 @@ public class RenameRefactorHandler { // Cursor is inside a method // Check if it's a method parameter first String methodHeader = text.substring(containingMethod.startOffset, - Math.min(containingMethod.startOffset + 500, containingMethod.endOffset)); + Math.min(containingMethod.startOffset + 500, containingMethod.endOffset)); int parenStart = methodHeader.indexOf('('); int parenEnd = methodHeader.indexOf(')'); if (parenStart >= 0 && parenEnd > parenStart) { @@ -861,9 +861,9 @@ public class RenameRefactorHandler { public boolean isWordFullySelected() { if (callback == null) return false; SelectionState selection = callback.getSelectionState(); - return selection.hasSelection() && - selection.getStartSelection() == primaryOccurrenceStart && - selection.getEndSelection() == primaryOccurrenceEnd; + return selection.hasSelection() && + selection.getStartSelection() == primaryOccurrenceStart && + selection.getEndSelection() == primaryOccurrenceEnd; } /** @@ -884,7 +884,7 @@ public class RenameRefactorHandler { public boolean handleClick(int clickPosInText) { if (!active || callback == null) return false; - + return false; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java b/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java index 2649563ff..4d71a6b62 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/ScrollState.java @@ -21,12 +21,12 @@ public class ScrollState { private int scrollbarDragOffset = 0; // Whether currently dragging the scrollbar private boolean clickScrolling = false; - + // Animation parameters private static final double TAU = 0.1; // Time constant (~55ms feels snappy) private static final double SNAP_THRESHOLD = 0.01; // Snap to target when this close private static final double MAX_DT = 0.05; // Max delta time for stability - + /** * Reset scroll state to initial values */ @@ -37,7 +37,7 @@ public class ScrollState { scrolledLine = 0; lastScrollTime = System.currentTimeMillis(); } - + /** * Initialize scroll position if not already initialized */ @@ -47,18 +47,19 @@ public class ScrollState { lastScrollTime = System.currentTimeMillis(); } } - + /** * Update scroll animation. Call once per frame. + * * @param maxScroll Maximum valid scroll position */ public void update(int maxScroll) { long nowMs = System.currentTimeMillis(); double dt = Math.min(MAX_DT, (nowMs - lastScrollTime) / 1000.0); lastScrollTime = nowMs; - + double dist = targetScroll - scrollPos; - + // Snap to target if close enough if (Math.abs(dist) < SNAP_THRESHOLD) { scrollPos = targetScroll; @@ -69,21 +70,21 @@ public class ScrollState { double prev = scrollPos; scrollPos += dist * alpha; scrollVelocity = (scrollPos - prev) / (dt > 0 ? dt : 1e-6); - + // Clamp overshoot if ((dist > 0 && scrollPos > targetScroll) || (dist < 0 && scrollPos < targetScroll)) { scrollPos = targetScroll; scrollVelocity = 0.0; } } - + // Keep in bounds clampToBounds(maxScroll); - + // Update integer line for rendering scrolledLine = Math.max(0, Math.min((int) Math.floor(scrollPos), maxScroll)); } - + /** * Clamp scroll position to valid bounds */ @@ -94,35 +95,37 @@ public class ScrollState { if (targetScroll > maxScroll) targetScroll = maxScroll; scrolledLine = Math.max(0, Math.min(scrolledLine, maxScroll)); } - + /** * Apply mouse wheel scroll + * * @param wheelDelta Positive = scroll up, negative = scroll down - * @param maxScroll Maximum valid scroll position + * @param maxScroll Maximum valid scroll position */ public void applyWheelScroll(int wheelDelta, int maxScroll) { double sign = Math.copySign(1, wheelDelta); targetScroll -= sign * 2.0; // 2 lines per wheel tick clampToBounds(maxScroll); } - + /** * Set target scroll directly (for scrollbar dragging) */ public void setTargetScroll(double target, int maxScroll) { targetScroll = Math.max(0, Math.min(target, maxScroll)); } - + /** * Scroll to make a specific line visible - * @param lineIdx Line index to make visible + * + * @param lineIdx Line index to make visible * @param visibleLines Number of visible lines in viewport - * @param maxScroll Maximum scroll position + * @param maxScroll Maximum scroll position */ public void scrollToLine(int lineIdx, int visibleLines, int maxScroll) { int firstVisible = scrolledLine; int lastFullyVisible = scrolledLine + visibleLines; - + if (lineIdx < firstVisible) { // Line is above viewport - scroll up targetScroll = lineIdx; @@ -132,41 +135,66 @@ public class ScrollState { } // If line is visible, don't change scroll } - - // Getters - public double getScrollPos() { return scrollPos; } - public double getTargetScroll() { return targetScroll; } - /** @return First visible line **/ + // Getters + public double getScrollPos() { + return scrollPos; + } + + public double getTargetScroll() { + return targetScroll; + } + + /** + * @return First visible line + **/ public int getScrolledLine() { return scrolledLine; } - public double getScrollVelocity() { return scrollVelocity; } - public boolean isClickScrolling() { return clickScrolling; } - public int getScrollbarDragOffset() { return scrollbarDragOffset; } - + + public double getScrollVelocity() { + return scrollVelocity; + } + + public boolean isClickScrolling() { + return clickScrolling; + } + + public int getScrollbarDragOffset() { + return scrollbarDragOffset; + } + /** * Get fractional offset for sub-pixel rendering */ public double getFractionalOffset() { return scrollPos - scrolledLine; } - + // Setters for scrollbar interaction - public void setClickScrolling(boolean scrolling) { this.clickScrolling = scrolling; } - public void setScrollbarDragOffset(int offset) { this.scrollbarDragOffset = offset; } - public void setScrolledLine(int line) { this.scrolledLine = line; } + public void setClickScrolling(boolean scrolling) { + this.clickScrolling = scrolling; + } + + public void setScrollbarDragOffset(int offset) { + this.scrollbarDragOffset = offset; + } + + public void setScrolledLine(int line) { + this.scrolledLine = line; + } /** * Handle scrollbar dragging interaction. This encapsulates the logic that was * previously in the GUI class for handling thumb clicks/drags. - * @param yMouse current mouse Y coordinate - * @param areaX GUI area X (unused but kept for parity) - * @param areaY GUI area Y (top of text area) - * @param areaHeight height of the scroll track area + * + * @param yMouse current mouse Y coordinate + * @param areaX GUI area X (unused but kept for parity) + * @param areaY GUI area Y (top of text area) + * @param areaHeight height of the scroll track area * @param visibleLines number of visible lines in viewport - * @param linesCount total number of lines - * @param maxScroll maximum allowed scroll value + * @param linesCount total number of lines + * @param maxScroll maximum allowed scroll value */ public void handleClickScrolling(int yMouse, int areaX, int areaY, int areaHeight, int visibleLines, int linesCount, int maxScroll) { // Keep dragging while mouse button is down @@ -210,4 +238,4 @@ public class ScrollState { int thumbTop = (int) (areaY + 1f * getScrollPos() / linesCountD * (areaHeight - 4)) + 1; setScrollbarDragOffset(yMouse - thumbTop); } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java index ef20e816a..32e835aaf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SearchReplaceBar.java @@ -5,6 +5,7 @@ import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.Gui; import net.minecraft.util.ChatAllowedCharacters; import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.gui.util.GuiScriptTextArea.UndoData; import noppes.npcs.client.gui.util.GuiUtil; import noppes.npcs.client.gui.util.key.OverlayKeyPresetViewer; import noppes.npcs.util.ValueUtil; @@ -16,8 +17,6 @@ import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; -import noppes.npcs.client.gui.util.GuiScriptTextArea.UndoData; - /** * Search and Replace bar component for the script editor. * Provides IntelliJ-like search functionality with: @@ -30,10 +29,10 @@ import noppes.npcs.client.gui.util.GuiScriptTextArea.UndoData; * - All matches highlighting */ public class SearchReplaceBar { - + // ==================== TEXTURE ==================== public static final ResourceLocation TEXTURE = OverlayKeyPresetViewer.TEXTURE; - + // ==================== DIMENSIONS ==================== private int x, y, width; private int barHeight = 24; @@ -43,17 +42,17 @@ public class SearchReplaceBar { private int buttonSize = 16; private int buttonSpacing = 2; private int padding = 4; - + // ==================== STATE ==================== private boolean visible = false; private boolean showReplace = false; private boolean searchFieldFocused = true; private boolean replaceFieldFocused = false; - + // ==================== SEARCH OPTIONS ==================== private boolean matchCase = false; private boolean wholeWord = false; - + // ==================== TEXT FIELDS ==================== private String searchText = ""; private String replaceText = ""; @@ -66,18 +65,18 @@ public class SearchReplaceBar { // Text scroll offsets for when text exceeds field width private int searchScrollOffset = 0; private int replaceScrollOffset = 0; - + // ==================== UNDO/REDO ==================== private List searchUndoList = new ArrayList<>(); private List searchRedoList = new ArrayList<>(); private List replaceUndoList = new ArrayList<>(); private List replaceRedoList = new ArrayList<>(); - + // ==================== MATCHES ==================== private List matches = new ArrayList<>(); private int currentMatchIndex = -1; private List excludedMatches = new ArrayList<>(); - + // ==================== HOVER STATE ==================== private boolean hoverClose = false; private boolean hoverSearchUnwind = false; @@ -88,21 +87,21 @@ public class SearchReplaceBar { private boolean hoverReplace = false; private boolean hoverReplaceAll = false; private boolean hoverExclude = false; - + // ==================== CURSOR BLINK ==================== private int cursorCounter = 0; private long lastInputTime = 0; // For cursor blink pause on activity - + // ==================== DOUBLE/TRIPLE CLICK ==================== private long lastClickTime = 0; private int clickCount = 0; private boolean lastClickWasSearch = true; - + // ==================== CALLBACK ==================== public SearchCallback callback; - + private final FontRenderer font = Minecraft.getMinecraft().fontRenderer; - + /** * Callback interface for search/replace operations */ @@ -115,24 +114,31 @@ public class SearchReplaceBar { int getSelectionStart(); int getSelectionEnd(); + void setText(String text); + void scrollToPosition(int position); + void setSelection(int start, int end); + int getGutterWidth(); + void unfocusMainEditor(); + void focusMainEditor(); + void onMatchesUpdated(); // Called when matches change (for undo/redo sync) void resizeEditor(boolean open, int barHeight); } - + public SearchReplaceBar() { } - + public void setCallback(SearchCallback callback) { this.callback = callback; } - + /** * Initialize bar position based on text area bounds. * Preserves existing state (search text, matches, etc.) across initGui calls. @@ -149,8 +155,8 @@ public class SearchReplaceBar { boolean matchHighlight = false; visible = true; if (callback != null) { - // Always notify callback that the search UI is open and provide its current height. - callback.resizeEditor(true, getTotalHeight()); + // Always notify callback that the search UI is open and provide its current height. + callback.resizeEditor(true, getTotalHeight()); callback.unfocusMainEditor(); String highlight = callback.getHighlightedWord(); @@ -179,9 +185,9 @@ public class SearchReplaceBar { searchFieldFocused = true; replaceFieldFocused = false; open(); - + } - + /** * Open search+replace bar (Ctrl+Shift+R) */ @@ -191,7 +197,7 @@ public class SearchReplaceBar { replaceFieldFocused = true; open(); } - + /** * Close the search bar and restore focus to main editor */ @@ -212,6 +218,7 @@ public class SearchReplaceBar { else searchFieldFocused = true; } + /** * Unfocus the search bar (when clicking main editor) */ @@ -219,7 +226,7 @@ public class SearchReplaceBar { searchFieldFocused = false; replaceFieldFocused = false; } - + /** * Save current state to undo list before making changes */ @@ -232,7 +239,7 @@ public class SearchReplaceBar { replaceRedoList.clear(); } } - + /** * Undo last change in focused field */ @@ -256,7 +263,7 @@ public class SearchReplaceBar { updateScrollOffset(false); } } - + /** * Redo last undone change in focused field */ @@ -280,7 +287,7 @@ public class SearchReplaceBar { updateScrollOffset(false); } } - + /** * Update scroll offset for text field to keep cursor visible */ @@ -298,14 +305,14 @@ public class SearchReplaceBar { } else if (cursor < scrollOffset) { scrollOffset = cursor; } - + if (isSearchField) { searchScrollOffset = scrollOffset; } else { replaceScrollOffset = scrollOffset; } } - + /** * Toggle visibility */ @@ -316,7 +323,7 @@ public class SearchReplaceBar { openSearch(); } } - + /** * Toggle replace mode */ @@ -327,23 +334,23 @@ public class SearchReplaceBar { openSearchReplace(); } } - + public boolean isVisible() { return visible; } - + public boolean isSearchFocused() { return visible && searchFieldFocused; } - + public boolean isReplaceFocused() { return visible && replaceFieldFocused; } - + public boolean hasFocus() { return visible && (searchFieldFocused || replaceFieldFocused); } - + /** * Get total height of the bar (for viewport offset) */ @@ -351,143 +358,143 @@ public class SearchReplaceBar { if (!visible) return 0; return showReplace ? barHeight + replaceBarHeight : barHeight; } - + /** * Get all current matches for highlighting */ public List getMatches() { return matches; } - + /** * Get current match index */ public int getCurrentMatchIndex() { return currentMatchIndex; } - + /** * Check if a match is excluded */ public boolean isMatchExcluded(int index) { return excludedMatches.contains(index); } - + /** * Update cursor blink counter */ public void updateCursor() { cursorCounter++; } - + /** * Mark activity to pause cursor blinking */ private void markActivity() { lastInputTime = System.currentTimeMillis(); } - + /** * Check if there was recent input (cursor stays visible during typing) */ private boolean hadRecentInput() { return System.currentTimeMillis() - lastInputTime < 500; } - + /** * Check if cursor should be visible (solid during typing, blinking otherwise) */ private boolean shouldShowCursor() { return hadRecentInput() || (cursorCounter / 10) % 2 == 0; } - + // ==================== DRAWING ==================== - + /** * Draw the search/replace bar */ public void draw(int mouseX, int mouseY) { if (!visible) return; - + updateHoverStates(mouseX, mouseY); - + int totalHeight = getTotalHeight(); Gui.drawRect(x, y, x + width, y + totalHeight, 0xFF2d2d30); Gui.drawRect(x, y + totalHeight - 1, x + width, y + totalHeight, 0xFF414141); - + drawSearchRow(mouseX, mouseY); - + if (showReplace) { drawReplaceRow(mouseX, mouseY); } } - + private void drawSearchRow(int mouseX, int mouseY) { int rowY = y + padding; int currentX = x + padding; - textFieldWidth=100; - drawTextField(currentX, rowY, textFieldWidth, textFieldHeight, - searchText, searchCursor, searchSelectionStart, searchSelectionEnd, - searchFieldFocused, "Search...", searchScrollOffset, true); + textFieldWidth = 100; + drawTextField(currentX, rowY, textFieldWidth, textFieldHeight, + searchText, searchCursor, searchSelectionStart, searchSelectionEnd, + searchFieldFocused, "Search...", searchScrollOffset, true); currentX += textFieldWidth + buttonSpacing + 4; - + // "0 results" in red, otherwise "X/Y" in normal color - String matchInfo = matches.isEmpty() ? "0 results" : - (currentMatchIndex + 1) + "/" + matches.size(); + String matchInfo = matches.isEmpty() ? "0 results" : + (currentMatchIndex + 1) + "/" + matches.size(); int matchInfoWidth = font.getStringWidth(matchInfo); font.drawString(matchInfo, currentX, rowY + 4, - matches.isEmpty() && !searchText.isEmpty() ? 0xFFff6666 : 0xFFcccccc); + matches.isEmpty() && !searchText.isEmpty() ? 0xFFff6666 : 0xFFcccccc); currentX += matchInfoWidth + 8; drawToggleButton(currentX, rowY, "Cc", matchCase, hoverMatchCase, "Match Case"); currentX += buttonSize + buttonSpacing; - + drawToggleButton(currentX, rowY, "W", wholeWord, hoverWholeWord, "Whole Word"); currentX += buttonSize + buttonSpacing + 4; - + boolean prevEnabled = !matches.isEmpty(); drawNavButton(currentX, rowY, "▲", prevEnabled, hoverPrev); currentX += buttonSize + buttonSpacing; - + boolean nextEnabled = !matches.isEmpty(); drawNavButton(currentX, rowY, "▼", nextEnabled, hoverNext); currentX += buttonSize + buttonSpacing; - + int closeX = x + width - buttonSize - padding; drawCloseButton(closeX, rowY, hoverClose); int replaceUnwindButton = closeX - buttonSize - buttonSpacing; drawActionButton(replaceUnwindButton, rowY, showReplace ? "▼" : "◀", hoverSearchUnwind, true); } - + private void drawReplaceRow(int mouseX, int mouseY) { int rowY = y + barHeight + 2; int currentX = x + padding; drawTextField(currentX, rowY, textFieldWidth, textFieldHeight, - replaceText, replaceCursor, replaceSelectionStart, replaceSelectionEnd, - replaceFieldFocused, "Replace...", replaceScrollOffset, false); + replaceText, replaceCursor, replaceSelectionStart, replaceSelectionEnd, + replaceFieldFocused, "Replace...", replaceScrollOffset, false); currentX += textFieldWidth + buttonSpacing + 4; - + drawActionButton(currentX, rowY, "Replace", hoverReplace, !matches.isEmpty()); currentX += font.getStringWidth("Replace") + 12 + buttonSpacing; - + drawActionButton(currentX, rowY, "Replace All", hoverReplaceAll, !matches.isEmpty()); currentX += font.getStringWidth("Replace All") + 12 + buttonSpacing; - + drawActionButton(currentX, rowY, "Exclude", hoverExclude, currentMatchIndex >= 0); } private void drawTextField(int fieldX, int fieldY, int fieldWidth, int fieldHeight, - String text, int cursor, int selStart, int selEnd, - boolean focused, String placeholder, int scrollOffset, boolean isSearchField) { + String text, int cursor, int selStart, int selEnd, + boolean focused, String placeholder, int scrollOffset, boolean isSearchField) { // Draw text field background Gui.drawRect(fieldX, fieldY, fieldX + fieldWidth, fieldY + fieldHeight, 0xFF1e1e1e); Gui.drawRect(fieldX, fieldY, fieldX + fieldWidth, fieldY + 1, focused ? 0xFF007acc : 0xFF3c3c3c); Gui.drawRect(fieldX, fieldY + fieldHeight - 1, fieldX + fieldWidth, fieldY + fieldHeight, - focused ? 0xFF007acc : 0xFF3c3c3c); + focused ? 0xFF007acc : 0xFF3c3c3c); Gui.drawRect(fieldX, fieldY, fieldX + 1, fieldY + fieldHeight, focused ? 0xFF007acc : 0xFF3c3c3c); Gui.drawRect(fieldX + fieldWidth - 1, fieldY, fieldX + fieldWidth, fieldY + fieldHeight, - focused ? 0xFF007acc : 0xFF3c3c3c); + focused ? 0xFF007acc : 0xFF3c3c3c); // Calculate visible area int textX = fieldX + 4; @@ -511,7 +518,7 @@ public class SearchReplaceBar { int maxSel = Math.max(adjustedSelStart, adjustedSelEnd); String beforeSel = visibleText.substring(0, Math.min(minSel, visibleText.length())); String inSel = visibleText.substring(Math.min(minSel, visibleText.length()), - Math.min(maxSel, visibleText.length())); + Math.min(maxSel, visibleText.length())); int selX = textX + font.getStringWidth(beforeSel); int selW = font.getStringWidth(inSel); Gui.drawRect(selX, textY - 1, selX + selW, textY + 9, 0xFF264f78); @@ -537,7 +544,7 @@ public class SearchReplaceBar { // Draw selection highlight if (selStart != selEnd && focused) { int startX = textX + font.getStringWidth( - text.substring(0, Math.min(selStart, text.length()))) - scrollOffset; + text.substring(0, Math.min(selStart, text.length()))) - scrollOffset; int endX = textX + font.getStringWidth(text.substring(0, Math.min(selEnd, text.length()))) - scrollOffset; Gui.drawRect(startX, textY - 1, endX, textY + font.FONT_HEIGHT, 0xFF2d5ca6); } @@ -555,21 +562,21 @@ public class SearchReplaceBar { int cursorDrawX = textX + cursorPosX - scrollOffset; Gui.drawRect(cursorDrawX, textY - 1, cursorDrawX + 1, textY + font.FONT_HEIGHT, 0xFFffffff); } - + GL11.glDisable(GL11.GL_SCISSOR_TEST); } - + private void oldDrawTextField(int fieldX, int fieldY, int fieldWidth, int fieldHeight, - String text, int cursor, int selStart, int selEnd, - boolean focused, String placeholder, int scrollOffset, boolean isSearchField) { + String text, int cursor, int selStart, int selEnd, + boolean focused, String placeholder, int scrollOffset, boolean isSearchField) { Minecraft.getMinecraft().getTextureManager().bindTexture(TEXTURE); GL11.glPushMatrix(); GL11.glColor4f(1, 1, 1, 1); float boxScaleX = fieldWidth / 32f, boxScaleY = 1.5f; GL11.glScalef(boxScaleX, boxScaleY, 1); - + if (focused) { GL11.glColor4f(0.28f, 0.45f, 0.7f, 1f); } else { @@ -577,11 +584,11 @@ public class SearchReplaceBar { } GuiUtil.drawTexturedModalRect(fieldX / boxScaleX, (fieldY - 16) / boxScaleY, 32, 20, 0, 492); GL11.glPopMatrix(); - + int textX = fieldX + 4; int textY = fieldY + (fieldHeight - font.FONT_HEIGHT) / 2; int visibleWidth = fieldWidth - 8; - + // Calculate scroll offset to keep cursor visible int cursorPosX = font.getStringWidth(text.substring(0, Math.min(cursor, text.length()))); if (cursorPosX - scrollOffset > visibleWidth - 2) { @@ -595,7 +602,7 @@ public class SearchReplaceBar { } else { replaceScrollOffset = scrollOffset; } - + GL11.glEnable(GL11.GL_SCISSOR_TEST); GuiUtil.setScissorClip(fieldX + 2, fieldY, fieldWidth - 4, fieldHeight); @@ -606,7 +613,7 @@ public class SearchReplaceBar { int endX = textX + font.getStringWidth(text.substring(0, Math.min(selEnd, text.length()))) - scrollOffset; Gui.drawRect(startX, textY - 1, endX, textY + font.FONT_HEIGHT, 0xFF2d5ca6); } - + // Draw text with scroll offset boolean noMatches = !text.isEmpty() && matches.isEmpty(); int col = text.isEmpty() ? 0xFFa0a0a0 : 0xFFe0e0e0; @@ -614,28 +621,28 @@ public class SearchReplaceBar { col = 0xFFff6666; // Red font.drawString(text.isEmpty() ? placeholder : text, textX - scrollOffset, textY, col); - + // Draw cursor (stays visible during typing) if (focused && shouldShowCursor()) { int cursorDrawX = textX + cursorPosX - scrollOffset; Gui.drawRect(cursorDrawX, textY - 1, cursorDrawX + 1, textY + font.FONT_HEIGHT, 0xFFffffff); } } - + GL11.glDisable(GL11.GL_SCISSOR_TEST); } - + private void drawToggleButton(int btnX, int btnY, String label, boolean active, boolean hovered, String tooltip) { int bgColor = active ? 0xFF4a6da7 : (hovered ? 0xFF505050 : 0xFF3c3c3c); Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + buttonSize, bgColor); Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + 1, 0xFF606060); Gui.drawRect(btnX, btnY + buttonSize - 1, btnX + buttonSize, btnY + buttonSize, 0xFF2a2a2a); - + int textColor = active ? 0xFFffffff : (hovered ? 0xFFcccccc : 0xFF999999); int labelWidth = font.getStringWidth(label); font.drawString(label, btnX + (buttonSize - labelWidth) / 2, btnY + (buttonSize - font.FONT_HEIGHT) / 2 + 1, textColor); } - + private void drawNavButton(int btnX, int btnY, String arrow, boolean enabled, boolean hovered) { int bgColor = !enabled ? 0xFF393939 : (hovered ? 0xFF505050 : 0xFF3c3c3c); Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + buttonSize, bgColor); @@ -646,57 +653,57 @@ public class SearchReplaceBar { int arrowWidth = font.getStringWidth(arrow); font.drawString(arrow, btnX + (buttonSize - arrowWidth) / 2, btnY + (buttonSize - font.FONT_HEIGHT) / 2, textColor); } - + private void drawCloseButton(int btnX, int btnY, boolean hovered) { int bgColor = hovered ? 0xFFc75050 : 0xFF3c3c3c; Gui.drawRect(btnX, btnY, btnX + buttonSize, btnY + buttonSize, bgColor); - + int textColor = hovered ? 0xFFffffff : 0xFFaaaaaa; String xChar = "X"; int xWidth = font.getStringWidth(xChar); font.drawString(xChar, btnX + (buttonSize - xWidth) / 2, btnY + (buttonSize - font.FONT_HEIGHT) / 2 + 1, textColor); } - + private void drawActionButton(int btnX, int btnY, String label, boolean hovered, boolean enabled) { int btnWidth = font.getStringWidth(label) + 10; int btnHeight = 14; - + int bgColor = !enabled ? 0xFF393939 : (hovered ? 0xFF505050 : 0xFF3c3c3c); Gui.drawRect(btnX, btnY + 1, btnX + btnWidth, btnY + btnHeight + 1, bgColor); Gui.drawRect(btnX, btnY + 1, btnX + btnWidth, btnY + 2, 0xFF555555); Gui.drawRect(btnX, btnY + btnHeight, btnX + btnWidth, btnY + btnHeight + 1, 0xFF2a2a2a); - + int textColor = !enabled ? 0xFF555555 : (hovered ? 0xFFffffff : 0xFFaaaaaa); font.drawString(label, btnX + 5, btnY + (btnHeight - font.FONT_HEIGHT) / 2 + 2, textColor); } - + // ==================== HOVER DETECTION ==================== - + private void updateHoverStates(int mouseX, int mouseY) { int rowY = y + padding; int currentX = x + padding; - + int searchFieldEndX = currentX + textFieldWidth; currentX = searchFieldEndX + buttonSpacing + 4; - + String matchInfo = matches.isEmpty() ? "0 results" : (currentMatchIndex + 1) + "/" + matches.size(); currentX += font.getStringWidth(matchInfo) + 8; - + int matchCaseX = currentX; hoverMatchCase = isMouseOver(mouseX, mouseY, matchCaseX, rowY, buttonSize, buttonSize); currentX += buttonSize + buttonSpacing; - + int wholeWordX = currentX; hoverWholeWord = isMouseOver(mouseX, mouseY, wholeWordX, rowY, buttonSize, buttonSize); currentX += buttonSize + buttonSpacing + 4; - + int prevX = currentX; hoverPrev = isMouseOver(mouseX, mouseY, prevX, rowY, buttonSize, buttonSize); currentX += buttonSize + buttonSpacing; - + int nextX = currentX; hoverNext = isMouseOver(mouseX, mouseY, nextX, rowY, buttonSize, buttonSize); - + int closeX = x + width - buttonSize - padding; hoverClose = isMouseOver(mouseX, mouseY, closeX, rowY, buttonSize, buttonSize); @@ -706,50 +713,51 @@ public class SearchReplaceBar { if (showReplace) { int replaceRowY = y + barHeight + 2; currentX = x + padding + textFieldWidth + buttonSpacing + 4; - + int replaceWidth = font.getStringWidth("Replace") + 12; hoverReplace = isMouseOver(mouseX, mouseY, currentX, replaceRowY + 1, replaceWidth, 14); currentX += replaceWidth + buttonSpacing; - + int replaceAllWidth = font.getStringWidth("Replace All") + 12; hoverReplaceAll = isMouseOver(mouseX, mouseY, currentX, replaceRowY + 1, replaceAllWidth, 14); currentX += replaceAllWidth + buttonSpacing; - + int excludeWidth = font.getStringWidth("Exclude") + 12; hoverExclude = isMouseOver(mouseX, mouseY, currentX, replaceRowY + 1, excludeWidth, 14); } else { hoverReplace = hoverReplaceAll = hoverExclude = false; } } - + private boolean isMouseOver(int mouseX, int mouseY, int bx, int by, int bw, int bh) { return mouseX >= bx && mouseX < bx + bw && mouseY >= by && mouseY < by + bh; } - + public void resetSelection() { searchSelectionStart = searchSelectionEnd = searchCursor; replaceSelectionStart = replaceSelectionEnd = replaceCursor; } // ==================== MOUSE INPUT ==================== - + /** * Handle mouse click - supports double-click for word selection, triple-click for all + * * @return true if click was consumed */ public boolean mouseClicked(int mouseX, int mouseY, int button) { if (!visible) return false; if (button != 0) return false; // Only handle left clicks - + int totalHeight = getTotalHeight(); // If click is outside the bar, don't consume it (let main editor handle) if (mouseX < x || mouseX > x + width || mouseY < y || mouseY > y + totalHeight) { resetSelection(); return false; } - + int rowY = y + padding; int currentX = x + padding; - + // Handle click timing for double/triple click long now = System.currentTimeMillis(); boolean isSearchFieldClick = isMouseOver(mouseX, mouseY, currentX, rowY - 2, textFieldWidth, textFieldHeight + 4); @@ -766,11 +774,11 @@ public class SearchReplaceBar { } lastClickTime = now; lastClickWasSearch = true; - + searchFieldFocused = true; replaceFieldFocused = false; markActivity(); - + if (clickCount == 1) { // Single click - position cursor int clickX = mouseX - currentX - 4 + searchScrollOffset; @@ -789,35 +797,35 @@ public class SearchReplaceBar { return true; } currentX += textFieldWidth + buttonSpacing + 4; - + String matchInfo = matches.isEmpty() ? "0 results" : (currentMatchIndex + 1) + "/" + matches.size(); currentX += font.getStringWidth(matchInfo) + 8; - + if (hoverMatchCase) { matchCase = !matchCase; updateMatches(); return true; } currentX += buttonSize + buttonSpacing; - + if (hoverWholeWord) { wholeWord = !wholeWord; updateMatches(); return true; } currentX += buttonSize + buttonSpacing + 4; - + if (hoverPrev && !matches.isEmpty()) { goToPreviousMatch(); return true; } currentX += buttonSize + buttonSpacing; - + if (hoverNext && !matches.isEmpty()) { goToNextMatch(); return true; } - + if (hoverClose) { close(); return true; @@ -830,7 +838,7 @@ public class SearchReplaceBar { if (showReplace) { int replaceRowY = y + barHeight + 2; - + if (isReplaceFieldClick || isMouseOver(mouseX, mouseY, x + padding, replaceRowY - 2, textFieldWidth, textFieldHeight + 4)) { // Handle click timing for replace field boolean sameField = !lastClickWasSearch; @@ -841,11 +849,11 @@ public class SearchReplaceBar { } lastClickTime = now; lastClickWasSearch = false; - + searchFieldFocused = false; replaceFieldFocused = true; markActivity(); - + if (clickCount == 1) { int clickX = mouseX - x - padding - 4 + replaceScrollOffset; replaceCursor = getTextCursorPosition(replaceText, clickX); @@ -860,30 +868,30 @@ public class SearchReplaceBar { } return true; } - + if (hoverReplace && !matches.isEmpty()) { replaceCurrent(); return true; } - + if (hoverReplaceAll && !matches.isEmpty()) { replaceAll(); return true; } - + if (hoverExclude && currentMatchIndex >= 0) { excludeCurrentMatch(); return true; } } - + // Click was within bar bounds but not on any control - consume it to prevent MTA from getting focus return true; } - + private int getTextCursorPosition(String text, int clickX) { if (text.isEmpty() || clickX <= 0) return 0; - + for (int i = 1; i <= text.length(); i++) { int w = font.getStringWidth(text.substring(0, i)); if (clickX < w) { @@ -893,17 +901,17 @@ public class SearchReplaceBar { } return text.length(); } - + /** * Select word at cursor position (for double-click) */ private void selectWordAt(String text, int pos, boolean isSearchField) { if (text.isEmpty()) return; - + // Find word boundaries int start = pos; int end = pos; - + // Expand left while (start > 0 && isWordChar(text.charAt(start - 1))) { start--; @@ -912,7 +920,7 @@ public class SearchReplaceBar { while (end < text.length() && isWordChar(text.charAt(end))) { end++; } - + if (isSearchField) { searchSelectionStart = start; searchSelectionEnd = end; @@ -923,15 +931,16 @@ public class SearchReplaceBar { replaceCursor = end; } } - + private boolean isWordChar(char c) { return Character.isLetterOrDigit(c) || c == '_'; } - + // ==================== KEYBOARD INPUT ==================== - + /** * Handle keyboard input + * * @return true if input was consumed */ public boolean keyTyped(char c, int keyCode) { @@ -941,12 +950,12 @@ public class SearchReplaceBar { } if (!visible || (!searchFieldFocused && !replaceFieldFocused)) return false; - + // Check modifier keys - don't let them trigger first-match issue boolean ctrl = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); boolean shift = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); boolean alt = Keyboard.isKeyDown(Keyboard.KEY_LMENU) || Keyboard.isKeyDown(Keyboard.KEY_RMENU); - + // Ignore lone modifier key presses if (keyCode == Keyboard.KEY_LCONTROL || keyCode == Keyboard.KEY_RCONTROL || keyCode == Keyboard.KEY_LSHIFT || keyCode == Keyboard.KEY_RSHIFT || @@ -965,7 +974,7 @@ public class SearchReplaceBar { markActivity(); return true; } - + // Enter/Shift+Enter for next/previous match if (keyCode == Keyboard.KEY_RETURN) { if (shift) { @@ -980,7 +989,7 @@ public class SearchReplaceBar { } return true; } - + // Up/Down arrows cycle through matches when SRB is focused if (keyCode == Keyboard.KEY_UP && !matches.isEmpty()) { goToPreviousMatch(); @@ -990,38 +999,36 @@ public class SearchReplaceBar { goToNextMatch(); return true; } - + if (searchFieldFocused) { handleTextFieldInput(c, keyCode, true); } else if (replaceFieldFocused) { handleTextFieldInput(c, keyCode, false); } - + return true; } - + private void handleTextFieldInput(char c, int keyCode, boolean isSearchField) { String text = isSearchField ? searchText : replaceText; int cursor = isSearchField ? searchCursor : replaceCursor; int selStart = isSearchField ? searchSelectionStart : replaceSelectionStart; int selEnd = isSearchField ? searchSelectionEnd : replaceSelectionEnd; - + boolean hasSelection = selStart != selEnd; boolean ctrl = Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL); boolean shift = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT); - + if (ctrl && keyCode == Keyboard.KEY_A) { selStart = 0; selEnd = text.length(); cursor = text.length(); - } - else if (ctrl && keyCode == Keyboard.KEY_C) { + } else if (ctrl && keyCode == Keyboard.KEY_C) { if (hasSelection) { String selected = text.substring(Math.min(selStart, selEnd), Math.max(selStart, selEnd)); setClipboard(selected); } - } - else if (ctrl && keyCode == Keyboard.KEY_X) { + } else if (ctrl && keyCode == Keyboard.KEY_X) { if (hasSelection) { String selected = text.substring(Math.min(selStart, selEnd), Math.max(selStart, selEnd)); setClipboard(selected); @@ -1030,8 +1037,7 @@ public class SearchReplaceBar { cursor = Math.min(selStart, selEnd); selStart = selEnd = cursor; } - } - else if (ctrl && keyCode == Keyboard.KEY_V) { + } else if (ctrl && keyCode == Keyboard.KEY_V) { String clipboard = getClipboard(); if (clipboard != null) { clipboard = clipboard.replace("\n", "").replace("\r", ""); @@ -1045,8 +1051,7 @@ public class SearchReplaceBar { } selStart = selEnd = cursor; } - } - else if (keyCode == Keyboard.KEY_BACK) { + } else if (keyCode == Keyboard.KEY_BACK) { saveState(isSearchField); if (hasSelection) { text = text.substring(0, Math.min(selStart, selEnd)) + text.substring(Math.max(selStart, selEnd)); @@ -1056,8 +1061,7 @@ public class SearchReplaceBar { cursor--; } selStart = selEnd = cursor; - } - else if (keyCode == Keyboard.KEY_DELETE) { + } else if (keyCode == Keyboard.KEY_DELETE) { saveState(isSearchField); if (hasSelection) { text = text.substring(0, Math.min(selStart, selEnd)) + text.substring(Math.max(selStart, selEnd)); @@ -1066,8 +1070,7 @@ public class SearchReplaceBar { text = text.substring(0, cursor) + text.substring(cursor + 1); } selStart = selEnd = cursor; - } - else if (keyCode == Keyboard.KEY_LEFT) { + } else if (keyCode == Keyboard.KEY_LEFT) { if (shift) { if (cursor > 0) { cursor--; @@ -1088,8 +1091,7 @@ public class SearchReplaceBar { } selStart = selEnd = cursor; } - } - else if (keyCode == Keyboard.KEY_RIGHT) { + } else if (keyCode == Keyboard.KEY_RIGHT) { if (shift) { if (cursor < text.length()) { cursor++; @@ -1110,8 +1112,7 @@ public class SearchReplaceBar { } selStart = selEnd = cursor; } - } - else if (keyCode == Keyboard.KEY_HOME) { + } else if (keyCode == Keyboard.KEY_HOME) { if (shift) { selEnd = selStart == selEnd ? cursor : selEnd; selStart = 0; @@ -1119,8 +1120,7 @@ public class SearchReplaceBar { selStart = selEnd = 0; } cursor = 0; - } - else if (keyCode == Keyboard.KEY_END) { + } else if (keyCode == Keyboard.KEY_END) { if (shift) { selStart = selStart == selEnd ? cursor : selStart; selEnd = text.length(); @@ -1128,8 +1128,7 @@ public class SearchReplaceBar { selStart = selEnd = text.length(); } cursor = text.length(); - } - else if (ChatAllowedCharacters.isAllowedCharacter(c)) { + } else if (ChatAllowedCharacters.isAllowedCharacter(c)) { saveState(isSearchField); if (hasSelection) { text = text.substring(0, Math.min(selStart, selEnd)) + c + text.substring(Math.max(selStart, selEnd)); @@ -1140,16 +1139,16 @@ public class SearchReplaceBar { } selStart = selEnd = cursor; } - + if (selStart > selEnd) { int temp = selStart; selStart = selEnd; selEnd = temp; } - + // Mark activity on any input to keep cursor visible markActivity(); - + if (isSearchField) { searchText = text; searchCursor = cursor; @@ -1163,14 +1162,15 @@ public class SearchReplaceBar { replaceSelectionEnd = selEnd; } } - + private void setClipboard(String text) { try { java.awt.datatransfer.StringSelection selection = new java.awt.datatransfer.StringSelection(text); java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, null); - } catch (Exception ignored) {} + } catch (Exception ignored) { + } } - + private String getClipboard() { try { return (String) java.awt.Toolkit.getDefaultToolkit().getSystemClipboard().getData(java.awt.datatransfer.DataFlavor.stringFlavor); @@ -1178,53 +1178,53 @@ public class SearchReplaceBar { return null; } } - + // ==================== SEARCH LOGIC ==================== - + /** * Update matches based on current search text and options */ public void updateMatches() { matches.clear(); excludedMatches.clear(); - + if (callback == null || searchText.isEmpty()) { if (callback != null) callback.onMatchesUpdated(); return; } - + String sourceText = callback.getText(); if (sourceText == null || sourceText.isEmpty()) { callback.onMatchesUpdated(); return; } - + String searchPattern = searchText; - + if (wholeWord) { searchPattern = "\\b" + Pattern.quote(searchPattern) + "\\b"; } else { searchPattern = Pattern.quote(searchPattern); } - + int flags = matchCase ? 0 : Pattern.CASE_INSENSITIVE; - + try { Pattern pattern = Pattern.compile(searchPattern, flags); Matcher matcher = pattern.matcher(sourceText); - + while (matcher.find()) { matches.add(new int[]{matcher.start(), matcher.end()}); } } catch (Exception ignored) { } - + // No need to navigate right now if (!matches.isEmpty()) { // currentMatchIndex = 0; // navigateToCurrentMatch(); } - + currentMatchIndex = ValueUtil.clamp(currentMatchIndex, 0, matches.size() - 1); callback.onMatchesUpdated(); } @@ -1251,7 +1251,7 @@ public class SearchReplaceBar { } catch (Exception ignored) { } } - + /** * Navigate to next match */ @@ -1263,7 +1263,7 @@ public class SearchReplaceBar { navigateToCurrentMatch(); } - + /** * Navigate to previous match */ @@ -1275,20 +1275,20 @@ public class SearchReplaceBar { navigateToCurrentMatch(); } - + /** * Navigate viewport to current match and select it */ private void navigateToCurrentMatch() { if (callback == null || currentMatchIndex < 0 || currentMatchIndex >= matches.size()) return; - + int[] match = matches.get(currentMatchIndex); callback.scrollToPosition(match[0]); callback.setSelection(match[0], match[1]); } - + // ==================== REPLACE LOGIC ==================== - + /** * Replace current match */ @@ -1298,15 +1298,15 @@ public class SearchReplaceBar { goToNextMatch(); return; } - + String sourceText = callback.getText(); int[] match = matches.get(currentMatchIndex); - + String newText = sourceText.substring(0, match[0]) + replaceText + sourceText.substring(match[1]); callback.setText(newText); - + updateMatches(); - + if (!matches.isEmpty()) { int replacementEnd = match[0] + replaceText.length(); for (int i = 0; i < matches.size(); i++) { @@ -1318,17 +1318,17 @@ public class SearchReplaceBar { navigateToCurrentMatch(); } } - + /** * Replace all matches */ public void replaceAll() { if (callback == null || matches.isEmpty()) return; - + String sourceText = callback.getText(); StringBuilder result = new StringBuilder(); int lastEnd = 0; - + for (int i = 0; i < matches.size(); i++) { if (excludedMatches.contains(i)) { continue; @@ -1339,11 +1339,11 @@ public class SearchReplaceBar { lastEnd = match[1]; } result.append(sourceText.substring(lastEnd)); - + callback.setText(result.toString()); updateMatches(); } - + /** * Exclude current match from replacement */ @@ -1359,16 +1359,16 @@ public class SearchReplaceBar { goToNextMatch(); } } - + // ==================== UTILITY ==================== - + /** * Get search text for external highlighting */ public String getSearchText() { return searchText; } - + /** * Check if bar consumes click at position */ diff --git a/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java b/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java index 242c16a93..89efa34cd 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/SelectionState.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.util.script; import noppes.npcs.client.gui.util.script.JavaTextContainer.LineData; + import java.util.List; import java.util.regex.Pattern; @@ -12,14 +13,14 @@ public class SelectionState { private int endSelection = 0; private int cursorPosition = 0; private long lastInputTime = 0L; - + /** * Reset all cursor selections to a single position */ public void reset(int position) { startSelection = endSelection = cursorPosition = position; } - + /** * Clamp all selection bounds to valid text range */ @@ -32,13 +33,13 @@ public class SelectionState { endSelection = Math.max(0, Math.min(endSelection, textLength)); cursorPosition = Math.max(0, Math.min(cursorPosition, textLength)); } - + /** * Set cursor position, optionally extending selection */ public void setCursor(int position, int textLength, boolean extendSelection) { position = Math.max(0, Math.min(position, textLength)); - + if (position != cursorPosition) { if (!extendSelection) { endSelection = startSelection = cursorPosition = position; @@ -49,7 +50,7 @@ public class SelectionState { } else if (cursorPosition == endSelection) { endSelection -= diff; } - + // Ensure start <= end if (startSelection > endSelection) { int temp = endSelection; @@ -62,7 +63,7 @@ public class SelectionState { markActivity(); } } - + /** * Select a range of text */ @@ -70,7 +71,7 @@ public class SelectionState { startSelection = Math.min(start, end); endSelection = Math.max(start, end); } - + /** * Select all text */ @@ -78,14 +79,14 @@ public class SelectionState { startSelection = cursorPosition = 0; endSelection = textLength; } - + /** * Check if there's an active selection (not just cursor) */ public boolean hasSelection() { return startSelection != endSelection; } - + /** * Get selected text */ @@ -95,7 +96,7 @@ public class SelectionState { int end = Math.max(0, Math.min(endSelection, text.length())); return text.substring(start, end); } - + /** * Get text before selection/cursor */ @@ -103,7 +104,7 @@ public class SelectionState { if (text == null || startSelection <= 0) return ""; return text.substring(0, Math.min(startSelection, text.length())); } - + /** * Get text after selection/cursor */ @@ -112,37 +113,37 @@ public class SelectionState { int pos = Math.min(endSelection, text.length()); return text.substring(pos); } - + /** * Find which line the cursor is on (0-indexed) */ public int getCursorLineIndex(List lines, int textLength) { if (lines == null || lines.isEmpty()) return 0; - + for (int i = 0; i < lines.size(); i++) { LineData ld = lines.get(i); boolean isLastLine = (i == lines.size() - 1); - boolean isOnLine = isLastLine - ? (cursorPosition >= ld.start && cursorPosition <= ld.end) - : (cursorPosition >= ld.start && cursorPosition < ld.end); + boolean isOnLine = isLastLine + ? (cursorPosition >= ld.start && cursorPosition <= ld.end) + : (cursorPosition >= ld.start && cursorPosition < ld.end); if (isOnLine) { return i; } } return lines.size() - 1; } - + /** - * Check if cursor is on a specific line, handling the last line specially - * (last line doesn't have trailing newline, so use <= for end boundary) + * Check if cursor is on a specific line, handling the last line specially + * (last line doesn't have trailing newline, so use <= for end boundary) */ public boolean isCursorOnLine(int lineIndex, LineData line, int totalLines) { boolean isLastLine = (lineIndex == totalLines - 1); - return isLastLine - ? (cursorPosition >= line.start && cursorPosition <= line.end) - : (cursorPosition >= line.start && cursorPosition < line.end); + return isLastLine + ? (cursorPosition >= line.start && cursorPosition <= line.end) + : (cursorPosition >= line.start && cursorPosition < line.end); } - + /** * Find line containing the cursor */ @@ -156,32 +157,51 @@ public class SelectionState { } return null; } - + /** * Mark user activity (for cursor blink timing) */ public void markActivity() { lastInputTime = System.currentTimeMillis(); } - + /** * Check if there was recent input (for cursor blink pause) */ public boolean hadRecentInput() { return System.currentTimeMillis() - lastInputTime < 500; } - + // Getters - public int getStartSelection() { return startSelection; } - public int getEndSelection() { return endSelection; } - public int getCursorPosition() { return cursorPosition; } - public long getLastInputTime() { return lastInputTime; } - + public int getStartSelection() { + return startSelection; + } + + public int getEndSelection() { + return endSelection; + } + + public int getCursorPosition() { + return cursorPosition; + } + + public long getLastInputTime() { + return lastInputTime; + } + // Direct setters (use with caution, prefer higher-level methods) - public void setStartSelection(int pos) { this.startSelection = pos; } - public void setEndSelection(int pos) { this.endSelection = pos; } - public void setCursorPositionDirect(int pos) { this.cursorPosition = pos; } - + public void setStartSelection(int pos) { + this.startSelection = pos; + } + + public void setEndSelection(int pos) { + this.endSelection = pos; + } + + public void setCursorPositionDirect(int pos) { + this.cursorPosition = pos; + } + /** * After text modification, update selection to new position */ @@ -219,4 +239,4 @@ public class SelectionState { } } } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/client/gui/util/script/UndoManager.java b/src/main/java/noppes/npcs/client/gui/util/script/UndoManager.java index d7cad5295..c5cfd9f77 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/UndoManager.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/UndoManager.java @@ -7,90 +7,92 @@ import java.util.List; * Manages undo/redo stacks for the text editor. */ public class UndoManager { - + /** * Stores a snapshot of text state for undo/redo */ public static class UndoState { public final String text; public final int cursorPosition; - + public UndoState(String text, int cursorPosition) { this.text = text; this.cursorPosition = cursorPosition; } } - + private final List undoStack = new ArrayList<>(); private final List redoStack = new ArrayList<>(); private boolean isUndoing = false; - + // Optional: limit stack size to prevent memory issues private static final int MAX_STACK_SIZE = 100; - + /** * Record current state before making a change */ public void recordState(String text, int cursorPosition) { if (isUndoing) return; - + undoStack.add(new UndoState(text, cursorPosition)); redoStack.clear(); - + // Trim stack if too large while (undoStack.size() > MAX_STACK_SIZE) { undoStack.remove(0); } } - + /** * Undo to previous state - * @param currentText Current text before undo + * + * @param currentText Current text before undo * @param currentCursor Current cursor position * @return Previous state, or null if nothing to undo */ public UndoState undo(String currentText, int currentCursor) { if (undoStack.isEmpty()) return null; - + isUndoing = true; redoStack.add(new UndoState(currentText, currentCursor)); UndoState state = undoStack.remove(undoStack.size() - 1); isUndoing = false; - + return state; } - + /** * Redo to next state - * @param currentText Current text before redo + * + * @param currentText Current text before redo * @param currentCursor Current cursor position * @return Next state, or null if nothing to redo */ public UndoState redo(String currentText, int currentCursor) { if (redoStack.isEmpty()) return null; - + isUndoing = true; undoStack.add(new UndoState(currentText, currentCursor)); UndoState state = redoStack.remove(redoStack.size() - 1); isUndoing = false; - + return state; } - + /** * Check if undo is available */ public boolean canUndo() { return !undoStack.isEmpty(); } - + /** * Check if redo is available */ public boolean canRedo() { return !redoStack.isEmpty(); } - + /** * Clear all history */ @@ -98,22 +100,27 @@ public class UndoManager { undoStack.clear(); redoStack.clear(); } - + /** * Check if currently performing an undo/redo operation */ public boolean isUndoing() { return isUndoing; } - + /** * Set undoing flag (for external control during setText) */ public void setUndoing(boolean undoing) { this.isUndoing = undoing; } - + // For backward compatibility with existing code - public List getUndoList() { return undoStack; } - public List getRedoList() { return redoStack; } -} \ No newline at end of file + public List getUndoList() { + return undoStack; + } + + public List getRedoList() { + return redoStack; + } +} diff --git a/src/main/java/noppes/npcs/client/key/KeyEvent.java b/src/main/java/noppes/npcs/client/key/KeyEvent.java index 53011f2e8..1948e2f78 100644 --- a/src/main/java/noppes/npcs/client/key/KeyEvent.java +++ b/src/main/java/noppes/npcs/client/key/KeyEvent.java @@ -15,10 +15,27 @@ public class KeyEvent { this.timestamp = System.currentTimeMillis(); } - public boolean is(KeyPreset.PressType t) { return this.type == t; } - public boolean isPress() { return is(KeyPreset.PressType.PRESS); } - public boolean isHold() { return is(KeyPreset.PressType.HOLD); } - public boolean isRelease() { return is(KeyPreset.PressType.RELEASE); } - public boolean isSinglePress() { return is(KeyPreset.PressType.SINGLE_PRESS); } - public boolean isPressRelease() { return is(KeyPreset.PressType.PRESS_RELEASE); } + public boolean is(KeyPreset.PressType t) { + return this.type == t; + } + + public boolean isPress() { + return is(KeyPreset.PressType.PRESS); + } + + public boolean isHold() { + return is(KeyPreset.PressType.HOLD); + } + + public boolean isRelease() { + return is(KeyPreset.PressType.RELEASE); + } + + public boolean isSinglePress() { + return is(KeyPreset.PressType.SINGLE_PRESS); + } + + public boolean isPressRelease() { + return is(KeyPreset.PressType.PRESS_RELEASE); + } } diff --git a/src/main/java/noppes/npcs/client/key/KeyPreset.java b/src/main/java/noppes/npcs/client/key/KeyPreset.java index 7bae2c58c..e76cf14ff 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPreset.java +++ b/src/main/java/noppes/npcs/client/key/KeyPreset.java @@ -13,8 +13,10 @@ public class KeyPreset { * @field SINGLE_PRESS => on releasing before 5 ticks press time * @field PRESS_RELEASE => on pressing and releasing */ - /** Press types for key events. */ - public enum PressType { PRESS, HOLD, RELEASE, SINGLE_PRESS, PRESS_RELEASE } + /** + * Press types for key events. + */ + public enum PressType {PRESS, HOLD, RELEASE, SINGLE_PRESS, PRESS_RELEASE} public KeyState defaultState = new KeyState(); public KeyState currentState = new KeyState(); @@ -81,7 +83,7 @@ public class KeyPreset { if (throttleInterval <= 0) return false; long currentTime = System.currentTimeMillis(); long intervalToUse = pressType == PressType.HOLD && isFirstAction && firstThrottleInterval > 0 ? - firstThrottleInterval : throttleInterval; + firstThrottleInterval : throttleInterval; if (currentTime - lastActionTime < intervalToUse) return true; lastActionTime = currentTime; isFirstAction = false; @@ -108,7 +110,7 @@ public class KeyPreset { } if (!down && isDown) { // just released - if (getPressTime() <= SHORT_PRESS_MS) + if (getPressTime() <= SHORT_PRESS_MS) onAction(PressType.SINGLE_PRESS); onAction(PressType.RELEASE, PressType.PRESS_RELEASE); pressStartMs = 0; @@ -116,10 +118,10 @@ public class KeyPreset { if (down) // while held onAction(PressType.HOLD); - + this.isDown = down; } - + public long getPressTime() { return pressStartMs > 0 ? (System.currentTimeMillis() - pressStartMs) : 0L; } diff --git a/src/main/java/noppes/npcs/client/key/KeyPresetManager.java b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java index 6f973e2b5..1a978cdc7 100644 --- a/src/main/java/noppes/npcs/client/key/KeyPresetManager.java +++ b/src/main/java/noppes/npcs/client/key/KeyPresetManager.java @@ -25,7 +25,7 @@ public class KeyPresetManager { } public void tick() { - for (KeyPreset key : keys) + for (KeyPreset key : keys) key.tick(); } diff --git a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java index f8f262893..4a9e294d0 100644 --- a/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java +++ b/src/main/java/noppes/npcs/client/key/impl/ScriptEditorKeys.java @@ -5,8 +5,8 @@ import noppes.npcs.client.key.KeyPresetManager; import org.lwjgl.input.Keyboard; public class ScriptEditorKeys extends KeyPresetManager { - - //////////////////// + + /// ///////////////// // Editor shortcuts (mapped from GuiScriptTextArea.handleShortcutKeys) public final KeyPreset COPY = add("Copy").setDefaultState(Keyboard.KEY_C, true, false, false); public final KeyPreset PASTE = add("Paste").setDefaultState(Keyboard.KEY_V, true, false, false); @@ -14,20 +14,20 @@ public class ScriptEditorKeys extends KeyPresetManager { public final KeyPreset DUPLICATE = add("Duplicate").setDefaultState(Keyboard.KEY_D, true, false, false); public final KeyPreset UNDO = add("Undo").setDefaultState(Keyboard.KEY_Z, true, false, false); public final KeyPreset REDO = add("Redo").setDefaultState(Keyboard.KEY_Y, true, false, false); - + public final KeyPreset FORMAT = add("Format Code").setDefaultState(Keyboard.KEY_F, false, true, false); public final KeyPreset TOGGLE_COMMENT = add("Toggle Comment").setDefaultState(Keyboard.KEY_SLASH, true, false, false); - + // Search/Replace public final KeyPreset SEARCH = add("Search").setDefaultState(Keyboard.KEY_F, true, false, false); public final KeyPreset SEARCH_REPLACE = add("Replace").setDefaultState(Keyboard.KEY_F, true, false, true); - + // Navigation public final KeyPreset GO_TO_LINE = add("Go to Line").setDefaultState(Keyboard.KEY_G, true, false, false); - + // Refactoring public final KeyPreset RENAME = add("Rename").setDefaultState(Keyboard.KEY_R, true, false, false); - + // View public final KeyPreset FULLSCREEN = add("Toggle Fullscreen").setDefaultState(Keyboard.KEY_F11, false, false, false); diff --git a/src/main/java/noppes/npcs/client/model/ModelPony.java b/src/main/java/noppes/npcs/client/model/ModelPony.java index b0d23b40a..203a1d12b 100644 --- a/src/main/java/noppes/npcs/client/model/ModelPony.java +++ b/src/main/java/noppes/npcs/client/model/ModelPony.java @@ -17,7 +17,7 @@ import noppes.npcs.entity.EntityNpcPony; import org.lwjgl.opengl.GL11; import javax.imageio.ImageIO; -import java.awt.*; +import java.awt.Color; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; diff --git a/src/main/java/noppes/npcs/client/renderer/ImageBufferDownloadAlt.java b/src/main/java/noppes/npcs/client/renderer/ImageBufferDownloadAlt.java index 252f0af65..90172ce6f 100644 --- a/src/main/java/noppes/npcs/client/renderer/ImageBufferDownloadAlt.java +++ b/src/main/java/noppes/npcs/client/renderer/ImageBufferDownloadAlt.java @@ -2,7 +2,7 @@ package noppes.npcs.client.renderer; import net.minecraft.client.renderer.ImageBufferDownload; -import java.awt.*; +import java.awt.Graphics; import java.awt.image.BufferedImage; import java.awt.image.DataBufferInt; diff --git a/src/main/java/noppes/npcs/client/renderer/RenderNPCPony.java b/src/main/java/noppes/npcs/client/renderer/RenderNPCPony.java index a82ba93a8..b018adf60 100644 --- a/src/main/java/noppes/npcs/client/renderer/RenderNPCPony.java +++ b/src/main/java/noppes/npcs/client/renderer/RenderNPCPony.java @@ -25,7 +25,7 @@ import noppes.npcs.entity.EntityNpcPony; import org.lwjgl.opengl.GL11; import javax.imageio.ImageIO; -import java.awt.*; +import java.awt.Color; import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; diff --git a/src/main/java/noppes/npcs/client/renderer/blocks/BlockScriptedRenderer.java b/src/main/java/noppes/npcs/client/renderer/blocks/BlockScriptedRenderer.java index 9aa8cdcaa..5be3a9492 100644 --- a/src/main/java/noppes/npcs/client/renderer/blocks/BlockScriptedRenderer.java +++ b/src/main/java/noppes/npcs/client/renderer/blocks/BlockScriptedRenderer.java @@ -23,7 +23,7 @@ import noppes.npcs.blocks.tiles.TileScripted.TextPlane; import noppes.npcs.client.TextBlockClient; import org.lwjgl.opengl.GL11; -import java.awt.*; +import java.awt.Color; import java.util.Random; public class BlockScriptedRenderer extends BlockRendererInterface { diff --git a/src/main/java/noppes/npcs/config/GlyphCache.java b/src/main/java/noppes/npcs/config/GlyphCache.java index 98474c493..86f34d433 100644 --- a/src/main/java/noppes/npcs/config/GlyphCache.java +++ b/src/main/java/noppes/npcs/config/GlyphCache.java @@ -5,7 +5,13 @@ import net.minecraft.client.renderer.GLAllocation; import net.minecraft.util.ResourceLocation; import org.lwjgl.opengl.GL11; -import java.awt.*; +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Font; +import java.awt.Graphics2D; +import java.awt.GraphicsEnvironment; +import java.awt.Rectangle; +import java.awt.RenderingHints; import java.awt.font.FontRenderContext; import java.awt.font.GlyphVector; import java.awt.geom.Point2D; diff --git a/src/main/java/noppes/npcs/config/StringCache.java b/src/main/java/noppes/npcs/config/StringCache.java index 263b480b9..c40d59f1c 100644 --- a/src/main/java/noppes/npcs/config/StringCache.java +++ b/src/main/java/noppes/npcs/config/StringCache.java @@ -5,7 +5,8 @@ import net.minecraft.client.renderer.Tessellator; import net.minecraft.util.ResourceLocation; import org.lwjgl.opengl.GL11; -import java.awt.*; +import java.awt.Font; +import java.awt.Point; import java.awt.font.GlyphVector; import java.lang.ref.WeakReference; import java.text.Bidi; diff --git a/src/main/java/noppes/npcs/constants/ScriptContext.java b/src/main/java/noppes/npcs/constants/ScriptContext.java index f2f614a1e..dcc5b5d95 100644 --- a/src/main/java/noppes/npcs/constants/ScriptContext.java +++ b/src/main/java/noppes/npcs/constants/ScriptContext.java @@ -1,13 +1,29 @@ package noppes.npcs.constants; -import noppes.npcs.api.event.*; +import noppes.npcs.api.event.IAnimationEvent; +import noppes.npcs.api.event.IBlockEvent; +import noppes.npcs.api.event.ICustomGuiEvent; +import noppes.npcs.api.event.IDialogEvent; +import noppes.npcs.api.event.IFactionEvent; +import noppes.npcs.api.event.IForgeEvent; +import noppes.npcs.api.event.IItemEvent; +import noppes.npcs.api.event.INpcEvent; +import noppes.npcs.api.event.IPartyEvent; +import noppes.npcs.api.event.IPlayerEvent; +import noppes.npcs.api.event.IProjectileEvent; +import noppes.npcs.api.event.IQuestEvent; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Represents a script context that maps to one or more event interface namespaces. - * + *

* A script context can have multiple namespaces because script editors often * support hooks from multiple event types. For example, Player scripts support: * - IPlayerEvent (core player hooks) @@ -15,17 +31,17 @@ import java.util.concurrent.ConcurrentHashMap; * - IPartyEvent (party hooks) * - ICustomGuiEvent (custom GUI hooks) * - etc. - * + *

* Each context also has a hookContext string used by ScriptHookController for * hook registration and lookup. - * + *

* This is a registerable system - any mod can register their own contexts: - * - * // Register a new context with multiple namespaces: - * ScriptContext.register("DBC", "dbc", IDBCEvent.class, IDBCPlayerEvent.class); - * - * // Or add namespaces to an existing context: - * ScriptContext.PLAYER.addNamespace("IDBCPlayerEvent"); + *

+ * // Register a new context with multiple namespaces: + * ScriptContext.register("DBC", "dbc", IDBCEvent.class, IDBCPlayerEvent.class); + *

+ * // Or add namespaces to an existing context: + * ScriptContext.PLAYER.addNamespace("IDBCPlayerEvent"); */ public class ScriptContext { @@ -35,42 +51,42 @@ public class ScriptContext { // ==================== BUILT-IN CONTEXTS ==================== public static final ScriptContext NPC = register("NPC", "npc", - INpcEvent.class, - IProjectileEvent.class + INpcEvent.class, + IProjectileEvent.class ); public static final ScriptContext PLAYER = register("PLAYER", "player", - IPlayerEvent.class, - IAnimationEvent.class, - IPartyEvent.class, - IDialogEvent.class, - IQuestEvent.class, - IFactionEvent.class, - ICustomGuiEvent.class + IPlayerEvent.class, + IAnimationEvent.class, + IPartyEvent.class, + IDialogEvent.class, + IQuestEvent.class, + IFactionEvent.class, + ICustomGuiEvent.class ); public static final ScriptContext BLOCK = register("BLOCK", "block", - IBlockEvent.class + IBlockEvent.class ); public static final ScriptContext ITEM = register("ITEM", "item", - IItemEvent.class + IItemEvent.class ); public static final ScriptContext FORGE = register("FORGE", "forge", - IForgeEvent.class + IForgeEvent.class ); public static final ScriptContext LINKED_ITEM = register("LINKED_ITEM", "linked_item", - IItemEvent.class + IItemEvent.class ); public static final ScriptContext RECIPE = register("RECIPE", "recipe", - "Recipe" + "Recipe" ); public static final ScriptContext EFFECT = register("EFFECT", "effect", - "Effect" + "Effect" ); public static final ScriptContext GLOBAL = register("GLOBAL", "", @@ -79,13 +95,19 @@ public class ScriptContext { // ==================== INSTANCE FIELDS ==================== - /** Unique identifier for this context (e.g., "NPC", "PLAYER", "DBC") */ + /** + * Unique identifier for this context (e.g., "NPC", "PLAYER", "DBC") + */ public final String id; - /** Hook context identifier used by ScriptHookController (e.g., "npc", "player") */ + /** + * Hook context identifier used by ScriptHookController (e.g., "npc", "player") + */ public final String hookContext; - /** The event interface namespaces this context supports */ + /** + * The event interface namespaces this context supports + */ private final List namespaces; // ==================== CONSTRUCTOR ==================== @@ -144,9 +166,9 @@ public class ScriptContext { /** * Register a new script context with a hook context and one or more namespaces. * - * @param id Unique identifier (e.g., "DBC", "CUSTOM") + * @param id Unique identifier (e.g., "DBC", "CUSTOM") * @param hookContext The hook context string for ScriptHookController (e.g., "dbc") - * @param namespaces The event interface names (e.g., "IDBCEvent", "IDBCPlayerEvent") + * @param namespaces The event interface names (e.g., "IDBCEvent", "IDBCPlayerEvent") * @return The registered ScriptContext */ public static ScriptContext register(String id, String hookContext, String... namespaces) { @@ -158,12 +180,12 @@ public class ScriptContext { /** * Register a new script context using event interface classes. * Automatically extracts the simple name from each class. - * + *

* Example: - * ScriptContext.register("NPC", "npc", INpcEvent.class, IProjectileEvent.class); + * ScriptContext.register("NPC", "npc", INpcEvent.class, IProjectileEvent.class); * - * @param id Unique identifier (e.g., "DBC", "CUSTOM") - * @param hookContext The hook context string for ScriptHookController (e.g., "npc") + * @param id Unique identifier (e.g., "DBC", "CUSTOM") + * @param hookContext The hook context string for ScriptHookController (e.g., "npc") * @param eventClasses The event interface classes (simple names will be extracted) * @return The registered ScriptContext */ diff --git a/src/main/java/noppes/npcs/controllers/AnimationController.java b/src/main/java/noppes/npcs/controllers/AnimationController.java index 65adfcd3d..e3caddce5 100644 --- a/src/main/java/noppes/npcs/controllers/AnimationController.java +++ b/src/main/java/noppes/npcs/controllers/AnimationController.java @@ -231,10 +231,9 @@ public class AnimationController implements IAnimationHandler { } - //////////////////////////////////////////////////////// - //////////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// // ANIMATION MAP - public File getMapDir() { File dir = CustomNpcs.getWorldSaveDirectory(); if (!dir.exists()) diff --git a/src/main/java/noppes/npcs/controllers/HookDefinition.java b/src/main/java/noppes/npcs/controllers/HookDefinition.java index 9d8c34fa6..864e37ee3 100644 --- a/src/main/java/noppes/npcs/controllers/HookDefinition.java +++ b/src/main/java/noppes/npcs/controllers/HookDefinition.java @@ -97,7 +97,7 @@ public class HookDefinition implements IHookDefinition { * Create a hook definition from a hook name and event class. * Automatically derives imports from the event class. * - * @param hookName The hook function name + * @param hookName The hook function name * @param eventClass The event class (e.g., INpcEvent.InitEvent.class) * @return A hook definition with auto-derived imports */ @@ -131,7 +131,7 @@ public class HookDefinition implements IHookDefinition { * This provides backward compatibility with the annotation-based hook definition approach. * * @param hookName The hook name (may differ from method name) - * @param method The method to extract metadata from + * @param method The method to extract metadata from * @return A hook definition with extracted metadata */ public static HookDefinition fromMethod(String hookName, Method method) { @@ -165,7 +165,7 @@ public class HookDefinition implements IHookDefinition { } else { // Fallback: use lowercase simple type name names[i] = Character.toLowerCase(params[i].getType().getSimpleName().charAt(0)) - + params[i].getType().getSimpleName().substring(1); + + params[i].getType().getSimpleName().substring(1); } } builder.paramNames(names); diff --git a/src/main/java/noppes/npcs/controllers/LinkedItemController.java b/src/main/java/noppes/npcs/controllers/LinkedItemController.java index 48f19958b..78534a82f 100644 --- a/src/main/java/noppes/npcs/controllers/LinkedItemController.java +++ b/src/main/java/noppes/npcs/controllers/LinkedItemController.java @@ -94,8 +94,8 @@ public class LinkedItemController { return this.linkedItemsScripts.get(id); } - //////////////////////////////////////////////////////// - //////////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// public void load() { lastUsedID = 0; @@ -247,10 +247,9 @@ public class LinkedItemController { } - //////////////////////////////////////////////////////// - //////////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// + /// ///////////////////////////////////////////////////// // LINKED ITEMS MAP - public File getMapDir() { File dir = CustomNpcs.getWorldSaveDirectory(); if (!dir.exists()) diff --git a/src/main/java/noppes/npcs/controllers/MarketRegistry.java b/src/main/java/noppes/npcs/controllers/MarketRegistry.java index 1110f9e5f..55de3a0bf 100644 --- a/src/main/java/noppes/npcs/controllers/MarketRegistry.java +++ b/src/main/java/noppes/npcs/controllers/MarketRegistry.java @@ -15,7 +15,7 @@ import java.util.concurrent.ConcurrentHashMap; * Central registry for linked market synchronization. * Tracks which RoleTrader instances use which markets, * and which players are viewing each market. - * + *

* This enables immediate sync across all traders sharing * the same market when a purchase occurs. */ diff --git a/src/main/java/noppes/npcs/controllers/ScriptContainer.java b/src/main/java/noppes/npcs/controllers/ScriptContainer.java index eb0b8b1c4..738819500 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptContainer.java +++ b/src/main/java/noppes/npcs/controllers/ScriptContainer.java @@ -270,41 +270,41 @@ public class ScriptContainer implements IScriptUnit { this.evaluated = false; } } - + // ==================== IScriptUnit IMPLEMENTATION ==================== - + @Override public String getScript() { return this.script; } - + @Override public void setScript(String script) { this.script = script; this.evaluated = false; } - + @Override public List getExternalScripts() { return this.scripts; } - + @Override public void setExternalScripts(List scripts) { this.scripts = scripts; this.evaluated = false; } - + @Override public TreeMap getConsole() { return this.console; } - + @Override public void clearConsole() { this.console.clear(); } - + @Override public String getLanguage() { // If a specific language is set for this container, use it @@ -314,7 +314,7 @@ public class ScriptContainer implements IScriptUnit { } return handler != null ? handler.getLanguage() : "ECMAScript"; } - + @Override public void setLanguage(String language) { if (!Objects.equals(this.currentScriptLanguage, language)) { @@ -322,7 +322,7 @@ public class ScriptContainer implements IScriptUnit { this.evaluated = false; } } - + @Override public String generateHookStub(String hookName, Object hookData) { // ECMAScript function template @@ -330,13 +330,14 @@ public class ScriptContainer implements IScriptUnit { } @Override - public void ensureCompiled() {} + public void ensureCompiled() { + } @Override public boolean hasErrored() { return this.errored; } - + @Override public void setErrored(boolean errored) { this.errored = errored; diff --git a/src/main/java/noppes/npcs/controllers/ScriptHookController.java b/src/main/java/noppes/npcs/controllers/ScriptHookController.java index f89177c9e..f3bad8e9d 100644 --- a/src/main/java/noppes/npcs/controllers/ScriptHookController.java +++ b/src/main/java/noppes/npcs/controllers/ScriptHookController.java @@ -1,16 +1,43 @@ package noppes.npcs.controllers; -import noppes.npcs.api.event.*; +import noppes.npcs.api.event.IAbilityEvent; +import noppes.npcs.api.event.IAnimationEvent; +import noppes.npcs.api.event.IBlockEvent; +import noppes.npcs.api.event.ICustomGuiEvent; +import noppes.npcs.api.event.ICustomNPCsEvent; +import noppes.npcs.api.event.IDialogEvent; +import noppes.npcs.api.event.IFactionEvent; +import noppes.npcs.api.event.IForgeEvent; +import noppes.npcs.api.event.IItemEvent; +import noppes.npcs.api.event.ILinkedItemEvent; +import noppes.npcs.api.event.INpcEvent; +import noppes.npcs.api.event.IPartyEvent; +import noppes.npcs.api.event.IPlayerEvent; +import noppes.npcs.api.event.IProjectileEvent; +import noppes.npcs.api.event.IQuestEvent; +import noppes.npcs.api.event.IRecipeEvent; import noppes.npcs.api.handler.IHookDefinition; import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.data.RecipeScript; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import static noppes.npcs.constants.EnumScriptType.*; -import static noppes.npcs.constants.ScriptContext.*; +import static noppes.npcs.constants.ScriptContext.BLOCK; +import static noppes.npcs.constants.ScriptContext.EFFECT; +import static noppes.npcs.constants.ScriptContext.FORGE; +import static noppes.npcs.constants.ScriptContext.ITEM; +import static noppes.npcs.constants.ScriptContext.LINKED_ITEM; +import static noppes.npcs.constants.ScriptContext.NPC; +import static noppes.npcs.constants.ScriptContext.PLAYER; +import static noppes.npcs.constants.ScriptContext.RECIPE; /** * Controller for managing script hooks. @@ -290,8 +317,8 @@ public class ScriptHookController implements IScriptHookHandler { * Register a hook with event class (auto-derives imports). * For addon mods to register custom hooks. * - * @param context Hook context (e.g., "npc", "player") - * @param hookName Function name in scripts + * @param context Hook context (e.g., "npc", "player") + * @param hookName Function name in scripts * @param eventClass The event class (imports auto-derived from enclosing class) */ public void registerHook(String context, String hookName, Class eventClass) { diff --git a/src/main/java/noppes/npcs/controllers/data/Animation.java b/src/main/java/noppes/npcs/controllers/data/Animation.java index c7d006198..8cf9b3827 100644 --- a/src/main/java/noppes/npcs/controllers/data/Animation.java +++ b/src/main/java/noppes/npcs/controllers/data/Animation.java @@ -185,16 +185,26 @@ public class Animation implements IAnimation { } @Override - public boolean hasData(String key) {return dataStore.containsKey(key);} - - @Override - public Object getData(String key) {return dataStore.get(key);} + public boolean hasData(String key) { + return dataStore.containsKey(key); + } @Override - public IAnimation setData(String key, Object v) {dataStore.put(key, v);return this;} + public Object getData(String key) { + return dataStore.get(key); + } @Override - public IAnimation removeData(String key) {dataStore.remove(key);return this;} + public IAnimation setData(String key, Object v) { + dataStore.put(key, v); + return this; + } + + @Override + public IAnimation removeData(String key) { + dataStore.remove(key); + return this; + } public void readFromNBT(NBTTagCompound compound) { if (compound.hasKey("ID")) { @@ -323,13 +333,13 @@ public class Animation implements IAnimation { this.onAnimationStart = task; return this; } - + @Override public IAnimation onFrame(BiConsumer task) { this.onAnimationFrame = task; return this; } - + @Override public IAnimation onEnd(Consumer task) { this.onAnimationEnd = task; @@ -355,7 +365,7 @@ public class Animation implements IAnimation { } /** - * Only called on setAnimation, to transfer tasks from + * Only called on setAnimation, to transfer tasks from * this global instance stored in AnimationController * to the new local instance on entity's AnimationData, * and clear them from the global instance. @@ -372,6 +382,6 @@ public class Animation implements IAnimation { this.dataStore.putAll(animation.dataStore); animation.dataStore.clear(); } - - + + } diff --git a/src/main/java/noppes/npcs/controllers/data/CustomEffect.java b/src/main/java/noppes/npcs/controllers/data/CustomEffect.java index 9b621234d..7a5db74de 100644 --- a/src/main/java/noppes/npcs/controllers/data/CustomEffect.java +++ b/src/main/java/noppes/npcs/controllers/data/CustomEffect.java @@ -194,7 +194,7 @@ public class CustomEffect implements ICustomEffect { return; } - script.callScript(EnumScriptType.ON_EFFECT_ADD, event); + script.callScript(EnumScriptType.ON_EFFECT_ADD, event); } EventHooks.onEffectAdded(iPlayer, playerEffect); @@ -214,7 +214,7 @@ public class CustomEffect implements ICustomEffect { return; } - script.callScript(EnumScriptType.ON_EFFECT_TICK, event); + script.callScript(EnumScriptType.ON_EFFECT_TICK, event); } EventHooks.onEffectTick(iPlayer, playerEffect); @@ -235,7 +235,7 @@ public class CustomEffect implements ICustomEffect { return; } - script.callScript(EnumScriptType.ON_EFFECT_REMOVE, event); + script.callScript(EnumScriptType.ON_EFFECT_REMOVE, event); } EventHooks.onEffectRemove(iPlayer, playerEffect, type); diff --git a/src/main/java/noppes/npcs/controllers/data/DataScript.java b/src/main/java/noppes/npcs/controllers/data/DataScript.java index ede4a4c96..74806605f 100644 --- a/src/main/java/noppes/npcs/controllers/data/DataScript.java +++ b/src/main/java/noppes/npcs/controllers/data/DataScript.java @@ -312,6 +312,7 @@ public class DataScript implements IScriptHandlerPacket { public Collection getNPCScripts() { return this.scripts.values(); } + @Override public IScriptUnit createJaninoScriptUnit() { return new EventJaninoScript(ScriptContext.NPC); diff --git a/src/main/java/noppes/npcs/controllers/data/EffectScript.java b/src/main/java/noppes/npcs/controllers/data/EffectScript.java index 821e542b4..457eb11d6 100644 --- a/src/main/java/noppes/npcs/controllers/data/EffectScript.java +++ b/src/main/java/noppes/npcs/controllers/data/EffectScript.java @@ -2,9 +2,7 @@ package noppes.npcs.controllers.data; import kamkeel.npcs.network.packets.request.script.EffectScriptPacket; import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IScriptHookHandler; import noppes.npcs.constants.ScriptContext; -import noppes.npcs.scripted.event.player.PlayerEvent; /** * Script handler for CustomEffect scripts. @@ -12,7 +10,9 @@ import noppes.npcs.scripted.event.player.PlayerEvent; */ public class EffectScript extends SingleScriptHandler implements IScriptHandlerPacket { - /** The effect ID for packet communication. -1 if not bound to an effect. */ + /** + * The effect ID for packet communication. -1 if not bound to an effect. + */ private int effectId = -1; /** @@ -23,6 +23,7 @@ public class EffectScript extends SingleScriptHandler implements IScriptHandlerP /** * Create an EffectScript bound to a specific effect (for GUI use). + * * @param effectId The ID of the CustomEffect */ public EffectScript(int effectId) { diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java index 93892f2a6..812bef6d8 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandler.java @@ -13,16 +13,16 @@ import java.util.TreeMap; public interface IScriptHandler { default void callScript(EnumScriptType type, Event event) { - if (type != null) + if (type != null) callScript(type.function, event); - + } void callScript(String hookName, Event event); /** * Call a function by name and return its result. - * + *

* Default implementation calls the function on each script unit (in order) and returns * the first non-null value. */ @@ -86,7 +86,7 @@ public interface IScriptHandler { default Map getConsoleText() { Map map = new TreeMap<>(); int tab = 0; - + for (IScriptUnit script : getScripts()) { ++tab; for (Map.Entry entry : script.getConsole().entrySet()) { @@ -97,24 +97,24 @@ public interface IScriptHandler { } default void clearConsole() { - for (IScriptUnit script : getScripts()) + for (IScriptUnit script : getScripts()) script.clearConsole(); - + } - + /** * Create a new Janino (Java) script unit for this handler. * Handlers that don't support Janino scripts should return null. - * + * * @return A new JaninoScript instance, or null if not supported */ default IScriptUnit createJaninoScriptUnit() { return null; // Default: Janino not supported } - + /** * Check if this handler supports Janino (Java) scripts. - * + * * @return true if createJaninoScriptUnit() can create valid Janino scripts */ default boolean supportsJanino() { @@ -133,7 +133,7 @@ public interface IScriptHandler { /** * Get the hook context identifier for this handler. * Used by ScriptHookController to look up available hooks. - * + *

* By default, derives from getContext().hookContext. * Override if a different hook context is needed. * @@ -152,9 +152,9 @@ public interface IScriptHandler { */ default List getHooks() { String context = getHookContext(); - if (context == null || context.isEmpty()) + if (context == null || context.isEmpty()) return Collections.emptyList(); - + return ScriptHookController.Instance.getAllHooks(context); } diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java b/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java index c307f508d..88ff5fb94 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptHandlerPacket.java @@ -15,9 +15,9 @@ public interface IScriptHandlerPacket extends IScriptHandler { /** * Client-side: send a script container packet to the server. * - * @param index {@code >= 0} for a script tab, {@code -1} for handler metadata (language/enabled/console) + * @param index {@code >= 0} for a script tab, {@code -1} for handler metadata (language/enabled/console) * @param totalCount Total number of scripts for this handler - * @param nbt The script container NBT (tab content or metadata) + * @param nbt The script container NBT (tab content or metadata) */ void sendSavePacket(int index, int totalCount, NBTTagCompound nbt); diff --git a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java index 258b584b8..b439d7d0a 100644 --- a/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java +++ b/src/main/java/noppes/npcs/controllers/data/IScriptUnit.java @@ -6,7 +6,6 @@ import noppes.npcs.constants.EnumScriptType; import noppes.npcs.controllers.ScriptContainer; import java.util.List; -import java.util.Map; import java.util.TreeMap; /** @@ -15,24 +14,24 @@ import java.util.TreeMap; * This allows GuiScriptInterface to work with both script types uniformly. */ public interface IScriptUnit { - + // NBT type identifiers String NBT_TYPE_KEY = "ScriptUnitType"; String TYPE_ECMASCRIPT = "ECMAScript"; String TYPE_JANINO = "Janino"; - + /** * Create an IScriptUnit from NBT data. * Uses the "ScriptUnitType" tag to determine which implementation to create. - * + * * @param compound The NBT data - * @param handler The script handler (used for ScriptContainer) + * @param handler The script handler (used for ScriptContainer) * @return A ScriptContainer or JaninoScript based on the NBT type */ static IScriptUnit createFromNBT(NBTTagCompound compound, IScriptHandler handler) { String type = compound.getString(NBT_TYPE_KEY); IScriptUnit unit; - + if (TYPE_JANINO.equals(type)) { // Create appropriate JaninoScript based on handler unit = handler.createJaninoScriptUnit(); @@ -44,96 +43,97 @@ public interface IScriptUnit { // Default to ScriptContainer for ECMAScript or missing type unit = new ScriptContainer(handler); } - + unit.readFromNBT(compound); return unit; } - + // ==================== SCRIPT CONTENT ==================== - + /** * Get the main script text content. */ String getScript(); - + /** * Set the main script text content. */ void setScript(String script); - + /** * Get the list of external script file names loaded for this unit. */ List getExternalScripts(); - + /** * Set the list of external script file names to load. */ void setExternalScripts(List scripts); - + // ==================== CONSOLE ==================== - + /** * Get the console output map (timestamp -> message). */ TreeMap getConsole(); - + /** * Clear all console output. */ void clearConsole(); - + /** * Append a message to the console. */ void appendConsole(String message); - + // ==================== LANGUAGE ==================== - + /** * Get the scripting language for this unit. * Returns "ECMAScript", "Java", etc. */ String getLanguage(); - + /** * Set the scripting language for this unit. + * * @param language The language name (e.g., "ECMAScript", "Java") */ void setLanguage(String language); - + /** * Check if this script unit uses Janino (Java) compilation. */ default boolean isJanino() { return "Java".equals(getLanguage()); } - + // ==================== HOOK GENERATION ==================== - + /** * Generate a stub/template for the given hook name. * ECMAScript: function hookName(event) { } * Janino: full Java method signature with return type - * + * * @param hookName The hook name to generate a stub for * @param hookData Optional data (e.g., Method object for Janino) * @return The generated stub string */ String generateHookStub(String hookName, Object hookData); - + // ==================== NBT ==================== - + /** * Write this script unit to NBT. */ NBTTagCompound writeToNBT(NBTTagCompound compound); - + /** * Read this script unit from NBT. */ void readFromNBT(NBTTagCompound compound); - + // ==================== STATE ==================== /** @@ -141,43 +141,43 @@ public interface IScriptUnit { * No-op for ScriptContainer. */ void ensureCompiled(); - + /** * Check if this script unit has any code (script or external scripts). */ boolean hasCode(); - + /** * Check if this script unit has encountered an error during execution. */ boolean hasErrored(); - + /** * Set the error state of this script unit. */ void setErrored(boolean errored); - + // ==================== EXECUTION ==================== - + /** * Execute this script unit for the given hook type and event. - * - * @param type The script hook type + * + * @param type The script hook type * @param event The event object */ void run(EnumScriptType type, Event event); - + /** * Execute this script unit for the given hook name and event. * * @param hookName The hook name (e.g., "init", "interact") - * @param event The event object + * @param event The event object */ void run(String hookName, Object event); /** * Call a script function by name and return its result. - * + *

* Default implementation returns null (not supported). */ default Object callFunction(String hookName, Object... args) { diff --git a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java index fb354e590..cbe1230e0 100644 --- a/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/JaninoScriptHandler.java @@ -11,13 +11,13 @@ import java.util.function.Supplier; /** * Reusable IScriptHandler implementation for objects that own an optional single JaninoScript unit. - * + *

* This is meant for "function provider" scripts (not EnumScriptType driven). Execution is typically * initiated by the owning system calling script.call(...) directly. - * + *

* NBT storage is kept under the parent compound key "Script" (same convention as JaninoScriptable). */ -public class JaninoScriptHandler> extends ScriptHandler { +public class JaninoScriptHandler> extends ScriptHandler { protected S script; @@ -41,9 +41,9 @@ public class JaninoScriptHandler> extends ScriptHandle } public JaninoScript createScript() { - if (script == null) + if (script == null) script = factory.get(); - + return script; } @@ -96,9 +96,9 @@ public class JaninoScriptHandler> extends ScriptHandle @Override public List getScripts() { - if (script == null) + if (script == null) return Collections.emptyList(); - + return Collections.singletonList(script); } diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItem.java b/src/main/java/noppes/npcs/controllers/data/LinkedItem.java index f1840a625..961c570f9 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItem.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItem.java @@ -249,8 +249,8 @@ public class LinkedItem implements ILinkedItem { @Override public void setAttackSpeed(int time) { - if(time <= 0) { - time = 10; + if (time <= 0) { + time = 10; } this.attackSpeed = time; } diff --git a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java index 1ee4537fb..a54c20727 100644 --- a/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java +++ b/src/main/java/noppes/npcs/controllers/data/LinkedItemScript.java @@ -9,44 +9,47 @@ import noppes.npcs.constants.ScriptContext; * Manages a single script container for linked item hooks. */ public class LinkedItemScript extends SingleScriptHandler implements IScriptHandlerPacket { - - /** The linked item ID for packet communication. -1 if not bound. */ + + /** + * The linked item ID for packet communication. -1 if not bound. + */ private int linkedItemId = -1; - + /** * Create an unbound LinkedItemScript (for server-side use). */ public LinkedItemScript() { } - + /** * Create a LinkedItemScript bound to a specific linked item (for GUI use). + * * @param linkedItemId The ID of the LinkedItem */ public LinkedItemScript(int linkedItemId) { this.linkedItemId = linkedItemId; } - + @Override public ScriptContext getContext() { return ScriptContext.LINKED_ITEM; } - + @Override public String noticeString() { return "LinkedItem"; } - + @Override public void requestData() { - if (linkedItemId >= 0) + if (linkedItemId >= 0) LinkedItemScriptPacket.Get(linkedItemId); } - + @Override public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { - if (linkedItemId >= 0) + if (linkedItemId >= 0) LinkedItemScriptPacket.Save(linkedItemId, index, totalCount, nbt); - + } } diff --git a/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java index d175eb3b9..d4b4c027b 100644 --- a/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/MultiScriptHandler.java @@ -7,13 +7,11 @@ import noppes.npcs.controllers.ScriptContainer; import noppes.npcs.controllers.ScriptController; import java.util.ArrayList; -import java.util.Map; -import java.util.TreeMap; /** * Base class for script handlers that manage multiple script containers (tabs). * Provides shared NBT serialization and common script execution patterns. - * + *

* Subclasses should: * - Override getContext() to return the appropriate ScriptContext * - Override getHookContext() if not using ScriptContext's hookContext @@ -21,7 +19,7 @@ import java.util.TreeMap; * - Override isEnabled() if custom enable logic is needed */ public abstract class MultiScriptHandler extends ScriptHandler implements IScriptHandlerPacket { - + // ==================== NBT SERIALIZATION ==================== /** diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java index 1271594ba..b2b8c4b57 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java @@ -99,6 +99,7 @@ public class PlayerCurrencyData implements IPlayerCurrencyData { /** * Add currency to balance (deposit) + * * @param amount Amount to add * @return true if successful, false if would exceed max balance */ @@ -135,6 +136,7 @@ public class PlayerCurrencyData implements IPlayerCurrencyData { /** * Remove currency from balance (withdraw) + * * @param amount Amount to remove * @return true if successful, false if insufficient funds */ diff --git a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java index 803a1b30d..97f81df90 100644 --- a/src/main/java/noppes/npcs/controllers/data/RecipeScript.java +++ b/src/main/java/noppes/npcs/controllers/data/RecipeScript.java @@ -9,48 +9,53 @@ import noppes.npcs.constants.ScriptContext; * Manages a single script container for recipe hooks (pre, post). */ public class RecipeScript extends SingleScriptHandler implements IScriptHandlerPacket { - - /** The recipe ID for packet communication. -1 if not bound. */ + + /** + * The recipe ID for packet communication. -1 if not bound. + */ private int recipeId = -1; - - /** Whether this is an anvil recipe (vs carpentry). */ + + /** + * Whether this is an anvil recipe (vs carpentry). + */ private boolean anvil = false; - + /** * Create an unbound RecipeScript (for server-side use). */ public RecipeScript() { } - + /** * Create a RecipeScript bound to a specific recipe (for GUI use). + * * @param recipeId The ID of the recipe - * @param anvil true for anvil recipe, false for carpentry recipe + * @param anvil true for anvil recipe, false for carpentry recipe */ public RecipeScript(int recipeId, boolean anvil) { this.recipeId = recipeId; this.anvil = anvil; } - + @Override public ScriptContext getContext() { return ScriptContext.RECIPE; } - + @Override public String noticeString() { return "RecipeScript"; } - + @Override public void requestData() { - if (recipeId >= 0) + if (recipeId >= 0) RecipeScriptPacket.Get(anvil, recipeId); } - + @Override public void sendSavePacket(int index, int totalCount, NBTTagCompound nbt) { - if (recipeId >= 0) + if (recipeId >= 0) RecipeScriptPacket.Save(anvil, recipeId, index, totalCount, nbt); } @@ -63,4 +68,4 @@ public class RecipeScript extends SingleScriptHandler implements IScriptHandlerP this.function = functionName; } } -} \ No newline at end of file +} diff --git a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java index dd57b05bb..684c9a45b 100644 --- a/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/ScriptHandler.java @@ -68,6 +68,7 @@ public abstract class ScriptHandler implements IScriptHandler { public void clear() { this.scripts = new ArrayList<>(); } + /** * Reset the lastInited timestamp to force re-initialization on next script call. */ diff --git a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java index a047b41b6..00c273bca 100644 --- a/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java +++ b/src/main/java/noppes/npcs/controllers/data/SingleScriptHandler.java @@ -16,7 +16,7 @@ import java.util.List; /** * Base class for script handlers that manage a single script container. * Provides shared NBT/ByteBuf serialization and packet handling. - * + *

* Subclasses should: * - Store their parent object (e.g., CustomEffect, LinkedItem) * - Override getHookContext() to return the appropriate context string @@ -57,9 +57,9 @@ public abstract class SingleScriptHandler extends ScriptHandler { @Override public List getScripts() { - if(container == null) + if (container == null) return new ArrayList<>(); - + return Collections.singletonList(container); } @@ -112,7 +112,7 @@ public abstract class SingleScriptHandler extends ScriptHandler { /** * Save script data from a ByteBuf packet. * Handles the tab/totalScripts protocol used by script packets. - * + *

* Protocol: * - tab == 0: Script content NBT follows * - tab != 0 (typically -1): Script metadata (language, enabled) follows diff --git a/src/main/java/noppes/npcs/controllers/data/TraderStock.java b/src/main/java/noppes/npcs/controllers/data/TraderStock.java index 68bc96c53..85e76ec02 100644 --- a/src/main/java/noppes/npcs/controllers/data/TraderStock.java +++ b/src/main/java/noppes/npcs/controllers/data/TraderStock.java @@ -41,7 +41,8 @@ public class TraderStock { /** * Get available stock for a slot - * @param slot The slot index + * + * @param slot The slot index * @param playerName The player name (for per-player mode) * @return Available stock count, or Integer.MAX_VALUE if unlimited */ @@ -73,6 +74,7 @@ public class TraderStock { /** * Consume stock when purchase is made + * * @return true if successful, false if insufficient stock */ public synchronized boolean consumeStock(int slot, String playerName, int amount) { @@ -98,6 +100,7 @@ public class TraderStock { /** * Check if stock should reset based on current time + * * @param currentTime Current MC world time (ticks) or real time (ms) depending on reset type */ public boolean shouldReset(long currentTime) { @@ -113,6 +116,7 @@ public class TraderStock { /** * Reset all stock to max values + * * @param currentTime Current time to record as reset time */ public synchronized void resetStock(long currentTime) { @@ -129,6 +133,7 @@ public class TraderStock { /** * Get reset interval based on reset type + * * @return Interval in ticks (MC time) or milliseconds (real time) */ public long getResetInterval() { @@ -152,6 +157,7 @@ public class TraderStock { /** * Get time until next reset + * * @param currentTime Current time * @return Time remaining until reset, or -1 if no reset scheduled */ @@ -166,6 +172,7 @@ public class TraderStock { /** * Format time until reset for display + * * @param currentTime Current time * @return Formatted string like "2 hours" or "1 day, 5 hours" */ @@ -183,7 +190,7 @@ public class TraderStock { if (days > 0) { return days + " day" + (days != 1 ? "s" : "") + - (hours > 0 ? ", " + hours + " hour" + (hours != 1 ? "s" : "") : ""); + (hours > 0 ? ", " + hours + " hour" + (hours != 1 ? "s" : "") : ""); } else if (hours > 0) { return hours + " hour" + (hours != 1 ? "s" : ""); } else { @@ -198,7 +205,7 @@ public class TraderStock { if (mcDays > 0) { return mcDays + " MC day" + (mcDays != 1 ? "s" : "") + - (mcHours > 0 ? ", " + mcHours + " MC hour" + (mcHours != 1 ? "s" : "") : ""); + (mcHours > 0 ? ", " + mcHours + " MC hour" + (mcHours != 1 ? "s" : "") : ""); } else { return mcHours + " MC hour" + (mcHours != 1 ? "s" : ""); } @@ -220,6 +227,7 @@ public class TraderStock { /** * Validate and clamp all current stock values to not exceed max stock. * Should be called when trader GUI opens to ensure consistency. + * * @return true if any stock was clamped */ public boolean validateStock() { diff --git a/src/main/java/noppes/npcs/controllers/data/action/Action.java b/src/main/java/noppes/npcs/controllers/data/action/Action.java index 729603019..b7bce1102 100644 --- a/src/main/java/noppes/npcs/controllers/data/action/Action.java +++ b/src/main/java/noppes/npcs/controllers/data/action/Action.java @@ -274,10 +274,9 @@ public class Action implements IAction { // return this; // } - /////////////////////////////////////////////////// - ////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// /////////////////////////////////////////////// // Handling - @Override public IAction start() { manager.start(); @@ -405,10 +404,10 @@ public class Action implements IAction { public String toString() { return String.format("%s [queue='%s', scheduled=%s, done=%s, paused=%s, updateEvery=%s, duration=%d/%d, count=%d/%d, threaded=%s]", getIdentifier(), getQueueName(), isScheduled, done, isPaused(), updateEveryXTick, duration, maxDuration, count, maxCount, isThreaded); } - /////////////////////////////////////////////////// - ////////////////////////////////////////////////// - // Thread + /// //////////////////////////////////////////////// + /// /////////////////////////////////////////////// + // Thread @Override public IAction threadify() { if (!isThreaded) { @@ -469,10 +468,9 @@ public class Action implements IAction { return startAfterTicks > 0; } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Sequential - @Override public IAction getNext() { if (queue == null) @@ -606,10 +604,9 @@ public class Action implements IAction { } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Parallels - @Override public IAction parallel(IAction act) { if (unscheduledList != null) @@ -658,10 +655,9 @@ public class Action implements IAction { return parallel(manager.create(name, maxDuration, delay, t)); } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Conditionals - @Override public IConditionalAction conditional(IConditionalAction after) { if (unscheduledList != null) diff --git a/src/main/java/noppes/npcs/controllers/data/action/ActionManager.java b/src/main/java/noppes/npcs/controllers/data/action/ActionManager.java index c36aa04a9..9cdaf4e57 100644 --- a/src/main/java/noppes/npcs/controllers/data/action/ActionManager.java +++ b/src/main/java/noppes/npcs/controllers/data/action/ActionManager.java @@ -117,10 +117,9 @@ public class ActionManager implements IActionManager { // return false; // } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Creators - @Override public IAction create(String name) { return new Action(this, name); @@ -186,10 +185,9 @@ public class ActionManager implements IActionManager { return new ConditionalAction(this, name, condition, task, terminateWhen, onTermination); } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Queues - @Override public IActionQueue createQueue(String name) { return createQueue(name, false); @@ -237,10 +235,9 @@ public class ActionManager implements IActionManager { return true; } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Sequential - @Override public IActionQueue getSequentialQueue() { return sequentialQueue; @@ -299,10 +296,9 @@ public class ActionManager implements IActionManager { return sequentialQueue.scheduleActionAt(index, action); } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Parallels - @Override public IActionQueue getParallelQueue() { return parallelQueue; @@ -345,7 +341,7 @@ public class ActionManager implements IActionManager { @Override public IAction scheduleParallel(String name, int delay, Consumer task) { - return parallelQueue.schedule(name,delay, task); + return parallelQueue.schedule(name, delay, task); } @Override @@ -353,10 +349,9 @@ public class ActionManager implements IActionManager { return parallelQueue.schedule(name, maxDuration, delay, task); } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Conditionals - @Override public IActionQueue getConditionalQueue() { return conditionalQueue; @@ -403,10 +398,9 @@ public class ActionManager implements IActionManager { return schedule(new ConditionalAction(this, name, condition, task, terminateWhen, onTermination)); } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Handling - @Override public IActionQueue[] getAllQueues() { ArrayList allQueues = new ArrayList<>(); diff --git a/src/main/java/noppes/npcs/controllers/data/action/ActionQueue.java b/src/main/java/noppes/npcs/controllers/data/action/ActionQueue.java index 0b3d0b483..332adab39 100644 --- a/src/main/java/noppes/npcs/controllers/data/action/ActionQueue.java +++ b/src/main/java/noppes/npcs/controllers/data/action/ActionQueue.java @@ -125,10 +125,9 @@ public class ActionQueue implements IActionQueue { return this; } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Schedulers - @Override public IAction schedule(IAction action) { Action act = (Action) action; @@ -203,10 +202,9 @@ public class ActionQueue implements IActionQueue { return action; } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Queue Data - @Override public boolean hasActiveTasks() { return !queue.isEmpty(); @@ -276,10 +274,9 @@ public class ActionQueue implements IActionQueue { return false; } - /////////////////////////////////////////////////// - /////////////////////////////////////////////////// + /// //////////////////////////////////////////////// + /// //////////////////////////////////////////////// // Handling - public boolean finish(Action a) { if (a.isDone()) { if (a.onDone != null) diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index c30f15f57..20d2f6d5c 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -67,14 +67,13 @@ import net.minecraft.util.ResourceLocation; import net.minecraft.util.Vec3; import net.minecraft.world.World; import net.minecraft.world.WorldServer; -import net.minecraftforge.common.ForgeHooks; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.util.FakePlayer; import net.minecraftforge.event.ServerChatEvent; import noppes.npcs.CustomItems; import noppes.npcs.CustomNpcs; -import noppes.npcs.DataAbilities; import noppes.npcs.DataAI; +import noppes.npcs.DataAbilities; import noppes.npcs.DataAdvanced; import noppes.npcs.DataDisplay; import noppes.npcs.DataInventory; @@ -96,12 +95,12 @@ import noppes.npcs.ai.EntityAIDodgeShoot; import noppes.npcs.ai.EntityAIFindShade; import noppes.npcs.ai.EntityAIFollow; import noppes.npcs.ai.EntityAIJob; +import noppes.npcs.ai.EntityAILeapAtTargetNpc; import noppes.npcs.ai.EntityAILook; import noppes.npcs.ai.EntityAIMoveIndoors; import noppes.npcs.ai.EntityAIMovingPath; import noppes.npcs.ai.EntityAIOrbitTarget; import noppes.npcs.ai.EntityAIPanic; -import noppes.npcs.ai.EntityAILeapAtTargetNpc; import noppes.npcs.ai.EntityAIPounceTarget; import noppes.npcs.ai.EntityAIRangedAttack; import noppes.npcs.ai.EntityAIReturn; @@ -155,8 +154,8 @@ import noppes.npcs.roles.JobFollower; import noppes.npcs.roles.JobInterface; import noppes.npcs.roles.RoleCompanion; import noppes.npcs.roles.RoleFollower; -import noppes.npcs.roles.RoleMount; import noppes.npcs.roles.RoleInterface; +import noppes.npcs.roles.RoleMount; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.entity.ScriptNpc; import noppes.npcs.scripted.event.NpcEvent; @@ -2150,7 +2149,7 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti protected boolean handleMountedMovement(float strafe, float forward) { return NPCMountUtil.handleMountedMovement(this, mountState, strafe, forward); } - + public void performMountedMovement(float strafe, float forward, float moveSpeed) { this.moveStrafing = strafe; this.moveForward = forward; diff --git a/src/main/java/noppes/npcs/items/ItemElementalStaff.java b/src/main/java/noppes/npcs/items/ItemElementalStaff.java index 45f45c17f..d786222b1 100644 --- a/src/main/java/noppes/npcs/items/ItemElementalStaff.java +++ b/src/main/java/noppes/npcs/items/ItemElementalStaff.java @@ -9,7 +9,7 @@ import noppes.npcs.CustomItems; import noppes.npcs.CustomNpcs; import noppes.npcs.constants.EnumNpcToolMaterial; -import java.awt.*; +import java.awt.Color; import java.util.List; public class ItemElementalStaff extends ItemStaff { diff --git a/src/main/java/noppes/npcs/items/ItemNpcScripter.java b/src/main/java/noppes/npcs/items/ItemNpcScripter.java index c3a88def0..4d503d97a 100644 --- a/src/main/java/noppes/npcs/items/ItemNpcScripter.java +++ b/src/main/java/noppes/npcs/items/ItemNpcScripter.java @@ -41,7 +41,7 @@ public class ItemNpcScripter extends Item { } return itemStack; } - + CustomNpcs.proxy.openGui(0, 0, 0, EnumGuiType.ScriptEvent, player); return itemStack; } diff --git a/src/main/java/noppes/npcs/items/ItemOrb.java b/src/main/java/noppes/npcs/items/ItemOrb.java index 04df0e16c..b43ccaef4 100644 --- a/src/main/java/noppes/npcs/items/ItemOrb.java +++ b/src/main/java/noppes/npcs/items/ItemOrb.java @@ -5,7 +5,7 @@ import net.minecraft.entity.passive.EntitySheep; import net.minecraft.item.Item; import net.minecraft.item.ItemStack; -import java.awt.*; +import java.awt.Color; import java.util.List; public class ItemOrb extends ItemNpcInterface { diff --git a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java index 2ff8b4afb..fc0c63f6d 100644 --- a/src/main/java/noppes/npcs/janino/JaninoHookResolver.java +++ b/src/main/java/noppes/npcs/janino/JaninoHookResolver.java @@ -4,7 +4,8 @@ import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.*; +import java.util.HashMap; +import java.util.Map; /** * Resolves script hook methods for compiled script classes. diff --git a/src/main/java/noppes/npcs/janino/JaninoScript.java b/src/main/java/noppes/npcs/janino/JaninoScript.java index 46d0bf164..1800cc895 100644 --- a/src/main/java/noppes/npcs/janino/JaninoScript.java +++ b/src/main/java/noppes/npcs/janino/JaninoScript.java @@ -8,13 +8,12 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.NBTTags; import noppes.npcs.api.handler.IHookDefinition; import noppes.npcs.config.ConfigScript; -import noppes.npcs.controllers.HookDefinition; -import noppes.npcs.janino.annotations.ParamName; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.data.IScriptUnit; +import noppes.npcs.janino.annotations.ParamName; import org.codehaus.commons.compiler.InternalCompilerException; import org.codehaus.commons.compiler.Sandbox; @@ -23,7 +22,14 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.security.Permissions; import java.security.PrivilegedAction; -import java.util.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -54,7 +60,7 @@ public abstract class JaninoScript implements IScriptUnit { this.type = type; this.defaultImports = defaultImports != null ? defaultImports : new String[0]; this.builder = IScriptBodyBuilder.getBuilder(type, - isClient ? CustomNpcs.getClientCompiler() : CustomNpcs.getDynamicCompiler()) + isClient ? CustomNpcs.getClientCompiler() : CustomNpcs.getDynamicCompiler()) .setDefaultImports(this.defaultImports); Permissions permissions = new Permissions(); @@ -67,7 +73,7 @@ public abstract class JaninoScript implements IScriptUnit { this(type, defaultImports, false); } - protected String getHookContext(){ + protected String getHookContext() { return this.context.hookContext; } @@ -326,7 +332,7 @@ public abstract class JaninoScript implements IScriptUnit { public static String generateMethodStub(Method method) { String mods = Modifier.toString(method.getModifiers()); mods = mods.replace("abstract ", "").replace("abstract", "") - .replace("default ", "").replace("default", "").trim(); + .replace("default ", "").replace("default", "").trim(); if (!mods.isEmpty()) mods += " "; String returnTypeStr = getUsableTypeName(method.getReturnType()); @@ -346,7 +352,7 @@ public abstract class JaninoScript implements IScriptUnit { } else { // Fallback to generated name String baseName = Character.toLowerCase(p.getType().getSimpleName().charAt(0)) - + p.getType().getSimpleName().substring(1); + + p.getType().getSimpleName().substring(1); int count = typeCount.getOrDefault(baseName, 0) + 1; typeCount.put(baseName, count); paramName = count == 1 ? baseName : baseName + (count - 1); @@ -393,7 +399,10 @@ public abstract class JaninoScript implements IScriptUnit { // ==================== IScriptUnit ==================== - @Override public String getScript() { return script; } + @Override + public String getScript() { + return script; + } @Override public void setScript(String script) { @@ -402,7 +411,10 @@ public abstract class JaninoScript implements IScriptUnit { hookResolver.clearResolutionCaches(); } - @Override public List getExternalScripts() { return externalScripts; } + @Override + public List getExternalScripts() { + return externalScripts; + } @Override public void setExternalScripts(List scripts) { @@ -411,8 +423,15 @@ public abstract class JaninoScript implements IScriptUnit { hookResolver.clearResolutionCaches(); } - @Override public TreeMap getConsole() { return console; } - @Override public void clearConsole() { console.clear(); } + @Override + public TreeMap getConsole() { + return console; + } + + @Override + public void clearConsole() { + console.clear(); + } @Override public void appendConsole(String message) { @@ -427,16 +446,29 @@ public abstract class JaninoScript implements IScriptUnit { } } - @Override public String getLanguage() { return "Java"; } - @Override public void setLanguage(String language) { } + @Override + public String getLanguage() { + return "Java"; + } + + @Override + public void setLanguage(String language) { + } @Override public boolean hasCode() { return !externalScripts.isEmpty() || (script != null && !script.isEmpty()); } - @Override public boolean hasErrored() { return errored; } - @Override public void setErrored(boolean errored) { this.errored = errored; } + @Override + public boolean hasErrored() { + return errored; + } + + @Override + public void setErrored(boolean errored) { + this.errored = errored; + } // ==================== NBT ==================== diff --git a/src/main/java/noppes/npcs/janino/annotations/ParamName.java b/src/main/java/noppes/npcs/janino/annotations/ParamName.java index 6b1e9ec25..deb8ed85c 100644 --- a/src/main/java/noppes/npcs/janino/annotations/ParamName.java +++ b/src/main/java/noppes/npcs/janino/annotations/ParamName.java @@ -12,9 +12,13 @@ import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface ParamName { - /** The parameter name (e.g., "event") */ + /** + * The parameter name (e.g., "event") + */ String value(); - - /** The fully qualified type name (e.g., "noppes.npcs.api.event.INpcEvent.InitEvent") */ + + /** + * The fully qualified type name (e.g., "noppes.npcs.api.event.INpcEvent.InitEvent") + */ String type() default ""; } diff --git a/src/main/java/noppes/npcs/roles/RoleTrader.java b/src/main/java/noppes/npcs/roles/RoleTrader.java index 107076a44..91a116efc 100644 --- a/src/main/java/noppes/npcs/roles/RoleTrader.java +++ b/src/main/java/noppes/npcs/roles/RoleTrader.java @@ -238,9 +238,9 @@ public class RoleTrader extends RoleInterface { * Consume stock after purchase. * For shared stock, syncs all viewers (market-wide for linked markets, per-NPC otherwise). * - * @param slot The slot purchased from + * @param slot The slot purchased from * @param playerName The player making the purchase - * @param amount Amount to consume + * @param amount Amount to consume * @return true if stock was consumed successfully */ public boolean consumeStock(int slot, String playerName, int amount) { @@ -284,6 +284,7 @@ public class RoleTrader extends RoleInterface { /** * Get time until stock reset in real-time milliseconds. * Converts MC ticks to approximate real-time for client countdown display. + * * @return Remaining time in milliseconds, or -1 if no reset scheduled */ public long getResetTimeRemainingMillis() { diff --git a/src/main/java/noppes/npcs/scripted/NpcAPI.java b/src/main/java/noppes/npcs/scripted/NpcAPI.java index 3d71e0023..ecf4b8ba0 100644 --- a/src/main/java/noppes/npcs/scripted/NpcAPI.java +++ b/src/main/java/noppes/npcs/scripted/NpcAPI.java @@ -5,6 +5,7 @@ import cpw.mods.fml.common.eventhandler.EventBus; import foxz.command.ScriptedCommand; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.ProfileController; +import kamkeel.npcs.controllers.data.ability.AbilityController; import net.minecraft.block.Block; import net.minecraft.command.CommandHandler; import net.minecraft.entity.Entity; @@ -59,7 +60,22 @@ import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.entity.IEntity; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.api.gui.ICustomGui; -import noppes.npcs.api.handler.*; +import noppes.npcs.api.handler.IAbilityHandler; +import noppes.npcs.api.handler.IActionManager; +import noppes.npcs.api.handler.IAnimationHandler; +import noppes.npcs.api.handler.IAttributeHandler; +import noppes.npcs.api.handler.ICloneHandler; +import noppes.npcs.api.handler.ICustomEffectHandler; +import noppes.npcs.api.handler.IDialogHandler; +import noppes.npcs.api.handler.IFactionHandler; +import noppes.npcs.api.handler.IMagicHandler; +import noppes.npcs.api.handler.INaturalSpawnsHandler; +import noppes.npcs.api.handler.IPartyHandler; +import noppes.npcs.api.handler.IProfileHandler; +import noppes.npcs.api.handler.IQuestHandler; +import noppes.npcs.api.handler.IRecipeHandler; +import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.api.handler.ITransportHandler; import noppes.npcs.api.handler.data.IAnimation; import noppes.npcs.api.handler.data.IFrame; import noppes.npcs.api.handler.data.IFramePart; @@ -70,7 +86,6 @@ import noppes.npcs.compat.PixelmonHelper; import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumAnimationPart; import noppes.npcs.containers.ContainerNpcInterface; -import kamkeel.npcs.controllers.data.ability.AbilityController; import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.ChunkController; import noppes.npcs.controllers.CustomEffectController; @@ -81,8 +96,8 @@ import noppes.npcs.controllers.PartyController; import noppes.npcs.controllers.QuestController; import noppes.npcs.controllers.RecipeController; import noppes.npcs.controllers.ScriptController; -import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.ScriptEntityData; +import noppes.npcs.controllers.ScriptHookController; import noppes.npcs.controllers.ServerCloneController; import noppes.npcs.controllers.SpawnController; import noppes.npcs.controllers.TransportController; diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java index 85a15041e..f2cfc9a34 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java @@ -39,7 +39,6 @@ import noppes.npcs.api.handler.IOverlayHandler; import noppes.npcs.api.handler.data.IAnimationData; import noppes.npcs.api.handler.data.IDialog; import noppes.npcs.api.handler.data.IMagicData; -import noppes.npcs.api.handler.data.IParty; import noppes.npcs.api.handler.data.IPlayerAttributes; import noppes.npcs.api.handler.data.IQuest; import noppes.npcs.api.handler.data.ISound; @@ -882,13 +881,13 @@ public class ScriptPlayer extends ScriptLivingBase @Override - public IPlayer[] getPartyMembers(){ + public IPlayer[] getPartyMembers() { Party party = this.getData().getPlayerParty(); if (party == null) { throw new CustomNPCsException("Player is not in party"); } - List list = new ArrayList<>(); - for(int i = 0; i < party.getPlayerNamesList().size();i++){ + List list = new ArrayList<>(); + for (int i = 0; i < party.getPlayerNamesList().size(); i++) { IPlayer player = NpcAPI.Instance().getPlayer(party.getPlayerNamesList().get(i)); if (player == null) continue; list.add(player); diff --git a/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java b/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java index 9baf245a2..9bf7719d5 100644 --- a/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java +++ b/src/main/java/noppes/npcs/scripted/event/AbilityEvent.java @@ -125,7 +125,7 @@ public class AbilityEvent extends NpcEvent implements IAbilityEvent { private float knockbackUp; public HitEvent(ICustomNpc npc, Ability ability, EntityLivingBase target, - EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { + EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { super(npc, ability, target); this.hitEntity = hitEntity != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(hitEntity) : null; this.damage = damage; diff --git a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java index 0ca2fa778..e3641570e 100644 --- a/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java +++ b/src/main/java/noppes/npcs/scripted/item/ScriptCustomItem.java @@ -10,10 +10,10 @@ import noppes.npcs.NBTTags; import noppes.npcs.api.item.IItemCustom; import noppes.npcs.constants.EnumScriptType; import noppes.npcs.constants.ScriptContext; -import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.controllers.ScriptController; import noppes.npcs.controllers.data.IScriptHandler; import noppes.npcs.controllers.data.IScriptHandlerPacket; +import noppes.npcs.controllers.data.IScriptUnit; import noppes.npcs.scripted.CustomNPCsException; import java.util.ArrayList; @@ -279,7 +279,7 @@ public class ScriptCustomItem extends ScriptCustomizableItem implements IItemCus } public void setAttackSpeed(int speed) { - if(speed <= 0) { + if (speed <= 0) { speed = 10; } this.attackSpeed = speed; diff --git a/src/main/java/noppes/npcs/util/NPCMountUtil.java b/src/main/java/noppes/npcs/util/NPCMountUtil.java index 6303f67b9..9c3cce57f 100644 --- a/src/main/java/noppes/npcs/util/NPCMountUtil.java +++ b/src/main/java/noppes/npcs/util/NPCMountUtil.java @@ -5,10 +5,10 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.util.MathHelper; import noppes.npcs.CustomNpcs; +import noppes.npcs.constants.EnumRoleType; import noppes.npcs.controllers.PlayerDataController; import noppes.npcs.controllers.data.PlayerData; import noppes.npcs.entity.EntityNPCInterface; -import noppes.npcs.constants.EnumRoleType; import noppes.npcs.roles.RoleMount; public final class NPCMountUtil { diff --git a/src/main/java/noppes/npcs/util/ValueUtil.java b/src/main/java/noppes/npcs/util/ValueUtil.java index a81fb9fef..71846afcc 100644 --- a/src/main/java/noppes/npcs/util/ValueUtil.java +++ b/src/main/java/noppes/npcs/util/ValueUtil.java @@ -9,7 +9,7 @@ public class ValueUtil { public static double lerp(double start, double end, double delta) { return start + delta * (end - start); } - + public static float clamp(float given, float min, float max) { if (given < min) return min; diff --git a/src/main/java/somehussar/janino/AdvancedClassFilter.java b/src/main/java/somehussar/janino/AdvancedClassFilter.java index b48de2c30..7c2d51834 100644 --- a/src/main/java/somehussar/janino/AdvancedClassFilter.java +++ b/src/main/java/somehussar/janino/AdvancedClassFilter.java @@ -24,14 +24,17 @@ public class AdvancedClassFilter implements LoadClassCondition { allowedWildCards.addAll(Arrays.stream(classRegexes).map(Pattern::compile).collect(Collectors.toList())); return this; } + public AdvancedClassFilter banRegexes(String... classRegexes) { bannedWildCards.addAll(Arrays.stream(classRegexes).map(Pattern::compile).collect(Collectors.toList())); return this; } + public AdvancedClassFilter addClasses(String... classPath) { allowedClasses.addAll(Arrays.asList(classPath)); return this; } + public AdvancedClassFilter banClasses(String... classPath) { bannedClasses.addAll(Arrays.asList(classPath)); return this; diff --git a/src/main/java/somehussar/janino/EntityUnloadListener.java b/src/main/java/somehussar/janino/EntityUnloadListener.java index 592b0750c..b4777e712 100644 --- a/src/main/java/somehussar/janino/EntityUnloadListener.java +++ b/src/main/java/somehussar/janino/EntityUnloadListener.java @@ -47,7 +47,7 @@ public class EntityUnloadListener implements IWorldAccess { EntityNPCInterface npc = (EntityNPCInterface) entity; // TODO: REMOVE SCRIPTED OBJECT AND CLASSLOADER FOR IT } - } + } @Override public void playRecord(String p_72702_1_, int p_72702_2_, int p_72702_3_, int p_72702_4_) { From aa923fae54b961bbda5cbdf05b6e0e083238ea40 Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 23 Jan 2026 00:51:01 -0500 Subject: [PATCH 267/358] Remove Analystics --- .../java/noppes/npcs/ServerTickHandler.java | 19 ------- .../noppes/npcs/client/AnalyticsTracking.java | 53 ------------------- 2 files changed, 72 deletions(-) delete mode 100644 src/main/java/noppes/npcs/client/AnalyticsTracking.java diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index 93acda2ea..ea42ce84c 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -34,25 +34,6 @@ public class ServerTickHandler { @SubscribeEvent public void playerLogin(PlayerEvent.PlayerLoggedInEvent event) { EntityPlayerMP player = (EntityPlayerMP) event.player; - // Temporary Disabled -// if (serverName == null) { -// String e = "local"; -// MinecraftServer server = MinecraftServer.getServer(); -// if (server.isDedicatedServer()) { -// try { -// e = InetAddress.getByName(server.getServerHostname()).getCanonicalHostName(); -// } catch (UnknownHostException e1) { -// e = MinecraftServer.getServer().getServerHostname(); -// } -// if (server.getPort() != 25565) -// e += ":" + server.getPort(); -// } -// if (e == null || e.startsWith("192.168") || e.contains("127.0.0.1") || e.startsWith("localhost")) -// e = "local"; -// serverName = e; -// } -// AnalyticsTracking.sendData(event.player, "join", serverName); - PlayerData playerData = PlayerData.get(event.player); if (playerData != null) { playerData.onLogin(); diff --git a/src/main/java/noppes/npcs/client/AnalyticsTracking.java b/src/main/java/noppes/npcs/client/AnalyticsTracking.java deleted file mode 100644 index ba745346d..000000000 --- a/src/main/java/noppes/npcs/client/AnalyticsTracking.java +++ /dev/null @@ -1,53 +0,0 @@ -package noppes.npcs.client; - -import net.minecraft.entity.player.EntityPlayer; - -import java.io.IOException; -import java.io.OutputStream; -import java.net.HttpURLConnection; -import java.net.URL; -import java.util.UUID; - -public class AnalyticsTracking { - - public static void sendData(final EntityPlayer player, final String event, final String data) { - Runnable r = new Runnable() { - - @Override - public void run() { - try { - UUID uuid = player.getUniqueID(); - - String analyticsPostData = "v=1" + - "&tid=UA-29079943-5" + - "&cid=" + uuid.toString() + - "&t=event" + - "&ec=customnpcs_1.7d" + - "&ea=" + event + - "&el=" + data + - "&ev=300"; - - URL url = new URL("http://www.google-analytics.com/collect"); - HttpURLConnection connection = (HttpURLConnection) url.openConnection(); - connection.setConnectTimeout(10000); - connection.setReadTimeout(10000); - connection.setDoOutput(true); - connection.setUseCaches(false); - connection.setRequestMethod("POST"); - connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - connection.setRequestProperty("Content-Length", Integer.toString(analyticsPostData.getBytes().length)); - OutputStream dataOutput = connection.getOutputStream(); - dataOutput.write(analyticsPostData.getBytes()); - dataOutput.close(); - connection.getInputStream().close(); - } catch (IOException e) { - - } - - } - }; - Thread t = new Thread(r); - t.start(); - } - -} From 9cbd9fd3eb849abbd2620cb8a2307fcce3a1cd2a Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 23 Jan 2026 01:41:57 -0500 Subject: [PATCH 268/358] Fix Block Waypoint --- src/main/java/noppes/npcs/blocks/BlockWaypoint.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/blocks/BlockWaypoint.java b/src/main/java/noppes/npcs/blocks/BlockWaypoint.java index 7e0fd2601..7e2dd87d3 100644 --- a/src/main/java/noppes/npcs/blocks/BlockWaypoint.java +++ b/src/main/java/noppes/npcs/blocks/BlockWaypoint.java @@ -41,8 +41,14 @@ public class BlockWaypoint extends BlockContainer { @Override public void onBlockPlacedBy(World world, int i, int j, int k, EntityLivingBase entityliving, ItemStack item) { - if (entityliving instanceof EntityPlayer && world.isRemote) { - CustomNpcs.proxy.openGui(i, j, k, EnumGuiType.Waypoint, (EntityPlayer) entityliving); + if (entityliving instanceof EntityPlayer) { + if (!world.isRemote) { + // Server-side: sync TileEntity data to client and open GUI (same as onBlockActivated) + TileEntity tile = world.getTileEntity(i, j, k); + NBTTagCompound compound = new NBTTagCompound(); + tile.writeToNBT(compound); + PacketHandler.Instance.sendToPlayer(new GuiWaypointPacket(compound), (EntityPlayerMP) entityliving); + } } } From 5afa1c895914a351d2a8eaf8245aeb9db4787fc8 Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 23 Jan 2026 21:53:12 -0500 Subject: [PATCH 269/358] Auction System Implementation --- 1.11_Market_Update.md | 879 ++++-------- .../kamkeel/npcs/network/PacketHandler.java | 4 + .../npcs/network/enums/EnumDataPacket.java | 3 + .../network/packets/data/LoginPacket.java | 12 + .../packets/player/AuctionActionPacket.java | 410 ++++++ src/main/java/noppes/npcs/CommonProxy.java | 17 + src/main/java/noppes/npcs/CustomNpcs.java | 8 + src/main/java/noppes/npcs/DataAdvanced.java | 3 + .../java/noppes/npcs/ServerTickHandler.java | 11 + .../npcs/client/AuctionClientConfig.java | 66 + .../java/noppes/npcs/client/ClientProxy.java | 21 + .../gui/player/AuctionTooltipHandler.java | 213 +++ .../client/gui/player/GuiAuctionBidding.java | 299 ++++ .../gui/player/GuiAuctionInterface.java | 232 +++ .../client/gui/player/GuiAuctionListing.java | 302 ++++ .../client/gui/player/GuiAuctionSell.java | 400 ++++++ .../client/gui/player/GuiAuctionTrades.java | 291 ++++ .../client/gui/player/SubGuiAuctionBid.java | 167 +++ .../gui/player/SubGuiAuctionBuyNow.java | 124 ++ .../gui/player/SubGuiAuctionSearch.java | 85 ++ .../client/gui/util/GuiAuctionNavButton.java | 197 +++ .../java/noppes/npcs/config/ConfigMarket.java | 111 ++ .../npcs/constants/EnumAuctionSort.java | 40 + .../npcs/constants/EnumAuctionStatus.java | 47 + .../noppes/npcs/constants/EnumClaimType.java | 38 + .../noppes/npcs/constants/EnumGuiType.java | 7 +- .../npcs/constants/EnumNotificationType.java | 32 + .../noppes/npcs/constants/EnumRoleType.java | 2 +- .../npcs/containers/ContainerAuction.java | 81 ++ .../containers/ContainerAuctionBidding.java | 63 + .../containers/ContainerAuctionListing.java | 133 ++ .../npcs/containers/ContainerAuctionSell.java | 125 ++ .../containers/ContainerAuctionTrades.java | 184 +++ .../containers/InventoryAuctionDisplay.java | 153 ++ .../npcs/containers/SlotAuctionDisplay.java | 31 + .../npcs/controllers/AuctionConfigSync.java | 49 + .../npcs/controllers/AuctionController.java | 1243 +++++++++++++++++ .../npcs/controllers/data/AuctionClaim.java | 120 ++ .../npcs/controllers/data/AuctionFilter.java | 102 ++ .../npcs/controllers/data/AuctionListing.java | 147 ++ .../controllers/data/AuctionNotification.java | 64 + .../npcs/mixin/CustomNPCsMixinPlugin.java | 1 + .../java/noppes/npcs/mixin/MixinItem.java | 27 + .../noppes/npcs/mixin/MixinItemStack.java | 11 + .../noppes/npcs/roles/RoleAuctioneer.java | 41 + .../assets/customnpcs/lang/en_US.lang | 180 +++ .../textures/gui/auction/auction.png | Bin 0 -> 2109 bytes .../textures/gui/auction/auction_slot.png | Bin 0 -> 124 bytes .../textures/gui/auction/button.png | Bin 0 -> 166 bytes .../textures/gui/auction/button_press.png | Bin 0 -> 165 bytes .../textures/gui/auction/check_icon.png | Bin 0 -> 207 bytes .../customnpcs/textures/gui/auction/down.png | Bin 0 -> 137 bytes .../textures/gui/auction/full_concept.png | Bin 0 -> 6161 bytes .../textures/gui/auction/my_auctions.png | Bin 0 -> 464 bytes .../textures/gui/auction/search.png | Bin 0 -> 310 bytes .../customnpcs/textures/gui/auction/sell.png | Bin 0 -> 367 bytes .../textures/gui/auction/small_button.png | Bin 0 -> 157 bytes .../gui/auction/small_button_press.png | Bin 0 -> 157 bytes .../customnpcs/textures/gui/auction/up.png | Bin 0 -> 156 bytes .../textures/gui/auction/x_icon.png | Bin 0 -> 471 bytes 60 files changed, 6187 insertions(+), 589 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java create mode 100644 src/main/java/noppes/npcs/client/AuctionClientConfig.java create mode 100644 src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java create mode 100644 src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java create mode 100644 src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java create mode 100644 src/main/java/noppes/npcs/client/gui/player/GuiAuctionListing.java create mode 100644 src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java create mode 100644 src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java create mode 100644 src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java create mode 100644 src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java create mode 100644 src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionSearch.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/GuiAuctionNavButton.java create mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionSort.java create mode 100644 src/main/java/noppes/npcs/constants/EnumAuctionStatus.java create mode 100644 src/main/java/noppes/npcs/constants/EnumClaimType.java create mode 100644 src/main/java/noppes/npcs/constants/EnumNotificationType.java create mode 100644 src/main/java/noppes/npcs/containers/ContainerAuction.java create mode 100644 src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java create mode 100644 src/main/java/noppes/npcs/containers/ContainerAuctionListing.java create mode 100644 src/main/java/noppes/npcs/containers/ContainerAuctionSell.java create mode 100644 src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java create mode 100644 src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java create mode 100644 src/main/java/noppes/npcs/containers/SlotAuctionDisplay.java create mode 100644 src/main/java/noppes/npcs/controllers/AuctionConfigSync.java create mode 100644 src/main/java/noppes/npcs/controllers/AuctionController.java create mode 100644 src/main/java/noppes/npcs/controllers/data/AuctionClaim.java create mode 100644 src/main/java/noppes/npcs/controllers/data/AuctionFilter.java create mode 100644 src/main/java/noppes/npcs/controllers/data/AuctionListing.java create mode 100644 src/main/java/noppes/npcs/controllers/data/AuctionNotification.java create mode 100644 src/main/java/noppes/npcs/mixin/MixinItem.java create mode 100644 src/main/java/noppes/npcs/roles/RoleAuctioneer.java create mode 100644 src/main/resources/assets/customnpcs/textures/gui/auction/auction.png create mode 100644 src/main/resources/assets/customnpcs/textures/gui/auction/auction_slot.png create mode 100644 src/main/resources/assets/customnpcs/textures/gui/auction/button.png create mode 100644 src/main/resources/assets/customnpcs/textures/gui/auction/button_press.png create mode 100644 src/main/resources/assets/customnpcs/textures/gui/auction/check_icon.png create mode 100644 src/main/resources/assets/customnpcs/textures/gui/auction/down.png create mode 100644 src/main/resources/assets/customnpcs/textures/gui/auction/full_concept.png create mode 100644 src/main/resources/assets/customnpcs/textures/gui/auction/my_auctions.png create mode 100644 src/main/resources/assets/customnpcs/textures/gui/auction/search.png create mode 100644 src/main/resources/assets/customnpcs/textures/gui/auction/sell.png create mode 100644 src/main/resources/assets/customnpcs/textures/gui/auction/small_button.png create mode 100644 src/main/resources/assets/customnpcs/textures/gui/auction/small_button_press.png create mode 100644 src/main/resources/assets/customnpcs/textures/gui/auction/up.png create mode 100644 src/main/resources/assets/customnpcs/textures/gui/auction/x_icon.png diff --git a/1.11_Market_Update.md b/1.11_Market_Update.md index 304965950..e512e7aa0 100644 --- a/1.11_Market_Update.md +++ b/1.11_Market_Update.md @@ -12,7 +12,7 @@ The 1.11 Market Update introduces a comprehensive economy system for CustomNPC+: ## 1. Currency System -### Status: IMPLEMENTED +### Status: COMPLETE ✅ The currency system provides a unified economy for CNPC+ features. @@ -20,6 +20,26 @@ The currency system provides a unified economy for CNPC+ features. - **Built-in CNPC+ Currency** - Stored in PlayerData, persists across profile slots - **Vault Integration** - Optional, when enabled all operations use Vault instead - **Scripting API** - Full access via `IPlayer` methods +- **Unified Usage** - Both Trader and Auction systems use `PlayerCurrencyData` which auto-delegates to Vault + +### Vault Integration Flow +``` +┌─────────────────────────────────────────────────────────────────┐ +│ PlayerCurrencyData │ +│ │ +│ shouldUseVault() = ConfigMarket.UseVault && VaultUtil.isEnabled()│ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ All methods check shouldUseVault() first: │ │ +│ │ - getBalance() → VaultUtil.getBalance() or local balance │ │ +│ │ - withdraw() → VaultUtil.withdrawMoney() or local │ │ +│ │ - deposit() → VaultUtil.addMoney() or local │ │ +│ │ - canAfford() → VaultUtil.has() or local check │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +│ │ +│ Used by: ContainerNPCTrader, AuctionController │ +└─────────────────────────────────────────────────────────────────┘ +``` ### Key Files | File | Purpose | @@ -53,11 +73,11 @@ player.getFormattedCurrencyBalance() // Get formatted string ## 2. Trader Enhancements -### Status: COMPLETE +### Status: COMPLETE ✅ --- -### 2.1 Stock System - IMPLEMENTED +### 2.1 Stock System - COMPLETE ✅ Traders can have limited stock that resets on configurable schedules. @@ -78,7 +98,7 @@ Traders can have limited stock that resets on configurable schedules. | `RLWEEKLY` | Resets every 7 real-life days | ✅ Working | | `RLCUSTOM` | Custom real-time interval | ✅ Working | -#### Stock UI Features - IMPLEMENTED +#### Stock UI Features - COMPLETE ✅ | Feature | Status | Notes | |---------|--------|-------| | Stock count per item | ✅ | Shows to right of output slot | @@ -90,22 +110,11 @@ Traders can have limited stock that resets on configurable schedules. | Reset stock button | ✅ | With confirmation dialog | | Reset cooldown button | ✅ | With confirmation dialog | -#### Key Files -| File | Purpose | -|------|---------| -| `TraderStock.java` | Core stock data class with per-player/global modes | -| `TraderStock.PlayerTraderStock` | Inner class for per-player tracking | -| `EnumStockReset.java` | Reset type enum with intervals | -| `SubGuiNpcTraderStock.java` | Main stock configuration GUI | -| `SubGuiNpcTraderStockSlots.java` | Per-slot stock limit editor | -| `SubGuiNpcTraderStockReset.java` | Stock reset confirmation | -| `SubGuiNpcTraderCooldownReset.java` | Cooldown reset confirmation | - --- -### 2.2 Trader Currency Support - IMPLEMENTED +### 2.2 Trader Currency Support - COMPLETE ✅ -Trades can require BOTH items AND currency (additive costs). +Trades can require BOTH items AND currency (additive costs). Uses `PlayerCurrencyData` which auto-delegates to Vault when enabled. #### Trade Entry Structure Each trade slot can have: @@ -114,30 +123,15 @@ Each trade slot can have: - **Currency 2** - Second item cost (existing) ✅ - **Currency Cost** - CNPC+ Currency / Vault cost ✅ NEW -#### Example Trades -| Sold Item | Item Cost | Currency Cost | -|-----------|-----------|---------------| -| Diamond | 2 Emeralds | $0 | -| Diamond Sword | 5 Diamonds | $100 | -| Special Armor | None | $500 | -| Enchanted Book | 1 Book + 10 Lapis | $50 | - #### Implementation Details - Currency cost stored as `long[] currencyCost[18]` in RoleTrader -- Default to 0 (no currency cost) for legacy compatibility ✅ -- Check both item AND currency requirements before purchase ✅ -- Withdraw currency on successful purchase ✅ +- Uses `data.currencyData.getBalance()` and `data.currencyData.withdraw()` +- Automatically uses Vault when `ConfigMarket.UseVault` is enabled - NBT serialization for persistence ✅ --- -### 2.3 Player Trader GUI - IMPLEMENTED - -#### Layout (256px wide) -- 3 columns × 6 rows = 18 trade slots -- Column width: 80px -- Per slot: [Currency1][Currency2] = [Output] -- Player inventory positioned below +### 2.3 Player Trader GUI - COMPLETE ✅ #### Display Features | Element | Position | Status | @@ -148,237 +142,132 @@ Each trade slot can have: | Player balance | Bottom-right | ✅ | | Status message | Bottom-left | ✅ On hover | -#### Status Messages (on hover) -| Condition | Message | Color | -|-----------|---------|-------| -| Out of stock | "Out of Stock ($COST)" | Red | -| Can't afford balance | "Insufficient Balance ($COST)" | Red | -| Missing item currency | "Unavailable ($COST)" | Red | -| Slot disabled | "Slot Disabled ($COST)" | Orange | -| Can purchase | "Available ($COST)" | Green | +--- -Cost suffix only appears if trade has currency cost > 0. +### 2.4 Multi-Player Sync - COMPLETE ✅ + +MarketRegistry provides linked market synchronization across all NPCs sharing the same market. --- -### 2.4 Multi-Player Sync - IMPLEMENTED +### 2.5 Scripting API - COMPLETE ✅ -#### MarketRegistry System -Central registry for linked market synchronization across all NPCs sharing the same market. - -| Component | Purpose | -|-----------|---------| -| `MarketRegistry.java` | Central registry tracking traders and viewers per market | -| `marketTraders` | `Map>` - All traders using each market | -| `marketViewers` | `Map>` - All players viewing each market | - -#### Viewer Tracking System -| Feature | Status | Notes | -|---------|--------|-------| -| Register viewer on GUI open | ✅ | For shared stock mode (linked or local) | -| Unregister on GUI close | ✅ | Via onContainerClosed | -| Sync after purchase | ✅ | All viewers updated market-wide | -| Per-player mode optimization | ✅ | Only sync to purchasing player | -| Disconnected player cleanup | ✅ | Removed before sync | -| NPC unload cleanup | ✅ | Via delete() → onUnload() | - -#### Sync Behavior by Mode -| Mode | Linked Market | Behavior | -|------|---------------|----------| -| Shared Stock | Yes | All viewers across ALL NPCs with same market see update immediately | -| Shared Stock | No | All viewers of THIS NPC see update immediately | -| Per-Player Stock | Yes/No | Only purchasing player receives update | - -#### Data Flow -``` -Purchase on NPC1 (market="shop1") - ↓ -consumeStock() → Market.save() → MarketRegistry.syncMarket("shop1") - ↓ -┌──────────────────────────────────────────────┐ -│ 1. Update ALL traders' stock data │ -│ (NPC1, NPC2, NPC3... all with "shop1") │ -│ 2. Sync ALL viewers across ALL those NPCs │ -└──────────────────────────────────────────────┘ -``` - ---- - -### 2.5 Key Files Modified/Created - -| File | Changes | -|------|---------| -| `MarketRegistry.java` | NEW - Central registry for linked market sync | -| `RoleTrader.java` | currencyCost[], viewer tracking (local + registry), syncAllViewers(), resetCooldown(), delete() cleanup | -| `ContainerNPCTrader.java` | Layout constants, currency/stock validation, viewer registration | -| `GuiNPCTrader.java` | Client-side countdown, stock display, status messages | -| `GetTraderData.java` | Network packet for requesting trader data (IGuiData pattern) | -| `TraderStock.java` | Stock management with per-player/global modes, thread-safe methods | -| `EnumStockReset.java` | Reset type definitions with lang key support | -| `SubGuiNpcTraderStock.java` | Stock configuration dialog, custom time validation | -| `SubGuiNpcTraderStockSlots.java` | Per-slot stock limits | -| `SubGuiNpcTraderStockReset.java` | Stock reset confirmation | -| `SubGuiNpcTraderCooldownReset.java` | Cooldown reset confirmation | -| `GuiNPCInterface.java` | Fixed SubGui background tiling for smaller widths | -| `Market.java` | Fixed save logic for linked markets | -| `IRoleTrader.java` | Extended API with stock manipulation methods | -| `ScriptRoleTrader.java` | Implemented new API methods | - ---- - -### 2.6 Resolved Issues - -All previously identified issues have been fixed: - -#### Critical Issues - FIXED -| Issue | Fix | -|-------|-----| -| Market load overwrites reset | Reordered `interact()` to load market BEFORE stock reset check | -| recordHistory flag inverted | Removed inverted early return in `Market.save()` | -| API returns wrong item | Fixed `getSellOption()` to return `inventorySold` instead of `inventoryCurrency` | - -#### Medium Priority - FIXED -| Issue | Fix | -|-------|-----| -| Thread safety | Added `synchronized` to `getAvailableStock()`, `consumeStock()`, `resetStock()` | -| Per-player cleanup | `playerStock.clear()` called on stock reset | -| Linked market sync | Implemented `MarketRegistry` for market-wide sync | -| Custom time validation | Added min validation (1 tick MC, 1000ms RL) in `SubGuiNpcTraderStock` | - -#### Low Priority - FIXED -| Issue | Fix | -|-------|-----| -| lastPurchaseTime unused | Removed from `PlayerTraderStock` | -| Currency format | Verified working correctly for all ranges | -| Hardcoded reset types | Now uses `EnumStockReset.getDisplayNames()` with lang keys | - ---- - -### 2.7 Scripting API - IMPLEMENTED - -Full API available via `IRoleTrader` interface: - -```javascript -// Stock Configuration -trader.isStockEnabled() -trader.setStockEnabled(boolean) -trader.isPerPlayerStock() -trader.setPerPlayerStock(boolean) -trader.getStockResetType() // Returns int (0-6 for EnumStockReset) -trader.setStockResetType(int) -trader.getCustomResetTime() -trader.setCustomResetTime(long) - -// Stock Limits -trader.getMaxStock(slot) // Max stock for slot (-1 = unlimited) -trader.setMaxStock(slot, amount) - -// Stock Queries -trader.getAvailableStock(slot) // Global mode -trader.getAvailableStock(slot, player) // Per-player mode -trader.getCurrentStock(slot) // Current stock (global) -trader.getPlayerPurchased(slot, player) // Amount purchased by player - -// Stock Manipulation -trader.setCurrentStock(slot, amount) // Set current stock (global) -trader.setPlayerPurchased(slot, player, amount) // Set player's purchased amount -trader.resetStock() // Reset all stock to max -trader.resetCooldown() // Reset cooldown timer - -// Time Queries -trader.getLastResetTime() // Timestamp of last reset -trader.getTimeUntilReset() // Time remaining until next reset - -// Currency Cost System -trader.getCurrencyCost(slot) -trader.setCurrencyCost(slot, amount) -trader.hasCurrencyCost(slot) -``` +Full API available via `IRoleTrader` interface. --- ## 3. Auction House System -### Status: NOT STARTED (Fresh implementation) +### Status: COMPLETE ✅ ### 3.1 Architecture Overview ``` -┌─────────────────────────────────────────────────────────────┐ -│ AuctionController │ -│ - Manages all auction data │ -│ - Handles listing/bidding/buyout logic │ -│ - Processes claims │ -│ - Saves/loads auction data │ -└─────────────────────────────────────────────────────────────┘ +┌─────────────────────────────────────────────────────────────────┐ +│ AuctionController │ +│ - Manages all auction data with player indices │ +│ - Handles listing/bidding/buyout logic │ +│ - Race-condition protected with ReentrantReadWriteLock │ +│ - Async saves on CNPC+ thread │ +│ - 20-second update cycle for performance │ +└─────────────────────────────────────────────────────────────────┘ │ - ▼ -┌─────────────────────────────────────────────────────────────┐ -│ AuctionNotification │ -│ - Tracks pending notifications per player │ -│ - Sends notifications on login │ -│ - Types: Won, Outbid, Sold, Expired, Claimed │ -└─────────────────────────────────────────────────────────────┘ + Uses PlayerCurrencyData (Vault-aware) + │ +┌─────────────────────────────────────────────────────────────────┐ +│ AuctionNotification │ +│ - Tracks pending notifications per player │ +│ - Sends notifications on login │ +│ - Types: Won, Outbid, Sold, Expired, Claimed │ +└─────────────────────────────────────────────────────────────────┘ ``` -### 3.2 GUI Structure +### 3.2 Vault Integration + +The Auction system uses `PlayerCurrencyData` for all currency operations: +- `AuctionController.createListing()` → `currencyData.withdraw(fee)` +- `AuctionController.placeBid()` → `currencyData.withdraw(bidAmount)` +- `AuctionController.buyout()` → `currencyData.withdraw(buyoutPrice)` +- `AuctionController.claimCurrency()` → `currencyData.deposit(amount)` + +**When Vault is enabled** (`ConfigMarket.UseVault = true`): +- All operations automatically use Vault economy +- No code changes required - `PlayerCurrencyData` handles delegation + +### 3.3 GUI Structure ``` -GuiAuctionHouse (Base GUI - opened via Auctioneer NPC) +GuiAuctionInterface (Base GUI - opened via Auctioneer NPC) │ - ├── GuiAuction (Browse/Search auctions) - │ - Paginated item display (ContainerAuction) - │ - Next/Back page buttons - │ - Filter button → opens GuiAuctionFilter - │ - My Auctions button → opens GuiAuctionClaims - │ - Sell button → opens GuiAuctionSell - │ - Click item → SubGuiAuctionDetails + ├── GuiAuctionListing (Browse auctions - PAGE_LISTINGS) + │ - 9x5 grid of auction items + │ - Filter button with tooltip showing current settings + │ - Click item → Opens GuiAuctionBidding + │ - Pagination with up/down arrows │ - ├── GuiAuctionSell (Create new listing) - │ - Item slot for item to sell - │ - Starting price input - │ - Buyout price input (0 = no buyout) + ├── GuiAuctionSell (Create new listing - PAGE_SELL) + │ - Sell slot for item to auction (cloned from inventory) + │ - Click inventory: left=add stack, right=add 1 + │ - Click sell slot: left=clear, right=remove 1 + │ - Starting price + buyout price text fields + │ - Allow Buyout toggle button │ - Listing fee display - │ - Create button + │ - Confirm button with coin icon │ - ├── GuiAuctionClaims (My Auctions / Claims) - │ - Active listings tab - │ - Pending claims tab (items + currency to collect) - │ - Cancel listing button - │ - Claim all button + ├── GuiAuctionTrades (My Trades - PAGE_CLAIMS) + │ - 9x5 grid showing: active listings, active bids, claims + │ - Blue tint: active listings/bids + │ - Green tint: claimable items + │ - Red tint: refunds + │ - Confirmation system for cancel/claim operations │ - └── GuiAuctionFilter (Filter settings) - - Search text - - Sort options (EnumAuctionSort) - - Price range (min/max) - - Show my listings only toggle + ├── GuiAuctionBidding (View listing details) + │ - Item display slot + │ - Auction info (seller, price, time, bids) + │ - Bid button → SubGuiAuctionBid + │ - Buy Now button → SubGuiAuctionBuyNow + │ - Own listings: buttons hidden, view-only + │ + ├── SubGuiAuctionBid (Bid confirmation popup) + │ - Shows minimum bid, player balance + │ - Bid amount input field + │ - Place Bid / Cancel buttons + │ + ├── SubGuiAuctionBuyNow (Buyout confirmation popup) + │ - Shows buyout price, player balance + │ - Yes / Cancel buttons + │ + └── SubGuiAuctionSearch (Filter popup) + - Search text input + - Apply/Cancel buttons ``` -### 3.3 Admin GUI +### 3.4 Client Config Sync -``` -GuiManageAuction (Admin tool - like ManageMagic, ManageQuests) - │ - ├── Active Auctions Tab - │ - View all auctions - │ - Cancel any auction (with reason) - │ - View auction details - │ - ├── Blacklist Tab (Stub for future) - │ - Add/remove blacklisted items - │ - Add/remove blacklisted mods - │ - └── Settings Tab - - View current config values - - Quick reference for config file +The client uses `AuctionClientConfig` to cache server settings: +- Synced via `LoginPacket` on player join +- Client never uses `ConfigMarket` directly for display +- Ensures server authority over fees, currency name, etc. + +```java +// Server sends config in LoginPacket +AuctionClientConfig.writeToNBT(compound); // Server side + +// Client receives and caches +AuctionClientConfig.readFromNBT(compound); // Client side + +// Client GUI uses cached values +AuctionClientConfig.getListingFee(); +AuctionClientConfig.getCurrencyName(); +AuctionClientConfig.getMinBidIncrement(); ``` -### 3.4 Data Structures +### 3.5 Data Structures #### AuctionListing ```java public class AuctionListing { - int id; // Unique listing ID + String id; // Unique listing ID (UUID-based) UUID sellerUUID; // Seller's UUID String sellerName; // Seller's display name ItemStack item; // Item being sold @@ -397,9 +286,9 @@ public class AuctionListing { #### AuctionClaim ```java public class AuctionClaim { - int id; // Unique claim ID + String id; // Unique claim ID UUID playerUUID; // Player who can claim - int listingId; // Related auction listing + String listingId; // Related auction listing EnumClaimType type; // ITEM, CURRENCY, REFUND ItemStack item; // Item to claim (if type = ITEM) long currency; // Currency to claim (if type = CURRENCY/REFUND) @@ -408,113 +297,19 @@ public class AuctionClaim { } ``` -#### AuctionFilter -```java -public class AuctionFilter { - String searchText; // Item name search - EnumAuctionSort sortBy; // Sort order - long minPrice; // Minimum price filter - long maxPrice; // Maximum price filter (0 = no max) - boolean myListingsOnly; // Show only my listings -} -``` +### 3.6 Security Features -#### Enums -```java -public enum EnumAuctionStatus { - ACTIVE, // Auction is live - ENDED, // Auction ended (winner determined) - CANCELLED, // Cancelled by seller or admin - CLAIMED // All claims processed -} +All actions are validated server-side: -public enum EnumAuctionSort { - NEWEST, // Most recently listed - ENDING_SOON, // Ending soonest - PRICE_LOW, // Lowest current price - PRICE_HIGH, // Highest current price - MOST_BIDS // Most bid activity -} +| Check | Location | Description | +|-------|----------|-------------| +| Own item bid prevention | `placeBid()` | `listing.isSeller(playerUUID)` check | +| Own item buyout prevention | `buyout()` | `listing.isSeller(playerUUID)` check | +| Auction expiry check | `placeBid()/buyout()` | Double-check inside write lock | +| Currency validation | All actions | Verify balance before operations | +| Claim ownership | `claimItem()/claimCurrency()` | `claim.isForPlayer(playerUUID)` check | -public enum EnumClaimType { - ITEM, // Winner claims item - CURRENCY, // Seller claims sale currency - REFUND // Outbid player claims refund -} -``` - -### 3.5 AuctionController - -Main controller handling all auction logic. - -#### Core Methods -```java -// Listing Management -int createListing(EntityPlayer seller, ItemStack item, long startingPrice, long buyoutPrice) -void cancelListing(int listingId, EntityPlayer player, boolean isAdmin) -List getActiveListings(AuctionFilter filter, int page, int pageSize) -AuctionListing getListing(int listingId) -int getPlayerListingCount(UUID playerUUID) - -// Bidding -boolean placeBid(int listingId, EntityPlayer bidder, long bidAmount) -boolean buyout(int listingId, EntityPlayer buyer) - -// Claims -List getPlayerClaims(UUID playerUUID) -boolean claimItem(int claimId, EntityPlayer player) -boolean claimCurrency(int claimId, EntityPlayer player) -void claimAll(EntityPlayer player) - -// Lifecycle -void processEndedAuctions() // Called on tick, creates claims for ended auctions -void onPlayerLogin(EntityPlayer player) // Send pending notifications - -// Persistence -void save() -void load() -``` - -#### Auction Flow - -**Creating a Listing:** -1. Validate item not blacklisted -2. Check player has listing slots available -3. Deduct listing fee from player -4. Remove item from player inventory -5. Create AuctionListing with ACTIVE status -6. Save - -**Placing a Bid:** -1. Validate auction is ACTIVE -2. Validate bid > current bid + minimum increment -3. Validate bidder has enough currency -4. Deduct bid amount from bidder -5. If previous bidder exists, create REFUND claim for them -6. Update listing with new bid/bidder -7. Apply snipe protection if needed -8. Save - -**Buyout:** -1. Validate auction is ACTIVE and has buyout price -2. Validate buyer has enough currency -3. Deduct buyout amount from buyer -4. If previous bidder exists, create REFUND claim -5. Create ITEM claim for buyer -6. Create CURRENCY claim for seller (minus sales tax) -7. Set status to ENDED -8. Save - -**Auction Ends (no buyout):** -1. If bids exist: - - Create ITEM claim for high bidder - - Create CURRENCY claim for seller (minus sales tax) -2. If no bids: - - Create ITEM claim for seller (item returned) -3. Set status to ENDED -4. Save - -### 3.6 Snipe Protection +### 3.7 Snipe Protection When a bid is placed with less than `SnipeProtectionMinutes` remaining: - Extend the auction end time TO `SnipeProtectionMinutes` from now @@ -523,307 +318,217 @@ When a bid is placed with less than `SnipeProtectionMinutes` remaining: Example: Auction ends in 30 seconds, someone bids → Timer resets to 2:00 -### 3.7 Notifications - -#### AuctionNotification Class -```java -public class AuctionNotification { - UUID playerUUID; - EnumNotificationType type; - int listingId; - String message; - long timestamp; -} - -public enum EnumNotificationType { - AUCTION_WON, // You won an auction - AUCTION_OUTBID, // You were outbid - AUCTION_SOLD, // Your item sold - AUCTION_EXPIRED, // Your auction expired (no bids) - CLAIM_READY // You have items/currency to claim -} -``` - -Notifications are queued and sent when player logs in. - -### 3.8 Permissions - -| Permission | Default | Description | -|------------|---------|-------------| -| `customnpcs.auction.use` | true | Can use auction house | -| `customnpcs.auction.sell` | true | Can create listings | -| `customnpcs.auction.slots.X` | 5 | Max listing slots (X = number) | -| `customnpcs.auction.admin` | op | Admin auction management | - --- -## 4. Configuration +## 4. AuctionController Performance & Thread Safety -### ConfigMarket.java Structure +### 4.1 Threading Model -```java -// ========================================= -// Market General -// ========================================= -public static boolean MarketEnabled = true; - -// ========================================= -// Currency (Market.Currency) -// ========================================= -public static boolean UseVault = false; -public static String CurrencyName = "Coins"; -public static long StartingBalance = 0; -public static long MaxBalance = Long.MAX_VALUE; - -// ========================================= -// Trader (Market.Trader) -// ========================================= -public static boolean EnableStockByDefault = false; -public static String DefaultResetType = "NONE"; - -// ========================================= -// Auction (Market.Auction) -// ========================================= -public static boolean AuctionEnabled = true; -public static int AuctionDuration = 24; // Hours -public static long ListingFee = 10; // Flat fee to create listing -public static double SalesTaxPercent = 0.05; // 5% tax on sales -public static int DefaultMaxListings = 5; // Per player -public static int SnipeProtectionMinutes = 2; // Snipe protection timer -public static int ClaimExpirationDays = 30; // Days before unclaimed items deleted -public static double MinBidIncrementPercent = 0.05; // 5% minimum bid increase ``` +┌─────────────────────────────────────────────────────────────────┐ +│ AuctionController │ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ ReentrantRWLock │ │ AtomicBoolean │ │ CNPC+ Thread │ │ +│ │ (dataLock) │ │ dirty, saving │ │ (async saves) │ │ +│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ │ +│ Read operations → Read Lock (concurrent) │ +│ Write operations → Write Lock (exclusive) │ +│ Save operations → Async on CNPC+ thread │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### 4.2 Player Indices for O(1) Lookups + +| Index | Type | Purpose | +|-------|------|---------| +| `playerListingIds` | `Map>` | Active listings by seller | +| `playerBidIds` | `Map>` | Listings where player has active bid | +| `playerClaimIds` | `Map>` | Unclaimed claims by player | + +**Performance Impact:** +- `getPlayerListingCount()`: O(n) → O(1) +- `getPlayerActiveListings()`: O(n) → O(k) where k = player's listings +- `getPlayerActiveBids()`: O(n) → O(k) +- `getPlayerClaims()`: O(n) → O(k) + +### 4.3 Update Intervals + +| Operation | Interval | Rationale | +|-----------|----------|-----------| +| Process ended auctions | 20 seconds (400 ticks) | Reduces server load | +| Process expired claims | 20 seconds (400 ticks) | Same cycle as auctions | +| Save dirty data | 60 seconds (1200 ticks) | Batches writes, reduces I/O | + +### 4.4 Race Condition Prevention + +1. **Bid vs Auction End** - Double-check `isExpired()` inside write lock +2. **Currency Deduction** - Withdraw before updating listing +3. **Previous Bidder Refund** - Store info before modification +4. **Concurrent Saves** - `AtomicBoolean` prevents duplicates +5. **Index Consistency** - All updates within same write lock + +### 4.5 Server Shutdown Handling + +On `FMLServerStoppedEvent`: +1. `AuctionController.save()` called synchronously +2. Uses CNPC+ thread (single-threaded executor) +3. All pending saves complete before shutdown --- ## 5. File Structure -### New Files to Create +### New Files Created ``` constants/ - EnumAuctionStatus.java - EnumAuctionSort.java - EnumClaimType.java - EnumNotificationType.java + EnumAuctionStatus.java ✅ + EnumAuctionSort.java ✅ + EnumClaimType.java ✅ + EnumNotificationType.java ✅ controllers/ - AuctionController.java - AuctionNotification.java + AuctionController.java ✅ (with threading, indices, Vault support) controllers/data/ - AuctionListing.java - AuctionClaim.java - AuctionFilter.java + AuctionListing.java ✅ + AuctionClaim.java ✅ + AuctionFilter.java ✅ + AuctionNotification.java ✅ roles/ - RoleAuctioneer.java + RoleAuctioneer.java ✅ containers/ - ContainerAuction.java + ContainerAuction.java ✅ + ContainerAuctionListing.java ✅ + ContainerAuctionBidding.java ✅ + ContainerAuctionTrades.java ✅ + ContainerAuctionSell.java ✅ + InventoryAuctionDisplay.java ✅ + SlotAuctionDisplay.java ✅ + +client/ + AuctionClientConfig.java ✅ (client-side config cache) client/gui/player/ - GuiAuctionHouse.java (Base GUI) - GuiAuction.java (Browse auctions) - GuiAuctionSell.java (Create listing) - GuiAuctionClaims.java (My auctions/claims) - GuiAuctionFilter.java (Filter settings) - SubGuiAuctionDetails.java (View listing details) + GuiAuctionInterface.java ✅ + GuiAuctionListing.java ✅ + GuiAuctionSell.java ✅ + GuiAuctionTrades.java ✅ + GuiAuctionBidding.java ✅ + SubGuiAuctionSearch.java ✅ + SubGuiAuctionBid.java ✅ + SubGuiAuctionBuyNow.java ✅ + AuctionTooltipHandler.java ✅ -client/gui/global/ - GuiManageAuction.java (Admin management) +client/gui/util/ + GuiAuctionNavButton.java ✅ -network/packets/ - AuctionDataPacket.java (Server → Client data sync) - AuctionActionPacket.java (Client → Server actions) +network/packets/player/ + AuctionDataPacket.java ✅ + AuctionActionPacket.java ✅ ``` -### Files to Modify +### Files Modified ``` -CustomNpcs.java - Initialize AuctionController -ServerTickHandler.java - Process ended auctions, login notifications -EnumRoleType.java - Add Auctioneer -EnumGuiType.java - Add auction GUI types -DataAdvanced.java - Handle Auctioneer role -PacketHandler.java - Register auction packets -CustomNpcsPermissions.java - Auction permissions -ConfigMarket.java - Add Auction section -RoleTrader.java - Add currency cost per trade -ContainerNPCTrader.java - Currency + stock integration -en_US.lang - All auction lang keys +CustomNpcs.java ✅ Initialize AuctionController, save on shutdown +EnumRoleType.java ✅ Added Auctioneer +EnumGuiType.java ✅ Added PlayerAuction, PlayerAuctionSell, PlayerAuctionTrades, PlayerAuctionBidding +EnumPlayerPacket.java ✅ Added AuctionData, AuctionAction +CommonProxy.java ✅ Handle auction container types +ClientProxy.java ✅ Handle auction GUI types +DataAdvanced.java ✅ Handle Auctioneer role +ConfigMarket.java ✅ Added Auction section +LoginPacket.java ✅ Sync auction config to client +en_US.lang ✅ Added auction lang keys ``` --- -## 6. Data Storage +## 6. Configuration -### Auction Data File -Location: `customnpcs/auction.dat` +### ConfigMarket.java Structure -Structure: -``` -NBTTagCompound root - ├── NextListingId: int - ├── NextClaimId: int - ├── Listings: NBTTagList - │ └── [AuctionListing NBT...] - ├── Claims: NBTTagList - │ └── [AuctionClaim NBT...] - └── PendingNotifications: NBTTagList - └── [AuctionNotification NBT...] +```java +// ========================================= +// Currency (Market.Currency) +// ========================================= +public static boolean UseVault = false; // Use Vault economy +public static String CurrencyName = "Coins"; // Display name +public static long StartingBalance = 0; // New player balance +public static long MaxBalance = Long.MAX_VALUE; // Balance cap + +// ========================================= +// Auction (Market.Auction) +// ========================================= +public static boolean AuctionEnabled = true; +public static int AuctionDurationHours = 48; // Auction length +public static long ListingFee = 10; // Fee to create listing +public static double SalesTaxPercent = 0.05; // 5% tax on sales +public static int DefaultMaxListings = 10; // Per player +public static int SnipeProtectionMinutes = 2; // Anti-snipe timer +public static int ClaimExpirationDays = 30; // Days before unclaimed deleted +public static double MinBidIncrementPercent = 0.05; // 5% minimum bid increase + +// ========================================= +// Auction Logging +// ========================================= +public static boolean AuctionLoggingEnabled = true; +public static boolean LogAuctionCreated = true; +public static boolean LogAuctionBid = true; +public static boolean LogAuctionBuyout = true; +public static boolean LogAuctionSold = true; +public static boolean LogAuctionExpired = true; +public static boolean LogAuctionCancelled = true; +public static boolean LogAuctionClaimed = true; ``` --- -## 7. Lang Keys +## 7. Implementation Status -```properties -# Auction House -auction.title=Auction House -auction.browse=Browse -auction.sell=Sell -auction.claims=My Auctions -auction.filter=Filter +### Phase 1: Currency System - COMPLETE ✅ +### Phase 2: Trader Enhancements - COMPLETE ✅ +### Phase 3: Auction Core - COMPLETE ✅ +### Phase 4: Auction GUI - COMPLETE ✅ +### Phase 5: Auction Network - COMPLETE ✅ +### Phase 6: Performance & Safety - COMPLETE ✅ -# Browse -auction.nextPage=Next > -auction.prevPage=< Prev -auction.page=Page %d of %d -auction.noResults=No auctions found -auction.refresh=Refresh +1. ~~Threading model with ReentrantReadWriteLock~~ +2. ~~Player indices for O(1) lookups~~ +3. ~~20-second update cycle~~ +4. ~~Async saves on CNPC+ thread~~ +5. ~~Race condition prevention~~ +6. ~~Server shutdown handling~~ +7. ~~Own listing view (buttons hidden)~~ +8. ~~Server-side security checks~~ -# Listing Display -auction.seller=Seller: %s -auction.currentBid=Current Bid: %s -auction.buyout=Buyout: %s -auction.timeLeft=Time Left: %s -auction.bids=%d bids -auction.noBids=No bids +### Remaining Work -# Create Listing -auction.sell.title=Create Listing -auction.sell.startingPrice=Starting Price -auction.sell.buyoutPrice=Buyout Price -auction.sell.noBuyout=(0 = no buyout) -auction.sell.fee=Listing Fee: %s -auction.sell.create=Create Listing -auction.sell.noItem=Place an item to sell - -# Claims -auction.claims.title=My Auctions -auction.claims.active=Active Listings -auction.claims.pending=Pending Claims -auction.claims.claimAll=Claim All -auction.claims.cancel=Cancel Listing -auction.claims.empty=No pending claims - -# Filter -auction.filter.title=Filter Auctions -auction.filter.search=Search -auction.filter.minPrice=Min Price -auction.filter.maxPrice=Max Price -auction.filter.myOnly=My Listings Only -auction.filter.apply=Apply - -# Sort Options -auction.sort.newest=Newest -auction.sort.endingSoon=Ending Soon -auction.sort.priceLow=Price: Low to High -auction.sort.priceHigh=Price: High to Low -auction.sort.mostBids=Most Bids - -# Notifications -auction.notify.won=You won the auction for %s! -auction.notify.outbid=You were outbid on %s! -auction.notify.sold=Your %s sold for %s! -auction.notify.expired=Your auction for %s expired with no bids. -auction.notify.claim=You have items to claim at the Auction House. - -# Errors -auction.error.notEnoughCurrency=Not enough currency! -auction.error.auctionEnded=This auction has ended. -auction.error.bidTooLow=Bid must be at least %s. -auction.error.maxListings=You have reached your maximum listings. -auction.error.blacklisted=This item cannot be auctioned. -auction.error.noAuctioneer=You must visit an Auctioneer NPC. - -# Trader Currency -trader.currencyCost=Currency Cost -trader.outOfStock=Out of Stock -trader.stock=Stock: %d -``` +- [ ] GuiManageAuction (Admin GUI) +- [ ] Permission system integration +- [ ] Additional testing --- -## 8. Implementation Order - -### Phase 1: Trader Currency Integration - COMPLETE ✅ -1. ~~Add `currencyCost` field to RoleTrader trades~~ -2. ~~Update ContainerNPCTrader to check/deduct currency~~ -3. ~~Update trader setup GUI with currency input~~ -4. ~~Test with existing item trades (backward compatibility)~~ - -### Phase 2: Trader Stock UI - COMPLETE ✅ -1. ~~Add stock display to player trader GUI~~ -2. ~~Show "Out of Stock" indicator~~ -3. ~~Verify reset timers working~~ -4. ~~Real-time client-side countdown~~ -5. ~~Status messages with costs~~ -6. ~~Reset cooldown button with confirmation~~ - -### Phase 2.5: Trader Sync System - COMPLETE ✅ -1. ~~Implement MarketRegistry for linked market sync~~ -2. ~~Viewer tracking (market-wide and local)~~ -3. ~~NPC unload cleanup~~ -4. ~~Thread safety for stock operations~~ - -### Phase 3: Auction Core -1. Create enums (EnumAuctionStatus, EnumAuctionSort, etc.) -2. Create data classes (AuctionListing, AuctionClaim, AuctionFilter) -3. Create AuctionController with core logic -4. Create RoleAuctioneer -5. Add to CustomNpcs initialization - -### Phase 4: Auction GUI -1. Create GuiAuctionHouse base -2. Create GuiAuction (browse) -3. Create GuiAuctionSell -4. Create GuiAuctionClaims -5. Create GuiAuctionFilter -6. Create SubGuiAuctionDetails -7. Create ContainerAuction - -### Phase 5: Auction Network -1. Create AuctionDataPacket -2. Create AuctionActionPacket -3. Register packets - -### Phase 6: Admin & Polish -1. Create GuiManageAuction -2. Add permissions -3. Add notifications system -4. Add lang keys -5. Testing - ---- - -## 9. Design Decisions +## 8. Design Decisions | Decision | Choice | Rationale | |----------|--------|-----------| | Claims System | Everything to claims | Prevents offline handling issues | -| Auction Duration | Single config value | Simplicity over flexibility | -| Snipe Protection | Reset to X minutes | Fair bidding, prevents last-second sniping | -| Listing Fees | Flat fee only | Simple, predictable | -| Sales Tax | Percentage of sale | Scales with item value | -| Data Storage | Single .dat file | Consistent with quests/dialogs | -| Blacklist | Stub for now | Complex feature for later | -| Currency per Trade | Additive with items | Backward compatible, flexible | +| Vault Integration | Via PlayerCurrencyData | Single point of delegation, reused by Trader & Auction | +| Update Interval | 20 seconds | Performance vs responsiveness tradeoff | +| Player Indices | ConcurrentHashMap + Sets | O(1) lookups, thread-safe | +| Async Saves | CNPC+ Thread | Doesn't block server tick | +| Config Sync | LoginPacket → AuctionClientConfig | Client never accesses server config directly | --- -## 10. Future Considerations (Not in 1.11) +## 9. Future Considerations (Not in 1.11) - Category-based filtering - Advanced blacklist system (items, mods, NBT patterns) diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 1405b44c9..8d008ceea 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -59,6 +59,7 @@ import kamkeel.npcs.network.packets.player.FollowerPacket; import kamkeel.npcs.network.packets.player.GetNPCRole; import kamkeel.npcs.network.packets.player.GetTraderData; import kamkeel.npcs.network.packets.player.InputDevicePacket; +import kamkeel.npcs.network.packets.player.AuctionActionPacket; import kamkeel.npcs.network.packets.player.MailActionPacket; import kamkeel.npcs.network.packets.player.SaveBookPacket; import kamkeel.npcs.network.packets.player.SaveSignPacket; @@ -591,6 +592,9 @@ public class PacketHandler { PLAYER_PACKET.registerPacket(new GetNPCRole()); PLAYER_PACKET.registerPacket(new GetTraderData()); + // Auction Packets + PLAYER_PACKET.registerPacket(new AuctionActionPacket()); + PLAYER_PACKET.registerPacket(new InputDevicePacket()); PLAYER_PACKET.registerPacket(new ScreenSizePacket()); diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index ebcbdfc59..a5c6c8ba9 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -62,4 +62,7 @@ public enum EnumDataPacket { // Ability System TELEGRAPH_SPAWN, TELEGRAPH_REMOVE, + + // Auction System + AUCTION_DATA, } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java index c341bd02e..2717d6277 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/LoginPacket.java @@ -12,8 +12,11 @@ import kamkeel.npcs.network.enums.EnumSyncType; import kamkeel.npcs.network.packets.player.SyncRevisionInfoPacket; import kamkeel.npcs.util.ByteBufUtils; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.client.AuctionClientConfig; import noppes.npcs.client.ClientCacheHandler; import noppes.npcs.config.ConfigMain; +import noppes.npcs.controllers.AuctionConfigSync; import java.io.IOException; import java.util.EnumMap; @@ -57,6 +60,11 @@ public final class LoginPacket extends AbstractPacket { out.writeInt(entry.getKey().ordinal()); out.writeInt(entry.getValue()); } + + // Send auction config (server-side sync) + NBTTagCompound auctionConfig = new NBTTagCompound(); + AuctionConfigSync.writeToNBT(auctionConfig); + ByteBufUtils.writeNBT(out, auctionConfig); } @SideOnly(Side.CLIENT) @@ -76,6 +84,10 @@ public final class LoginPacket extends AbstractPacket { ClientCacheHandler.setActiveServer(serverKey, serverRevisions); + // Read auction config + NBTTagCompound auctionConfig = ByteBufUtils.readNBT(in); + AuctionClientConfig.readFromNBT(auctionConfig); + PacketClient.sendClient(new SyncRevisionInfoPacket( serverKey, ClientCacheHandler.getLastServerKey(), diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java new file mode 100644 index 000000000..45d38068b --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java @@ -0,0 +1,410 @@ +package kamkeel.npcs.network.packets.player; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumPlayerPacket; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.constants.EnumRoleType; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.data.AuctionFilter; +import noppes.npcs.controllers.data.AuctionListing; + +import java.io.IOException; +import java.util.List; + +/** + * Handles all auction-related client-server communication. + */ +public class AuctionActionPacket extends AbstractPacket { + public static final String packetName = "Player|Auction"; + private static final int PAGE_SIZE = 45; // 9x5 grid + + private Action action; + private String listingId; + private long amount; + private NBTTagCompound data; + + public AuctionActionPacket() {} + + private AuctionActionPacket(Action action) { + this.action = action; + } + + private AuctionActionPacket(Action action, String listingId) { + this.action = action; + this.listingId = listingId; + } + + private AuctionActionPacket(Action action, String listingId, long amount) { + this.action = action; + this.listingId = listingId; + this.amount = amount; + } + + @Override + public boolean needsNPC() { return true; } + + // ========================================= + // Client-side static methods + // ========================================= + + @SideOnly(Side.CLIENT) + public static void placeBid(String listingId, long bidAmount) { + AuctionActionPacket packet = new AuctionActionPacket(Action.Bid, listingId, bidAmount); + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void buyout(String listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.Buyout, listingId); + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void cancelListing(String listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.Cancel, listingId); + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void claimItem(String claimId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.ClaimItem, claimId); + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void claimCurrency(String claimId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.ClaimCurrency, claimId); + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void createListing(ItemStack item, long startingPrice, long buyoutPrice) { + AuctionActionPacket packet = new AuctionActionPacket(Action.CreateListing); + packet.amount = startingPrice; + packet.listingId = String.valueOf(buyoutPrice); + packet.data = new NBTTagCompound(); + item.writeToNBT(packet.data); + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void openPage(int page) { + AuctionActionPacket packet = new AuctionActionPacket(Action.OpenPage); + packet.amount = page; + PacketClient.sendClient(packet); + } + + @SideOnly(Side.CLIENT) + public static void openBidding(String listingId) { + AuctionActionPacket packet = new AuctionActionPacket(Action.OpenBidding, listingId); + PacketClient.sendClient(packet); + } + + /** Request listings from server with filter and page */ + @SideOnly(Side.CLIENT) + public static void requestListings(AuctionFilter filter, int page) { + AuctionActionPacket packet = new AuctionActionPacket(Action.RequestListings); + packet.amount = page; + packet.data = filter.writeToNBT(new NBTTagCompound()); + PacketClient.sendClient(packet); + } + + // ========================================= + // Packet Implementation + // ========================================= + + @Override + public Enum getType() { return EnumPlayerPacket.AuctionAction; } + + @Override + public PacketChannel getChannel() { return PacketHandler.PLAYER_PACKET; } + + @Override + @SideOnly(Side.CLIENT) + public void sendData(ByteBuf out) throws IOException { + out.writeInt(action.ordinal()); + + switch (action) { + case Bid: + ByteBufUtils.writeString(out, listingId); + out.writeLong(amount); + break; + case Buyout: + case Cancel: + case ClaimItem: + case ClaimCurrency: + ByteBufUtils.writeString(out, listingId); + break; + case CreateListing: + out.writeLong(amount); + ByteBufUtils.writeString(out, listingId); + ByteBufUtils.writeNBT(out, data); + break; + case OpenPage: + out.writeInt((int) amount); + break; + case OpenBidding: + ByteBufUtils.writeString(out, listingId); + break; + case RequestListings: + out.writeInt((int) amount); + ByteBufUtils.writeNBT(out, data); + break; + } + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) return; + EntityPlayerMP playerMP = (EntityPlayerMP) player; + + if (npc.advanced.role != EnumRoleType.Auctioneer) return; + if (!ConfigMarket.AuctionEnabled) return; + + Action requestedAction = Action.values()[in.readInt()]; + AuctionController controller = AuctionController.getInstance(); + String result = null; + + switch (requestedAction) { + case Bid: + result = handleBid(in, playerMP, controller); + break; + + case Buyout: + result = handleBuyout(in, playerMP, controller); + break; + + case Cancel: + result = handleCancel(in, playerMP, controller); + break; + + case ClaimItem: + result = handleClaimItem(in, playerMP, controller); + break; + + case ClaimCurrency: + result = handleClaimCurrency(in, playerMP, controller); + break; + + case CreateListing: + result = handleCreateListing(in, playerMP, controller); + break; + + case OpenPage: + handleOpenPage(in, playerMP); + break; + + case OpenBidding: + handleOpenBidding(in, playerMP, controller); + break; + + case RequestListings: + handleRequestListings(in, playerMP, controller); + break; + } + + if (result != null) { + sendError(playerMP, result); + } + } + + // ========================================= + // Action Handlers + // ========================================= + + private String handleBid(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + String id = ByteBufUtils.readString(in); + long amount = in.readLong(); + String result = controller.placeBid(id, player, amount); + if (result == null) { + sendListingsUpdate(player); + } + return result; + } + + private String handleBuyout(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + String id = ByteBufUtils.readString(in); + String result = controller.buyout(id, player); + if (result == null) { + sendListingsUpdate(player); + } + return result; + } + + private String handleCancel(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + String id = ByteBufUtils.readString(in); + String result = controller.cancelListing(id, player, false); + if (result == null) { + sendTradesUpdate(player); + } + return result; + } + + private String handleClaimItem(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + String id = ByteBufUtils.readString(in); + String result = controller.claimItem(id, player); + if (result == null) { + sendTradesUpdate(player); + } + return result; + } + + private String handleClaimCurrency(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + String id = ByteBufUtils.readString(in); + String result = controller.claimCurrency(id, player); + if (result == null) { + sendTradesUpdate(player); + } + return result; + } + + private String handleCreateListing(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + long startingPrice = in.readLong(); + String buyoutStr = ByteBufUtils.readString(in); + long buyoutPrice = 0; + try { buyoutPrice = Long.parseLong(buyoutStr); } catch (NumberFormatException ignored) {} + + NBTTagCompound itemNBT = ByteBufUtils.readNBT(in); + if (itemNBT == null) return "No item data received."; + + ItemStack item = ItemStack.loadItemStackFromNBT(itemNBT); + if (item == null) return "Invalid item data."; + + // Remove item from inventory first + if (!removeItemFromInventory(player, item)) { + return "Item not found in inventory."; + } + + String result = controller.createListing(player, item, startingPrice, buyoutPrice); + if (result != null) { + // Failed - return item + player.inventory.addItemStackToInventory(item); + } + return result; + } + + private void handleOpenPage(ByteBuf in, EntityPlayerMP player) { + int page = in.readInt(); + EnumGuiType guiType; + switch (page) { + case 0: guiType = EnumGuiType.PlayerAuction; break; + case 1: guiType = EnumGuiType.PlayerAuctionSell; break; + case 2: guiType = EnumGuiType.PlayerAuctionTrades; break; + default: guiType = EnumGuiType.PlayerAuction; break; + } + NoppesUtilServer.sendOpenGui(player, guiType, npc); + } + + private void handleOpenBidding(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + String listingId = ByteBufUtils.readString(in); + AuctionListing listing = controller.getListing(listingId); + + if (listing == null || !listing.isActive()) { + sendError(player, "Listing not found or has ended."); + return; + } + + // Open bidding GUI + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuctionBidding, npc); + + // Check if this is the player's own listing + boolean isOwnListing = listing.isSeller(player.getUniqueID()); + + // Send listing data + NBTTagCompound response = new NBTTagCompound(); + response.setBoolean("BiddingData", true); + response.setTag("Listing", listing.writeToNBT(new NBTTagCompound())); + response.setLong("Balance", controller.getPlayerBalance(player)); + response.setBoolean("IsOwnListing", isOwnListing); + GuiDataPacket.sendGuiData(player, response); + } + + private void handleRequestListings(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { + int page = in.readInt(); + NBTTagCompound filterNBT = ByteBufUtils.readNBT(in); + + AuctionFilter filter = new AuctionFilter(); + if (filterNBT != null) { + filter.readFromNBT(filterNBT); + } + + // Get listings for this page + List listings = controller.getActiveListings(filter, page, PAGE_SIZE); + int totalListings = controller.getTotalActiveListings(filter); + int totalPages = Math.max(1, (int) Math.ceil((double) totalListings / PAGE_SIZE)); + + // Build response + NBTTagCompound response = new NBTTagCompound(); + response.setBoolean("ListingsData", true); + response.setInteger("Page", page); + response.setInteger("TotalPages", totalPages); + response.setInteger("TotalListings", totalListings); + + NBTTagList listingsList = new NBTTagList(); + for (AuctionListing listing : listings) { + listingsList.appendTag(listing.writeToNBT(new NBTTagCompound())); + } + response.setTag("Listings", listingsList); + + GuiDataPacket.sendGuiData(player, response); + } + + // ========================================= + // Helpers + // ========================================= + + private boolean removeItemFromInventory(EntityPlayerMP player, ItemStack target) { + for (int i = 0; i < player.inventory.getSizeInventory(); i++) { + ItemStack stack = player.inventory.getStackInSlot(i); + if (stack != null && ItemStack.areItemStacksEqual(stack, target)) { + player.inventory.setInventorySlotContents(i, null); + return true; + } + } + return false; + } + + private void sendError(EntityPlayerMP player, String message) { + player.addChatMessage(new net.minecraft.util.ChatComponentText( + net.minecraft.util.EnumChatFormatting.RED + "[Auction House] " + message)); + } + + private void sendTradesUpdate(EntityPlayerMP player) { + NBTTagCompound compound = new NBTTagCompound(); + compound.setBoolean("TradesUpdate", true); + GuiDataPacket.sendGuiData(player, compound); + } + + private void sendListingsUpdate(EntityPlayerMP player) { + NBTTagCompound compound = new NBTTagCompound(); + compound.setBoolean("ListingsRefresh", true); + GuiDataPacket.sendGuiData(player, compound); + } + + private enum Action { + Bid, + Buyout, + Cancel, + ClaimItem, + ClaimCurrency, + CreateListing, + OpenPage, + OpenBidding, + RequestListings + } +} diff --git a/src/main/java/noppes/npcs/CommonProxy.java b/src/main/java/noppes/npcs/CommonProxy.java index 251272c1d..6fefe01c1 100644 --- a/src/main/java/noppes/npcs/CommonProxy.java +++ b/src/main/java/noppes/npcs/CommonProxy.java @@ -14,6 +14,11 @@ import net.minecraftforge.common.util.FakePlayer; import noppes.npcs.api.IWorld; import noppes.npcs.blocks.tiles.TileNpcContainer; import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.containers.ContainerAuction; +import noppes.npcs.containers.ContainerAuctionBidding; +import noppes.npcs.containers.ContainerAuctionTrades; +import noppes.npcs.containers.ContainerAuctionListing; +import noppes.npcs.containers.ContainerAuctionSell; import noppes.npcs.containers.ContainerAnvilRepair; import noppes.npcs.containers.ContainerCarpentryBench; import noppes.npcs.containers.ContainerCrate; @@ -92,6 +97,18 @@ public class CommonProxy implements IGuiHandler { if (gui == EnumGuiType.PlayerTrader) return new ContainerNPCTrader(npc, player); + if (gui == EnumGuiType.PlayerAuction) + return new ContainerAuctionListing(npc, player); + + if (gui == EnumGuiType.PlayerAuctionSell) + return new ContainerAuctionSell(npc, player); + + if (gui == EnumGuiType.PlayerAuctionTrades) + return new ContainerAuctionTrades(npc, player); + + if (gui == EnumGuiType.PlayerAuctionBidding) + return new ContainerAuctionBidding(npc, player); + if (gui == EnumGuiType.PlayerCarpentryBench) return new ContainerCarpentryBench(player.inventory, player.worldObj, x, y, z); diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index c5b82d72a..cb9241865 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -55,6 +55,7 @@ import noppes.npcs.config.ConfigMain; import noppes.npcs.config.LoadConfiguration; import noppes.npcs.config.legacy.LegacyConfig; import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.BankController; import noppes.npcs.controllers.ChunkController; import noppes.npcs.controllers.CustomEffectController; @@ -369,6 +370,7 @@ public class CustomNpcs { public void started(FMLServerStartedEvent event) { RecipeController.Instance.load(); new BankController(); + new AuctionController(); DialogController.Instance.load(); QuestController.Instance.load(); ScriptController.HasStart = true; @@ -389,6 +391,12 @@ public class CustomNpcs { ScriptController.Instance.saveForgeScripts(); ScriptController.Instance.savePlayerScripts(); ScriptController.Instance.saveGlobalNpcScripts(); + + // Save auction data synchronously on shutdown + if (AuctionController.Instance != null) { + AuctionController.Instance.save(); + } + if (FMLCommonHandler.instance().getSide().isClient()) clientJaninoCompiler = null; globalJaninoCompiler = null; diff --git a/src/main/java/noppes/npcs/DataAdvanced.java b/src/main/java/noppes/npcs/DataAdvanced.java index 45ecfe4a8..f03714506 100644 --- a/src/main/java/noppes/npcs/DataAdvanced.java +++ b/src/main/java/noppes/npcs/DataAdvanced.java @@ -18,6 +18,7 @@ import noppes.npcs.roles.JobGuard; import noppes.npcs.roles.JobHealer; import noppes.npcs.roles.JobItemGiver; import noppes.npcs.roles.JobSpawner; +import noppes.npcs.roles.RoleAuctioneer; import noppes.npcs.roles.RoleBank; import noppes.npcs.roles.RoleCompanion; import noppes.npcs.roles.RoleFollower; @@ -218,6 +219,8 @@ public class DataAdvanced { npc.roleInterface = new RoleCompanion(npc); else if (role == EnumRoleType.Mount && !(npc.roleInterface instanceof RoleMount)) npc.roleInterface = new RoleMount(npc); + else if (role == EnumRoleType.Auctioneer && !(npc.roleInterface instanceof RoleAuctioneer)) + npc.roleInterface = new RoleAuctioneer(npc); } public void setJob(int i) { diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index ea42ce84c..27995f3cb 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -6,6 +6,7 @@ import cpw.mods.fml.common.gameevent.TickEvent; import cpw.mods.fml.common.gameevent.TickEvent.Phase; import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.SyncController; +import noppes.npcs.controllers.AuctionController; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.world.WorldServer; import noppes.npcs.controllers.ScriptController; @@ -19,6 +20,11 @@ public class ServerTickHandler { public void onServerTick(TickEvent.ServerTickEvent event) { if (event.phase == Phase.END) { ActionManager.GLOBAL.tick(); + + // Process auction system tick + if (AuctionController.Instance != null) { + AuctionController.Instance.onServerTick(); + } } } @@ -43,6 +49,11 @@ public class ServerTickHandler { SyncController.beginLogin(player); SyncController.syncEffects(player); ScriptController.Instance.syncClientScripts(player); + + // Send auction notifications on login + if (AuctionController.Instance != null) { + AuctionController.Instance.onPlayerLogin(player); + } } @SubscribeEvent diff --git a/src/main/java/noppes/npcs/client/AuctionClientConfig.java b/src/main/java/noppes/npcs/client/AuctionClientConfig.java new file mode 100644 index 000000000..aaae441bf --- /dev/null +++ b/src/main/java/noppes/npcs/client/AuctionClientConfig.java @@ -0,0 +1,66 @@ +package noppes.npcs.client; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.nbt.NBTTagCompound; + +/** + * Client-side cache for auction configuration synced from server. + * Prevents client from using local ConfigMarket which may differ from server settings. + * + * IMPORTANT: This class is CLIENT-ONLY. Server-side NBT writing is done in + * AuctionConfigSync to avoid @SideOnly crashes on dedicated servers. + */ +@SideOnly(Side.CLIENT) +public class AuctionClientConfig { + // Cached values from server + private static long listingFee = 0; + private static String currencyName = "Gold"; + private static int auctionDurationHours = 48; + private static double minBidIncrement = 0.05; + private static int maxActiveListings = 10; + private static int claimExpirationDays = 30; + private static boolean auctionEnabled = false; + + /** + * Update cached config from server NBT data. + * Called when receiving login packet. + */ + public static void readFromNBT(NBTTagCompound compound) { + if (compound == null) return; + + auctionEnabled = compound.getBoolean("AuctionEnabled"); + listingFee = compound.getLong("ListingFee"); + currencyName = compound.getString("CurrencyName"); + auctionDurationHours = compound.getInteger("AuctionDurationHours"); + minBidIncrement = compound.getDouble("MinBidIncrement"); + maxActiveListings = compound.getInteger("MaxActiveListings"); + claimExpirationDays = compound.getInteger("ClaimExpirationDays"); + + if (currencyName.isEmpty()) { + currencyName = "Gold"; + } + } + + /** + * Reset to defaults (called on disconnect). + */ + public static void reset() { + listingFee = 0; + currencyName = "Gold"; + auctionDurationHours = 48; + minBidIncrement = 0.05; + maxActiveListings = 10; + claimExpirationDays = 30; + auctionEnabled = false; + } + + // Getters + public static boolean isAuctionEnabled() { return auctionEnabled; } + public static long getListingFee() { return listingFee; } + public static String getCurrencyName() { return currencyName; } + public static int getAuctionDurationHours() { return auctionDurationHours; } + public static double getMinBidIncrement() { return minBidIncrement; } + public static int getMaxActiveListings() { return maxActiveListings; } + public static int getClaimExpirationDays() { return claimExpirationDays; } +} diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 656efc97b..8eea9e27b 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -108,6 +108,10 @@ import noppes.npcs.client.gui.player.GuiCrate; import noppes.npcs.client.gui.player.GuiMailbox; import noppes.npcs.client.gui.player.GuiMailmanWrite; import noppes.npcs.client.gui.player.GuiNPCBankChest; +import noppes.npcs.client.gui.player.GuiAuctionBidding; +import noppes.npcs.client.gui.player.GuiAuctionTrades; +import noppes.npcs.client.gui.player.GuiAuctionListing; +import noppes.npcs.client.gui.player.GuiAuctionSell; import noppes.npcs.client.gui.player.GuiNPCTrader; import noppes.npcs.client.gui.player.GuiNpcAnvil; import noppes.npcs.client.gui.player.GuiNpcCarpentryBench; @@ -193,6 +197,11 @@ import noppes.npcs.containers.ContainerNPCFollower; import noppes.npcs.containers.ContainerNPCFollowerHire; import noppes.npcs.containers.ContainerNPCFollowerSetup; import noppes.npcs.containers.ContainerNPCInv; +import noppes.npcs.containers.ContainerAuction; +import noppes.npcs.containers.ContainerAuctionBidding; +import noppes.npcs.containers.ContainerAuctionTrades; +import noppes.npcs.containers.ContainerAuctionListing; +import noppes.npcs.containers.ContainerAuctionSell; import noppes.npcs.containers.ContainerNPCTrader; import noppes.npcs.containers.ContainerNPCTraderSetup; import noppes.npcs.containers.ContainerNpcItemGiver; @@ -484,6 +493,18 @@ public class ClientProxy extends CommonProxy { else if (gui == EnumGuiType.PlayerTrader) return new GuiNPCTrader(npc, (ContainerNPCTrader) container); + else if (gui == EnumGuiType.PlayerAuction) + return new GuiAuctionListing(npc, (ContainerAuctionListing) container); + + else if (gui == EnumGuiType.PlayerAuctionSell) + return new GuiAuctionSell(npc, (ContainerAuctionSell) container); + + else if (gui == EnumGuiType.PlayerAuctionTrades) + return new GuiAuctionTrades(npc, (ContainerAuctionTrades) container); + + else if (gui == EnumGuiType.PlayerAuctionBidding) + return new GuiAuctionBidding(npc, (ContainerAuctionBidding) container); + else if (gui == EnumGuiType.PlayerBankSmall || gui == EnumGuiType.PlayerBankUnlock || gui == EnumGuiType.PlayerBankUprade || gui == EnumGuiType.PlayerBankLarge) return new GuiNPCBankChest(npc, (ContainerNPCBankInterface) container); diff --git a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java new file mode 100644 index 000000000..f8f58e29b --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java @@ -0,0 +1,213 @@ +package noppes.npcs.client.gui.player; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.item.ItemStack; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.AuctionClientConfig; +import noppes.npcs.containers.ContainerAuctionListing; +import noppes.npcs.containers.ContainerAuctionTrades; +import noppes.npcs.controllers.data.AuctionClaim; +import noppes.npcs.controllers.data.AuctionListing; + +import java.util.List; + +/** + * Handles adding auction information to item tooltips when viewing auction GUIs. + * Called from MixinItem when Item.addInformation is invoked. + */ +@SideOnly(Side.CLIENT) +public class AuctionTooltipHandler { + + /** + * Add auction information to an item's tooltip if we're in an auction GUI. + */ + public static void addAuctionInfo(ItemStack stack, List tooltip) { + if (stack == null || tooltip == null) return; + + Minecraft mc = Minecraft.getMinecraft(); + if (mc == null || mc.currentScreen == null) return; + + GuiScreen screen = mc.currentScreen; + + // Check if we're in an auction listing GUI + if (screen instanceof GuiAuctionListing) { + addListingTooltip((GuiAuctionListing) screen, stack, tooltip); + } + // Check if we're in the trades GUI + else if (screen instanceof GuiAuctionTrades) { + addTradesTooltip((GuiAuctionTrades) screen, stack, tooltip); + } + } + + /** + * Add auction info when viewing the main listings page. + */ + private static void addListingTooltip(GuiAuctionListing gui, ItemStack stack, List tooltip) { + ContainerAuctionListing container = (ContainerAuctionListing) gui.inventorySlots; + if (container == null) return; + + // Find the listing that matches this ItemStack + AuctionListing listing = container.getListingForItem(stack); + if (listing == null) return; + + // Add separator + tooltip.add(""); + tooltip.add(EnumChatFormatting.DARK_GRAY + "--- " + EnumChatFormatting.GOLD + + StatCollector.translateToLocal("auction.title") + EnumChatFormatting.DARK_GRAY + " ---"); + + // Seller + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.seller") + .replace("%s", EnumChatFormatting.WHITE + listing.sellerName)); + + // Current bid or starting price + if (listing.hasBids()) { + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid") + .replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.currentBid))); + tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), listing.bidCount)); + } else { + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.startingPrice") + .replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.startingPrice))); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.noBids")); + } + + // Buyout price + if (listing.hasBuyout()) { + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.buyout") + .replace("%s", EnumChatFormatting.GREEN + formatCurrency(listing.buyoutPrice))); + } + + // Time remaining + long timeRemaining = listing.getTimeRemaining(); + String timeText = StatCollector.translateToLocal("auction.timeLeft") + .replace("%s", formatTimeRemaining(timeRemaining)); + tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); + } + + /** + * Add auction info when viewing the trades page. + */ + private static void addTradesTooltip(GuiAuctionTrades gui, ItemStack stack, List tooltip) { + ContainerAuctionTrades container = (ContainerAuctionTrades) gui.inventorySlots; + if (container == null) return; + + // Check if it's an active listing + AuctionListing listing = container.getListingForItem(stack); + if (listing != null) { + tooltip.add(""); + tooltip.add(EnumChatFormatting.DARK_GRAY + "--- " + EnumChatFormatting.GOLD + + StatCollector.translateToLocal("auction.trades.activeListing") + EnumChatFormatting.DARK_GRAY + " ---"); + + if (listing.hasBids()) { + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid") + .replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.currentBid))); + tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), listing.bidCount)); + } else { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.noBids")); + } + + long timeRemaining = listing.getTimeRemaining(); + String timeText = StatCollector.translateToLocal("auction.timeLeft") + .replace("%s", formatTimeRemaining(timeRemaining)); + tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); + + tooltip.add(""); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickToCancel")); + return; + } + + // Check if it's a claim + AuctionClaim claim = container.getClaimForItem(stack); + if (claim != null) { + tooltip.add(""); + + String claimTitle; + switch (claim.type) { + case ITEM: + claimTitle = StatCollector.translateToLocal("auction.claim.item"); + break; + case CURRENCY: + claimTitle = StatCollector.translateToLocal("auction.claim.currency"); + break; + case REFUND: + claimTitle = StatCollector.translateToLocal("auction.claim.refund"); + break; + default: + claimTitle = "Claim"; + } + + tooltip.add(EnumChatFormatting.DARK_GRAY + "--- " + EnumChatFormatting.GOLD + + claimTitle + EnumChatFormatting.DARK_GRAY + " ---"); + + if (claim.type.isCurrency()) { + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.claim.amount") + .replace("%s", EnumChatFormatting.GOLD + formatCurrency(claim.currency))); + } + + // Expiration + int expirationDays = AuctionClientConfig.getClaimExpirationDays(); + long daysLeft = claim.getDaysUntilExpiration(expirationDays); + if (daysLeft <= 1) { + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.claim.expiresSoon")); + } else { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.expires") + .replace("%s", String.valueOf(daysLeft))); + } + + tooltip.add(""); + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.claim.clickToClaim")); + } + } + + /** + * Format currency with commas. + */ + private static String formatCurrency(long amount) { + if (amount < 1000) { + return amount + " " + AuctionClientConfig.getCurrencyName(); + } + StringBuilder sb = new StringBuilder(); + String str = String.valueOf(amount); + int count = 0; + for (int i = str.length() - 1; i >= 0; i--) { + if (count > 0 && count % 3 == 0) { + sb.insert(0, ','); + } + sb.insert(0, str.charAt(i)); + count++; + } + return sb.toString() + " " + AuctionClientConfig.getCurrencyName(); + } + + /** + * Format time remaining. + */ + private static String formatTimeRemaining(long milliseconds) { + if (milliseconds <= 0) { + return StatCollector.translateToLocal("auction.ended"); + } + + long seconds = (milliseconds / 1000) % 60; + long minutes = (milliseconds / 60000) % 60; + long hours = (milliseconds / 3600000) % 24; + long days = milliseconds / 86400000; + + StringBuilder sb = new StringBuilder(); + if (days > 0) { + sb.append(days).append("d "); + } + if (hours > 0 || days > 0) { + sb.append(hours).append("h "); + } + if (minutes > 0 || hours > 0 || days > 0) { + sb.append(minutes).append("m"); + } else { + sb.append(seconds).append("s"); + } + + return sb.toString(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java new file mode 100644 index 000000000..5159b09de --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java @@ -0,0 +1,299 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.inventory.Slot; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.AuctionClientConfig; +import noppes.npcs.client.gui.util.GuiAuctionNavButton; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.containers.ContainerAuctionBidding; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.entity.EntityNPCInterface; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +/** + * GUI for viewing and bidding on a specific auction listing. + * Shows item, auction info, and bid/buyout buttons. + */ +public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiListener, IGuiData { + // Icons + private static final ResourceLocation ICON_BID = new ResourceLocation("customnpcs", "textures/items/npcCoinDiamond.png"); + private static final ResourceLocation ICON_BUYOUT = new ResourceLocation("customnpcs", "textures/items/npcCoinEmerald.png"); + + // Layout - non-final for testing/positioning + protected int itemSlotX = 56; + protected int itemSlotY = 52; + protected int btnBidX = 56; + protected int btnBidY = 80; + protected int btnBuyoutX = 80; + protected int btnBuyoutY = 80; + protected int infoX = 110; + protected int infoY = 50; + protected int infoWidth = 130; + + // Button IDs + private static final int BTN_BID = 200; + private static final int BTN_BUYOUT = 201; + + private final ContainerAuctionBidding biddingContainer; + private AuctionListing listing; + private long playerBalance = 0; + private boolean dataLoaded = false; + private boolean isOwnListing = false; + + // Buttons + private GuiAuctionNavButton btnBid; + private GuiAuctionNavButton btnBuyout; + + public GuiAuctionBidding(EntityNPCInterface npc, ContainerAuctionBidding container) { + super(npc, container); + this.biddingContainer = container; + } + + @Override + public void initGui() { + super.initGui(); + + // Bid button (diamond coin) + btnBid = new GuiAuctionNavButton(BTN_BID, guiLeft + btnBidX, guiTop + btnBidY, + "auction.bid.place", ICON_BID); + addButton(btnBid); + + // Buy Now button (emerald coin) - only visible if listing has buyout + btnBuyout = new GuiAuctionNavButton(BTN_BUYOUT, guiLeft + btnBuyoutX, guiTop + btnBuyoutY, + "auction.bid.buyout", ICON_BUYOUT); + addButton(btnBuyout); + + updateButtonState(); + } + + @Override + protected int getCurrentPage() { + // Not a standard page - return -1 so nav buttons aren't selected + return -1; + } + + /** Update button visibility based on listing state */ + private void updateButtonState() { + if (listing == null) { + if (btnBid != null) btnBid.setVisible(false); + if (btnBuyout != null) btnBuyout.setVisible(false); + return; + } + + // Hide buttons for own listings - can view but not bid/buy + if (isOwnListing) { + if (btnBid != null) btnBid.setVisible(false); + if (btnBuyout != null) btnBuyout.setVisible(false); + return; + } + + // Bid button visible for other players' listings + if (btnBid != null) btnBid.setVisible(true); + + // Buyout button only if listing has buyout + if (btnBuyout != null) { + btnBuyout.setVisible(listing.hasBuyout()); + } + } + + @Override + public void actionPerformed(GuiButton button) { + super.actionPerformed(button); + + if (listing == null) return; + + switch (button.id) { + case BTN_BID: + openBidSubGui(); + break; + case BTN_BUYOUT: + if (listing.hasBuyout()) { + openBuyNowSubGui(); + } + break; + } + } + + private void openBidSubGui() { + if (listing == null) return; + long minBid = listing.getMinimumBid(AuctionClientConfig.getMinBidIncrement()); + setSubGui(new SubGuiAuctionBid(listing.id, minBid, playerBalance)); + } + + private void openBuyNowSubGui() { + if (listing == null || !listing.hasBuyout()) return; + setSubGui(new SubGuiAuctionBuyNow(listing.id, listing.buyoutPrice, playerBalance)); + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiAuctionBid) { + SubGuiAuctionBid bidGui = (SubGuiAuctionBid) subgui; + if (bidGui.wasSuccessful()) { + // Bid placed - go to listings + AuctionActionPacket.openPage(PAGE_LISTINGS); + } + } else if (subgui instanceof SubGuiAuctionBuyNow) { + SubGuiAuctionBuyNow buyGui = (SubGuiAuctionBuyNow) subgui; + if (buyGui.wasSuccessful()) { + // Bought out - go to my trades + AuctionActionPacket.openPage(PAGE_CLAIMS); + } + } + } + + // ========== Data Handling ========== + + @Override + public void setGuiData(NBTTagCompound compound) { + if (compound.hasKey("BiddingData")) { + playerBalance = compound.getLong("Balance"); + isOwnListing = compound.getBoolean("IsOwnListing"); + + if (compound.hasKey("Listing")) { + listing = AuctionListing.fromNBT(compound.getCompoundTag("Listing")); + biddingContainer.setListing(listing); + } + + dataLoaded = true; + updateButtonState(); + } + } + + // ========== Drawing ========== + + @Override + protected void drawAuctionContent(float partialTicks, int mouseX, int mouseY) { + // Draw item slot background + drawAuctionSlot(guiLeft + itemSlotX, guiTop + itemSlotY); + + if (!dataLoaded || listing == null) { + // Loading text + String loading = StatCollector.translateToLocal("gui.loading"); + fontRendererObj.drawString(loading, guiLeft + infoX, guiTop + infoY, 0xFFFFFF); + return; + } + + // Draw auction info on the right side + drawAuctionInfo(); + } + + /** Draw auction listing information on the right side */ + private void drawAuctionInfo() { + int x = guiLeft + infoX; + int y = guiTop + infoY; + int lineHeight = 12; + String currencyName = AuctionClientConfig.getCurrencyName(); + + // Seller + String sellerLabel = StatCollector.translateToLocal("auction.seller").replace("%s", ""); + fontRendererObj.drawString(EnumChatFormatting.GRAY + sellerLabel, x, y, 0xFFFFFF); + y += lineHeight; + fontRendererObj.drawString(EnumChatFormatting.WHITE + listing.sellerName, x + 10, y, 0xFFFFFF); + y += lineHeight + 4; + + // Current Bid or Starting Price + if (listing.hasBids()) { + String bidLabel = StatCollector.translateToLocal("auction.info.currentBid"); + fontRendererObj.drawString(EnumChatFormatting.YELLOW + bidLabel, x, y, 0xFFFFFF); + y += lineHeight; + fontRendererObj.drawString(EnumChatFormatting.GOLD + formatCurrency(listing.currentBid) + " " + currencyName, x + 10, y, 0xFFFFFF); + y += lineHeight + 4; + + // Current Bidder + String bidderLabel = StatCollector.translateToLocal("auction.info.highBidder"); + fontRendererObj.drawString(EnumChatFormatting.GRAY + bidderLabel, x, y, 0xFFFFFF); + y += lineHeight; + String bidderName = listing.highBidderName != null ? listing.highBidderName : "???"; + fontRendererObj.drawString(EnumChatFormatting.WHITE + bidderName, x + 10, y, 0xFFFFFF); + y += lineHeight + 4; + } else { + String startLabel = StatCollector.translateToLocal("auction.info.startingPrice"); + fontRendererObj.drawString(EnumChatFormatting.YELLOW + startLabel, x, y, 0xFFFFFF); + y += lineHeight; + fontRendererObj.drawString(EnumChatFormatting.GOLD + formatCurrency(listing.startingPrice) + " " + currencyName, x + 10, y, 0xFFFFFF); + y += lineHeight + 4; + } + + // Buyout Price (if available) + if (listing.hasBuyout()) { + String buyoutLabel = StatCollector.translateToLocal("auction.info.buyoutPrice"); + fontRendererObj.drawString(EnumChatFormatting.GREEN + buyoutLabel, x, y, 0xFFFFFF); + y += lineHeight; + fontRendererObj.drawString(EnumChatFormatting.GREEN + formatCurrency(listing.buyoutPrice) + " " + currencyName, x + 10, y, 0xFFFFFF); + y += lineHeight + 4; + } + + // Time Remaining + String timeLabel = StatCollector.translateToLocal("auction.info.timeLeft"); + fontRendererObj.drawString(EnumChatFormatting.GRAY + timeLabel, x, y, 0xFFFFFF); + y += lineHeight; + long timeRemaining = listing.getTimeRemaining(); + EnumChatFormatting timeColor = timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE; + fontRendererObj.drawString(timeColor + formatTimeRemaining(timeRemaining), x + 10, y, 0xFFFFFF); + y += lineHeight + 4; + + // Bid Count + String bidCountLabel = StatCollector.translateToLocal("auction.info.bidCount"); + fontRendererObj.drawString(EnumChatFormatting.GRAY + bidCountLabel, x, y, 0xFFFFFF); + y += lineHeight; + fontRendererObj.drawString(EnumChatFormatting.WHITE + String.valueOf(listing.bidCount), x + 10, y, 0xFFFFFF); + } + + @Override + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + super.drawGuiContainerForegroundLayer(mouseX, mouseY); + if (hasSubGui()) return; + + // Draw button tooltips + drawBiddingPageTooltips(mouseX, mouseY); + } + + private void drawBiddingPageTooltips(int mouseX, int mouseY) { + List tooltip = null; + + if (btnBid != null && btnBid.visible && btnBid.isHovered()) { + tooltip = new ArrayList<>(); + tooltip.add(EnumChatFormatting.GOLD + StatCollector.translateToLocal("auction.bid.place")); + if (listing != null) { + long minBid = listing.getMinimumBid(AuctionClientConfig.getMinBidIncrement()); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.bid.minimum") + .replace("%s", EnumChatFormatting.WHITE + formatCurrency(minBid) + " " + AuctionClientConfig.getCurrencyName())); + } + } else if (btnBuyout != null && btnBuyout.visible && btnBuyout.isHovered()) { + tooltip = new ArrayList<>(); + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.bid.buyout")); + if (listing != null && listing.hasBuyout()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.info.buyoutPrice") + ":"); + tooltip.add(EnumChatFormatting.GREEN + formatCurrency(listing.buyoutPrice) + " " + AuctionClientConfig.getCurrencyName()); + } + } + + if (tooltip != null && !tooltip.isEmpty()) { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + drawHoveringText(tooltip, mouseX - guiLeft, mouseY - guiTop, fontRendererObj); + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } + } + + // ========== Slot Handling ========== + + @Override + protected void handleMouseClick(Slot slot, int slotIndex, int mouseButton, int clickType) { + // Block all slot interactions - view only + } + + public AuctionListing getListing() { return listing; } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java new file mode 100644 index 000000000..851c92263 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java @@ -0,0 +1,232 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.util.ResourceLocation; +import noppes.npcs.client.gui.util.GuiAuctionNavButton; +import noppes.npcs.client.gui.util.GuiContainerNPCInterface; +import noppes.npcs.containers.ContainerAuction; +import noppes.npcs.entity.EntityNPCInterface; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.util.List; + +/** + * Base GUI for all auction interfaces. + * 256x256 texture with player inventory at bottom. + */ +public abstract class GuiAuctionInterface extends GuiContainerNPCInterface { + // Textures + protected static final ResourceLocation AUCTION_BACKGROUND = new ResourceLocation("customnpcs", "textures/gui/auction/auction.png"); + protected static final ResourceLocation AUCTION_SLOT = new ResourceLocation("customnpcs", "textures/gui/auction/auction_slot.png"); + + // Navigation icons + protected static final ResourceLocation ICON_LISTINGS = new ResourceLocation("customnpcs", "textures/items/npcAncientScroll.png"); + protected static final ResourceLocation ICON_SELL = new ResourceLocation("customnpcs", "textures/gui/auction/sell.png"); + protected static final ResourceLocation ICON_CLAIMS = new ResourceLocation("customnpcs", "textures/gui/auction/my_auctions.png"); + protected static final ResourceLocation ICON_FILTER = new ResourceLocation("customnpcs", "textures/gui/auction/search.png"); + + // Page constants + public static final int PAGE_LISTINGS = 0; + public static final int PAGE_SELL = 1; + public static final int PAGE_CLAIMS = 2; + + // Navigation button IDs + protected static final int BTN_NAV_LISTINGS = 100; + protected static final int BTN_NAV_SELL = 101; + protected static final int BTN_NAV_CLAIMS = 102; + protected static final int BTN_NAV_FILTER = 103; + + // Navigation positions - non-final for testing/positioning + protected int navX = 35; + protected int navYListings = 46; + protected int navYSell = 67; + protected int navYClaims = 88; + + protected final ContainerAuction auctionContainer; + protected GuiAuctionNavButton btnListings; + protected GuiAuctionNavButton btnSell; + protected GuiAuctionNavButton btnClaims; + + public GuiAuctionInterface(EntityNPCInterface npc, ContainerAuction container) { + super(npc, container); + this.auctionContainer = container; + this.xSize = 256; + this.ySize = 256; + this.closeOnEsc = true; + this.title = ""; + } + + @Override + public void initGui() { + super.initGui(); + initNavigationButtons(); + } + + /** Initialize navigation buttons */ + protected void initNavigationButtons() { + int page = getCurrentPage(); + + btnListings = new GuiAuctionNavButton(BTN_NAV_LISTINGS, guiLeft + navX, guiTop + navYListings, + "auction.nav.listings", ICON_LISTINGS); + btnListings.setSelected(page == PAGE_LISTINGS); + addButton(btnListings); + + btnSell = new GuiAuctionNavButton(BTN_NAV_SELL, guiLeft + navX, guiTop + navYSell, + "auction.nav.sell", ICON_SELL); + btnSell.setSelected(page == PAGE_SELL); + addButton(btnSell); + + btnClaims = new GuiAuctionNavButton(BTN_NAV_CLAIMS, guiLeft + navX, guiTop + navYClaims, + "auction.nav.trades", ICON_CLAIMS); + btnClaims.setSelected(page == PAGE_CLAIMS); + addButton(btnClaims); + } + + /** Returns current page constant - implement in subclass */ + protected abstract int getCurrentPage(); + + @Override + public void actionPerformed(GuiButton button) { + super.actionPerformed(button); + int page = getCurrentPage(); + if (button.id == BTN_NAV_LISTINGS && page != PAGE_LISTINGS) { + AuctionActionPacket.openPage(PAGE_LISTINGS); + } else if (button.id == BTN_NAV_SELL && page != PAGE_SELL) { + AuctionActionPacket.openPage(PAGE_SELL); + } else if (button.id == BTN_NAV_CLAIMS && page != PAGE_CLAIMS) { + AuctionActionPacket.openPage(PAGE_CLAIMS); + } + } + + // ========== Background Drawing ========== + + @Override + protected void drawGuiContainerBackgroundLayer(float partialTicks, int mouseX, int mouseY) { + drawWorldBackground(0); + + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + mc.renderEngine.bindTexture(AUCTION_BACKGROUND); + drawTexturedModalRect(guiLeft, guiTop, 0, 0, xSize, ySize); + + RenderHelper.enableGUIStandardItemLighting(); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + GL11.glEnable(GL11.GL_COLOR_MATERIAL); + GL11.glEnable(GL11.GL_LIGHTING); + + drawAuctionContent(partialTicks, mouseX, mouseY); + + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_DEPTH_TEST); + RenderHelper.enableStandardItemLighting(); + + super.drawGuiContainerBackgroundLayer(partialTicks, mouseX, mouseY); + } + + @Override + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + if (!hasSubGui()) { + drawNavButtonTooltips(mouseX, mouseY); + } + } + + /** Draw navigation button tooltips */ + protected void drawNavButtonTooltips(int mouseX, int mouseY) { + List tooltip = null; + if (btnListings != null && btnListings.isHovered()) tooltip = btnListings.getTooltipLines(); + else if (btnSell != null && btnSell.isHovered()) tooltip = btnSell.getTooltipLines(); + else if (btnClaims != null && btnClaims.isHovered()) tooltip = btnClaims.getTooltipLines(); + + if (tooltip != null) { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + drawHoveringText(tooltip, mouseX - guiLeft, mouseY - guiTop, fontRendererObj); + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } + } + + // ========== Slot Drawing Utilities ========== + + /** Draw auction slot background (18x18) */ + protected void drawAuctionSlot(int x, int y) { + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + mc.renderEngine.bindTexture(AUCTION_SLOT); + Gui.func_146110_a(x, y, 0, 0, 18, 18, 18, 18); + } + + /** Draw colored overlay on slot (ARGB format) */ + protected void drawColoredOverlay(int x, int y, int color) { + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_DEPTH_TEST); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + Gui.drawRect(x + 1, y + 1, x + 17, y + 17, color); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_DEPTH_TEST); + GL11.glEnable(GL11.GL_LIGHTING); + } + + /** Draw dark overlay for unavailable slots */ + protected void drawDarkenedOverlay(int x, int y) { + drawColoredOverlay(x, y, 0x80000000); + } + + /** Draw icon texture on slot (16x16) */ + protected void drawIconOverlay(int x, int y, ResourceLocation icon) { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + mc.renderEngine.bindTexture(icon); + Gui.func_146110_a(x + 1, y + 1, 0, 0, 16, 16, 16, 16); + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } + + /** Subclasses implement this to draw specific content */ + protected abstract void drawAuctionContent(float partialTicks, int mouseX, int mouseY); + + // ========== Formatting Utilities ========== + + /** Format currency with commas (e.g., 1000 -> "1,000") */ + protected String formatCurrency(long amount) { + if (amount < 1000) return String.valueOf(amount); + StringBuilder sb = new StringBuilder(); + String str = String.valueOf(amount); + int count = 0; + for (int i = str.length() - 1; i >= 0; i--) { + if (count > 0 && count % 3 == 0) sb.insert(0, ','); + sb.insert(0, str.charAt(i)); + count++; + } + return sb.toString(); + } + + /** Format time remaining (e.g., "2d 5h 30m") */ + protected String formatTimeRemaining(long ms) { + if (ms <= 0) return "Ended"; + + long seconds = (ms / 1000) % 60; + long minutes = (ms / 60000) % 60; + long hours = (ms / 3600000) % 24; + long days = ms / 86400000; + + StringBuilder sb = new StringBuilder(); + if (days > 0) sb.append(days).append("d "); + if (hours > 0 || days > 0) sb.append(hours).append("h "); + if (minutes > 0 || hours > 0 || days > 0) sb.append(minutes).append("m"); + else sb.append(seconds).append("s"); + return sb.toString(); + } + + @Override + public void save() { + // Override in subclasses if needed + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionListing.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionListing.java new file mode 100644 index 000000000..381747c6a --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionListing.java @@ -0,0 +1,302 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.inventory.Slot; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.GuiAuctionNavButton; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.constants.EnumAuctionSort; +import noppes.npcs.containers.ContainerAuctionListing; +import noppes.npcs.controllers.data.AuctionFilter; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.entity.EntityNPCInterface; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +/** + * GUI for browsing auction listings. + * Displays 9x5 grid with pagination and filter controls. + */ +public class GuiAuctionListing extends GuiAuctionInterface implements ISubGuiListener, IGuiData { + // Small button textures (10x10) + private static final ResourceLocation SMALL_BUTTON = new ResourceLocation("customnpcs", "textures/gui/auction/small_button.png"); + private static final ResourceLocation SMALL_BUTTON_PRESS = new ResourceLocation("customnpcs", "textures/gui/auction/small_button_press.png"); + + // Arrow icons (8x8) + private static final ResourceLocation ICON_UP = new ResourceLocation("customnpcs", "textures/gui/auction/up.png"); + private static final ResourceLocation ICON_DOWN = new ResourceLocation("customnpcs", "textures/gui/auction/down.png"); + + // Grid layout - non-final for testing/positioning + protected int gridX = 56; + protected int gridY = 46; + protected int cols = 9; + protected int rows = 5; + + // Pagination positions (side by side horizontally) + protected int pageUpX = 33; + protected int pageDownX = 45; + protected int pageBtnY = 128; + + // Filter button position + protected int navYFilter = 109; + + // Button IDs + private static final int BTN_PAGE_UP = 200; + private static final int BTN_PAGE_DOWN = 201; + + private final ContainerAuctionListing listingContainer; + private AuctionFilter filter; + private int currentPage = 0; + private int totalPages = 1; + private int totalListings = 0; + private boolean dataLoaded = false; + + // Buttons defined on this page + private GuiAuctionNavButton btnFilter; + private GuiAuctionNavButton btnPageUp; + private GuiAuctionNavButton btnPageDown; + + public GuiAuctionListing(EntityNPCInterface npc, ContainerAuctionListing container) { + super(npc, container); + this.listingContainer = container; + this.filter = new AuctionFilter(); + } + + @Override + public void initGui() { + super.initGui(); + + // Filter button (18x18 with 16x16 icon) + btnFilter = new GuiAuctionNavButton(BTN_NAV_FILTER, guiLeft + navX, guiTop + navYFilter, + "auction.nav.filter", ICON_FILTER); + addButton(btnFilter); + + // Page Up button (10x10 with 8x8 icon) + btnPageUp = new GuiAuctionNavButton(BTN_PAGE_UP, guiLeft + pageUpX, guiTop + pageBtnY, + 10, 8, "auction.page.prev", ICON_UP, SMALL_BUTTON, SMALL_BUTTON_PRESS); + addButton(btnPageUp); + + // Page Down button (10x10 with 8x8 icon) + btnPageDown = new GuiAuctionNavButton(BTN_PAGE_DOWN, guiLeft + pageDownX, guiTop + pageBtnY, + 10, 8, "auction.page.next", ICON_DOWN, SMALL_BUTTON, SMALL_BUTTON_PRESS); + addButton(btnPageDown); + + updateFilterTooltip(); + requestListings(); + } + + @Override + protected int getCurrentPage() { + return PAGE_LISTINGS; + } + + /** Request listings from server */ + private void requestListings() { + AuctionActionPacket.requestListings(filter, currentPage); + } + + /** Update pagination button visibility based on current page state */ + private void updatePaginationVisibility() { + if (btnPageUp != null) { + btnPageUp.setVisible(currentPage > 0); + } + if (btnPageDown != null) { + btnPageDown.setVisible(currentPage < totalPages - 1); + } + } + + // ========== Filter Handling ========== + + private void updateFilterTooltip() { + if (btnFilter == null) return; + + List tooltip = new ArrayList<>(); + tooltip.add(EnumChatFormatting.GOLD + StatCollector.translateToLocal("auction.nav.filter")); + tooltip.add(""); + + // Sort mode + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.filter.sort") + ":"); + tooltip.add(EnumChatFormatting.WHITE + " " + filter.getSortBy().getDisplayName()); + tooltip.add(""); + + // Search term + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.filter.search") + ":"); + if (filter.hasSearchText()) { + tooltip.add(EnumChatFormatting.GREEN + " \"" + filter.getSearchText() + "\""); + } else { + tooltip.add(EnumChatFormatting.DARK_GRAY + " " + StatCollector.translateToLocal("auction.filter.none")); + } + tooltip.add(""); + + // Instructions + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.filter.leftClick")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.filter.rightClick")); + + btnFilter.setCustomTooltip(tooltip); + } + + @Override + public void mouseEvent(int mouseX, int mouseY, int mouseButton) { + if (btnFilter != null && btnFilter.isMouseOver(mouseX, mouseY) && mouseButton == 1) { + cycleSortMode(); + } + } + + @Override + public void actionPerformed(GuiButton button) { + super.actionPerformed(button); + + switch (button.id) { + case BTN_NAV_FILTER: + setSubGui(new SubGuiAuctionSearch(filter.getSearchText())); + break; + case BTN_PAGE_UP: + if (currentPage > 0) { + currentPage--; + requestListings(); + NoppesUtil.clickSound(); + } + break; + case BTN_PAGE_DOWN: + if (currentPage < totalPages - 1) { + currentPage++; + requestListings(); + NoppesUtil.clickSound(); + } + break; + } + } + + private void cycleSortMode() { + EnumAuctionSort[] values = EnumAuctionSort.values(); + int next = (filter.getSortBy().ordinal() + 1) % values.length; + filter.setSortBy(values[next]); + updateFilterTooltip(); + currentPage = 0; + requestListings(); + NoppesUtil.clickSound(); + } + + public void onSearchSubmit(String searchText) { + filter.setSearchText(searchText); + updateFilterTooltip(); + currentPage = 0; + requestListings(); + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) {} + + // ========== Data Handling ========== + + @Override + public void setGuiData(NBTTagCompound compound) { + if (compound.hasKey("ListingsData")) { + currentPage = compound.getInteger("Page"); + totalPages = compound.getInteger("TotalPages"); + totalListings = compound.getInteger("TotalListings"); + + // Load listings + listingContainer.displayInventory.clear(); + NBTTagList list = compound.getTagList("Listings", 10); + for (int i = 0; i < list.tagCount() && i < 45; i++) { + AuctionListing listing = AuctionListing.fromNBT(list.getCompoundTagAt(i)); + listingContainer.displayInventory.setListing(i, listing); + } + dataLoaded = true; + } else if (compound.hasKey("ListingsRefresh")) { + requestListings(); + } + } + + // ========== Drawing ========== + + @Override + protected void drawAuctionContent(float partialTicks, int mouseX, int mouseY) { + // Draw slot backgrounds + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + int x = guiLeft + gridX + col * 18; + int y = guiTop + gridY + row * 18; + drawAuctionSlot(x, y); + } + } + + // Draw page indicator below pagination buttons + drawPageIndicator(); + } + + private void drawPageIndicator() { + if (totalPages > 1) { + String text = (currentPage + 1) + "/" + totalPages; + // Center between the two buttons (horizontally centered), below buttons + int centerX = guiLeft + pageUpX + 11; + int x = centerX - fontRendererObj.getStringWidth(text) / 2; + fontRendererObj.drawString(text, x, guiTop + pageBtnY + 12, 0xFFFFFF); + } + } + + @Override + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + super.drawGuiContainerForegroundLayer(mouseX, mouseY); + if (hasSubGui()) return; + + // Draw tooltips for buttons on this page + drawListingPageTooltips(mouseX, mouseY); + } + + private void drawListingPageTooltips(int mouseX, int mouseY) { + List tooltip = null; + + // Filter button tooltip + if (btnFilter != null && btnFilter.isHovered()) { + tooltip = btnFilter.getTooltipLines(); + } + // Page up tooltip + else if (btnPageUp != null && btnPageUp.visible && btnPageUp.isHovered()) { + tooltip = btnPageUp.getTooltipLines(); + } + // Page down tooltip + else if (btnPageDown != null && btnPageDown.visible && btnPageDown.isHovered()) { + tooltip = btnPageDown.getTooltipLines(); + } + + if (tooltip != null && !tooltip.isEmpty()) { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + drawHoveringText(tooltip, mouseX - guiLeft, mouseY - guiTop, fontRendererObj); + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } + } + + // ========== Slot Handling ========== + + @Override + protected void handleMouseClick(Slot slot, int slotIndex, int mouseButton, int clickType) { + // Check if clicking on a display slot with a listing + if (slot != null && listingContainer.isDisplaySlot(slot.slotNumber)) { + int listingIndex = slot.slotNumber - listingContainer.getDisplaySlotStart(); + AuctionListing listing = listingContainer.displayInventory.getListing(listingIndex); + if (listing != null) { + // Open bidding page for this listing + AuctionActionPacket.openBidding(listing.id); + NoppesUtil.clickSound(); + } + } + // Block other slot interactions + } + + public AuctionFilter getFilter() { return filter; } + public ContainerAuctionListing getListingContainer() { return listingContainer; } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java new file mode 100644 index 000000000..268ddc701 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java @@ -0,0 +1,400 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.GuiAuctionNavButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.AuctionClientConfig; +import noppes.npcs.containers.ContainerAuctionSell; +import noppes.npcs.entity.EntityNPCInterface; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +/** + * GUI for creating auction listings. + * Sell slot interactions: left-click=clear, right-click=remove 1. + * Inventory interactions: left-click=add stack, right-click=add 1. + */ +public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldListener { + private static final ResourceLocation ICON_COIN = new ResourceLocation("customnpcs", "textures/items/npcCoinBronze.png"); + private static final ResourceLocation ICON_BAG = new ResourceLocation("customnpcs", "textures/items/npcBag.png"); + + // Layout - non-final for testing/positioning + protected int contentX = 50; + protected int contentY = 50; + protected int confirmBtnX = 180; + protected int confirmBtnY = 120; + protected int navYBuyout = 109; + + // Component IDs + private static final int TXT_STARTING_PRICE = 1; + private static final int TXT_BUYOUT_PRICE = 2; + private static final int BTN_CONFIRM = 10; + private static final int BTN_ALLOW_BUYOUT = 11; + + private final ContainerAuctionSell sellContainer; + private long startingPrice = 0; + private long buyoutPrice = 0; + private boolean allowBuyout = false; + private String errorMessage = null; + + // Buttons + private GuiAuctionNavButton btnAllowBuyout; + private GuiNpcLabel lblBuyout; + private GuiNpcTextField txtBuyout; + + public GuiAuctionSell(EntityNPCInterface npc, ContainerAuctionSell container) { + super(npc, container); + this.sellContainer = container; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + contentY; + + // Starting Price label and field + addLabel(new GuiNpcLabel(1, "auction.sell.startingPrice", guiLeft + contentX, y + 5, 0xFFFFFF)); + GuiNpcTextField startingField = new GuiNpcTextField(TXT_STARTING_PRICE, this, fontRendererObj, + guiLeft + contentX + 80, y, 80, 18, startingPrice > 0 ? String.valueOf(startingPrice) : ""); + startingField.setIntegersOnly(); + addTextField(startingField); + + y += 25; + + // Buyout Price label and field (only visible when allowBuyout is true) + lblBuyout = new GuiNpcLabel(2, "auction.sell.buyoutPrice", guiLeft + contentX, y + 5, 0xFFFFFF); + addLabel(lblBuyout); + + txtBuyout = new GuiNpcTextField(TXT_BUYOUT_PRICE, this, fontRendererObj, + guiLeft + contentX + 80, y, 80, 18, buyoutPrice > 0 ? String.valueOf(buyoutPrice) : ""); + txtBuyout.setIntegersOnly(); + addTextField(txtBuyout); + + // Allow Buyout toggle button (18x18 with bag icon) + btnAllowBuyout = new GuiAuctionNavButton(BTN_ALLOW_BUYOUT, guiLeft + navX, guiTop + navYBuyout, + "auction.sell.allowBuyout", ICON_BAG); + btnAllowBuyout.setToggle(true); // This is a toggle, not a nav button + btnAllowBuyout.setSelected(allowBuyout); + addButton(btnAllowBuyout); + + // Confirm button (custom textured button - using GuiAuctionNavButton) + GuiAuctionNavButton btnConfirm = new GuiAuctionNavButton(BTN_CONFIRM, guiLeft + confirmBtnX, guiTop + confirmBtnY, + "auction.sell.confirm", ICON_COIN); + addButton(btnConfirm); + + updateBuyoutVisibility(); + updateBuyoutTooltip(); + } + + @Override + protected int getCurrentPage() { + return PAGE_SELL; + } + + /** Update visibility of buyout price field based on toggle state */ + private void updateBuyoutVisibility() { + if (lblBuyout != null) { + lblBuyout.enabled = allowBuyout; + } + if (txtBuyout != null) { + txtBuyout.setVisible(allowBuyout); + if (!allowBuyout) { + txtBuyout.setText(""); + buyoutPrice = 0; + } + } + } + + /** Update the buyout toggle button tooltip */ + private void updateBuyoutTooltip() { + if (btnAllowBuyout == null) return; + + List tooltip = new ArrayList<>(); + tooltip.add(EnumChatFormatting.GOLD + StatCollector.translateToLocal("auction.sell.allowBuyout")); + tooltip.add(""); + if (allowBuyout) { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.sell.buyoutEnabled")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.buyoutEnabledDesc")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.buyoutEnabledDesc2")); + } else { + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.sell.buyoutDisabled")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.buyoutDisabledDesc")); + } + tooltip.add(""); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.clickToToggle")); + + btnAllowBuyout.setCustomTooltip(tooltip); + } + + @Override + public void unFocused(GuiNpcTextField textfield) { + if (textfield.id == TXT_STARTING_PRICE) { + try { + startingPrice = Long.parseLong(textfield.getText()); + } catch (NumberFormatException e) { + startingPrice = 0; + } + } else if (textfield.id == TXT_BUYOUT_PRICE) { + try { + String text = textfield.getText().trim(); + buyoutPrice = text.isEmpty() ? 0 : Long.parseLong(text); + } catch (NumberFormatException e) { + buyoutPrice = 0; + } + } + errorMessage = null; + } + + @Override + public void actionPerformed(GuiButton button) { + super.actionPerformed(button); + + if (button.id == BTN_CONFIRM) { + createListing(); + } else if (button.id == BTN_ALLOW_BUYOUT) { + allowBuyout = !allowBuyout; + btnAllowBuyout.setSelected(allowBuyout); + updateBuyoutVisibility(); + updateBuyoutTooltip(); + NoppesUtil.clickSound(); + } + } + + private void createListing() { + // Get latest values from text fields + GuiNpcTextField startingField = getTextField(TXT_STARTING_PRICE); + GuiNpcTextField buyoutField = getTextField(TXT_BUYOUT_PRICE); + + if (startingField != null) { + try { + startingPrice = Long.parseLong(startingField.getText()); + } catch (NumberFormatException e) { + startingPrice = 0; + } + } + + if (buyoutField != null && allowBuyout) { + try { + String text = buyoutField.getText().trim(); + buyoutPrice = text.isEmpty() ? 0 : Long.parseLong(text); + } catch (NumberFormatException e) { + buyoutPrice = 0; + } + } else { + buyoutPrice = 0; + } + + // Validate + ItemStack item = sellContainer.getItemToSell(); + if (item == null) { + errorMessage = StatCollector.translateToLocal("auction.sell.noItem"); + return; + } + + if (startingPrice <= 0) { + errorMessage = StatCollector.translateToLocal("auction.sell.invalidPrice"); + return; + } + + if (allowBuyout && buyoutPrice > 0 && buyoutPrice < startingPrice) { + errorMessage = StatCollector.translateToLocal("auction.sell.buyoutTooLow"); + return; + } + + // Validate player has enough items + int inventoryCount = sellContainer.countItemInInventory(item); + if (item.stackSize > inventoryCount) { + errorMessage = StatCollector.translateToLocal("auction.sell.notEnoughItems"); + return; + } + + // Send create listing packet + AuctionActionPacket.createListing(item, startingPrice, allowBuyout ? buyoutPrice : 0); + + // Clear the sell slot (server will also clear it) + sellContainer.clearSellSlot(); + + // Navigate back to listings page + AuctionActionPacket.openPage(PAGE_LISTINGS); + } + + @Override + protected void drawAuctionContent(float partialTicks, int mouseX, int mouseY) { + // Draw sell slot background (centered in content area) + int slotX = guiLeft + contentX + 30; + int slotY = guiTop + contentY + 55; + drawAuctionSlot(slotX, slotY); + + // Draw listing fee info (uses cached config from server) + String feeText = StatCollector.translateToLocal("auction.sell.fee") + .replace("%s", formatCurrency(AuctionClientConfig.getListingFee()) + " " + AuctionClientConfig.getCurrencyName()); + fontRendererObj.drawString(EnumChatFormatting.GRAY + feeText, guiLeft + contentX, guiTop + contentY + 80, 0xFFFFFF); + + // Draw error message if any + if (errorMessage != null) { + fontRendererObj.drawString(EnumChatFormatting.RED + errorMessage, guiLeft + contentX, guiTop + contentY + 95, 0xFFFFFF); + } + + // Draw item count indicator for sell slot + drawSellSlotIndicator(slotX, slotY); + } + + /** Draw indicator showing staged vs available items */ + private void drawSellSlotIndicator(int slotX, int slotY) { + ItemStack sellItem = sellContainer.getItemToSell(); + if (sellItem != null) { + int inventoryCount = sellContainer.countItemInInventory(sellItem); + String countText = sellItem.stackSize + "/" + inventoryCount; + + // Color: green if valid, red if over limit + int color = sellItem.stackSize <= inventoryCount ? 0x55FF55 : 0xFF5555; + + int textWidth = fontRendererObj.getStringWidth(countText); + fontRendererObj.drawStringWithShadow(countText, slotX + 18 + 4, slotY + 5, color); + } else { + // Draw "Place item here" hint + String hint = StatCollector.translateToLocal("auction.sell.placeItem"); + int hintWidth = fontRendererObj.getStringWidth(hint); + fontRendererObj.drawString(EnumChatFormatting.GRAY + hint, slotX + 9 - hintWidth / 2 + 20, slotY + 5, 0xFFFFFF); + } + } + + /** + * Handle sell page click behavior. + * Sell slot: left=clear, right=remove 1. + * Inventory: left=add stack, right=add 1. + */ + @Override + protected void handleMouseClick(Slot slot, int slotIndex, int mouseButton, int clickType) { + if (slot == null) return; + + // Sell slot clicks + if (sellContainer.isSellSlot(slotIndex)) { + ItemStack sellItem = sellContainer.getItemToSell(); + if (sellItem != null) { + boolean removeAll = (mouseButton == 0); // Left=clear, Right=remove 1 + sellContainer.removeFromSellSlot(removeAll); + NoppesUtil.clickSound(); + } + return; + } + + // Inventory clicks + if (sellContainer.isPlayerInventorySlot(slotIndex)) { + ItemStack sourceStack = slot.getStack(); + if (sourceStack != null) { + boolean fullStack = (mouseButton == 0); // Left=stack, Right=1 item + sellContainer.addToSellSlot(slotIndex, fullStack); + NoppesUtil.clickSound(); + } + return; + } + } + + @Override + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + super.drawGuiContainerForegroundLayer(mouseX, mouseY); + if (hasSubGui()) return; + + // Draw tooltips for buttons + drawSellPageTooltips(mouseX, mouseY); + + // Sell slot tooltip + drawSellSlotTooltip(mouseX, mouseY); + + // Inventory item tooltip (only add extra info, item tooltip is handled by AuctionTooltipHandler) + drawInventoryTooltip(mouseX, mouseY); + } + + /** Draw tooltips for sell page buttons */ + private void drawSellPageTooltips(int mouseX, int mouseY) { + List tooltip = null; + + // Check all buttons + GuiNpcButton confirmBtn = getButton(BTN_CONFIRM); + if (confirmBtn instanceof GuiAuctionNavButton) { + GuiAuctionNavButton btn = (GuiAuctionNavButton) confirmBtn; + if (btn.isHovered()) { + tooltip = btn.getTooltipLines(); + } + } + + if (btnAllowBuyout != null && btnAllowBuyout.isHovered()) { + tooltip = btnAllowBuyout.getTooltipLines(); + } + + if (tooltip != null && !tooltip.isEmpty()) { + drawTooltip(tooltip, mouseX - guiLeft, mouseY - guiTop); + } + } + + /** Draw tooltip for sell slot */ + private void drawSellSlotTooltip(int mouseX, int mouseY) { + int slotX = guiLeft + contentX + 30; + int slotY = guiTop + contentY + 55; + + if (mouseX >= slotX && mouseX < slotX + 18 && mouseY >= slotY && mouseY < slotY + 18) { + ItemStack sellItem = sellContainer.getItemToSell(); + if (sellItem != null) { + List tooltip = new ArrayList<>(); + tooltip.add(EnumChatFormatting.GOLD + StatCollector.translateToLocal("auction.sell.slotTitle")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.leftClear")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.rightRemove")); + drawTooltip(tooltip, mouseX - guiLeft, mouseY - guiTop); + } + } + } + + /** Draw tooltip for inventory items */ + private void drawInventoryTooltip(int mouseX, int mouseY) { + Slot hoveredSlot = getSlotUnderMouse(mouseX, mouseY); + if (hoveredSlot != null && sellContainer.isPlayerInventorySlot(hoveredSlot.slotNumber)) { + if (hoveredSlot.getStack() != null) { + List tooltip = new ArrayList<>(); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.leftAdd")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.rightAddOne")); + drawTooltip(tooltip, mouseX - guiLeft, mouseY - guiTop + 40); + } + } + } + + /** Get slot under mouse position */ + private Slot getSlotUnderMouse(int mouseX, int mouseY) { + for (int i = 0; i < inventorySlots.inventorySlots.size(); i++) { + Slot slot = (Slot) inventorySlots.inventorySlots.get(i); + if (isMouseOverSlot(slot, mouseX, mouseY)) { + return slot; + } + } + return null; + } + + /** Check if mouse is over a slot */ + private boolean isMouseOverSlot(Slot slot, int mouseX, int mouseY) { + int x = guiLeft + slot.xDisplayPosition; + int y = guiTop + slot.yDisplayPosition; + return mouseX >= x && mouseX < x + 16 && mouseY >= y && mouseY < y + 16; + } + + /** Draw tooltip with proper GL state */ + private void drawTooltip(List tooltip, int x, int y) { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + drawHoveringText(tooltip, x, y, fontRendererObj); + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java new file mode 100644 index 000000000..df9298426 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java @@ -0,0 +1,291 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.audio.PositionedSoundRecord; +import net.minecraft.inventory.Slot; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.AuctionClientConfig; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.constants.EnumClaimType; +import noppes.npcs.containers.ContainerAuctionTrades; +import noppes.npcs.controllers.data.AuctionClaim; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.entity.EntityNPCInterface; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +/** + * GUI for viewing player's auction trades. + * Shows active listings, bids, and claims with confirmation system. + */ +public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { + // Icon textures + private static final ResourceLocation ICON_X = new ResourceLocation("customnpcs", "textures/gui/auction/x_icon.png"); + private static final ResourceLocation ICON_CHECK = new ResourceLocation("customnpcs", "textures/gui/auction/check_icon.png"); + + // Grid layout + private static final int GRID_X = 56; + private static final int GRID_Y = 46; + private static final int COLS = 9; + private static final int ROWS = 5; + private static final int TOTAL_SLOTS = COLS * ROWS; + + // Slot tint colors (ARGB) + private static final int TINT_BLUE = 0x303060FF; // Active listing/bid + private static final int TINT_GREEN = 0x3030FF60; // Claimable + private static final int TINT_RED = 0x30FF4040; // Refund + + private final ContainerAuctionTrades tradesContainer; + private int maxTradeSlots; + + // Pending operation state + private int pendingSlot = -1; + private PendingOp pendingOp = PendingOp.NONE; + + private enum PendingOp { NONE, CANCEL, CLAIM } + + public GuiAuctionTrades(EntityNPCInterface npc, ContainerAuctionTrades container) { + super(npc, container); + this.tradesContainer = container; + this.maxTradeSlots = AuctionClientConfig.getMaxActiveListings() * 2; + } + + @Override + protected int getCurrentPage() { + return PAGE_CLAIMS; + } + + // ========== Mouse Handling ========== + + @Override + public void mouseEvent(int mouseX, int mouseY, int mouseButton) { + if (hasSubGui()) return; + + int slot = getSlotAt(mouseX, mouseY); + if (slot < 0 || slot >= TOTAL_SLOTS) { + clearPending(); + return; + } + + // Clicking on pending slot - confirm or cancel + if (slot == pendingSlot && pendingOp != PendingOp.NONE) { + handlePendingClick(mouseButton); + return; + } + + // Clear existing pending, start new operation + clearPending(); + + AuctionListing listing = tradesContainer.getListingAt(slot); + AuctionClaim claim = tradesContainer.getClaimAt(slot); + + if (listing != null && tradesContainer.isSellingAt(slot) && mouseButton == 1) { + // Right-click on own listing = show cancel + setPending(slot, PendingOp.CANCEL); + NoppesUtil.clickSound(); + } else if (claim != null && mouseButton == 0) { + // Left-click on claim = show claim + setPending(slot, PendingOp.CLAIM); + NoppesUtil.clickSound(); + } + } + + /** Handle click on pending operation slot */ + private void handlePendingClick(int mouseButton) { + if (pendingOp == PendingOp.CANCEL && mouseButton == 1) { + // Right-click confirms cancel + AuctionListing listing = tradesContainer.getListingAt(pendingSlot); + if (listing != null) { + AuctionActionPacket.cancelListing(listing.id); + playConfirmSound(); + } + clearPending(); + } else if (pendingOp == PendingOp.CLAIM && mouseButton == 0) { + // Left-click confirms claim + AuctionClaim claim = tradesContainer.getClaimAt(pendingSlot); + if (claim != null) { + if (claim.type.isItem()) { + AuctionActionPacket.claimItem(claim.id); + } else { + AuctionActionPacket.claimCurrency(claim.id); + } + playConfirmSound(); + } + clearPending(); + } else { + // Wrong button - cancel operation + clearPending(); + NoppesUtil.clickSound(); + } + } + + /** Set pending operation and hide slot item */ + private void setPending(int slot, PendingOp op) { + pendingSlot = slot; + pendingOp = op; + tradesContainer.setHiddenSlot(slot); + } + + /** Clear pending operation and restore slot */ + private void clearPending() { + if (pendingSlot >= 0) { + tradesContainer.clearHiddenSlot(); + } + pendingSlot = -1; + pendingOp = PendingOp.NONE; + } + + /** Play confirmation sound */ + private void playConfirmSound() { + mc.getSoundHandler().playSound(PositionedSoundRecord.func_147674_a( + new ResourceLocation("random.orb"), 1.0F)); + } + + /** Get slot index at mouse position */ + private int getSlotAt(int mouseX, int mouseY) { + int relX = mouseX - guiLeft - GRID_X; + int relY = mouseY - guiTop - GRID_Y; + if (relX < 0 || relY < 0) return -1; + + int col = relX / 18; + int row = relY / 18; + if (col >= COLS || row >= ROWS) return -1; + if (relX % 18 >= 18 || relY % 18 >= 18) return -1; + + return col + row * COLS; + } + + // ========== Drawing ========== + + @Override + protected void drawAuctionContent(float partialTicks, int mouseX, int mouseY) { + for (int row = 0; row < ROWS; row++) { + for (int col = 0; col < COLS; col++) { + int slot = col + row * COLS; + int x = guiLeft + GRID_X + col * 18; + int y = guiTop + GRID_Y + row * 18; + + drawAuctionSlot(x, y); + + // Color tint based on slot contents + int tint = getSlotTint(slot); + if (tint != 0 && slot != pendingSlot) { + drawColoredOverlay(x, y, tint); + } + + // Darken unavailable slots + if (slot >= maxTradeSlots) { + drawDarkenedOverlay(x, y); + } + + // Pending operation icon + if (slot == pendingSlot) { + if (pendingOp == PendingOp.CANCEL) { + drawIconOverlay(x, y, ICON_X); + } else if (pendingOp == PendingOp.CLAIM) { + drawIconOverlay(x, y, ICON_CHECK); + } + } + } + } + } + + /** Get tint color for slot based on contents */ + private int getSlotTint(int slot) { + AuctionListing listing = tradesContainer.getListingAt(slot); + AuctionClaim claim = tradesContainer.getClaimAt(slot); + + if (listing != null) { + return TINT_BLUE; + } else if (claim != null) { + return (claim.type == EnumClaimType.REFUND) ? TINT_RED : TINT_GREEN; + } + return 0; + } + + @Override + protected void drawGuiContainerForegroundLayer(int mouseX, int mouseY) { + super.drawGuiContainerForegroundLayer(mouseX, mouseY); + if (hasSubGui()) return; + + int slot = getSlotAt(mouseX, mouseY); + if (slot >= 0 && slot < TOTAL_SLOTS) { + drawTradeTooltip(slot, mouseX - guiLeft, mouseY - guiTop); + } + } + + /** Draw tooltip for a trade slot */ + private void drawTradeTooltip(int slot, int x, int y) { + List tooltip = new ArrayList<>(); + + if (slot == pendingSlot) { + // Pending operation tooltip + if (pendingOp == PendingOp.CANCEL) { + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.confirmCancel")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.rightClickConfirm")); + tooltip.add(EnumChatFormatting.DARK_GRAY + StatCollector.translateToLocal("auction.trades.otherClickCancel")); + } else if (pendingOp == PendingOp.CLAIM) { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.confirmClaim")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.leftClickConfirm")); + tooltip.add(EnumChatFormatting.DARK_GRAY + StatCollector.translateToLocal("auction.trades.otherClickCancel")); + } + } else { + // Normal slot tooltip + AuctionListing listing = tradesContainer.getListingAt(slot); + AuctionClaim claim = tradesContainer.getClaimAt(slot); + + if (listing != null) { + if (tradesContainer.isSellingAt(slot)) { + tooltip.add(EnumChatFormatting.GOLD + StatCollector.translateToLocal("auction.trades.activeListing")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickToCancel")); + } else if (tradesContainer.isBiddingAt(slot)) { + tooltip.add(EnumChatFormatting.AQUA + StatCollector.translateToLocal("auction.trades.activeBid")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.winningBid")); + } + } else if (claim != null) { + if (claim.type == EnumClaimType.REFUND) { + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.refundClaim")); + } else { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.pendingClaim")); + } + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.leftClickToClaim")); + } else if (slot >= maxTradeSlots) { + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.unavailable")); + } + } + + if (!tooltip.isEmpty()) { + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + drawHoveringText(tooltip, x, y, fontRendererObj); + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } + } + + // ========== Slot Handling ========== + + @Override + protected void handleMouseClick(Slot slot, int slotIndex, int mouseButton, int clickType) { + // Block all slot interactions + } + + // ========== Server Updates ========== + + @Override + public void setGuiData(NBTTagCompound compound) { + if (compound.hasKey("TradesUpdate")) { + tradesContainer.refreshData(); + } + } + + public ContainerAuctionTrades getTradesContainer() { + return tradesContainer; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java new file mode 100644 index 000000000..da828b7a9 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -0,0 +1,167 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.AuctionClientConfig; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * Sub GUI for placing a bid on an auction. + * Shows minimum bid, player balance, and input field. + */ +public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListener { + private static final int TXT_BID = 1; + private static final int BTN_BID = 10; + private static final int BTN_CANCEL = 11; + + private final String listingId; + private final long minimumBid; + private final long playerBalance; + private long bidAmount; + private boolean successful = false; + private String errorMessage = null; + + public SubGuiAuctionBid(String listingId, long minimumBid, long playerBalance) { + this.listingId = listingId; + this.minimumBid = minimumBid; + this.playerBalance = playerBalance; + this.bidAmount = minimumBid; + + setBackground("menubg.png"); + xSize = 200; + ySize = 120; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int centerX = guiLeft + xSize / 2; + int y = guiTop + 15; + String currencyName = AuctionClientConfig.getCurrencyName(); + + // Title + addLabel(new GuiNpcLabel(0, "auction.bid.title", centerX - 50, y, 0xFFFFFF)); + y += 18; + + // Minimum bid info + String minText = StatCollector.translateToLocal("auction.bid.minimum") + .replace("%s", EnumChatFormatting.GOLD + formatCurrency(minimumBid) + " " + currencyName); + addLabel(new GuiNpcLabel(1, minText, guiLeft + 15, y, 0xAAAAAA)); + y += 14; + + // Your balance + String balanceText = StatCollector.translateToLocal("auction.bid.yourBalance") + .replace("%s", EnumChatFormatting.WHITE + formatCurrency(playerBalance) + " " + currencyName); + addLabel(new GuiNpcLabel(2, balanceText, guiLeft + 15, y, 0xAAAAAA)); + y += 18; + + // Bid amount field + addLabel(new GuiNpcLabel(3, "auction.bid.yourBid", guiLeft + 15, y + 4, 0xFFFFFF)); + GuiNpcTextField bidField = new GuiNpcTextField(TXT_BID, this, fontRendererObj, + guiLeft + 80, y, 100, 18, String.valueOf(minimumBid)); + bidField.setIntegersOnly(); + addTextField(bidField); + y += 28; + + // Error message area (empty initially) + addLabel(new GuiNpcLabel(4, "", guiLeft + 15, y, 0xFF5555)); + y += 16; + + // Buttons + int btnWidth = 70; + int btnSpacing = 20; + int totalBtnWidth = btnWidth * 2 + btnSpacing; + int btnX = centerX - totalBtnWidth / 2; + + addButton(new GuiNpcButton(BTN_BID, btnX, y, btnWidth, 20, "auction.bid.placeBid")); + addButton(new GuiNpcButton(BTN_CANCEL, btnX + btnWidth + btnSpacing, y, btnWidth, 20, "gui.cancel")); + } + + @Override + public void unFocused(GuiNpcTextField textfield) { + if (textfield.id == TXT_BID) { + try { + bidAmount = Long.parseLong(textfield.getText()); + } catch (NumberFormatException e) { + bidAmount = 0; + } + errorMessage = null; + updateErrorLabel(); + } + } + + @Override + public void buttonEvent(GuiButton button) { + switch (button.id) { + case BTN_BID: + placeBid(); + break; + case BTN_CANCEL: + close(); + break; + } + } + + private void placeBid() { + // Get latest value from text field + GuiNpcTextField bidField = getTextField(TXT_BID); + if (bidField != null) { + try { + bidAmount = Long.parseLong(bidField.getText()); + } catch (NumberFormatException e) { + bidAmount = 0; + } + } + + // Validate + if (bidAmount < minimumBid) { + errorMessage = StatCollector.translateToLocal("auction.error.bidTooLow") + .replace("%s", formatCurrency(minimumBid)); + updateErrorLabel(); + return; + } + + if (bidAmount > playerBalance) { + errorMessage = StatCollector.translateToLocal("auction.error.notEnoughCurrency"); + updateErrorLabel(); + return; + } + + // Send bid packet + AuctionActionPacket.placeBid(listingId, bidAmount); + successful = true; + close(); + } + + private void updateErrorLabel() { + GuiNpcLabel errorLabel = getLabel(4); + if (errorLabel != null) { + errorLabel.label = errorMessage != null ? EnumChatFormatting.RED + errorMessage : ""; + } + } + + public boolean wasSuccessful() { + return successful; + } + + private String formatCurrency(long amount) { + if (amount < 1000) return String.valueOf(amount); + StringBuilder sb = new StringBuilder(); + String str = String.valueOf(amount); + int count = 0; + for (int i = str.length() - 1; i >= 0; i--) { + if (count > 0 && count % 3 == 0) sb.insert(0, ','); + sb.insert(0, str.charAt(i)); + count++; + } + return sb.toString(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java new file mode 100644 index 000000000..4f20b4ded --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java @@ -0,0 +1,124 @@ +package noppes.npcs.client.gui.player; + +import kamkeel.npcs.network.packets.player.AuctionActionPacket; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.AuctionClientConfig; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * Sub GUI for confirming a buyout purchase. + * Shows buyout price, player balance, and confirmation buttons. + */ +public class SubGuiAuctionBuyNow extends SubGuiInterface { + private static final int BTN_CONFIRM = 10; + private static final int BTN_CANCEL = 11; + + private final String listingId; + private final long buyoutPrice; + private final long playerBalance; + private boolean successful = false; + + public SubGuiAuctionBuyNow(String listingId, long buyoutPrice, long playerBalance) { + this.listingId = listingId; + this.buyoutPrice = buyoutPrice; + this.playerBalance = playerBalance; + + setBackground("menubg.png"); + xSize = 220; + ySize = 130; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + int centerX = guiLeft + xSize / 2; + int y = guiTop + 15; + String currencyName = AuctionClientConfig.getCurrencyName(); + + // Title + String title = StatCollector.translateToLocal("auction.buyout.title"); + int titleWidth = fontRendererObj.getStringWidth(title); + addLabel(new GuiNpcLabel(0, title, centerX - titleWidth / 2, y, 0xFFFFFF)); + y += 20; + + // Confirmation message + String confirmMsg = StatCollector.translateToLocal("auction.buyout.confirm"); + int confirmWidth = fontRendererObj.getStringWidth(confirmMsg); + addLabel(new GuiNpcLabel(1, confirmMsg, centerX - confirmWidth / 2, y, 0xAAAAAA)); + y += 16; + + // Buyout price + String priceText = EnumChatFormatting.GREEN + formatCurrency(buyoutPrice) + " " + currencyName; + int priceWidth = fontRendererObj.getStringWidth(priceText); + addLabel(new GuiNpcLabel(2, priceText, centerX - priceWidth / 2, y, 0xFFFFFF)); + y += 20; + + // Your balance + String balanceLabel = StatCollector.translateToLocal("auction.bid.yourBalance"); + String balanceText = balanceLabel.replace("%s", EnumChatFormatting.WHITE + formatCurrency(playerBalance) + " " + currencyName); + addLabel(new GuiNpcLabel(3, balanceText, guiLeft + 20, y, 0xAAAAAA)); + y += 14; + + // Insufficient funds warning + if (playerBalance < buyoutPrice) { + String warning = StatCollector.translateToLocal("auction.error.notEnoughCurrency"); + addLabel(new GuiNpcLabel(4, EnumChatFormatting.RED + warning, guiLeft + 20, y, 0xFFFFFF)); + } + y += 20; + + // Buttons + int btnWidth = 70; + int btnSpacing = 20; + int totalBtnWidth = btnWidth * 2 + btnSpacing; + int btnX = centerX - totalBtnWidth / 2; + + GuiNpcButton confirmBtn = new GuiNpcButton(BTN_CONFIRM, btnX, y, btnWidth, 20, "gui.yes"); + confirmBtn.enabled = playerBalance >= buyoutPrice; + addButton(confirmBtn); + addButton(new GuiNpcButton(BTN_CANCEL, btnX + btnWidth + btnSpacing, y, btnWidth, 20, "gui.cancel")); + } + + @Override + public void buttonEvent(GuiButton button) { + switch (button.id) { + case BTN_CONFIRM: + if (playerBalance >= buyoutPrice) { + confirmPurchase(); + } + break; + case BTN_CANCEL: + close(); + break; + } + } + + private void confirmPurchase() { + // Send buyout packet + AuctionActionPacket.buyout(listingId); + successful = true; + close(); + } + + public boolean wasSuccessful() { + return successful; + } + + private String formatCurrency(long amount) { + if (amount < 1000) return String.valueOf(amount); + StringBuilder sb = new StringBuilder(); + String str = String.valueOf(amount); + int count = 0; + for (int i = str.length() - 1; i >= 0; i--) { + if (count > 0 && count % 3 == 0) sb.insert(0, ','); + sb.insert(0, str.charAt(i)); + count++; + } + return sb.toString(); + } +} diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionSearch.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionSearch.java new file mode 100644 index 000000000..57d52f95d --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionSearch.java @@ -0,0 +1,85 @@ +package noppes.npcs.client.gui.player; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * Small SubGui for entering auction search text. + */ +public class SubGuiAuctionSearch extends SubGuiInterface implements ITextfieldListener { + private static final int TXT_SEARCH = 0; + private static final int BTN_SUBMIT = 1; + private static final int BTN_CLEAR = 2; + private static final int BTN_CLOSE = 3; + + private String searchText; + + public SubGuiAuctionSearch(String currentSearchText) { + this.searchText = currentSearchText != null ? currentSearchText : ""; + setBackground("menubg.png"); + xSize = 180; + ySize = 80; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + + // Close button + addButton(new GuiNpcButton(BTN_CLOSE, guiLeft + xSize - 22, guiTop + 4, 18, 18, "X")); + + // Search label + addLabel(new GuiNpcLabel(0, "auction.filter.search", guiLeft + 10, guiTop + 10, 0xFFFFFF)); + + // Search text field + addTextField(new GuiNpcTextField(TXT_SEARCH, this, fontRendererObj, guiLeft + 10, guiTop + 25, 160, 18, searchText)); + + // Submit and Clear buttons + addButton(new GuiNpcButton(BTN_SUBMIT, guiLeft + 10, guiTop + 50, 70, 20, "gui.apply")); + addButton(new GuiNpcButton(BTN_CLEAR, guiLeft + 85, guiTop + 50, 70, 20, "gui.clear")); + } + + @Override + public void unFocused(GuiNpcTextField textfield) { + if (textfield.id == TXT_SEARCH) { + searchText = textfield.getText(); + } + } + + @Override + protected void actionPerformed(GuiButton guibutton) { + if (guibutton.id == BTN_SUBMIT) { + GuiNpcTextField field = getTextField(TXT_SEARCH); + if (field != null) { + searchText = field.getText(); + } + // Notify parent and close + if (parent instanceof GuiAuctionListing) { + ((GuiAuctionListing) parent).onSearchSubmit(searchText); + } + close(); + } else if (guibutton.id == BTN_CLEAR) { + searchText = ""; + GuiNpcTextField field = getTextField(TXT_SEARCH); + if (field != null) { + field.setText(""); + } + // Notify parent and close + if (parent instanceof GuiAuctionListing) { + ((GuiAuctionListing) parent).onSearchSubmit(""); + } + close(); + } else if (guibutton.id == BTN_CLOSE) { + close(); + } + } + + public String getSearchText() { + return searchText; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiAuctionNavButton.java b/src/main/java/noppes/npcs/client/gui/util/GuiAuctionNavButton.java new file mode 100644 index 000000000..665dd2259 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/GuiAuctionNavButton.java @@ -0,0 +1,197 @@ +package noppes.npcs.client.gui.util; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.entity.RenderItem; +import net.minecraft.item.ItemStack; +import net.minecraft.util.ResourceLocation; +import net.minecraft.util.StatCollector; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.util.ArrayList; +import java.util.List; + +/** + * Configurable textured button for auction GUI. + * Supports custom button sizes, icon sizes, and textures. + */ +public class GuiAuctionNavButton extends GuiNpcButton { + // Default auction button textures (18x18) + private static final ResourceLocation DEFAULT_NORMAL = new ResourceLocation("customnpcs", "textures/gui/auction/button.png"); + private static final ResourceLocation DEFAULT_PRESSED = new ResourceLocation("customnpcs", "textures/gui/auction/button_press.png"); + + // Textures + private ResourceLocation textureNormal; + private ResourceLocation texturePressed; + private ResourceLocation iconTexture; + private ItemStack iconItem; + + // Dimensions + private int buttonSize; + private int iconSize; + + // State + private boolean selected; + private boolean isToggle; // Toggle buttons can be clicked when selected, nav buttons cannot + private final String tooltipKey; + private List customTooltip; + + /** + * Create a button with default 18x18 size and 16x16 icon using default textures. + */ + public GuiAuctionNavButton(int id, int x, int y, String tooltipKey, ResourceLocation iconTexture) { + this(id, x, y, 18, 16, tooltipKey, iconTexture, DEFAULT_NORMAL, DEFAULT_PRESSED); + } + + /** + * Create a button with default 18x18 size and 16x16 icon with an item icon. + */ + public GuiAuctionNavButton(int id, int x, int y, String tooltipKey, ItemStack iconItem) { + super(id, x, y, 18, 18, ""); + this.tooltipKey = tooltipKey; + this.iconTexture = null; + this.iconItem = iconItem; + this.buttonSize = 18; + this.iconSize = 16; + this.textureNormal = DEFAULT_NORMAL; + this.texturePressed = DEFAULT_PRESSED; + this.selected = false; + this.customTooltip = null; + } + + /** + * Create a fully customizable button. + * + * @param id Button ID + * @param x X position + * @param y Y position + * @param buttonSize Size of the button background texture (e.g., 18 for 18x18, 10 for 10x10) + * @param iconSize Size of the icon texture (e.g., 16 for 16x16, 8 for 8x8) + * @param tooltipKey Lang key for tooltip + * @param iconTexture Icon texture to draw inside the button + * @param textureNormal Background texture for normal state + * @param texturePressed Background texture for pressed/hover state + */ + public GuiAuctionNavButton(int id, int x, int y, int buttonSize, int iconSize, String tooltipKey, + ResourceLocation iconTexture, ResourceLocation textureNormal, ResourceLocation texturePressed) { + super(id, x, y, buttonSize, buttonSize, ""); + this.tooltipKey = tooltipKey; + this.iconTexture = iconTexture; + this.iconItem = null; + this.buttonSize = buttonSize; + this.iconSize = iconSize; + this.textureNormal = textureNormal; + this.texturePressed = texturePressed; + this.selected = false; + this.customTooltip = null; + } + + public void setSelected(boolean selected) { + this.selected = selected; + } + + public boolean isSelected() { + return selected; + } + + /** + * Set whether this is a toggle button. + * Toggle buttons can be clicked when selected (to toggle off). + * Nav buttons cannot be clicked when selected. + */ + public GuiAuctionNavButton setToggle(boolean toggle) { + this.isToggle = toggle; + return this; + } + + public boolean isHovered() { + return field_146123_n; + } + + /** + * Set a custom multi-line tooltip. If null, uses the default tooltipKey translation. + */ + public void setCustomTooltip(List tooltip) { + this.customTooltip = tooltip; + } + + /** + * Get the tooltip lines for display. + */ + public List getTooltipLines() { + if (customTooltip != null && !customTooltip.isEmpty()) { + return customTooltip; + } + List lines = new ArrayList<>(); + if (tooltipKey != null && !tooltipKey.isEmpty()) { + lines.add(StatCollector.translateToLocal(tooltipKey)); + } + return lines; + } + + public String getTooltipText() { + return tooltipKey != null ? StatCollector.translateToLocal(tooltipKey) : ""; + } + + @Override + public void drawButton(Minecraft mc, int mouseX, int mouseY) { + if (!this.visible) { + return; + } + + // Check if mouse is hovering + boolean hovered = mouseX >= xPosition && mouseX < xPosition + width + && mouseY >= yPosition && mouseY < yPosition + height; + field_146123_n = hovered; + + // Draw button background + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + if (selected || hovered) { + mc.getTextureManager().bindTexture(texturePressed); + } else { + mc.getTextureManager().bindTexture(textureNormal); + } + Gui.func_146110_a(xPosition, yPosition, 0, 0, buttonSize, buttonSize, buttonSize, buttonSize); + + // Calculate icon offset to center it + int iconOffset = (buttonSize - iconSize) / 2; + int iconX = xPosition + iconOffset; + int iconY = yPosition + iconOffset; + + if (iconItem != null) { + // Render item + RenderHelper.enableGUIStandardItemLighting(); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + RenderItem itemRenderer = RenderItem.getInstance(); + itemRenderer.renderItemAndEffectIntoGUI(mc.fontRenderer, mc.renderEngine, iconItem, iconX, iconY); + GL11.glDisable(GL11.GL_LIGHTING); + RenderHelper.disableStandardItemLighting(); + } else if (iconTexture != null) { + // Render texture icon + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + mc.getTextureManager().bindTexture(iconTexture); + Gui.func_146110_a(iconX, iconY, 0, 0, iconSize, iconSize, iconSize, iconSize); + } + } + + @Override + public boolean mousePressed(Minecraft minecraft, int mouseX, int mouseY) { + // Don't allow clicking if selected (for nav buttons, not toggle buttons) + if (selected && !isToggle) { + return false; + } + return enabled && visible && mouseX >= xPosition && mouseX < xPosition + width + && mouseY >= yPosition && mouseY < yPosition + height; + } + + /** + * Check if this button contains the given mouse coordinates. + */ + public boolean isMouseOver(int mouseX, int mouseY) { + return mouseX >= xPosition && mouseX < xPosition + width + && mouseY >= yPosition && mouseY < yPosition + height; + } +} diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java index c84ec3604..557a08b6f 100644 --- a/src/main/java/noppes/npcs/config/ConfigMarket.java +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -14,6 +14,8 @@ public class ConfigMarket { public final static String MARKET = "Market"; public final static String CURRENCY = "Market.Currency"; public final static String TRADER = "Market.Trader"; + public final static String AUCTION = "Market.Auction"; + public final static String AUCTION_LOGGING = "Market.Auction.Logging"; // ========================================= // Market General @@ -34,6 +36,31 @@ public class ConfigMarket { public static boolean EnableStockByDefault = false; public static String DefaultResetType = "NONE"; + // ========================================= + // Auction Settings + // ========================================= + public static boolean AuctionEnabled = true; + public static int AuctionDurationHours = 24; + public static long ListingFee = 10; + public static double SalesTaxPercent = 0.05; + public static int DefaultMaxListings = 5; + public static int SnipeProtectionMinutes = 2; + public static int ClaimExpirationDays = 20; + public static double MinBidIncrementPercent = 0.05; + public static double CancellationPenaltyPercent = 0.10; + + // ========================================= + // Auction Logging Settings + // ========================================= + public static boolean AuctionLoggingEnabled = false; + public static boolean LogAuctionCreated = true; + public static boolean LogAuctionBid = true; + public static boolean LogAuctionBuyout = true; + public static boolean LogAuctionSold = true; + public static boolean LogAuctionExpired = true; + public static boolean LogAuctionCancelled = true; + public static boolean LogAuctionClaimed = true; + public static void init(File configFile) { config = new Configuration(configFile); @@ -74,6 +101,67 @@ public class ConfigMarket { DefaultResetType = config.get(TRADER, "Default Reset Type", "NONE", "Default stock reset type (NONE, MCDAILY, MCWEEKLY, RLDAILY, RLWEEKLY)").getString(); + // ========================================= + // Auction Settings + // ========================================= + config.setCategoryComment(AUCTION, "Auction House system settings. The Auction House allows players to list items for sale with bidding and buyout options."); + + AuctionEnabled = config.get(AUCTION, "Enable Auction House", true, + "Enable the Auction House system").getBoolean(true); + + AuctionDurationHours = config.get(AUCTION, "Auction Duration Hours", 24, + "Default duration for auctions in hours").getInt(24); + + ListingFee = config.get(AUCTION, "Listing Fee", 10, + "Flat fee charged when creating a listing").getInt(10); + + SalesTaxPercent = config.get(AUCTION, "Sales Tax Percent", 0.05, + "Percentage of sale price taken as tax (0.05 = 5%). Tax is deleted as a currency sink.").getDouble(0.05); + + DefaultMaxListings = config.get(AUCTION, "Default Max Listings", 5, + "Default maximum number of active listings per player").getInt(5); + + SnipeProtectionMinutes = config.get(AUCTION, "Snipe Protection Minutes", 2, + "When a bid is placed with less than this many minutes remaining, the auction is extended to this duration").getInt(2); + + ClaimExpirationDays = config.get(AUCTION, "Claim Expiration Days", 20, + "Number of days before unclaimed items/currency are deleted").getInt(20); + + MinBidIncrementPercent = config.get(AUCTION, "Min Bid Increment Percent", 0.05, + "Minimum bid increment as a percentage of current bid (0.05 = 5%)").getDouble(0.05); + + CancellationPenaltyPercent = config.get(AUCTION, "Cancellation Penalty Percent", 0.10, + "Percentage of current bid taken as penalty when seller cancels an auction with bids (0.10 = 10%)").getDouble(0.10); + + // ========================================= + // Auction Logging Settings + // ========================================= + config.setCategoryComment(AUCTION_LOGGING, "Auction House logging settings. Enable specific log types to track auction activity."); + + AuctionLoggingEnabled = config.get(AUCTION_LOGGING, "Enable Auction Logging", false, + "Master switch for auction logging. If false, no auction events are logged.").getBoolean(false); + + LogAuctionCreated = config.get(AUCTION_LOGGING, "Log Created", true, + "Log when auctions are created").getBoolean(true); + + LogAuctionBid = config.get(AUCTION_LOGGING, "Log Bid", true, + "Log when bids are placed").getBoolean(true); + + LogAuctionBuyout = config.get(AUCTION_LOGGING, "Log Buyout", true, + "Log when auctions are bought out").getBoolean(true); + + LogAuctionSold = config.get(AUCTION_LOGGING, "Log Sold", true, + "Log when auctions end with a winner").getBoolean(true); + + LogAuctionExpired = config.get(AUCTION_LOGGING, "Log Expired", true, + "Log when auctions expire with no bids").getBoolean(true); + + LogAuctionCancelled = config.get(AUCTION_LOGGING, "Log Cancelled", true, + "Log when auctions are cancelled").getBoolean(true); + + LogAuctionClaimed = config.get(AUCTION_LOGGING, "Log Claimed", true, + "Log when claims are collected").getBoolean(true); + // Set category order config.setCategoryPropertyOrder(MARKET, new ArrayList<>(Arrays.asList( "Enable Market System" @@ -91,6 +179,29 @@ public class ConfigMarket { "Default Reset Type" ))); + config.setCategoryPropertyOrder(AUCTION, new ArrayList<>(Arrays.asList( + "Enable Auction House", + "Auction Duration Hours", + "Listing Fee", + "Sales Tax Percent", + "Default Max Listings", + "Snipe Protection Minutes", + "Claim Expiration Days", + "Min Bid Increment Percent", + "Cancellation Penalty Percent" + ))); + + config.setCategoryPropertyOrder(AUCTION_LOGGING, new ArrayList<>(Arrays.asList( + "Enable Auction Logging", + "Log Created", + "Log Bid", + "Log Buyout", + "Log Sold", + "Log Expired", + "Log Cancelled", + "Log Claimed" + ))); + } catch (Exception e) { FMLLog.log(Level.ERROR, e, "CNPC+ has had a problem loading its market configuration"); } finally { diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionSort.java b/src/main/java/noppes/npcs/constants/EnumAuctionSort.java new file mode 100644 index 000000000..115f13dc9 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumAuctionSort.java @@ -0,0 +1,40 @@ +package noppes.npcs.constants; + +import net.minecraft.util.StatCollector; + +public enum EnumAuctionSort { + NEWEST("auction.sort.newest"), + ENDING_SOON("auction.sort.endingSoon"), + PRICE_LOW("auction.sort.priceLow"), + PRICE_HIGH("auction.sort.priceHigh"), + MOST_BIDS("auction.sort.mostBids"); + + private final String langKey; + + EnumAuctionSort(String langKey) { + this.langKey = langKey; + } + + public String getLangKey() { + return langKey; + } + + public String getDisplayName() { + return StatCollector.translateToLocal(langKey); + } + + public static String[] getDisplayNames() { + String[] names = new String[values().length]; + for (int i = 0; i < values().length; i++) { + names[i] = values()[i].getDisplayName(); + } + return names; + } + + public static EnumAuctionSort fromOrdinal(int ordinal) { + if (ordinal >= 0 && ordinal < values().length) { + return values()[ordinal]; + } + return NEWEST; + } +} diff --git a/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java b/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java new file mode 100644 index 000000000..14fc0f4ff --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumAuctionStatus.java @@ -0,0 +1,47 @@ +package noppes.npcs.constants; + +import net.minecraft.util.StatCollector; + +public enum EnumAuctionStatus { + ACTIVE("auction.status.active"), + ENDED("auction.status.ended"), + CANCELLED("auction.status.cancelled"), + CLAIMED("auction.status.claimed"); + + private final String langKey; + + EnumAuctionStatus(String langKey) { + this.langKey = langKey; + } + + public String getLangKey() { + return langKey; + } + + public String getDisplayName() { + return StatCollector.translateToLocal(langKey); + } + + public boolean isActive() { + return this == ACTIVE; + } + + public boolean isEnded() { + return this == ENDED || this == CANCELLED || this == CLAIMED; + } + + public boolean canBid() { + return this == ACTIVE; + } + + public boolean canCancel() { + return this == ACTIVE; + } + + public static EnumAuctionStatus fromOrdinal(int ordinal) { + if (ordinal >= 0 && ordinal < values().length) { + return values()[ordinal]; + } + return ACTIVE; + } +} diff --git a/src/main/java/noppes/npcs/constants/EnumClaimType.java b/src/main/java/noppes/npcs/constants/EnumClaimType.java new file mode 100644 index 000000000..35fbf2003 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumClaimType.java @@ -0,0 +1,38 @@ +package noppes.npcs.constants; + +import net.minecraft.util.StatCollector; + +public enum EnumClaimType { + ITEM("auction.claim.item"), + CURRENCY("auction.claim.currency"), + REFUND("auction.claim.refund"); + + private final String langKey; + + EnumClaimType(String langKey) { + this.langKey = langKey; + } + + public String getLangKey() { + return langKey; + } + + public String getDisplayName() { + return StatCollector.translateToLocal(langKey); + } + + public boolean isItem() { + return this == ITEM; + } + + public boolean isCurrency() { + return this == CURRENCY || this == REFUND; + } + + public static EnumClaimType fromOrdinal(int ordinal) { + if (ordinal >= 0 && ordinal < values().length) { + return values()[ordinal]; + } + return ITEM; + } +} diff --git a/src/main/java/noppes/npcs/constants/EnumGuiType.java b/src/main/java/noppes/npcs/constants/EnumGuiType.java index f2d911561..1859b2616 100644 --- a/src/main/java/noppes/npcs/constants/EnumGuiType.java +++ b/src/main/java/noppes/npcs/constants/EnumGuiType.java @@ -64,5 +64,10 @@ public enum EnumGuiType { ScriptBlock, Paintbrush, MagicBook, - ManageAbilities; + ManageAbilities, + PlayerAuction, + PlayerAuctionSell, + PlayerAuctionTrades, + PlayerAuctionBidding, + ManageAuction; } diff --git a/src/main/java/noppes/npcs/constants/EnumNotificationType.java b/src/main/java/noppes/npcs/constants/EnumNotificationType.java new file mode 100644 index 000000000..02130e710 --- /dev/null +++ b/src/main/java/noppes/npcs/constants/EnumNotificationType.java @@ -0,0 +1,32 @@ +package noppes.npcs.constants; + +import net.minecraft.util.StatCollector; + +public enum EnumNotificationType { + AUCTION_WON("auction.notify.won"), + AUCTION_OUTBID("auction.notify.outbid"), + AUCTION_SOLD("auction.notify.sold"), + AUCTION_EXPIRED("auction.notify.expired"), + CLAIM_READY("auction.notify.claim"); + + private final String langKey; + + EnumNotificationType(String langKey) { + this.langKey = langKey; + } + + public String getLangKey() { + return langKey; + } + + public String getDisplayName() { + return StatCollector.translateToLocal(langKey); + } + + public static EnumNotificationType fromOrdinal(int ordinal) { + if (ordinal >= 0 && ordinal < values().length) { + return values()[ordinal]; + } + return CLAIM_READY; + } +} diff --git a/src/main/java/noppes/npcs/constants/EnumRoleType.java b/src/main/java/noppes/npcs/constants/EnumRoleType.java index 624658fb3..33f36941a 100644 --- a/src/main/java/noppes/npcs/constants/EnumRoleType.java +++ b/src/main/java/noppes/npcs/constants/EnumRoleType.java @@ -2,5 +2,5 @@ package noppes.npcs.constants; public enum EnumRoleType { - None, Trader, Follower, Bank, Transporter, Postman, Companion, Mount + None, Trader, Follower, Bank, Transporter, Postman, Companion, Mount, Auctioneer } diff --git a/src/main/java/noppes/npcs/containers/ContainerAuction.java b/src/main/java/noppes/npcs/containers/ContainerAuction.java new file mode 100644 index 000000000..2ccaa2e52 --- /dev/null +++ b/src/main/java/noppes/npcs/containers/ContainerAuction.java @@ -0,0 +1,81 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Base container for all auction GUIs. + * Provides player inventory at the correct position for the 256x256 auction texture. + * Player inventory is display-only - no modifications allowed. + */ +public class ContainerAuction extends ContainerNpcInterface { + public final EntityNPCInterface npc; + + // Player inventory layout for 256x256 auction texture + // Top-left slot background at (47, 163) + // Item renders at +1 offset inside the 18x18 slot background + public static final int PLAYER_INV_X = 48; // 47 + 1 for item offset + public static final int PLAYER_INV_Y = 164; // 163 + 1 for item offset + public static final int HOTBAR_Y = 222; // PLAYER_INV_Y + (3 * 18) + 4 gap + + // Player inventory slots are added first (36 slots: 0-35) + public static final int PLAYER_INV_SLOT_COUNT = 36; + + public ContainerAuction(EntityNPCInterface npc, EntityPlayer player) { + super(player); + this.npc = npc; + this.player = player; + + addPlayerInventory(); + } + + protected void addPlayerInventory() { + // Main inventory (3 rows of 9) + for (int row = 0; row < 3; row++) { + for (int col = 0; col < 9; col++) { + int slotIndex = col + row * 9 + 9; // Slots 9-35 + int x = PLAYER_INV_X + col * 18; + int y = PLAYER_INV_Y + row * 18; + addSlotToContainer(new Slot(player.inventory, slotIndex, x, y)); + } + } + + // Hotbar (1 row of 9) + for (int col = 0; col < 9; col++) { + int x = PLAYER_INV_X + col * 18; + addSlotToContainer(new Slot(player.inventory, col, x, HOTBAR_Y)); + } + } + + @Override + public ItemStack transferStackInSlot(EntityPlayer player, int slotIndex) { + return null; // Disable shift-clicking + } + + @Override + public ItemStack slotClick(int slotIndex, int mouseButton, int mode, EntityPlayer player) { + // Block all slot interactions by default in auction containers + // This prevents picking up, placing, and swapping items + // Subclasses can override to allow specific interactions + return null; + } + + /** + * Check if the slot index is a player inventory slot. + */ + public boolean isPlayerInventorySlot(int slotIndex) { + return slotIndex >= 0 && slotIndex < PLAYER_INV_SLOT_COUNT; + } + + /** + * Get the ItemStack in a player inventory slot. + * @param containerSlotIndex The container slot index (0-35 for player inv) + */ + public ItemStack getPlayerInventoryStack(int containerSlotIndex) { + if (!isPlayerInventorySlot(containerSlotIndex)) return null; + Slot slot = (Slot) inventorySlots.get(containerSlotIndex); + return slot != null ? slot.getStack() : null; + } +} diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java b/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java new file mode 100644 index 000000000..1d1faebe9 --- /dev/null +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java @@ -0,0 +1,63 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.IInventory; +import net.minecraft.inventory.InventoryBasic; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Container for the auction bidding/purchase view. + * Displays a single item slot showing the listing's item. + */ +public class ContainerAuctionBidding extends ContainerAuction { + // Display slot layout + public static final int ITEM_SLOT_X = 75; + public static final int ITEM_SLOT_Y = 70; + public static final int ITEM_SLOT_INDEX = PLAYER_INV_SLOT_COUNT; + + private final IInventory displayInventory; + private AuctionListing listing; + + public ContainerAuctionBidding(EntityNPCInterface npc, EntityPlayer player) { + super(npc, player); + displayInventory = new InventoryBasic("Display", false, 1); + + // Display slot (read-only) + addSlotToContainer(new Slot(displayInventory, 0, ITEM_SLOT_X, ITEM_SLOT_Y) { + @Override + public boolean isItemValid(ItemStack stack) { return false; } + @Override + public boolean canTakeStack(EntityPlayer player) { return false; } + }); + } + + /** + * Set the listing to display. + */ + public void setListing(AuctionListing listing) { + this.listing = listing; + if (listing != null && listing.item != null) { + displayInventory.setInventorySlotContents(0, listing.item.copy()); + } else { + displayInventory.setInventorySlotContents(0, null); + } + } + + public AuctionListing getListing() { + return listing; + } + + public ItemStack getDisplayItem() { + return displayInventory.getStackInSlot(0); + } + + /** + * Check if this is the display slot. + */ + public boolean isDisplaySlot(int slotIndex) { + return slotIndex == ITEM_SLOT_INDEX; + } +} diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionListing.java b/src/main/java/noppes/npcs/containers/ContainerAuctionListing.java new file mode 100644 index 000000000..9691935be --- /dev/null +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionListing.java @@ -0,0 +1,133 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.data.AuctionFilter; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.List; + +/** + * Container for the auction listing browse view. + * Displays a 9x5 grid of auction listings (45 slots). + */ +public class ContainerAuctionListing extends ContainerAuction { + // Listing grid layout + public static final int LISTING_START_X = 57; // 56 + 1 for item offset + public static final int LISTING_START_Y = 47; // 46 + 1 for item offset + public static final int LISTING_COLS = 9; + public static final int LISTING_ROWS = 5; + public static final int LISTING_SLOT_COUNT = LISTING_COLS * LISTING_ROWS; + + public final InventoryAuctionDisplay displayInventory; + private int currentPage = 0; + private AuctionFilter filter = new AuctionFilter(); + + public ContainerAuctionListing(EntityNPCInterface npc, EntityPlayer player) { + super(npc, player); + + // Create display inventory for listings + displayInventory = new InventoryAuctionDisplay(LISTING_SLOT_COUNT); + + // Add listing display slots (9x5 grid) + for (int row = 0; row < LISTING_ROWS; row++) { + for (int col = 0; col < LISTING_COLS; col++) { + int slotIndex = col + row * LISTING_COLS; + int x = LISTING_START_X + col * 18; + int y = LISTING_START_Y + row * 18; + addSlotToContainer(new SlotAuctionDisplay(displayInventory, slotIndex, x, y)); + } + } + + // Load initial listings + refreshListings(); + } + + public void refreshListings() { + if (AuctionController.Instance == null) return; + + List listings = AuctionController.Instance.getActiveListings( + filter, currentPage, LISTING_SLOT_COUNT); + + displayInventory.clear(); + for (int i = 0; i < listings.size() && i < LISTING_SLOT_COUNT; i++) { + displayInventory.setListing(i, listings.get(i)); + } + } + + public AuctionListing getListingAt(int slotIndex) { + return displayInventory.getListing(slotIndex); + } + + /** + * Find a listing by matching ItemStack. + */ + public AuctionListing getListingForItem(ItemStack stack) { + if (stack == null) return null; + + for (int i = 0; i < LISTING_SLOT_COUNT; i++) { + AuctionListing listing = displayInventory.getListing(i); + if (listing != null && listing.item != null && ItemStack.areItemStacksEqual(listing.item, stack)) { + return listing; + } + } + return null; + } + + public int getCurrentPage() { + return currentPage; + } + + public void setPage(int page) { + this.currentPage = Math.max(0, page); + refreshListings(); + } + + public void nextPage() { + setPage(currentPage + 1); + } + + public void prevPage() { + if (currentPage > 0) { + setPage(currentPage - 1); + } + } + + public int getTotalPages() { + if (AuctionController.Instance == null) return 1; + int totalListings = AuctionController.Instance.getTotalActiveListings(filter); + return Math.max(1, (int) Math.ceil((double) totalListings / LISTING_SLOT_COUNT)); + } + + public int getTotalListings() { + if (AuctionController.Instance == null) return 0; + return AuctionController.Instance.getTotalActiveListings(filter); + } + + public AuctionFilter getFilter() { + return filter; + } + + public void setFilter(AuctionFilter filter) { + this.filter = filter; + this.currentPage = 0; + refreshListings(); + } + + /** + * Check if a slot index is a display slot. + */ + public boolean isDisplaySlot(int slotIndex) { + int start = getDisplaySlotStart(); + return slotIndex >= start && slotIndex < start + LISTING_SLOT_COUNT; + } + + /** + * Get the starting slot index for display slots. + */ + public int getDisplaySlotStart() { + return PLAYER_INV_SLOT_COUNT; + } +} diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java b/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java new file mode 100644 index 000000000..a4bab6efe --- /dev/null +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java @@ -0,0 +1,125 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.IInventory; +import net.minecraft.inventory.InventoryBasic; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; +import noppes.npcs.NoppesUtilPlayer; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Container for creating auction listings with a single sell slot. + * Items are cloned from inventory, not moved. Validation happens on submit. + */ +public class ContainerAuctionSell extends ContainerAuction { + public static final int SELL_SLOT_X = 101; + public static final int SELL_SLOT_Y = 106; + public static final int SELL_SLOT_INDEX = PLAYER_INV_SLOT_COUNT; + private static final int MAX_STACK = 64; + + private final IInventory sellInventory; + + public ContainerAuctionSell(EntityNPCInterface npc, EntityPlayer player) { + super(npc, player); + sellInventory = new InventoryBasic("Sell", false, 1); + + // Sell slot blocks direct interaction + addSlotToContainer(new Slot(sellInventory, 0, SELL_SLOT_X, SELL_SLOT_Y) { + @Override + public boolean isItemValid(ItemStack stack) { return false; } + @Override + public boolean canTakeStack(EntityPlayer player) { return false; } + }); + } + + public ItemStack getItemToSell() { + return sellInventory.getStackInSlot(0); + } + + public void clearSellSlot() { + sellInventory.setInventorySlotContents(0, null); + } + + /** + * Add items from inventory to sell slot. + * Not limited by inventory - just stages items for sale. Validation on submit. + * @param sourceSlot Inventory slot index + * @param fullStack True=full stack, False=1 item + */ + public void addToSellSlot(int sourceSlot, boolean fullStack) { + if (!isPlayerInventorySlot(sourceSlot)) return; + + ItemStack source = getPlayerInventoryStack(sourceSlot); + if (source == null) return; + + ItemStack sellStack = sellInventory.getStackInSlot(0); + int toAdd = fullStack ? source.stackSize : 1; + + if (sellStack == null) { + // Empty slot - create new stack + ItemStack newStack = source.copy(); + newStack.stackSize = Math.min(toAdd, MAX_STACK); + sellInventory.setInventorySlotContents(0, newStack); + } else if (itemsMatch(sellStack, source)) { + // Same item - add to existing (capped at 64) + int space = MAX_STACK - sellStack.stackSize; + int add = Math.min(toAdd, space); + if (add > 0) { + sellStack.stackSize += add; + } + } else { + // Different item - replace + ItemStack newStack = source.copy(); + newStack.stackSize = Math.min(toAdd, MAX_STACK); + sellInventory.setInventorySlotContents(0, newStack); + } + } + + /** + * Remove items from sell slot. + * @param removeAll True=clear slot, False=remove 1 item + */ + public void removeFromSellSlot(boolean removeAll) { + ItemStack sellStack = sellInventory.getStackInSlot(0); + if (sellStack == null) return; + + if (removeAll || sellStack.stackSize <= 1) { + sellInventory.setInventorySlotContents(0, null); + } else { + sellStack.stackSize--; + } + } + + /** Check if sell slot is this container slot index */ + public boolean isSellSlot(int slotIndex) { + return slotIndex == SELL_SLOT_INDEX; + } + + /** Check if two stacks match using NoppesUtilPlayer.compareItems (same item, damage, and NBT) */ + private boolean itemsMatch(ItemStack a, ItemStack b) { + return NoppesUtilPlayer.compareItems(a, b, false, false); + } + + /** + * Count total items of a specific type across player inventory. + * Used for submit validation and UI indicator. + */ + public int countItemInInventory(ItemStack target) { + if (target == null) return 0; + int total = 0; + for (int i = 0; i < PLAYER_INV_SLOT_COUNT; i++) { + ItemStack stack = getPlayerInventoryStack(i); + if (stack != null && itemsMatch(stack, target)) { + total += stack.stackSize; + } + } + return total; + } + + @Override + public void onContainerClosed(EntityPlayer player) { + super.onContainerClosed(player); + clearSellSlot(); + } +} diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java b/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java new file mode 100644 index 000000000..8481612d5 --- /dev/null +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java @@ -0,0 +1,184 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.data.AuctionClaim; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.ArrayList; +import java.util.List; + +/** + * Container for viewing player's auction trades. + * Shows active listings, bids, and claims in a 9x5 grid. + */ +public class ContainerAuctionTrades extends ContainerAuction { + // Grid layout + public static final int GRID_X = 57; + public static final int GRID_Y = 47; + public static final int COLS = 9; + public static final int ROWS = 5; + public static final int SLOT_COUNT = COLS * ROWS; + + public final InventoryAuctionDisplay displayInventory; + private List activeListings = new ArrayList<>(); + private List activeBids = new ArrayList<>(); + private List claims = new ArrayList<>(); + private int hiddenSlot = -1; + + public ContainerAuctionTrades(EntityNPCInterface npc, EntityPlayer player) { + super(npc, player); + displayInventory = new InventoryAuctionDisplay(SLOT_COUNT); + + // Add display slots (9x5 grid) + for (int row = 0; row < ROWS; row++) { + for (int col = 0; col < COLS; col++) { + int idx = col + row * COLS; + int x = GRID_X + col * 18; + int y = GRID_Y + row * 18; + addSlotToContainer(new SlotAuctionDisplay(displayInventory, idx, x, y)); + } + } + refreshData(); + } + + /** Refresh data from controller */ + public void refreshData() { + activeListings.clear(); + activeBids.clear(); + claims.clear(); + displayInventory.clear(); + + if (AuctionController.Instance == null) return; + + activeListings = AuctionController.Instance.getPlayerActiveListings(player.getUniqueID()); + activeBids = AuctionController.Instance.getPlayerActiveBids(player.getUniqueID()); + claims = AuctionController.Instance.getPlayerClaims(player.getUniqueID()); + + // Populate display: listings first, then bids, then claims + int slot = 0; + for (AuctionListing listing : activeListings) { + if (slot >= SLOT_COUNT) break; + if (listing.item != null) displayInventory.setInventorySlotContents(slot, listing.item.copy()); + slot++; + } + for (AuctionListing bid : activeBids) { + if (slot >= SLOT_COUNT) break; + if (bid.item != null) displayInventory.setInventorySlotContents(slot, bid.item.copy()); + slot++; + } + for (AuctionClaim claim : claims) { + if (slot >= SLOT_COUNT) break; + displayInventory.setClaimItem(slot, claim); + slot++; + } + } + + // ========== Slot Data Accessors ========== + + /** Get listing at slot (or null if claim/empty) */ + public AuctionListing getListingAt(int slot) { + if (slot >= 0 && slot < activeListings.size()) { + return activeListings.get(slot); + } + int bidIdx = slot - activeListings.size(); + if (bidIdx >= 0 && bidIdx < activeBids.size()) { + return activeBids.get(bidIdx); + } + return null; + } + + /** Get claim at slot (or null if listing/empty) */ + public AuctionClaim getClaimAt(int slot) { + int claimIdx = slot - activeListings.size() - activeBids.size(); + if (claimIdx >= 0 && claimIdx < claims.size()) { + return claims.get(claimIdx); + } + return null; + } + + /** Check if slot is player's active listing */ + public boolean isSellingAt(int slot) { + return slot >= 0 && slot < activeListings.size(); + } + + /** Check if slot is player's active bid */ + public boolean isBiddingAt(int slot) { + int bidIdx = slot - activeListings.size(); + return bidIdx >= 0 && bidIdx < activeBids.size(); + } + + /** Find listing matching item */ + public AuctionListing getListingForItem(ItemStack stack) { + if (stack == null) return null; + for (AuctionListing listing : activeListings) { + if (listing.item != null && ItemStack.areItemStacksEqual(listing.item, stack)) return listing; + } + for (AuctionListing bid : activeBids) { + if (bid.item != null && ItemStack.areItemStacksEqual(bid.item, stack)) return bid; + } + return null; + } + + /** Find claim matching item */ + public AuctionClaim getClaimForItem(ItemStack stack) { + if (stack == null) return null; + for (AuctionClaim claim : claims) { + if (claim.item != null && ItemStack.areItemStacksEqual(claim.item, stack)) return claim; + } + return null; + } + + // ========== List Getters ========== + + public List getActiveListings() { return activeListings; } + public List getActiveBids() { return activeBids; } + public List getClaims() { return claims; } + public int getTotalTradeCount() { return activeListings.size() + activeBids.size() + claims.size(); } + + // ========== Slot Hiding (for pending operations) ========== + + /** Hide slot for pending operation display */ + public void setHiddenSlot(int slot) { + if (hiddenSlot >= 0 && hiddenSlot < SLOT_COUNT) { + restoreSlotItem(hiddenSlot); + } + hiddenSlot = slot; + if (hiddenSlot >= 0 && hiddenSlot < SLOT_COUNT) { + displayInventory.setInventorySlotContents(hiddenSlot, null); + } + } + + /** Restore hidden slot */ + public void clearHiddenSlot() { + if (hiddenSlot >= 0 && hiddenSlot < SLOT_COUNT) { + restoreSlotItem(hiddenSlot); + } + hiddenSlot = -1; + } + + /** Restore item at slot from data */ + private void restoreSlotItem(int slot) { + if (slot < activeListings.size()) { + AuctionListing listing = activeListings.get(slot); + if (listing != null && listing.item != null) { + displayInventory.setInventorySlotContents(slot, listing.item.copy()); + } + } else if (slot < activeListings.size() + activeBids.size()) { + int bidIdx = slot - activeListings.size(); + AuctionListing bid = activeBids.get(bidIdx); + if (bid != null && bid.item != null) { + displayInventory.setInventorySlotContents(slot, bid.item.copy()); + } + } else { + int claimIdx = slot - activeListings.size() - activeBids.size(); + if (claimIdx < claims.size()) { + displayInventory.setClaimItem(slot, claims.get(claimIdx)); + } + } + } + + public int getHiddenSlot() { return hiddenSlot; } +} diff --git a/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java b/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java new file mode 100644 index 000000000..20c03f18f --- /dev/null +++ b/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java @@ -0,0 +1,153 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.IInventory; +import net.minecraft.item.ItemStack; +import noppes.npcs.CustomItems; +import noppes.npcs.constants.EnumClaimType; +import noppes.npcs.controllers.data.AuctionClaim; +import noppes.npcs.controllers.data.AuctionListing; + +import java.util.ArrayList; +import java.util.List; + +/** + * Virtual inventory for displaying auction listings and claims. + * This inventory holds ItemStacks from AuctionListings/Claims for display purposes. + */ +public class InventoryAuctionDisplay implements IInventory { + private final ItemStack[] items; + private final List listings; + private final List claims; + private final int size; + + public InventoryAuctionDisplay(int size) { + this.size = size; + this.items = new ItemStack[size]; + this.listings = new ArrayList<>(size); + this.claims = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + listings.add(null); + claims.add(null); + } + } + + public void setListing(int slot, AuctionListing listing) { + if (slot >= 0 && slot < size) { + listings.set(slot, listing); + claims.set(slot, null); + items[slot] = listing != null ? listing.item : null; + } + } + + public AuctionListing getListing(int slot) { + if (slot >= 0 && slot < size) { + return listings.get(slot); + } + return null; + } + + /** + * Set a claim item in the inventory. + * For ITEM claims, shows the item. + * For CURRENCY/REFUND claims, shows a gold coin as visual representation. + */ + public void setClaimItem(int slot, AuctionClaim claim) { + if (slot >= 0 && slot < size) { + claims.set(slot, claim); + listings.set(slot, null); + if (claim != null) { + if (claim.type == EnumClaimType.ITEM && claim.item != null) { + items[slot] = claim.item; + } else { + // For currency claims, show a gold coin + items[slot] = new ItemStack(CustomItems.coinGold); + } + } else { + items[slot] = null; + } + } + } + + public AuctionClaim getClaim(int slot) { + if (slot >= 0 && slot < size) { + return claims.get(slot); + } + return null; + } + + public void clear() { + for (int i = 0; i < size; i++) { + items[i] = null; + listings.set(i, null); + claims.set(i, null); + } + } + + @Override + public int getSizeInventory() { + return size; + } + + @Override + public ItemStack getStackInSlot(int slot) { + if (slot >= 0 && slot < size) { + return items[slot]; + } + return null; + } + + @Override + public ItemStack decrStackSize(int slot, int amount) { + return null; // Display only + } + + @Override + public ItemStack getStackInSlotOnClosing(int slot) { + return null; + } + + @Override + public void setInventorySlotContents(int slot, ItemStack stack) { + if (slot >= 0 && slot < size) { + items[slot] = stack; + } + } + + @Override + public String getInventoryName() { + return "Auction Display"; + } + + @Override + public boolean hasCustomInventoryName() { + return false; + } + + @Override + public int getInventoryStackLimit() { + return 64; + } + + @Override + public void markDirty() { + } + + @Override + public boolean isUseableByPlayer(EntityPlayer player) { + return true; + } + + @Override + public void openInventory() { + } + + @Override + public void closeInventory() { + } + + @Override + public boolean isItemValidForSlot(int slot, ItemStack stack) { + return false; + } +} diff --git a/src/main/java/noppes/npcs/containers/SlotAuctionDisplay.java b/src/main/java/noppes/npcs/containers/SlotAuctionDisplay.java new file mode 100644 index 000000000..43b745051 --- /dev/null +++ b/src/main/java/noppes/npcs/containers/SlotAuctionDisplay.java @@ -0,0 +1,31 @@ +package noppes.npcs.containers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.inventory.IInventory; +import net.minecraft.inventory.Slot; +import net.minecraft.item.ItemStack; + +/** + * A display-only slot for auction listings. + * Items cannot be placed or taken from this slot. + */ +public class SlotAuctionDisplay extends Slot { + public SlotAuctionDisplay(IInventory inventory, int slotIndex, int x, int y) { + super(inventory, slotIndex, x, y); + } + + @Override + public boolean isItemValid(ItemStack stack) { + return false; + } + + @Override + public boolean canTakeStack(EntityPlayer player) { + return false; + } + + @Override + public int getSlotStackLimit() { + return 64; + } +} diff --git a/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java b/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java new file mode 100644 index 000000000..58e4b8d86 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java @@ -0,0 +1,49 @@ +package noppes.npcs.controllers; + +import kamkeel.npcs.util.VaultUtil; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.config.ConfigMarket; + +/** + * Server-side utility for syncing auction configuration to clients. + * Handles Vault integration for currency name resolution. + * + * This class is SERVER-SIDE ONLY. Client reads via AuctionClientConfig. + */ +public class AuctionConfigSync { + + /** + * Write auction config to NBT for sending to client. + * Called on server side when building login packet. + * + * Currency name is resolved from Vault if enabled and available, + * otherwise falls back to ConfigMarket.CurrencyName. + */ + public static NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setBoolean("AuctionEnabled", ConfigMarket.AuctionEnabled); + compound.setLong("ListingFee", ConfigMarket.ListingFee); + compound.setString("CurrencyName", getEffectiveCurrencyName()); + compound.setInteger("AuctionDurationHours", ConfigMarket.AuctionDurationHours); + compound.setDouble("MinBidIncrement", ConfigMarket.MinBidIncrementPercent); + compound.setInteger("MaxActiveListings", ConfigMarket.DefaultMaxListings); + compound.setInteger("ClaimExpirationDays", ConfigMarket.ClaimExpirationDays); + return compound; + } + + /** + * Get the effective currency name for display. + * Uses Vault currency name if Vault is enabled and available, + * otherwise uses ConfigMarket.CurrencyName. + * + * @return The currency name to display + */ + public static String getEffectiveCurrencyName() { + if (ConfigMarket.UseVault && VaultUtil.isEnabled()) { + String vaultName = VaultUtil.getCurrencyNamePlural(); + if (vaultName != null && !vaultName.isEmpty() && !vaultName.equals("coins")) { + return vaultName; + } + } + return ConfigMarket.CurrencyName; + } +} diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java new file mode 100644 index 000000000..92f790b99 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -0,0 +1,1243 @@ +package noppes.npcs.controllers; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.CompressedStreamTools; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.EnumChatFormatting; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumAuctionSort; +import noppes.npcs.constants.EnumAuctionStatus; +import noppes.npcs.constants.EnumClaimType; +import noppes.npcs.constants.EnumNotificationType; +import noppes.npcs.controllers.data.AuctionClaim; +import noppes.npcs.controllers.data.AuctionFilter; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.controllers.data.AuctionNotification; +import noppes.npcs.controllers.data.PlayerCurrencyData; +import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.util.CustomNPCsThreader; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Main controller for the Auction House system. + * + * Thread Safety: + * - All data modifications go through synchronized methods or use proper locking + * - Player indices are maintained for O(1) lookups + * - Saves are performed asynchronously on the CNPC+ thread + * + * Performance: + * - Auction processing runs every 20 seconds (400 ticks) to reduce server load + * - Player-specific queries use indexed maps for fast lookups + * - Read operations use read locks, write operations use write locks + */ +public class AuctionController { + public static AuctionController Instance; + + // Main data stores + private final Map listings = new ConcurrentHashMap<>(); + private final Map claims = new ConcurrentHashMap<>(); + private final List pendingNotifications = Collections.synchronizedList(new ArrayList<>()); + + // ========================================= + // Player Indices for O(1) Lookups + // ========================================= + + /** Listings by seller UUID - for quick "my listings" lookups */ + private final Map> playerListingIds = new ConcurrentHashMap<>(); + + /** Listings where player is current high bidder */ + private final Map> playerBidIds = new ConcurrentHashMap<>(); + + /** Claims by player UUID - for quick "my claims" lookups */ + private final Map> playerClaimIds = new ConcurrentHashMap<>(); + + // ========================================= + // Threading & State + // ========================================= + + /** Lock for data modifications - allows concurrent reads */ + private final ReentrantReadWriteLock dataLock = new ReentrantReadWriteLock(); + + /** Flag indicating data needs to be saved */ + private final AtomicBoolean dirty = new AtomicBoolean(false); + + /** Flag to prevent concurrent saves */ + private final AtomicBoolean saving = new AtomicBoolean(false); + + private String filePath = ""; + private int tickCounter = 0; + + /** Process auctions every 20 seconds (400 ticks) for performance */ + private static final int PROCESS_INTERVAL_TICKS = 400; + + /** Save dirty data every 60 seconds (1200 ticks) */ + private static final int SAVE_INTERVAL_TICKS = 1200; + private int saveTickCounter = 0; + + public AuctionController() { + Instance = this; + load(); + } + + public static AuctionController getInstance() { + if (Instance == null || needsNewInstance()) { + Instance = new AuctionController(); + } + return Instance; + } + + private static boolean needsNewInstance() { + if (Instance == null) return true; + File file = CustomNpcs.getWorldSaveDirectory(); + if (file == null) return false; + return !Instance.filePath.equals(file.getAbsolutePath()); + } + + // ========================================= + // Tick Processing + // ========================================= + + public void onServerTick() { + if (!ConfigMarket.AuctionEnabled) return; + + tickCounter++; + saveTickCounter++; + + // Process ended auctions every 20 seconds + if (tickCounter >= PROCESS_INTERVAL_TICKS) { + tickCounter = 0; + processEndedAuctions(); + processExpiredClaims(); + } + + // Save dirty data periodically + if (saveTickCounter >= SAVE_INTERVAL_TICKS) { + saveTickCounter = 0; + if (dirty.get()) { + saveAsync(); + } + } + } + + private void processEndedAuctions() { + dataLock.writeLock().lock(); + try { + boolean changed = false; + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) { + endAuction(listing); + changed = true; + } + } + if (changed) { + markDirty(); + } + } finally { + dataLock.writeLock().unlock(); + } + } + + private void endAuction(AuctionListing listing) { + // Must be called within write lock + listing.status = EnumAuctionStatus.ENDED; + + // Remove from seller's active listings + removeFromPlayerListings(listing.sellerUUID, listing.id); + + // Remove from bidder's active bids if any + if (listing.highBidderUUID != null) { + removeFromPlayerBids(listing.highBidderUUID, listing.id); + } + + if (listing.hasBids()) { + // Auction sold - create claims + AuctionClaim itemClaim = AuctionClaim.createItemClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.item); + addClaim(itemClaim); + + long saleAmount = listing.currentBid; + long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent); + long sellerReceives = saleAmount - tax; + + AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( + listing.sellerUUID, listing.sellerName, listing.id, sellerReceives); + addClaim(currencyClaim); + + // Notifications + queueNotification(listing.highBidderUUID, EnumNotificationType.AUCTION_WON, listing.id, + "You won the auction for " + listing.item.getDisplayName() + "!"); + queueNotification(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, + "Your " + listing.item.getDisplayName() + " sold for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); + + logAuction("SOLD", listing.sellerName, listing.item.getDisplayName(), saleAmount, + "Winner: " + listing.highBidderName + ", Tax: " + tax); + } else { + // No bids - return item to seller + AuctionClaim returnClaim = AuctionClaim.createItemClaim( + listing.sellerUUID, listing.sellerName, listing.id, listing.item); + addClaim(returnClaim); + + queueNotification(listing.sellerUUID, EnumNotificationType.AUCTION_EXPIRED, listing.id, + "Your auction for " + listing.item.getDisplayName() + " expired with no bids."); + + logAuction("EXPIRED", listing.sellerName, listing.item.getDisplayName(), listing.startingPrice, "No bids"); + } + } + + private void processExpiredClaims() { + int expirationDays = ConfigMarket.ClaimExpirationDays; + + dataLock.writeLock().lock(); + try { + Iterator> iterator = claims.entrySet().iterator(); + boolean changed = false; + + while (iterator.hasNext()) { + AuctionClaim claim = iterator.next().getValue(); + if (!claim.claimed && claim.isExpired(expirationDays)) { + logAuction("CLAIM_EXPIRED", claim.playerName, + claim.type.isItem() ? (claim.item != null ? claim.item.getDisplayName() : "Unknown") : "Currency", + claim.currency, "Claim expired after " + expirationDays + " days"); + + // Remove from player index + removeFromPlayerClaims(claim.playerUUID, claim.id); + iterator.remove(); + changed = true; + } + } + + if (changed) { + markDirty(); + } + } finally { + dataLock.writeLock().unlock(); + } + } + + // ========================================= + // Index Management + // ========================================= + + private void addToPlayerListings(UUID playerUUID, String listingId) { + playerListingIds.computeIfAbsent(playerUUID, k -> ConcurrentHashMap.newKeySet()).add(listingId); + } + + private void removeFromPlayerListings(UUID playerUUID, String listingId) { + Set ids = playerListingIds.get(playerUUID); + if (ids != null) { + ids.remove(listingId); + if (ids.isEmpty()) { + playerListingIds.remove(playerUUID); + } + } + } + + private void addToPlayerBids(UUID playerUUID, String listingId) { + playerBidIds.computeIfAbsent(playerUUID, k -> ConcurrentHashMap.newKeySet()).add(listingId); + } + + private void removeFromPlayerBids(UUID playerUUID, String listingId) { + Set ids = playerBidIds.get(playerUUID); + if (ids != null) { + ids.remove(listingId); + if (ids.isEmpty()) { + playerBidIds.remove(playerUUID); + } + } + } + + private void addToPlayerClaims(UUID playerUUID, String claimId) { + playerClaimIds.computeIfAbsent(playerUUID, k -> ConcurrentHashMap.newKeySet()).add(claimId); + } + + private void removeFromPlayerClaims(UUID playerUUID, String claimId) { + Set ids = playerClaimIds.get(playerUUID); + if (ids != null) { + ids.remove(claimId); + if (ids.isEmpty()) { + playerClaimIds.remove(playerUUID); + } + } + } + + /** Add a claim and update indices */ + private void addClaim(AuctionClaim claim) { + claims.put(claim.id, claim); + addToPlayerClaims(claim.playerUUID, claim.id); + } + + // ========================================= + // Listing Management + // ========================================= + + public String createListing(EntityPlayer player, ItemStack item, long startingPrice, long buyoutPrice) { + if (!ConfigMarket.AuctionEnabled) { + return "Auction House is disabled."; + } + + UUID playerUUID = player.getUniqueID(); + String playerName = player.getCommandSenderName(); + + dataLock.writeLock().lock(); + try { + // Check listing limit + int currentListings = getPlayerListingCountFast(playerUUID); + int maxListings = getMaxListingsForPlayer(player); + if (currentListings >= maxListings) { + return "You have reached your maximum listings (" + maxListings + ")."; + } + + // Check listing fee + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerCurrencyData currency = playerData.currencyData; + long fee = ConfigMarket.ListingFee; + if (!currency.canAfford(fee)) { + return "You cannot afford the listing fee (" + fee + " " + ConfigMarket.CurrencyName + ")."; + } + + // Deduct fee + if (!currency.withdraw(fee)) { + return "Failed to deduct listing fee."; + } + + // Calculate duration + long durationMs = ConfigMarket.AuctionDurationHours * 60L * 60L * 1000L; + + // Create listing + AuctionListing listing = new AuctionListing(playerUUID, playerName, item, startingPrice, buyoutPrice, durationMs); + listings.put(listing.id, listing); + addToPlayerListings(playerUUID, listing.id); + + markDirty(); + + logAuction("CREATED", playerName, item.getDisplayName(), startingPrice, + "Buyout: " + (buyoutPrice > 0 ? buyoutPrice : "None") + ", Fee: " + fee); + + return null; // Success + } finally { + dataLock.writeLock().unlock(); + } + } + + public String cancelListing(String listingId, EntityPlayer player, boolean isAdmin) { + dataLock.writeLock().lock(); + try { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; + } + + if (!listing.status.canCancel()) { + return "This auction cannot be cancelled."; + } + + UUID playerUUID = player.getUniqueID(); + if (!isAdmin && !listing.isSeller(playerUUID)) { + return "You can only cancel your own listings."; + } + + // Handle penalty if there are bids + if (listing.hasBids()) { + long penalty = (long) (listing.currentBid * ConfigMarket.CancellationPenaltyPercent); + + // Refund bidder + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid); + addClaim(refundClaim); + + // Remove from bidder's active bids + removeFromPlayerBids(listing.highBidderUUID, listing.id); + + queueNotification(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id, + "The auction for " + listing.item.getDisplayName() + " was cancelled. Your bid has been refunded."); + + // Return item to seller + AuctionClaim itemClaim = AuctionClaim.createItemClaim( + listing.sellerUUID, listing.sellerName, listing.id, listing.item); + addClaim(itemClaim); + + logAuction("CANCELLED", listing.sellerName, listing.item.getDisplayName(), listing.currentBid, + "Penalty: " + penalty + ", Bidder refunded: " + listing.highBidderName + + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); + } else { + // No bids - just return item + AuctionClaim itemClaim = AuctionClaim.createItemClaim( + listing.sellerUUID, listing.sellerName, listing.id, listing.item); + addClaim(itemClaim); + + logAuction("CANCELLED", listing.sellerName, listing.item.getDisplayName(), listing.startingPrice, + "No bids" + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); + } + + // Remove from seller's active listings + removeFromPlayerListings(listing.sellerUUID, listing.id); + + listing.status = EnumAuctionStatus.CANCELLED; + markDirty(); + + return null; // Success + } finally { + dataLock.writeLock().unlock(); + } + } + + // ========================================= + // Bidding + // ========================================= + + public String placeBid(String listingId, EntityPlayer player, long bidAmount) { + if (!ConfigMarket.AuctionEnabled) { + return "Auction House is disabled."; + } + + UUID playerUUID = player.getUniqueID(); + String playerName = player.getCommandSenderName(); + + dataLock.writeLock().lock(); + try { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; + } + + // Double-check status and expiry inside lock to prevent race conditions + if (!listing.status.canBid()) { + return "This auction has ended."; + } + + if (listing.isExpired()) { + return "This auction has ended."; + } + + // CRITICAL: Prevent bidding on own items + if (listing.isSeller(playerUUID)) { + return "You cannot bid on your own auction."; + } + + // Check minimum bid + long minBid = listing.getMinimumBid(ConfigMarket.MinBidIncrementPercent); + if (bidAmount < minBid) { + return "Bid must be at least " + minBid + " " + ConfigMarket.CurrencyName + "."; + } + + // Check currency + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerCurrencyData currency = playerData.currencyData; + if (!currency.canAfford(bidAmount)) { + return "You cannot afford this bid."; + } + + // Store previous bidder info for refund + UUID previousBidder = listing.highBidderUUID; + long previousBid = listing.currentBid; + + // Deduct bid from new bidder FIRST + if (!currency.withdraw(bidAmount)) { + return "Failed to deduct bid amount."; + } + + // Refund previous bidder if any + if (listing.hasBids() && previousBidder != null) { + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + previousBidder, listing.highBidderName, listing.id, previousBid); + addClaim(refundClaim); + + // Remove from previous bidder's active bids + removeFromPlayerBids(previousBidder, listing.id); + + queueNotification(previousBidder, EnumNotificationType.AUCTION_OUTBID, listing.id, + "You were outbid on " + listing.item.getDisplayName() + "!"); + } + + // Update listing + listing.highBidderUUID = playerUUID; + listing.highBidderName = playerName; + listing.currentBid = bidAmount; + listing.bidCount++; + + // Add to new bidder's active bids + addToPlayerBids(playerUUID, listing.id); + + // Snipe protection + listing.extendForSnipeProtection(ConfigMarket.SnipeProtectionMinutes); + + markDirty(); + + logAuction("BID", playerName, listing.item.getDisplayName(), bidAmount, + "Bids: " + listing.bidCount + ", Seller: " + listing.sellerName); + + return null; // Success + } finally { + dataLock.writeLock().unlock(); + } + } + + public String buyout(String listingId, EntityPlayer player) { + if (!ConfigMarket.AuctionEnabled) { + return "Auction House is disabled."; + } + + UUID playerUUID = player.getUniqueID(); + String playerName = player.getCommandSenderName(); + + dataLock.writeLock().lock(); + try { + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; + } + + // Double-check status and expiry inside lock + if (!listing.status.canBid()) { + return "This auction has ended."; + } + + if (listing.isExpired()) { + return "This auction has ended."; + } + + if (!listing.hasBuyout()) { + return "This auction does not have a buyout price."; + } + + // CRITICAL: Prevent buying own items + if (listing.isSeller(playerUUID)) { + return "You cannot buy your own auction."; + } + + // Check currency + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerCurrencyData currency = playerData.currencyData; + if (!currency.canAfford(listing.buyoutPrice)) { + return "You cannot afford the buyout price."; + } + + // Deduct buyout FIRST + if (!currency.withdraw(listing.buyoutPrice)) { + return "Failed to deduct buyout amount."; + } + + // Refund previous bidder if any + if (listing.hasBids() && listing.highBidderUUID != null) { + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid); + addClaim(refundClaim); + + // Remove from previous bidder's active bids + removeFromPlayerBids(listing.highBidderUUID, listing.id); + + queueNotification(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id, + "The auction for " + listing.item.getDisplayName() + " was bought out. Your bid has been refunded."); + } + + // Create claims + AuctionClaim itemClaim = AuctionClaim.createItemClaim(playerUUID, playerName, listing.id, listing.item); + addClaim(itemClaim); + + long saleAmount = listing.buyoutPrice; + long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent); + long sellerReceives = saleAmount - tax; + + AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( + listing.sellerUUID, listing.sellerName, listing.id, sellerReceives); + addClaim(currencyClaim); + + // Notifications + queueNotification(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, + "Your " + listing.item.getDisplayName() + " was bought out for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); + + // Remove from seller's active listings + removeFromPlayerListings(listing.sellerUUID, listing.id); + + listing.status = EnumAuctionStatus.ENDED; + listing.highBidderUUID = playerUUID; + listing.highBidderName = playerName; + listing.currentBid = listing.buyoutPrice; + + markDirty(); + + logAuction("BUYOUT", playerName, listing.item.getDisplayName(), listing.buyoutPrice, + "Seller: " + listing.sellerName + ", Tax: " + tax); + + return null; // Success + } finally { + dataLock.writeLock().unlock(); + } + } + + // ========================================= + // Claims + // ========================================= + + /** + * Get player claims using indexed lookup - O(n) where n = player's claims, not all claims + */ + public List getPlayerClaims(UUID playerUUID) { + Set claimIds = playerClaimIds.get(playerUUID); + if (claimIds == null || claimIds.isEmpty()) { + return new ArrayList<>(); + } + + List playerClaims = new ArrayList<>(); + for (String claimId : claimIds) { + AuctionClaim claim = claims.get(claimId); + if (claim != null && !claim.claimed) { + playerClaims.add(claim); + } + } + return playerClaims; + } + + public String claimItem(String claimId, EntityPlayer player) { + dataLock.writeLock().lock(); + try { + AuctionClaim claim = claims.get(claimId); + if (claim == null) { + return "Claim not found."; + } + + if (claim.claimed) { + return "Already claimed."; + } + + if (!claim.isForPlayer(player.getUniqueID())) { + return "This claim is not for you."; + } + + if (!claim.type.isItem()) { + return "This is not an item claim."; + } + + if (claim.item == null) { + return "Item data is missing."; + } + + // Check inventory space + if (!player.inventory.addItemStackToInventory(claim.item.copy())) { + return "Your inventory is full."; + } + + claim.claimed = true; + removeFromPlayerClaims(claim.playerUUID, claim.id); + markDirty(); + + logAuction("CLAIMED", player.getCommandSenderName(), claim.item.getDisplayName(), 0, "Item claimed"); + + return null; // Success + } finally { + dataLock.writeLock().unlock(); + } + } + + public String claimCurrency(String claimId, EntityPlayer player) { + dataLock.writeLock().lock(); + try { + AuctionClaim claim = claims.get(claimId); + if (claim == null) { + return "Claim not found."; + } + + if (claim.claimed) { + return "Already claimed."; + } + + if (!claim.isForPlayer(player.getUniqueID())) { + return "This claim is not for you."; + } + + if (!claim.type.isCurrency()) { + return "This is not a currency claim."; + } + + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + if (!playerData.currencyData.deposit(claim.currency)) { + return "Failed to deposit currency."; + } + + claim.claimed = true; + removeFromPlayerClaims(claim.playerUUID, claim.id); + markDirty(); + + logAuction("CLAIMED", player.getCommandSenderName(), ConfigMarket.CurrencyName, claim.currency, + claim.type == EnumClaimType.REFUND ? "Refund claimed" : "Sale proceeds claimed"); + + return null; // Success + } finally { + dataLock.writeLock().unlock(); + } + } + + public int claimAll(EntityPlayer player) { + int claimedCount = 0; + List playerClaims = getPlayerClaims(player.getUniqueID()); + + for (AuctionClaim claim : playerClaims) { + String result; + if (claim.type.isItem()) { + result = claimItem(claim.id, player); + } else { + result = claimCurrency(claim.id, player); + } + if (result == null) { + claimedCount++; + } + } + + return claimedCount; + } + + // ========================================= + // Queries - Using Indexed Lookups + // ========================================= + + public List getActiveListings(AuctionFilter filter, int page, int pageSize) { + List result = new ArrayList<>(); + + dataLock.readLock().lock(); + try { + for (AuctionListing listing : listings.values()) { + if (listing.status != EnumAuctionStatus.ACTIVE) continue; + + // Apply search filter + if (filter.hasSearchText()) { + String itemName = listing.item != null ? listing.item.getDisplayName() : ""; + if (!filter.matchesSearch(itemName)) continue; + } + + result.add(listing); + } + } finally { + dataLock.readLock().unlock(); + } + + // Sort + sortListings(result, filter.sortBy); + + // Paginate + int start = page * pageSize; + int end = Math.min(start + pageSize, result.size()); + if (start >= result.size()) { + return new ArrayList<>(); + } + return new ArrayList<>(result.subList(start, end)); + } + + private void sortListings(List list, EnumAuctionSort sortBy) { + Comparator comparator; + switch (sortBy) { + case ENDING_SOON: + comparator = Comparator.comparingLong(AuctionListing::getTimeRemaining); + break; + case PRICE_LOW: + comparator = Comparator.comparingLong(AuctionListing::getEffectivePrice); + break; + case PRICE_HIGH: + comparator = Comparator.comparingLong(AuctionListing::getEffectivePrice).reversed(); + break; + case MOST_BIDS: + comparator = Comparator.comparingInt(l -> -l.bidCount); + break; + case NEWEST: + default: + comparator = Comparator.comparingLong(l -> -l.createdTime); + break; + } + list.sort(comparator); + } + + public int getTotalActiveListings(AuctionFilter filter) { + int count = 0; + dataLock.readLock().lock(); + try { + for (AuctionListing listing : listings.values()) { + if (listing.status != EnumAuctionStatus.ACTIVE) continue; + if (filter.hasSearchText()) { + String itemName = listing.item != null ? listing.item.getDisplayName() : ""; + if (!filter.matchesSearch(itemName)) continue; + } + count++; + } + } finally { + dataLock.readLock().unlock(); + } + return count; + } + + public AuctionListing getListing(String listingId) { + return listings.get(listingId); + } + + /** + * Get a player's current currency balance. + */ + public long getPlayerBalance(EntityPlayer player) { + PlayerData playerData = PlayerData.get(player); + if (playerData == null || playerData.currencyData == null) { + return 0; + } + return playerData.currencyData.getBalance(); + } + + /** + * Fast O(1) lookup for player listing count using index + */ + public int getPlayerListingCountFast(UUID playerUUID) { + Set ids = playerListingIds.get(playerUUID); + return ids != null ? ids.size() : 0; + } + + /** + * Legacy method - still iterates for accuracy (in case index is stale) + */ + public int getPlayerListingCount(UUID playerUUID) { + return getPlayerListingCountFast(playerUUID); + } + + /** + * Get player's active listings using indexed lookup + */ + public List getPlayerActiveListings(UUID playerUUID) { + Set listingIds = playerListingIds.get(playerUUID); + if (listingIds == null || listingIds.isEmpty()) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + for (String listingId : listingIds) { + AuctionListing listing = listings.get(listingId); + if (listing != null && listing.status == EnumAuctionStatus.ACTIVE) { + result.add(listing); + } + } + return result; + } + + /** + * Get all active auctions where the player is currently the highest bidder. + * Uses indexed lookup for O(1) access to player's bid set. + */ + public List getPlayerActiveBids(UUID playerUUID) { + Set bidListingIds = playerBidIds.get(playerUUID); + if (bidListingIds == null || bidListingIds.isEmpty()) { + return new ArrayList<>(); + } + + List result = new ArrayList<>(); + for (String listingId : bidListingIds) { + AuctionListing listing = listings.get(listingId); + if (listing != null && listing.status == EnumAuctionStatus.ACTIVE && + listing.highBidderUUID != null && listing.highBidderUUID.equals(playerUUID)) { + result.add(listing); + } + } + return result; + } + + /** + * Get the total number of trade slots used by a player. + * Uses indexed lookups for O(1) performance. + */ + public int getPlayerTradeSlotCount(UUID playerUUID) { + int count = 0; + // Active listings + Set listingIds = playerListingIds.get(playerUUID); + if (listingIds != null) count += listingIds.size(); + // Active bids + Set bidIds = playerBidIds.get(playerUUID); + if (bidIds != null) count += bidIds.size(); + // Pending claims + Set claimIds = playerClaimIds.get(playerUUID); + if (claimIds != null) count += claimIds.size(); + return count; + } + + private int getMaxListingsForPlayer(EntityPlayer player) { + // TODO: Check permissions for customnpcs.auction.slots.X + return ConfigMarket.DefaultMaxListings; + } + + // ========================================= + // Notifications + // ========================================= + + private void queueNotification(UUID playerUUID, EnumNotificationType type, String listingId, String message) { + AuctionNotification notification = new AuctionNotification(playerUUID, type, listingId, message); + pendingNotifications.add(notification); + + // Try to send immediately if player is online + EntityPlayerMP player = getOnlinePlayer(playerUUID); + if (player != null) { + sendNotification(player, notification); + notification.sent = true; + } + } + + public void onPlayerLogin(EntityPlayer player) { + UUID playerUUID = player.getUniqueID(); + boolean hasClaims = !getPlayerClaims(playerUUID).isEmpty(); + + synchronized (pendingNotifications) { + Iterator iterator = pendingNotifications.iterator(); + while (iterator.hasNext()) { + AuctionNotification notification = iterator.next(); + if (notification.isForPlayer(playerUUID) && !notification.sent) { + sendNotification((EntityPlayerMP) player, notification); + notification.sent = true; + iterator.remove(); + } + } + } + + if (hasClaims) { + player.addChatMessage(new ChatComponentText(EnumChatFormatting.GOLD + + "[Auction House] " + EnumChatFormatting.YELLOW + "You have items or currency to claim!")); + } + } + + private void sendNotification(EntityPlayerMP player, AuctionNotification notification) { + player.addChatMessage(new ChatComponentText(EnumChatFormatting.GOLD + + "[Auction House] " + EnumChatFormatting.WHITE + notification.message)); + } + + private EntityPlayerMP getOnlinePlayer(UUID uuid) { + MinecraftServer server = MinecraftServer.getServer(); + if (server == null) return null; + + for (Object obj : server.getConfigurationManager().playerEntityList) { + EntityPlayerMP player = (EntityPlayerMP) obj; + if (player.getUniqueID().equals(uuid)) { + return player; + } + } + return null; + } + + // ========================================= + // Logging + // ========================================= + + private void logAuction(String action, String playerName, String itemName, long amount, String details) { + if (!ConfigMarket.AuctionLoggingEnabled) return; + + boolean shouldLog = false; + switch (action) { + case "CREATED": + shouldLog = ConfigMarket.LogAuctionCreated; + break; + case "BID": + shouldLog = ConfigMarket.LogAuctionBid; + break; + case "BUYOUT": + shouldLog = ConfigMarket.LogAuctionBuyout; + break; + case "SOLD": + shouldLog = ConfigMarket.LogAuctionSold; + break; + case "EXPIRED": + shouldLog = ConfigMarket.LogAuctionExpired; + break; + case "CANCELLED": + shouldLog = ConfigMarket.LogAuctionCancelled; + break; + case "CLAIMED": + case "CLAIM_EXPIRED": + shouldLog = ConfigMarket.LogAuctionClaimed; + break; + } + + if (shouldLog) { + String logMessage = String.format("[AUCTION:%s] Player: %s, Item: %s, Amount: %d, Details: %s", + action, playerName, itemName, amount, details); + LogWriter.info(logMessage); + } + } + + // ========================================= + // Save / Load - Async on CNPC+ Thread + // ========================================= + + /** Mark data as dirty - will be saved on next save cycle */ + private void markDirty() { + dirty.set(true); + } + + /** Save synchronously - used for server shutdown */ + public void save() { + if (!dirty.compareAndSet(true, false)) { + return; // Nothing to save + } + saveInternal(); + } + + /** Save asynchronously on CNPC+ thread */ + public void saveAsync() { + if (!dirty.compareAndSet(true, false)) { + return; // Nothing to save + } + + if (!saving.compareAndSet(false, true)) { + // Another save is in progress, mark dirty again + dirty.set(true); + return; + } + + // Take a snapshot of the data for async save + final NBTTagCompound compound; + dataLock.readLock().lock(); + try { + compound = writeToNBT(new NBTTagCompound()); + } finally { + dataLock.readLock().unlock(); + } + + CustomNPCsThreader.customNPCThread.execute(() -> { + try { + saveToFile(compound); + } catch (Exception e) { + LogWriter.error("Error saving auction data asynchronously", e); + // Mark dirty again so we retry + dirty.set(true); + } finally { + saving.set(false); + } + }); + } + + /** Internal synchronous save */ + private void saveInternal() { + dataLock.readLock().lock(); + try { + NBTTagCompound compound = writeToNBT(new NBTTagCompound()); + saveToFile(compound); + } catch (Exception e) { + LogWriter.error("Error saving auction data", e); + } finally { + dataLock.readLock().unlock(); + } + } + + private void saveToFile(NBTTagCompound compound) { + try { + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) return; + + File fileNew = new File(saveDir, "auction.dat_new"); + File fileOld = new File(saveDir, "auction.dat_old"); + File fileCurrent = new File(saveDir, "auction.dat"); + + CompressedStreamTools.writeCompressed(compound, new FileOutputStream(fileNew)); + + // Backup rotation + if (fileOld.exists()) fileOld.delete(); + if (fileCurrent.exists()) fileCurrent.renameTo(fileOld); + if (fileCurrent.exists()) fileCurrent.delete(); + fileNew.renameTo(fileCurrent); + if (fileNew.exists()) fileNew.delete(); + + } catch (Exception e) { + LogWriter.error("Error saving auction data to file", e); + } + } + + public void load() { + listings.clear(); + claims.clear(); + pendingNotifications.clear(); + playerListingIds.clear(); + playerBidIds.clear(); + playerClaimIds.clear(); + + File saveDir = CustomNpcs.getWorldSaveDirectory(); + if (saveDir == null) return; + filePath = saveDir.getAbsolutePath(); + + // Try current file + try { + File file = new File(saveDir, "auction.dat"); + if (file.exists()) { + loadFromFile(file); + rebuildIndices(); + return; + } + } catch (Exception e) { + LogWriter.error("Error loading auction.dat, trying backup", e); + } + + // Try backup file + try { + File file = new File(saveDir, "auction.dat_old"); + if (file.exists()) { + loadFromFile(file); + rebuildIndices(); + LogWriter.info("Loaded auction data from backup file"); + } + } catch (Exception e) { + LogWriter.error("Error loading auction.dat_old", e); + } + } + + /** Rebuild all indices from loaded data */ + private void rebuildIndices() { + playerListingIds.clear(); + playerBidIds.clear(); + playerClaimIds.clear(); + + // Index listings + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE) { + addToPlayerListings(listing.sellerUUID, listing.id); + if (listing.highBidderUUID != null) { + addToPlayerBids(listing.highBidderUUID, listing.id); + } + } + } + + // Index claims + for (AuctionClaim claim : claims.values()) { + if (!claim.claimed) { + addToPlayerClaims(claim.playerUUID, claim.id); + } + } + + LogWriter.info("Rebuilt auction indices: " + playerListingIds.size() + " sellers, " + + playerBidIds.size() + " bidders, " + playerClaimIds.size() + " claimants"); + } + + private void loadFromFile(File file) throws Exception { + NBTTagCompound compound = CompressedStreamTools.readCompressed(new FileInputStream(file)); + readFromNBT(compound); + } + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + // Listings + NBTTagList listingsList = new NBTTagList(); + for (AuctionListing listing : listings.values()) { + listingsList.appendTag(listing.writeToNBT(new NBTTagCompound())); + } + compound.setTag("Listings", listingsList); + + // Claims + NBTTagList claimsList = new NBTTagList(); + for (AuctionClaim claim : claims.values()) { + claimsList.appendTag(claim.writeToNBT(new NBTTagCompound())); + } + compound.setTag("Claims", claimsList); + + // Pending Notifications + NBTTagList notificationsList = new NBTTagList(); + synchronized (pendingNotifications) { + for (AuctionNotification notification : pendingNotifications) { + if (!notification.sent) { + notificationsList.appendTag(notification.writeToNBT(new NBTTagCompound())); + } + } + } + compound.setTag("PendingNotifications", notificationsList); + + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + // Listings + NBTTagList listingsList = compound.getTagList("Listings", 10); + for (int i = 0; i < listingsList.tagCount(); i++) { + AuctionListing listing = AuctionListing.fromNBT(listingsList.getCompoundTagAt(i)); + listings.put(listing.id, listing); + } + + // Claims + NBTTagList claimsList = compound.getTagList("Claims", 10); + for (int i = 0; i < claimsList.tagCount(); i++) { + AuctionClaim claim = AuctionClaim.fromNBT(claimsList.getCompoundTagAt(i)); + claims.put(claim.id, claim); + } + + // Pending Notifications + NBTTagList notificationsList = compound.getTagList("PendingNotifications", 10); + for (int i = 0; i < notificationsList.tagCount(); i++) { + AuctionNotification notification = AuctionNotification.fromNBT(notificationsList.getCompoundTagAt(i)); + pendingNotifications.add(notification); + } + } + + // ========================================= + // Admin Methods + // ========================================= + + public List getAllListings() { + return new ArrayList<>(listings.values()); + } + + public void adminCancelListing(String listingId, EntityPlayer admin, String reason) { + cancelListing(listingId, admin, true); + } + + public void clearClaimedData() { + dataLock.writeLock().lock(); + try { + // Remove claimed claims + claims.entrySet().removeIf(entry -> entry.getValue().claimed); + + // Remove ended listings that have no unclaimed claims + listings.entrySet().removeIf(entry -> + entry.getValue().status == EnumAuctionStatus.CLAIMED || + (entry.getValue().status == EnumAuctionStatus.ENDED && + !hasUnclaimedClaims(entry.getValue().id))); + + // Rebuild indices + rebuildIndices(); + markDirty(); + } finally { + dataLock.writeLock().unlock(); + } + } + + private boolean hasUnclaimedClaims(String listingId) { + for (AuctionClaim claim : claims.values()) { + if (listingId.equals(claim.listingId) && !claim.claimed) { + return true; + } + } + return false; + } + + /** + * Check if a player is the seller of a listing. + * Used for GUI visibility checks. + */ + public boolean isPlayerSeller(String listingId, UUID playerUUID) { + AuctionListing listing = listings.get(listingId); + return listing != null && listing.isSeller(playerUUID); + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java b/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java new file mode 100644 index 000000000..7a4e45a5d --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java @@ -0,0 +1,120 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.constants.EnumClaimType; + +import java.util.UUID; + +public class AuctionClaim { + public String id; + public UUID playerUUID; + public String playerName; + public String listingId; + public EnumClaimType type; + public ItemStack item; + public long currency; + public long createdTime; + public boolean claimed; + + public AuctionClaim() { + this.id = UUID.randomUUID().toString(); + this.createdTime = System.currentTimeMillis(); + this.claimed = false; + } + + public static AuctionClaim createItemClaim(UUID playerUUID, String playerName, String listingId, ItemStack item) { + AuctionClaim claim = new AuctionClaim(); + claim.playerUUID = playerUUID; + claim.playerName = playerName; + claim.listingId = listingId; + claim.type = EnumClaimType.ITEM; + claim.item = item != null ? item.copy() : null; + claim.currency = 0; + return claim; + } + + public static AuctionClaim createCurrencyClaim(UUID playerUUID, String playerName, String listingId, long currency) { + AuctionClaim claim = new AuctionClaim(); + claim.playerUUID = playerUUID; + claim.playerName = playerName; + claim.listingId = listingId; + claim.type = EnumClaimType.CURRENCY; + claim.item = null; + claim.currency = currency; + return claim; + } + + public static AuctionClaim createRefundClaim(UUID playerUUID, String playerName, String listingId, long currency) { + AuctionClaim claim = new AuctionClaim(); + claim.playerUUID = playerUUID; + claim.playerName = playerName; + claim.listingId = listingId; + claim.type = EnumClaimType.REFUND; + claim.item = null; + claim.currency = currency; + return claim; + } + + public boolean isExpired(int expirationDays) { + long expirationMs = expirationDays * 24L * 60L * 60L * 1000L; + return System.currentTimeMillis() - createdTime > expirationMs; + } + + public long getDaysUntilExpiration(int expirationDays) { + long expirationMs = expirationDays * 24L * 60L * 60L * 1000L; + long remaining = (createdTime + expirationMs) - System.currentTimeMillis(); + return Math.max(0, remaining / (24L * 60L * 60L * 1000L)); + } + + public boolean isForPlayer(UUID uuid) { + return playerUUID != null && playerUUID.equals(uuid); + } + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString("ID", id); + compound.setString("PlayerUUID", playerUUID != null ? playerUUID.toString() : ""); + compound.setString("PlayerName", playerName != null ? playerName : ""); + compound.setString("ListingID", listingId != null ? listingId : ""); + compound.setInteger("Type", type.ordinal()); + + if (item != null) { + NBTTagCompound itemTag = new NBTTagCompound(); + item.writeToNBT(itemTag); + compound.setTag("Item", itemTag); + } + + compound.setLong("Currency", currency); + compound.setLong("CreatedTime", createdTime); + compound.setBoolean("Claimed", claimed); + + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + id = compound.getString("ID"); + + String playerUUIDStr = compound.getString("PlayerUUID"); + playerUUID = playerUUIDStr.isEmpty() ? null : UUID.fromString(playerUUIDStr); + playerName = compound.getString("PlayerName"); + + listingId = compound.getString("ListingID"); + type = EnumClaimType.fromOrdinal(compound.getInteger("Type")); + + if (compound.hasKey("Item")) { + item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); + } else { + item = null; + } + + currency = compound.getLong("Currency"); + createdTime = compound.getLong("CreatedTime"); + claimed = compound.getBoolean("Claimed"); + } + + public static AuctionClaim fromNBT(NBTTagCompound compound) { + AuctionClaim claim = new AuctionClaim(); + claim.readFromNBT(compound); + return claim; + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java new file mode 100644 index 000000000..49ed0a29c --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java @@ -0,0 +1,102 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.EnumChatFormatting; +import noppes.npcs.constants.EnumAuctionSort; + +import java.util.regex.Pattern; + +/** + * Filter for auction listings. + * Supports search by item name and sorting options. + */ +public class AuctionFilter { + private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("§[0-9a-fk-or]", Pattern.CASE_INSENSITIVE); + + public String searchText; + public EnumAuctionSort sortBy; + + // Cached normalized search for efficiency + private String normalizedSearch; + + public AuctionFilter() { + this.searchText = ""; + this.sortBy = EnumAuctionSort.NEWEST; + this.normalizedSearch = ""; + } + + public boolean hasSearchText() { + return searchText != null && !searchText.trim().isEmpty(); + } + + /** Normalize string for search (strip colors, lowercase, trim) */ + public static String normalizeForSearch(String input) { + if (input == null || input.isEmpty()) return ""; + String stripped = COLOR_CODE_PATTERN.matcher(input).replaceAll(""); + stripped = EnumChatFormatting.getTextWithoutFormattingCodes(stripped); + return stripped.toLowerCase().trim(); + } + + /** Check if item name matches search text */ + public boolean matchesSearch(String itemName) { + if (!hasSearchText()) return true; + String normalized = normalizeForSearch(itemName); + return normalized.contains(normalizedSearch); + } + + /** Advanced search - all words must be present */ + public boolean matchesSearchAdvanced(String itemName) { + if (!hasSearchText()) return true; + String normalized = normalizeForSearch(itemName); + String[] words = normalizedSearch.split("\\s+"); + for (String word : words) { + if (!word.isEmpty() && !normalized.contains(word)) { + return false; + } + } + return true; + } + + public void reset() { + searchText = ""; + sortBy = EnumAuctionSort.NEWEST; + normalizedSearch = ""; + } + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString("SearchText", searchText != null ? searchText : ""); + compound.setInteger("SortBy", sortBy.ordinal()); + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + searchText = compound.getString("SearchText"); + normalizedSearch = normalizeForSearch(searchText); + sortBy = EnumAuctionSort.fromOrdinal(compound.getInteger("SortBy")); + } + + public static AuctionFilter fromNBT(NBTTagCompound compound) { + AuctionFilter filter = new AuctionFilter(); + filter.readFromNBT(compound); + return filter; + } + + public AuctionFilter copy() { + AuctionFilter copy = new AuctionFilter(); + copy.searchText = this.searchText; + copy.normalizedSearch = this.normalizedSearch; + copy.sortBy = this.sortBy; + return copy; + } + + // Getters and setters + public String getSearchText() { return searchText; } + + public void setSearchText(String searchText) { + this.searchText = searchText; + this.normalizedSearch = normalizeForSearch(searchText); + } + + public EnumAuctionSort getSortBy() { return sortBy; } + public void setSortBy(EnumAuctionSort sortBy) { this.sortBy = sortBy; } +} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java new file mode 100644 index 000000000..3106a9fb2 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java @@ -0,0 +1,147 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.constants.EnumAuctionStatus; + +import java.util.UUID; + +public class AuctionListing { + public String id; + public UUID sellerUUID; + public String sellerName; + public ItemStack item; + public long startingPrice; + public long buyoutPrice; + public long currentBid; + public UUID highBidderUUID; + public String highBidderName; + public int bidCount; + public long createdTime; + public long endTime; + public EnumAuctionStatus status; + + public AuctionListing() { + this.id = UUID.randomUUID().toString(); + this.status = EnumAuctionStatus.ACTIVE; + this.bidCount = 0; + this.currentBid = 0; + this.createdTime = System.currentTimeMillis(); + } + + public AuctionListing(UUID sellerUUID, String sellerName, ItemStack item, long startingPrice, long buyoutPrice, long durationMs) { + this(); + this.sellerUUID = sellerUUID; + this.sellerName = sellerName; + this.item = item.copy(); + this.startingPrice = startingPrice; + this.buyoutPrice = buyoutPrice; + this.currentBid = 0; + this.endTime = this.createdTime + durationMs; + } + + public boolean hasBids() { + return bidCount > 0 && highBidderUUID != null; + } + + public boolean hasBuyout() { + return buyoutPrice > 0; + } + + public boolean isExpired() { + return System.currentTimeMillis() >= endTime; + } + + public boolean isActive() { + return status == EnumAuctionStatus.ACTIVE && !isExpired(); + } + + public long getTimeRemaining() { + return Math.max(0, endTime - System.currentTimeMillis()); + } + + public long getEffectivePrice() { + return hasBids() ? currentBid : startingPrice; + } + + public long getMinimumBid(double minIncrementPercent) { + if (!hasBids()) { + return startingPrice; + } + long increment = (long) Math.ceil(currentBid * minIncrementPercent); + return currentBid + Math.max(1, increment); + } + + public boolean isSeller(UUID playerUUID) { + return sellerUUID != null && sellerUUID.equals(playerUUID); + } + + public boolean isHighBidder(UUID playerUUID) { + return highBidderUUID != null && highBidderUUID.equals(playerUUID); + } + + public void extendForSnipeProtection(int snipeProtectionMinutes) { + long snipeProtectionMs = snipeProtectionMinutes * 60 * 1000L; + long timeRemaining = getTimeRemaining(); + if (timeRemaining < snipeProtectionMs) { + endTime = System.currentTimeMillis() + snipeProtectionMs; + } + } + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString("ID", id); + compound.setString("SellerUUID", sellerUUID != null ? sellerUUID.toString() : ""); + compound.setString("SellerName", sellerName != null ? sellerName : ""); + + if (item != null) { + NBTTagCompound itemTag = new NBTTagCompound(); + item.writeToNBT(itemTag); + compound.setTag("Item", itemTag); + } + + compound.setLong("StartingPrice", startingPrice); + compound.setLong("BuyoutPrice", buyoutPrice); + compound.setLong("CurrentBid", currentBid); + + compound.setString("HighBidderUUID", highBidderUUID != null ? highBidderUUID.toString() : ""); + compound.setString("HighBidderName", highBidderName != null ? highBidderName : ""); + + compound.setInteger("BidCount", bidCount); + compound.setLong("CreatedTime", createdTime); + compound.setLong("EndTime", endTime); + compound.setInteger("Status", status.ordinal()); + + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + id = compound.getString("ID"); + + String sellerUUIDStr = compound.getString("SellerUUID"); + sellerUUID = sellerUUIDStr.isEmpty() ? null : UUID.fromString(sellerUUIDStr); + sellerName = compound.getString("SellerName"); + + if (compound.hasKey("Item")) { + item = ItemStack.loadItemStackFromNBT(compound.getCompoundTag("Item")); + } + + startingPrice = compound.getLong("StartingPrice"); + buyoutPrice = compound.getLong("BuyoutPrice"); + currentBid = compound.getLong("CurrentBid"); + + String highBidderUUIDStr = compound.getString("HighBidderUUID"); + highBidderUUID = highBidderUUIDStr.isEmpty() ? null : UUID.fromString(highBidderUUIDStr); + highBidderName = compound.getString("HighBidderName"); + + bidCount = compound.getInteger("BidCount"); + createdTime = compound.getLong("CreatedTime"); + endTime = compound.getLong("EndTime"); + status = EnumAuctionStatus.fromOrdinal(compound.getInteger("Status")); + } + + public static AuctionListing fromNBT(NBTTagCompound compound) { + AuctionListing listing = new AuctionListing(); + listing.readFromNBT(compound); + return listing; + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionNotification.java b/src/main/java/noppes/npcs/controllers/data/AuctionNotification.java new file mode 100644 index 000000000..01ebd1e47 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AuctionNotification.java @@ -0,0 +1,64 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.constants.EnumNotificationType; + +import java.util.UUID; + +public class AuctionNotification { + public String id; + public UUID playerUUID; + public EnumNotificationType type; + public String listingId; + public String message; + public long timestamp; + public boolean sent; + + public AuctionNotification() { + this.id = UUID.randomUUID().toString(); + this.timestamp = System.currentTimeMillis(); + this.sent = false; + } + + public AuctionNotification(UUID playerUUID, EnumNotificationType type, String listingId, String message) { + this(); + this.playerUUID = playerUUID; + this.type = type; + this.listingId = listingId; + this.message = message; + } + + public boolean isForPlayer(UUID uuid) { + return playerUUID != null && playerUUID.equals(uuid); + } + + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + compound.setString("ID", id); + compound.setString("PlayerUUID", playerUUID != null ? playerUUID.toString() : ""); + compound.setInteger("Type", type.ordinal()); + compound.setString("ListingID", listingId != null ? listingId : ""); + compound.setString("Message", message != null ? message : ""); + compound.setLong("Timestamp", timestamp); + compound.setBoolean("Sent", sent); + return compound; + } + + public void readFromNBT(NBTTagCompound compound) { + id = compound.getString("ID"); + + String playerUUIDStr = compound.getString("PlayerUUID"); + playerUUID = playerUUIDStr.isEmpty() ? null : UUID.fromString(playerUUIDStr); + + type = EnumNotificationType.fromOrdinal(compound.getInteger("Type")); + listingId = compound.getString("ListingID"); + message = compound.getString("Message"); + timestamp = compound.getLong("Timestamp"); + sent = compound.getBoolean("Sent"); + } + + public static AuctionNotification fromNBT(NBTTagCompound compound) { + AuctionNotification notification = new AuctionNotification(); + notification.readFromNBT(compound); + return notification; + } +} diff --git a/src/main/java/noppes/npcs/mixin/CustomNPCsMixinPlugin.java b/src/main/java/noppes/npcs/mixin/CustomNPCsMixinPlugin.java index 9b09759c0..c997729ca 100644 --- a/src/main/java/noppes/npcs/mixin/CustomNPCsMixinPlugin.java +++ b/src/main/java/noppes/npcs/mixin/CustomNPCsMixinPlugin.java @@ -56,6 +56,7 @@ public class CustomNPCsMixinPlugin implements IMixinConfigPlugin { } mixins.add("MixinItemStack"); + mixins.add("MixinItem"); } return mixins; } diff --git a/src/main/java/noppes/npcs/mixin/MixinItem.java b/src/main/java/noppes/npcs/mixin/MixinItem.java new file mode 100644 index 000000000..7f2156919 --- /dev/null +++ b/src/main/java/noppes/npcs/mixin/MixinItem.java @@ -0,0 +1,27 @@ +package noppes.npcs.mixin; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.Item; +import net.minecraft.item.ItemStack; +import noppes.npcs.client.gui.player.AuctionTooltipHandler; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +/** + * Mixin to inject auction information into item tooltips when viewing auction GUIs. + */ +@Mixin(Item.class) +public class MixinItem { + + @SideOnly(Side.CLIENT) + @Inject(method = "addInformation", at = @At("HEAD")) + public void onAddInformation(ItemStack stack, EntityPlayer player, List tooltip, boolean advanced, CallbackInfo ci) { + AuctionTooltipHandler.addAuctionInfo(stack, tooltip); + } +} diff --git a/src/main/java/noppes/npcs/mixin/MixinItemStack.java b/src/main/java/noppes/npcs/mixin/MixinItemStack.java index 8615573c0..054269e23 100644 --- a/src/main/java/noppes/npcs/mixin/MixinItemStack.java +++ b/src/main/java/noppes/npcs/mixin/MixinItemStack.java @@ -1,9 +1,20 @@ package noppes.npcs.mixin; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.util.AttributeItemUtil; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiScreen; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.EnumChatFormatting; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.gui.player.GuiAuctionInterface; +import noppes.npcs.client.gui.player.GuiAuctionListing; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.containers.ContainerAuctionListing; +import noppes.npcs.controllers.data.AuctionListing; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; diff --git a/src/main/java/noppes/npcs/roles/RoleAuctioneer.java b/src/main/java/noppes/npcs/roles/RoleAuctioneer.java new file mode 100644 index 000000000..727e72378 --- /dev/null +++ b/src/main/java/noppes/npcs/roles/RoleAuctioneer.java @@ -0,0 +1,41 @@ +package noppes.npcs.roles; + +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.entity.EntityNPCInterface; + +public class RoleAuctioneer extends RoleInterface { + + public RoleAuctioneer(EntityNPCInterface npc) { + super(npc); + } + + @Override + public NBTTagCompound writeToNBT(NBTTagCompound compound) { + // Simple role - no custom data needed + return compound; + } + + @Override + public void readFromNBT(NBTTagCompound compound) { + // Simple role - no custom data needed + } + + @Override + public void interact(EntityPlayer player) { + if (!ConfigMarket.AuctionEnabled) { + return; + } + + // Open the auction house GUI + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuction, npc); + } + + @Override + public void delete() { + // No cleanup needed + } +} diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 715ca4884..6862e5455 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1705,3 +1705,183 @@ condition.hit_count=Hit Count condition.percent=Threshold condition.hits=Required Hits condition.within=Within (ticks) + +# ========================================= +# Auction House +# ========================================= + +# Auction Status +auction.status.active=Active +auction.status.ended=Ended +auction.status.cancelled=Cancelled +auction.status.claimed=Claimed + +# Auction Sort +auction.sort.newest=Newest +auction.sort.endingSoon=Ending Soon +auction.sort.priceLow=Price: Low to High +auction.sort.priceHigh=Price: High to Low +auction.sort.mostBids=Most Bids + +# Claim Types +auction.claim.item=Item +auction.claim.currency=Sale Proceeds +auction.claim.refund=Bid Refund + +# Notifications +auction.notify.won=You won an auction! +auction.notify.outbid=You were outbid! +auction.notify.sold=Your item sold! +auction.notify.expired=Your auction expired. +auction.notify.claim=You have items to claim. + +# Main GUI +auction.title=Auction House +auction.browse=Browse +auction.sell=Sell +auction.trades=My Trades +auction.filter=Filter + +# Browse +auction.nextPage=Next > +auction.prevPage=< Prev +auction.page=Page %d of %d +auction.noResults=No auctions found +auction.refresh=Refresh + +# Listing Display +auction.seller=Seller: %s +auction.currentBid=Current Bid: %s +auction.buyout=Buyout: %s +auction.timeLeft=Time Left: %s +auction.bids=%d bids +auction.noBids=No bids +auction.startingPrice=Starting: %s + +# Create Listing +auction.sell.title=Create Listing +auction.sell.startingPrice=Starting Price +auction.sell.buyoutPrice=Buyout Price +auction.sell.noBuyout=(0 = no buyout) +auction.sell.fee=Listing Fee: %s +auction.sell.create=Create Listing +auction.sell.noItem=Place an item to sell + +# Trades +auction.trades.title=My Trades +auction.trades.active=Active Listings +auction.trades.activeBids=Active Bids +auction.trades.pending=Pending Claims +auction.trades.claimAll=Claim All +auction.trades.cancel=Cancel Listing +auction.trades.empty=No active trades + +# Filter +auction.filter.title=Filter Auctions +auction.filter.search=Search +auction.filter.minPrice=Min Price +auction.filter.maxPrice=Max Price +auction.filter.myOnly=My Listings Only +auction.filter.apply=Apply +auction.filter.reset=Reset + +# Bid Dialog +auction.bid.title=Place Bid +auction.bid.current=Current Bid: %s +auction.bid.minimum=Minimum Bid: %s +auction.bid.yourBid=Your Bid: +auction.bid.place=Place Bid +auction.bid.buyout=Buyout Now + +# Errors +auction.error.notEnoughCurrency=Not enough currency! +auction.error.auctionEnded=This auction has ended. +auction.error.bidTooLow=Bid must be at least %s. +auction.error.maxListings=You have reached your maximum listings. +auction.error.blacklisted=This item cannot be auctioned. +auction.error.noAuctioneer=You must visit an Auctioneer NPC. +auction.error.inventoryFull=Your inventory is full. +auction.error.cannotAfford=You cannot afford this. + +# Navigation +auction.nav.listings=Browse Auctions +auction.nav.sell=Sell Item +auction.nav.trades=My Trades +auction.nav.filter=Filter + +# Claim Details +auction.claim.amount=Amount: %s +auction.claim.expires=Expires in %s days +auction.claim.expiresSoon=Expires soon! +auction.claim.clickToClaim=Click to claim + +# Create Listing +auction.createListing=Create Listing + +# Filter +auction.filter.sort=Sort By +auction.filter.none=(none) +auction.filter.leftClick=Left-click: Search +auction.filter.rightClick=Right-click: Cycle Sort + +# Pagination +auction.page.prev=Previous Page +auction.page.next=Next Page +auction.listings.count=%d Listings + +# Sell Page +auction.sell.confirm=Confirm Listing +auction.sell.invalidPrice=Enter a valid starting price +auction.sell.buyoutTooLow=Buyout must be higher than starting price +auction.sell.placeItem=Place item here +auction.sell.slotTitle=Item to Sell +auction.sell.leftClear=Left-click to clear +auction.sell.rightRemove=Right-click to remove 1 +auction.sell.leftAdd=Left-click to add stack +auction.sell.rightAddOne=Right-click to add 1 + +# Trades Page +auction.trades.activeListing=Active Listing +auction.trades.activeBid=Active Bid +auction.trades.unavailable=Slot Unavailable +auction.trades.rightClickToCancel=Right-click to cancel +auction.trades.leftClickToClaim=Left-click to claim +auction.trades.winningBid=You are the highest bidder +auction.trades.pendingClaim=Pending Claim +auction.trades.refundClaim=Bid Refund +auction.trades.confirmCancel=Cancel Listing? +auction.trades.confirmClaim=Claim Item? +auction.trades.rightClickConfirm=Right-click again to confirm +auction.trades.leftClickConfirm=Left-click again to confirm +auction.trades.otherClickCancel=Any other click to cancel + +# Sell Page - Buyout Toggle +auction.sell.allowBuyout=Allow Buy Now +auction.sell.buyoutEnabled=Enabled +auction.sell.buyoutEnabledDesc=Buyers can purchase immediately +auction.sell.buyoutEnabledDesc2=at the buyout price +auction.sell.buyoutDisabled=Disabled +auction.sell.buyoutDisabledDesc=Auction only - no instant buy +auction.sell.clickToToggle=Click to toggle +auction.sell.notEnoughItems=Not enough items in inventory + +# Auction Bidding Info +auction.info.currentBid=Current Bid +auction.info.highBidder=High Bidder +auction.info.startingPrice=Starting Price +auction.info.buyoutPrice=Buyout Price +auction.info.timeLeft=Time Left +auction.info.bidCount=Total Bids + +# Auction Bid SubGui +auction.bid.yourBalance=Your Balance: %s + +# Auction Buyout SubGui +auction.buyout.title=Buy Now +auction.buyout.confirm=Purchase this item for: + +# Auction Status +auction.ended=Ended + +# Role +role.auctioneer=Auctioneer diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/auction.png b/src/main/resources/assets/customnpcs/textures/gui/auction/auction.png new file mode 100644 index 0000000000000000000000000000000000000000..e7b2990934c944be38e83e205803120c5c632d24 GIT binary patch literal 2109 zcmb_dX;hPE7QSB+LI@y1maxhW6*16?YzhfWgs?=0tt`qiY60106;M9{N+~FcGl+;R zRS|H3Dpr=FA5uXSA%k#`7BL8fMK%QyWNTulGxMW=dd|G(zUMyop1Ym*KIgnyu55cr zvJx2pBv}r2ZUBe?B0$0mRM<^#51|$7W^V(xJ5?qG1|itm#Twv;T=CT)5rOUC>g>Th zxEa(H$XN+Krduhcs!+u79;8A?)e(CWSsy#acyEo&L%J*$i^Jh0B_?WUX!!d23Q|2i zJzHB_O-)U+v$HogHUyqCDK?z~r4Z-Fb{Ba5?7(vw+N2;xEZV^*7GP70@FHzd6$b%C zeOY$a9!dVwm(z}1F{9YoP7S1)Huf*kE^TbQxN$B+;U2-kq_g>I<6-xA6*@iD7_R@H z$3E@jo|g`yZ4a6HM-Qp1-XrW^Cr`7pY<>8Xj$vo6TE-{Yyr~7-zjI9sy50_-nMATU63CeuRsrm}??3Z&BJ zHP_(ucw0S$0*G;)+GKc5a%O3zA=abm?(Y7vq$i?gs?aZ^6tHlz-zxDb z<-~+Z?4DcZy}U$=ltT`Sgp~(v(|5;$*O+Nbetq%djlGKZ_#Wx6$EH@Slomv__Rkt0 zvsF(mNR6{DKKoUxSIN1!wX-tldYjD8=Q7ftmVg3LBXA35BykANU`Eb_5vmN7+vQoc z2N1X)^~~MbuUPA<`>Zmaz4*8dba4op9v#hEl4fYoadrDB*pLX&E{K2(^3aQfK5HI@ zoTKkob%5Fkv@mePwSw%tAsX*iP^Af*Q(8;LTI>Q6GrW zs3DXl;bcMkI;sf6VhkuS6*UUMc|p!{7%i<#nRp3KIl|h4cen_)r0`OPAqPy1h!&K9 zK2oYd|-_@JkZqB1Oq`Wp`fX|`BC3S zK=j@^0TCi#BvTRah%wCNO6np#mJ-r}7>tBH)w7+3gG03h#v2M_8@Nl^3ZjMLD#@5p zl)zQ2f3JK5kjDz%Is7@Khevi;pH-({Cg8qSX#V+eH59Sj>>poiL%O}C+a)4Cg3oh z{fp<2O5XuEBCZ0j9+?hR`OP$3v}(kvnwG|CJW6nb{fUz!QL_^pS4Vk{_=Q(uln{8* zd7BTbOS-;*n#yE54u*Hf0OonM1INOPHd$U=D|k#jxh1I1=;jUCj-mLV9}g6}I*{E z;)C_&H(lvzclb!ps8ki5ccs_j{Ap16S{&Z&Jm4$fNw&u~-Xd;I0o5BS{(WzE@w)}I z2@w(kHhnal#EWkviUj|(6P4nYG&s33e9Jq`)Z~YD$?x zXg1KM)-$MYHhdc45uHQ#62$}jniZoY)pw09dNnJ`2+RL(rweM)QF29J3dtHs+m6FH z@A#_~(}Yh%%-ejn>U1^@dXs(Sv-aJo9|0o+HgOvlb V;-^_l=K@V&@O1TaS?83{1ORfhBPIX< literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/button.png b/src/main/resources/assets/customnpcs/textures/gui/auction/button.png new file mode 100644 index 0000000000000000000000000000000000000000..275629c32f06f0b12db731349a6efdb2ff7593a4 GIT binary patch literal 166 zcmeAS@N?(olHy`uVBq!ia0vp^LLkh+1|-AI^@Rf|#^NA%Cx&(BWL^R}37#&FAr`&K zDG3P)9VHY0oOfW1*!SF0*?UR%lm7zRcAI#2e*I&A*!JxE|B(||PcXi>`=16>nvkG$ zTQei!%7VaVH{M14jygOS=J20B`h>}i4G4;T8N0Z)O}=h#z{K!tj&y#?0+9xwT@0SC KelF{r5}E+)kU4z- literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/button_press.png b/src/main/resources/assets/customnpcs/textures/gui/auction/button_press.png new file mode 100644 index 0000000000000000000000000000000000000000..7152e278ede8ccd6005802f7b8951ed42ea9cf62 GIT binary patch literal 165 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 t7%znszeX>e*n}}Ot}2QQB{1*&An3n(m literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/my_auctions.png b/src/main/resources/assets/customnpcs/textures/gui/auction/my_auctions.png new file mode 100644 index 0000000000000000000000000000000000000000..67d0faa4c685bddbf169b8eb99ca7d74fa980b0e GIT binary patch literal 464 zcmV;>0WbcEP)Px$ib+I4R5*>Lld(#}P#A^3NE%AClGZ|sNa>)of`Xe{pTh^x(WP#EfNr`t>I3)? zPQk%$LWL@b6s^)ijiIqonm`=Vdrc}Wf@k`3?mg!}_x}m}6AD17poeBwW4rJCNz^h8 z`tM^pJx@^txt}Afr@+!6IM`#SvP$mo1OWTA#?9R;>e&U=F*JNQx1;6@ak0QoWfh3b zEG@|a)KifLK#7Yj4YrPJ0Nfs(P#w?b8$PJQ)zaW<1i{@a0M{d^PK>8{7Lg{Z5XbTM zp^)JHh+A0-zSMLWd9vP^7`IFVCfjP6Ml{LYO(QfiD)&zw zW;SZue;?1R%S9c>4QRFPx#@JU2LR5*>Lk-ZASFcgJPMR0Jm6$Nqf6|y+^0Nn&fxvTgT?m`DAaqZOSu;P-z z2e4BSEEXqm5QI2LG-=b~&zWv+@_lCr@Mo;DOCl24o$vb^Qz#%JadY~)eJ$N;5~D!E z)5!$A?t34_z;d~|Qz1IAQ!$~@35K>z>%07*qo IM6N<$fPx$DM>^@R5*>Tld)=pP!xuLwQCRwxCH7_$QyJMvgRpVoYFZ@A$076NQaJvETsX* z3zAT1hr>u}F?bA|8s&&J6-IGR`uh&C{lyMhceZ8X4C zPGgVEAOMI=>2Hy#?iEbsn(pd84Bf_{*=vBQoaD*^j-5}J)$5D_Xx}ae>Z)N$t}K4e z$zp#oXe+miVQK8q{p=A=;A4wddRXi)hb*TyEvI(xAD6}+@kRej{Q~^Gyb4->q!a)E N002ovPDHLkV1l=OnLz*m literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/small_button.png b/src/main/resources/assets/customnpcs/textures/gui/auction/small_button.png new file mode 100644 index 0000000000000000000000000000000000000000..4ca4aedfe10eafbf1beed37679ef6f2085376c52 GIT binary patch literal 157 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4F%}28J29*~C-V}>3HNky43U_c zoRW}`&`~n+&-n&+o%^5XOjNz(`l-G{RNl{Z`@H|@JC^_aYtOxj!R*7k&-rX1pjH#6 z)Vy%PgjU8^$ur&;FuH*Ri+vgQFxGwg#qP$C*&rRipuqey&f4F%}28J29*~C-V}>3HNky43U_c zoRW}`&`~n+&-n&+o%^5XOjNz(`f2|>C4E2F_jdo&46cj(INzkxz-v?axw#PpIOk72 zk)WZGlGyM|;+b_!!$hFi!I)r%{D%2^{xu3R*f&b<6`niC0%!$;r>mdKI;Vst06JJW Aa{vGU literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/up.png b/src/main/resources/assets/customnpcs/textures/gui/auction/up.png new file mode 100644 index 0000000000000000000000000000000000000000..f81ba084f2d7acdb1e273811ec6d5aa6000086a0 GIT binary patch literal 156 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|!aQ9ZLnNjq zCn&JycnSWAUv=(_{OXjIR+sNS%LiC0G|+7Sy}1-_D=upUCuZ-|F?6JlarH{mi%k)_}|GvSXfw0Oyrk?>v=oJ zPu`)=e8T?QJIKn)p0RcOZ|n5W#^tYr%U?U!f3{9%?VKbeBo4Xz{C9Bu=jeLC-A7zp z^kqQsZ)evJKH)MlQvdAT|JynLb#VLV;Pl_talf1QQMbVJ4o;_Ro&P&HpR;#7WA6%d zQU2{l10bbe666>BpCrH#wbbG}P%CGFM`SSr1K(i~W;~w1A_XWo!_&nvL?hU>|F%$* z0gub(25|w=jaos18z0U6`~UpqYh`DA(#)PKS_)2biZ|oX{BVSIA;UyvAx5W(5)MX6 z$AuX-a>&kHcJs{HJokAFpIJAYxy!dHVrur*o6~kH%~|tp>!q&?xt8CQDP8?_CFcTb z>BUnFr=RQodf;nWxP$zW2{xDB{5%x@CiiI0hor;)wH!%>73bLgZ&bhkkKw}+N#*np S3&enKWbkzLb6Mw<&;$T}=g$rR literal 0 HcmV?d00001 From d528483e9b55fe3e40ec7173a1a42a7f33e78717 Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 23 Jan 2026 22:27:49 -0500 Subject: [PATCH 270/358] 1.11-beta --- build.gradle | 2 +- gradle.properties | 2 +- src/main/java/noppes/npcs/CustomNpcs.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index b76ad9d0c..de4d414d0 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ plugins { id 'idea' } -version = "1.10.3" +version = "1.11-beta1" group= "kamkeel.customnpc-plus" archivesBaseName = "CustomNPC-Plus" diff --git a/gradle.properties b/gradle.properties index 48416e9a4..f2e101204 100644 --- a/gradle.properties +++ b/gradle.properties @@ -199,7 +199,7 @@ disableCheckstyle = true # ideaCheckSpotlessOnBuild = true # Non-GTNH properties -modVersion = 1.10.3 +modVersion = 1.11.0 # Increase Gradle's JVM memory usage to avoid daemon expiration due to # insufficient heap space on CI builds. diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index cb9241865..5977d7263 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -111,7 +111,7 @@ import java.util.HashSet; import java.util.Set; import java.util.function.Consumer; -@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.10.3") +@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11-beta1") public class CustomNpcs { @SidedProxy(clientSide = "noppes.npcs.client.ClientProxy", serverSide = "noppes.npcs.CommonProxy") From a4ac6bb1c1668ec35649f17049da675e1c0e7547 Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 23 Jan 2026 23:27:53 -0500 Subject: [PATCH 271/358] Update Icons for GUI --- .../textures/gui/auction/check_icon.png | Bin 207 -> 328 bytes .../customnpcs/textures/gui/auction/x_icon.png | Bin 471 -> 389 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/check_icon.png b/src/main/resources/assets/customnpcs/textures/gui/auction/check_icon.png index b4ecd939e153bedf96aacbbcfa3d84dd9ce3827c..07c9335ff8553b00fc96551b818351ff2d31a18c 100644 GIT binary patch delta 312 zcmV-80muH&0muT78Gi-<001BJ|6u?C00DDSM?wIu&K&6g009C?L_t(Ijn&byO2a@L z$MJ9O4h~99DIMZ$hfoM&y88qj%<3EUJsj%b;G=X18Xuq{2$r^?5HS5alpZIhtqKnQ zr@Q0-`SHIy_;+QpP)rueuW?kQGF***9%8akhO3dB+?`2Oq<<0>si@eB`7S=cxnwqb z2B5lqqnyqGP<~U>SUz6o3joULoXum!=BWyl-_#rTQL&XnSm(9F0D;Lu>5cmUtn&q} zq{Hjg18QJt!pq5v!!DqG(WOo_IX?*~1Kb|DOY<;o-0000< KMNUMnLSTZ~Hjs<} delta 190 zcmX@Xbe?g7WIZzj1A~Sxe=v|@EDmyaVpw-h<|UBBlJ4m1$iT3%pZiZDE0E6?;1lAS zdCV5beAjU+3P>@P1o;L3XL!2ZzzfLZEbxddW?9p2%x|_{aQSMV6!(>ZYo?^e!P&wWjvtqiS|jqj i%k-UX&ch$vubEO98McJYzGe+HkipZ{&t;ucLK6U@>pkB9 diff --git a/src/main/resources/assets/customnpcs/textures/gui/auction/x_icon.png b/src/main/resources/assets/customnpcs/textures/gui/auction/x_icon.png index 90253fc9a5a44e31e4bc07aeb06174e785e69f08..a801d21aea77f46f9f269c1b5003f03d4804e2ec 100644 GIT binary patch delta 373 zcmV-*0gC?D1BC;S8Gi-<001BJ|6u?C00DDSM?wIu&K&6g00BQqL_t(IjfIoFP69y` zg})hQ3l`AA5DM&x$;PblMYc7;_wW&X4~uP~^c_qjhJ>K4RD^<`Yy&%Efis!?aXsao zbI!eU&kXz`QGrQRVE6}Rp0veryF=XVF_Nu6O%fHD=c_*Bc7Mn5q%B%JTwK{Ayjp!T&obsM>5N1ZA>C~OnDz$%?re70v(TEQ&V?vva;AM5nysUipP(4qA)Cod@)cE11Nt{lhaYKS-b0yWd(7%+ZM!y2`m;`pT#B2 z4*N>2Vvwl7yl%z!Zeg~2lxyN{j~gi~5m%O27gZVlRb-Pm-ECcz{!;x(M4h-C)kwC? zv_B}ATaE-8#Q;m4i>J-H@LJ<;4*+GO8@D?yg>qDQ5nEm*(nJNO=_mR;|2zKxPOR-b T3S_FZ00000NkvXXu0mjf+0~}b literal 471 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJV{wqX6T`Z5GB1G~mUKs7M+SzC z{oH>NS%LiC0G|+7Sy}1-_D=upUCuZ-|F?6JlarH{mi%k)_}|GvSXfw0Oyrk?>v=oJ zPu`)=e8T?QJIKn)p0RcOZ|n5W#^tYr%U?U!f3{9%?VKbeBo4Xz{C9Bu=jeLC-A7zp z^kqQsZ)evJKH)MlQvdAT|JynLb#VLV;Pl_talf1QQMbVJ4o;_Ro&P&HpR;#7WA6%d zQU2{l10bbe666>BpCrH#wbbG}P%CGFM`SSr1K(i~W;~w1A_XWo!_&nvL?hU>|F%$* z0gub(25|w=jaos18z0U6`~UpqYh`DA(#)PKS_)2biZ|oX{BVSIA;UyvAx5W(5)MX6 z$AuX-a>&kHcJs{HJokAFpIJAYxy!dHVrur*o6~kH%~|tp>!q&?xt8CQDP8?_CFcTb z>BUnFr=RQodf;nWxP$zW2{xDB{5%x@CiiI0hor;)wH!%>73bLgZ&bhkkKw}+N#*np S3&enKWbkzLb6Mw<&;$T}=g$rR From 5992e1badc2fdc3cc4ed11551e7f1df579001d70 Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 23 Jan 2026 23:59:19 -0500 Subject: [PATCH 272/358] Some Ability Fixes to Defaults and Langs --- .../data/ability/type/AbilityCharge.java | 4 +- .../data/ability/type/AbilityCutter.java | 4 +- .../data/ability/type/AbilityGuard.java | 4 +- .../data/ability/type/AbilityHazard.java | 39 +++++++++---- .../data/ability/type/AbilityHeavyHit.java | 4 +- .../data/ability/type/AbilityOrb.java | 4 +- .../data/ability/type/AbilityProjectile.java | 4 +- .../data/ability/type/AbilityShockwave.java | 4 +- .../data/ability/type/AbilitySlam.java | 4 +- .../data/ability/type/AbilityTrap.java | 4 +- .../advanced/ability/SubGuiAbilityHazard.java | 8 +-- .../assets/customnpcs/lang/en_US.lang | 56 ++++--------------- 12 files changed, 62 insertions(+), 77 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index abe05c643..a98cbcc59 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -29,7 +29,7 @@ public class AbilityCharge extends Ability { // Type-specific parameters private float chargeSpeed = 0.8f; - private float damage = 15.0f; + private float damage = 8.0f; private float knockback = 3.0f; private float hitWidth = 1.5f; @@ -296,7 +296,7 @@ public class AbilityCharge extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { this.chargeSpeed = nbt.hasKey("chargeSpeed") ? nbt.getFloat("chargeSpeed") : 0.8f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 15.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 3.0f; if (nbt.hasKey("hitWidth")) { this.hitWidth = nbt.getFloat("hitWidth"); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index 89ff0e3f0..7095e2879 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -35,7 +35,7 @@ public class AbilityCutter extends Ability { private float arcAngle = 90.0f; private float range = 6.0f; - private float damage = 12.0f; + private float damage = 7.0f; private float knockback = 1.5f; private SweepMode sweepMode = SweepMode.SWIPE; @@ -217,7 +217,7 @@ public class AbilityCutter extends Ability { public void readTypeNBT(NBTTagCompound nbt) { this.arcAngle = nbt.hasKey("arcAngle") ? nbt.getFloat("arcAngle") : 90.0f; this.range = nbt.hasKey("range") ? nbt.getFloat("range") : 6.0f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 12.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 7.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.5f; try { this.sweepMode = SweepMode.valueOf(nbt.getString("sweepMode")); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index 2427255fe..b78aec03d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -28,7 +28,7 @@ public class AbilityGuard extends Ability { private float damageReduction = 0.5f; private boolean canCounter = false; private CounterType counterType = CounterType.FLAT; - private float counterValue = 10.0f; + private float counterValue = 6.0f; private float counterChance = 0.3f; private String counterSound = "random.wood_click"; private int counterAnimationId = -1; @@ -207,7 +207,7 @@ public class AbilityGuard extends Ability { } else if (nbt.hasKey("counterDamage")) { this.counterValue = nbt.getFloat("counterDamage"); } else { - this.counterValue = 10.0f; + this.counterValue = 6.0f; } this.counterChance = nbt.hasKey("counterChance") ? nbt.getFloat("counterChance") : 0.3f; this.counterSound = nbt.hasKey("counterSound") ? nbt.getString("counterSound") : "random.wood_click"; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index b8ce70929..d63c19e76 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -48,7 +48,7 @@ public class AbilityHazard extends Ability { private HazardShape shape = HazardShape.CIRCLE; private PlacementMode placement = PlacementMode.AT_TARGET; - private float damagePerTick = 1.0f; + private float damagePerSecond = 1.0f; private int damageInterval = 20; private boolean ignoreInvulnFrames = false; @@ -290,12 +290,12 @@ public class AbilityHazard extends Ability { if (damagedThisTick.contains(entity.getEntityId())) continue; if (!isInZone(entity, npc)) continue; - if (damagePerTick > 0) { + if (damagePerSecond > 0) { if (ignoreInvulnFrames) { entity.hurtResistantTime = 0; } - // Apply damage with scripted event support (no knockback for hazard ticks) - boolean wasHit = applyAbilityDamage(npc, entity, damagePerTick, 0); + // Apply damage with scripted event support (no knockback for hazard) + boolean wasHit = applyAbilityDamage(npc, entity, damagePerSecond, 0); if (!wasHit) continue; // Skip debuffs if hit was cancelled } @@ -369,7 +369,7 @@ public class AbilityHazard extends Ability { nbt.setFloat("coneAngle", coneAngle); nbt.setString("shape", shape.name()); nbt.setString("placement", placement.name()); - nbt.setFloat("damagePerTick", damagePerTick); + nbt.setFloat("damagePerSecond", damagePerSecond); nbt.setInteger("damageInterval", damageInterval); nbt.setBoolean("ignoreInvulnFrames", ignoreInvulnFrames); nbt.setInteger("slownessLevel", slownessLevel); @@ -401,7 +401,14 @@ public class AbilityHazard extends Ability { } catch (Exception e) { this.placement = PlacementMode.AT_TARGET; } - this.damagePerTick = nbt.hasKey("damagePerTick") ? nbt.getFloat("damagePerTick") : 1.0f; + // Support legacy "damagePerTick" key for backwards compatibility + if (nbt.hasKey("damagePerSecond")) { + this.damagePerSecond = nbt.getFloat("damagePerSecond"); + } else if (nbt.hasKey("damagePerTick")) { + this.damagePerSecond = nbt.getFloat("damagePerTick"); + } else { + this.damagePerSecond = 1.0f; + } this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 20; this.ignoreInvulnFrames = nbt.hasKey("ignoreInvulnFrames") && nbt.getBoolean("ignoreInvulnFrames"); this.slownessLevel = nbt.hasKey("slownessLevel") ? nbt.getInteger("slownessLevel") : -1; @@ -459,12 +466,24 @@ public class AbilityHazard extends Ability { this.placement = placement; } - public float getDamagePerTick() { - return damagePerTick; + public float getDamagePerSecond() { + return damagePerSecond; } - public void setDamagePerTick(float damagePerTick) { - this.damagePerTick = damagePerTick; + public void setDamagePerSecond(float damagePerSecond) { + this.damagePerSecond = damagePerSecond; + } + + /** @deprecated Use {@link #getDamagePerSecond()} instead */ + @Deprecated + public float getDamagePerTick() { + return damagePerSecond; + } + + /** @deprecated Use {@link #setDamagePerSecond(float)} instead */ + @Deprecated + public void setDamagePerTick(float damage) { + this.damagePerSecond = damage; } public int getDamageInterval() { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index caafc9d5d..efbe8f337 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -21,7 +21,7 @@ import noppes.npcs.entity.EntityNPCInterface; */ public class AbilityHeavyHit extends Ability { - private float damage = 15.0f; + private float damage = 8.0f; private float knockback = 2.0f; private int slownessLevel = 1; private int weaknessLevel = 0; @@ -100,7 +100,7 @@ public class AbilityHeavyHit extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 15.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 2.0f; this.slownessLevel = nbt.hasKey("slownessLevel") ? nbt.getInteger("slownessLevel") : 1; this.weaknessLevel = nbt.hasKey("weaknessLevel") ? nbt.getInteger("weaknessLevel") : 0; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index a3677a9db..1fac0ac43 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -27,7 +27,7 @@ public class AbilityOrb extends Ability { private float orbSpeed = 0.5f; private float orbSize = 1.0f; - private float damage = 15.0f; + private float damage = 7.0f; private float knockback = 1.0f; private float maxDistance = 30.0f; private int maxLifetime = 200; @@ -301,7 +301,7 @@ public class AbilityOrb extends Ability { public void readTypeNBT(NBTTagCompound nbt) { this.orbSpeed = nbt.hasKey("orbSpeed") ? nbt.getFloat("orbSpeed") : 0.5f; this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 15.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 7.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.0f; this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 30.0f; this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index b47cd244a..b37832535 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -23,7 +23,7 @@ import java.util.List; public class AbilityProjectile extends Ability { // Type-specific parameters - private float damage = 8.0f; + private float damage = 6.0f; private float speed = 1.5f; private float knockback = 0.5f; private String projectileType = "fireball"; @@ -179,7 +179,7 @@ public class AbilityProjectile extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 6.0f; this.speed = nbt.hasKey("speed") ? nbt.getFloat("speed") : 1.5f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f; this.projectileType = nbt.hasKey("projectileType") ? nbt.getString("projectileType") : "fireball"; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index e15cf683a..9826fe599 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -27,7 +27,7 @@ public class AbilityShockwave extends Ability { private float pushRadius = 8.0f; private float pushStrength = 1.5f; - private float damage = 10.0f; + private float damage = 8.0f; private int stunDuration = 0; private int maxTargets = 10; @@ -153,7 +153,7 @@ public class AbilityShockwave extends Ability { public void readTypeNBT(NBTTagCompound nbt) { this.pushRadius = nbt.hasKey("pushRadius") ? nbt.getFloat("pushRadius") : 8.0f; this.pushStrength = nbt.hasKey("pushStrength") ? nbt.getFloat("pushStrength") : 1.5f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 10.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; this.maxTargets = nbt.hasKey("maxTargets") ? nbt.getInteger("maxTargets") : 10; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 1b0eb246f..5734bf271 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -31,7 +31,7 @@ import java.util.List; public class AbilitySlam extends Ability { // Type-specific config - private float damage = 20.0f; + private float damage = 10.0f; private float radius = 5.0f; private float knockbackStrength = 1.5f; private float leapSpeed = 1.0f; @@ -420,7 +420,7 @@ public class AbilitySlam extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { - damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 20.0f; + damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 10.0f; radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 5.0f; knockbackStrength = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.5f; leapSpeed = nbt.hasKey("leapSpeed") ? nbt.getFloat("leapSpeed") : 1.0f; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 021b1f7d6..40ca4b4d3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -42,7 +42,7 @@ public class AbilityTrap extends Ability { private int armTime = 20; private int maxTriggers = 1; private int triggerCooldown = 20; - private float damage = 10.0f; + private float damage = 6.0f; private float damageRadius = 0.0f; private float knockback = 0.5f; private int stunDuration = 0; @@ -352,7 +352,7 @@ public class AbilityTrap extends Ability { this.armTime = nbt.hasKey("armTime") ? nbt.getInteger("armTime") : 20; this.maxTriggers = nbt.hasKey("maxTriggers") ? nbt.getInteger("maxTriggers") : 1; this.triggerCooldown = nbt.hasKey("triggerCooldown") ? nbt.getInteger("triggerCooldown") : 20; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 10.0f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 6.0f; this.damageRadius = nbt.hasKey("damageRadius") ? nbt.getFloat("damageRadius") : 0.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f; this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java index e44eb6984..1ec615d7a 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java @@ -47,9 +47,9 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { y += 24; - // Row 3: Damage Per Tick + Damage Interval - addLabel(new GuiNpcLabel(104, "ability.dmgPerTick", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, hazard.getDamagePerTick())); + // Row 3: Damage Per Second + Damage Interval + addLabel(new GuiNpcLabel(104, "ability.dmgPerSec", labelX, y + 5)); + addTextField(createFloatField(104, fieldX, y, 50, hazard.getDamagePerSecond())); addLabel(new GuiNpcLabel(105, "ability.dmgInterval", col2LabelX, y + 5)); addTextField(createIntField(105, col2FieldX, y, 50, hazard.getDamageInterval())); @@ -99,7 +99,7 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { hazard.setInnerRadius(parseFloat(field, hazard.getInnerRadius())); break; case 104: - hazard.setDamagePerTick(parseFloat(field, hazard.getDamagePerTick())); + hazard.setDamagePerSecond(parseFloat(field, hazard.getDamagePerSecond())); break; case 105: hazard.setDamageInterval(field.getInteger()); diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 6862e5455..8c168168c 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -157,6 +157,9 @@ gui.create=Create gui.rename=Rename gui.playerMap=Regen PlayerMap gui.unused=Unused +gui.apply=Apply +gui.cancel=Cancel +gui.yes=Yes gui.lineColor1=Upper line color gui.lineColor2=Middle line color @@ -1647,7 +1650,7 @@ ability.healSelf=Heal Self ability.healAllies=Heal Allies # Hazard Settings -ability.dmgPerTick=Dmg/Tick +ability.dmgPerSec=Dmg/Sec ability.debuffDur=Debuff Dur ability.shape=Shape ability.placement=Placement @@ -1737,17 +1740,6 @@ auction.notify.claim=You have items to claim. # Main GUI auction.title=Auction House -auction.browse=Browse -auction.sell=Sell -auction.trades=My Trades -auction.filter=Filter - -# Browse -auction.nextPage=Next > -auction.prevPage=< Prev -auction.page=Page %d of %d -auction.noResults=No auctions found -auction.refresh=Refresh # Listing Display auction.seller=Seller: %s @@ -1758,50 +1750,25 @@ auction.bids=%d bids auction.noBids=No bids auction.startingPrice=Starting: %s -# Create Listing -auction.sell.title=Create Listing +# Sell Page auction.sell.startingPrice=Starting Price auction.sell.buyoutPrice=Buyout Price -auction.sell.noBuyout=(0 = no buyout) auction.sell.fee=Listing Fee: %s -auction.sell.create=Create Listing auction.sell.noItem=Place an item to sell -# Trades -auction.trades.title=My Trades -auction.trades.active=Active Listings -auction.trades.activeBids=Active Bids -auction.trades.pending=Pending Claims -auction.trades.claimAll=Claim All -auction.trades.cancel=Cancel Listing -auction.trades.empty=No active trades - # Filter -auction.filter.title=Filter Auctions auction.filter.search=Search -auction.filter.minPrice=Min Price -auction.filter.maxPrice=Max Price -auction.filter.myOnly=My Listings Only -auction.filter.apply=Apply -auction.filter.reset=Reset # Bid Dialog -auction.bid.title=Place Bid -auction.bid.current=Current Bid: %s auction.bid.minimum=Minimum Bid: %s auction.bid.yourBid=Your Bid: auction.bid.place=Place Bid +auction.bid.placeBid=Place Bid auction.bid.buyout=Buyout Now # Errors auction.error.notEnoughCurrency=Not enough currency! -auction.error.auctionEnded=This auction has ended. auction.error.bidTooLow=Bid must be at least %s. -auction.error.maxListings=You have reached your maximum listings. -auction.error.blacklisted=This item cannot be auctioned. -auction.error.noAuctioneer=You must visit an Auctioneer NPC. -auction.error.inventoryFull=Your inventory is full. -auction.error.cannotAfford=You cannot afford this. # Navigation auction.nav.listings=Browse Auctions @@ -1815,9 +1782,6 @@ auction.claim.expires=Expires in %s days auction.claim.expiresSoon=Expires soon! auction.claim.clickToClaim=Click to claim -# Create Listing -auction.createListing=Create Listing - # Filter auction.filter.sort=Sort By auction.filter.none=(none) @@ -1827,7 +1791,6 @@ auction.filter.rightClick=Right-click: Cycle Sort # Pagination auction.page.prev=Previous Page auction.page.next=Next Page -auction.listings.count=%d Listings # Sell Page auction.sell.confirm=Confirm Listing @@ -1846,9 +1809,12 @@ auction.trades.activeBid=Active Bid auction.trades.unavailable=Slot Unavailable auction.trades.rightClickToCancel=Right-click to cancel auction.trades.leftClickToClaim=Left-click to claim +auction.trades.leftClickToIncrease=Left-click to increase bid auction.trades.winningBid=You are the highest bidder -auction.trades.pendingClaim=Pending Claim -auction.trades.refundClaim=Bid Refund +auction.trades.soldClaim=Item Sold! +auction.trades.wonClaim=Auction Won! +auction.trades.expiredClaim=Auction Expired +auction.trades.refundClaim=Outbid - Refund Available auction.trades.confirmCancel=Cancel Listing? auction.trades.confirmClaim=Claim Item? auction.trades.rightClickConfirm=Right-click again to confirm From 3e57b837aae0c3497dd6baca6dd5f111ebec86c1 Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 24 Jan 2026 01:57:46 -0500 Subject: [PATCH 273/358] Auction Fixes --- .../packets/player/AuctionActionPacket.java | 133 ++++++++++-- .../client/gui/player/GuiAuctionBidding.java | 94 ++++---- .../client/gui/player/GuiAuctionSell.java | 54 +++-- .../client/gui/player/GuiAuctionTrades.java | 76 ++++++- .../client/gui/player/SubGuiAuctionBid.java | 41 ++-- .../gui/player/SubGuiAuctionBuyNow.java | 35 ++- .../containers/ContainerAuctionBidding.java | 10 +- .../npcs/containers/ContainerAuctionSell.java | 18 +- .../containers/ContainerAuctionTrades.java | 202 +++++++++++++++--- .../containers/InventoryAuctionDisplay.java | 15 +- .../npcs/controllers/AuctionController.java | 38 ++-- .../npcs/controllers/data/AuctionClaim.java | 47 +++- .../assets/customnpcs/lang/en_US.lang | 2 + 13 files changed, 555 insertions(+), 210 deletions(-) diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java index 45d38068b..a7549ab85 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java @@ -20,6 +20,7 @@ import noppes.npcs.config.ConfigMarket; import noppes.npcs.constants.EnumGuiType; import noppes.npcs.constants.EnumRoleType; import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.data.AuctionClaim; import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; @@ -176,8 +177,18 @@ public class AuctionActionPacket extends AbstractPacket { if (npc.advanced.role != EnumRoleType.Auctioneer) return; if (!ConfigMarket.AuctionEnabled) return; - Action requestedAction = Action.values()[in.readInt()]; + int actionOrdinal = in.readInt(); + if (actionOrdinal < 0 || actionOrdinal >= Action.values().length) { + return; // Invalid action + } + Action requestedAction = Action.values()[actionOrdinal]; + AuctionController controller = AuctionController.getInstance(); + if (controller == null) { + sendError(playerMP, "Auction system is not available."); + return; + } + String result = null; switch (requestedAction) { @@ -292,8 +303,10 @@ public class AuctionActionPacket extends AbstractPacket { String result = controller.createListing(player, item, startingPrice, buyoutPrice); if (result != null) { - // Failed - return item - player.inventory.addItemStackToInventory(item); + // Failed - return item (drop if inventory full to prevent item loss) + if (!player.inventory.addItemStackToInventory(item)) { + player.entityDropItem(item, 0.5f); + } } return result; } @@ -308,6 +321,46 @@ public class AuctionActionPacket extends AbstractPacket { default: guiType = EnumGuiType.PlayerAuction; break; } NoppesUtilServer.sendOpenGui(player, guiType, npc); + + // Send trades data when opening the trades page + if (page == 2) { + AuctionController controller = AuctionController.getInstance(); + if (controller != null) { + sendTradesData(player, controller); + } + } + } + + /** Send full trades data to player (listings, bids, claims) */ + private void sendTradesData(EntityPlayerMP player, AuctionController controller) { + NBTTagCompound response = new NBTTagCompound(); + response.setBoolean("TradesData", true); + + // Active listings (items player is selling) + List listings = controller.getPlayerActiveListings(player.getUniqueID()); + NBTTagList listingsNBT = new NBTTagList(); + for (AuctionListing listing : listings) { + listingsNBT.appendTag(listing.writeToNBT(new NBTTagCompound())); + } + response.setTag("ActiveListings", listingsNBT); + + // Active bids (items player is bidding on) + List bids = controller.getPlayerActiveBids(player.getUniqueID()); + NBTTagList bidsNBT = new NBTTagList(); + for (AuctionListing bid : bids) { + bidsNBT.appendTag(bid.writeToNBT(new NBTTagCompound())); + } + response.setTag("ActiveBids", bidsNBT); + + // Claims (items/currency to claim) + List claims = controller.getPlayerClaims(player.getUniqueID()); + NBTTagList claimsNBT = new NBTTagList(); + for (AuctionClaim claim : claims) { + claimsNBT.appendTag(claim.writeToNBT(new NBTTagCompound())); + } + response.setTag("Claims", claimsNBT); + + GuiDataPacket.sendGuiData(player, response); } private void handleOpenBidding(ByteBuf in, EntityPlayerMP player, AuctionController controller) throws IOException { @@ -368,15 +421,67 @@ public class AuctionActionPacket extends AbstractPacket { // Helpers // ========================================= - private boolean removeItemFromInventory(EntityPlayerMP player, ItemStack target) { - for (int i = 0; i < player.inventory.getSizeInventory(); i++) { - ItemStack stack = player.inventory.getStackInSlot(i); - if (stack != null && ItemStack.areItemStacksEqual(stack, target)) { - player.inventory.setInventorySlotContents(i, null); - return true; + /** + * Checks if two ItemStacks are the same type (item, damage, NBT) ignoring stack size. + */ + private boolean areItemsSameType(ItemStack item1, ItemStack item2) { + if (item1 == null || item2 == null) + return item1 == item2; + if (item1.getItem() != item2.getItem()) + return false; + if (item1.getItemDamage() != item2.getItemDamage()) + return false; + return ItemStack.areItemStackTagsEqual(item1, item2); + } + + /** + * Counts total matching items across all inventory stacks. + */ + private int countItemsInInventory(EntityPlayerMP player, ItemStack target) { + int count = 0; + for (int i = 0; i < player.inventory.mainInventory.length; i++) { + ItemStack stack = player.inventory.mainInventory[i]; + if (stack != null && areItemsSameType(stack, target)) { + count += stack.stackSize; } } - return false; + return count; + } + + /** + * Removes items from inventory, consuming from multiple stacks if needed. + * Returns true if successful, false if not enough items. + */ + private boolean removeItemFromInventory(EntityPlayerMP player, ItemStack target) { + if (target == null) return false; + + int needed = target.stackSize; + + // First verify player has enough total items + int available = countItemsInInventory(player, target); + if (available < needed) { + return false; + } + + // Consume items from matching stacks + for (int i = 0; i < player.inventory.mainInventory.length && needed > 0; i++) { + ItemStack stack = player.inventory.mainInventory[i]; + if (stack == null || !areItemsSameType(stack, target)) + continue; + + if (needed >= stack.stackSize) { + // Need entire stack + needed -= stack.stackSize; + player.inventory.mainInventory[i] = null; + } else { + // Need only part of stack + stack.splitStack(needed); + needed = 0; + } + } + + player.inventory.markDirty(); + return true; } private void sendError(EntityPlayerMP player, String message) { @@ -385,9 +490,11 @@ public class AuctionActionPacket extends AbstractPacket { } private void sendTradesUpdate(EntityPlayerMP player) { - NBTTagCompound compound = new NBTTagCompound(); - compound.setBoolean("TradesUpdate", true); - GuiDataPacket.sendGuiData(player, compound); + // Send full trades data to refresh the client + AuctionController controller = AuctionController.getInstance(); + if (controller != null) { + sendTradesData(player, controller); + } } private void sendListingsUpdate(EntityPlayerMP player) { diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java index 5159b09de..cc5e5d804 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java @@ -29,20 +29,19 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis private static final ResourceLocation ICON_BID = new ResourceLocation("customnpcs", "textures/items/npcCoinDiamond.png"); private static final ResourceLocation ICON_BUYOUT = new ResourceLocation("customnpcs", "textures/items/npcCoinEmerald.png"); - // Layout - non-final for testing/positioning - protected int itemSlotX = 56; - protected int itemSlotY = 52; - protected int btnBidX = 56; - protected int btnBidY = 80; - protected int btnBuyoutX = 80; - protected int btnBuyoutY = 80; - protected int infoX = 110; - protected int infoY = 50; - protected int infoWidth = 130; + // Layout + private int itemSlotX = 70; + private int itemSlotY = 75; + private int btnBidX = 58; + private int btnBidY = 110; + private int btnBuyoutX = 82; + private int btnBuyoutY = 110; + private int infoX = 110; + private int infoY = 50; // Button IDs - private static final int BTN_BID = 200; - private static final int BTN_BUYOUT = 201; + private int btnBidId = 200; + private int btnBuyoutId = 201; private final ContainerAuctionBidding biddingContainer; private AuctionListing listing; @@ -64,12 +63,12 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis super.initGui(); // Bid button (diamond coin) - btnBid = new GuiAuctionNavButton(BTN_BID, guiLeft + btnBidX, guiTop + btnBidY, + btnBid = new GuiAuctionNavButton(btnBidId, guiLeft + btnBidX, guiTop + btnBidY, "auction.bid.place", ICON_BID); addButton(btnBid); // Buy Now button (emerald coin) - only visible if listing has buyout - btnBuyout = new GuiAuctionNavButton(BTN_BUYOUT, guiLeft + btnBuyoutX, guiTop + btnBuyoutY, + btnBuyout = new GuiAuctionNavButton(btnBuyoutId, guiLeft + btnBuyoutX, guiTop + btnBuyoutY, "auction.bid.buyout", ICON_BUYOUT); addButton(btnBuyout); @@ -112,15 +111,12 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis if (listing == null) return; - switch (button.id) { - case BTN_BID: - openBidSubGui(); - break; - case BTN_BUYOUT: - if (listing.hasBuyout()) { - openBuyNowSubGui(); - } - break; + if (button.id == btnBidId) { + openBidSubGui(); + } else if (button.id == btnBuyoutId) { + if (listing.hasBuyout()) { + openBuyNowSubGui(); + } } } @@ -190,38 +186,46 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis /** Draw auction listing information on the right side */ private void drawAuctionInfo() { - int x = guiLeft + infoX; + int x = guiLeft + 105; int y = guiTop + infoY; - int lineHeight = 12; - String currencyName = AuctionClientConfig.getCurrencyName(); + int xRight = guiLeft + 215; + + int lineHeight = 9; // Seller String sellerLabel = StatCollector.translateToLocal("auction.seller").replace("%s", ""); fontRendererObj.drawString(EnumChatFormatting.GRAY + sellerLabel, x, y, 0xFFFFFF); - y += lineHeight; - fontRendererObj.drawString(EnumChatFormatting.WHITE + listing.sellerName, x + 10, y, 0xFFFFFF); + + String label = listing.sellerName; + int length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.WHITE + listing.sellerName, xRight - length, y, 0xFFFFFF); y += lineHeight + 4; // Current Bid or Starting Price if (listing.hasBids()) { - String bidLabel = StatCollector.translateToLocal("auction.info.currentBid"); + String bidLabel = StatCollector.translateToLocal("auction.info.currentBid") + ":"; fontRendererObj.drawString(EnumChatFormatting.YELLOW + bidLabel, x, y, 0xFFFFFF); - y += lineHeight; - fontRendererObj.drawString(EnumChatFormatting.GOLD + formatCurrency(listing.currentBid) + " " + currencyName, x + 10, y, 0xFFFFFF); + + label = formatCurrency(listing.currentBid); + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.GOLD + label, xRight - length, y, 0xFFFFFF); y += lineHeight + 4; // Current Bidder String bidderLabel = StatCollector.translateToLocal("auction.info.highBidder"); fontRendererObj.drawString(EnumChatFormatting.GRAY + bidderLabel, x, y, 0xFFFFFF); - y += lineHeight; - String bidderName = listing.highBidderName != null ? listing.highBidderName : "???"; - fontRendererObj.drawString(EnumChatFormatting.WHITE + bidderName, x + 10, y, 0xFFFFFF); + + label = listing.highBidderName != null ? listing.highBidderName : "???"; + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.WHITE + label, xRight - length, y, 0xFFFFFF); y += lineHeight + 4; } else { String startLabel = StatCollector.translateToLocal("auction.info.startingPrice"); fontRendererObj.drawString(EnumChatFormatting.YELLOW + startLabel, x, y, 0xFFFFFF); - y += lineHeight; - fontRendererObj.drawString(EnumChatFormatting.GOLD + formatCurrency(listing.startingPrice) + " " + currencyName, x + 10, y, 0xFFFFFF); + + label = formatCurrency(listing.startingPrice); + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.GOLD + label, xRight - length, y, 0xFFFFFF); y += lineHeight + 4; } @@ -229,25 +233,31 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis if (listing.hasBuyout()) { String buyoutLabel = StatCollector.translateToLocal("auction.info.buyoutPrice"); fontRendererObj.drawString(EnumChatFormatting.GREEN + buyoutLabel, x, y, 0xFFFFFF); - y += lineHeight; - fontRendererObj.drawString(EnumChatFormatting.GREEN + formatCurrency(listing.buyoutPrice) + " " + currencyName, x + 10, y, 0xFFFFFF); + + label = formatCurrency(listing.buyoutPrice); + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.GREEN + label, xRight - length, y, 0xFFFFFF); y += lineHeight + 4; } // Time Remaining String timeLabel = StatCollector.translateToLocal("auction.info.timeLeft"); fontRendererObj.drawString(EnumChatFormatting.GRAY + timeLabel, x, y, 0xFFFFFF); - y += lineHeight; long timeRemaining = listing.getTimeRemaining(); EnumChatFormatting timeColor = timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE; - fontRendererObj.drawString(timeColor + formatTimeRemaining(timeRemaining), x + 10, y, 0xFFFFFF); + + label = timeColor + formatTimeRemaining(timeRemaining); + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(label, xRight - length, y, 0xFFFFFF); y += lineHeight + 4; // Bid Count String bidCountLabel = StatCollector.translateToLocal("auction.info.bidCount"); fontRendererObj.drawString(EnumChatFormatting.GRAY + bidCountLabel, x, y, 0xFFFFFF); - y += lineHeight; - fontRendererObj.drawString(EnumChatFormatting.WHITE + String.valueOf(listing.bidCount), x + 10, y, 0xFFFFFF); + + label = String.valueOf(listing.bidCount); + length = fontRendererObj.getStringWidth(label); + fontRendererObj.drawString(EnumChatFormatting.WHITE + label, xRight - length, y, 0xFFFFFF); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java index 268ddc701..8fe8c2d9c 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java @@ -30,18 +30,18 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis private static final ResourceLocation ICON_COIN = new ResourceLocation("customnpcs", "textures/items/npcCoinBronze.png"); private static final ResourceLocation ICON_BAG = new ResourceLocation("customnpcs", "textures/items/npcBag.png"); - // Layout - non-final for testing/positioning - protected int contentX = 50; - protected int contentY = 50; - protected int confirmBtnX = 180; - protected int confirmBtnY = 120; - protected int navYBuyout = 109; + // Layout + private int contentX = 58; + private int contentY = 50; + private int confirmBtnX = 200; + private int confirmBtnY = 118; + private int navYBuyout = 109; // Component IDs - private static final int TXT_STARTING_PRICE = 1; - private static final int TXT_BUYOUT_PRICE = 2; - private static final int BTN_CONFIRM = 10; - private static final int BTN_ALLOW_BUYOUT = 11; + private int txtStartingPriceId = 1; + private int txtBuyoutPriceId = 2; + private int btnConfirmId = 10; + private int btnAllowBuyoutId = 11; private final ContainerAuctionSell sellContainer; private long startingPrice = 0; @@ -67,7 +67,7 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis // Starting Price label and field addLabel(new GuiNpcLabel(1, "auction.sell.startingPrice", guiLeft + contentX, y + 5, 0xFFFFFF)); - GuiNpcTextField startingField = new GuiNpcTextField(TXT_STARTING_PRICE, this, fontRendererObj, + GuiNpcTextField startingField = new GuiNpcTextField(txtStartingPriceId, this, fontRendererObj, guiLeft + contentX + 80, y, 80, 18, startingPrice > 0 ? String.valueOf(startingPrice) : ""); startingField.setIntegersOnly(); addTextField(startingField); @@ -78,20 +78,20 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis lblBuyout = new GuiNpcLabel(2, "auction.sell.buyoutPrice", guiLeft + contentX, y + 5, 0xFFFFFF); addLabel(lblBuyout); - txtBuyout = new GuiNpcTextField(TXT_BUYOUT_PRICE, this, fontRendererObj, + txtBuyout = new GuiNpcTextField(txtBuyoutPriceId, this, fontRendererObj, guiLeft + contentX + 80, y, 80, 18, buyoutPrice > 0 ? String.valueOf(buyoutPrice) : ""); txtBuyout.setIntegersOnly(); addTextField(txtBuyout); // Allow Buyout toggle button (18x18 with bag icon) - btnAllowBuyout = new GuiAuctionNavButton(BTN_ALLOW_BUYOUT, guiLeft + navX, guiTop + navYBuyout, + btnAllowBuyout = new GuiAuctionNavButton(btnAllowBuyoutId, guiLeft + navX, guiTop + navYBuyout, "auction.sell.allowBuyout", ICON_BAG); btnAllowBuyout.setToggle(true); // This is a toggle, not a nav button btnAllowBuyout.setSelected(allowBuyout); addButton(btnAllowBuyout); // Confirm button (custom textured button - using GuiAuctionNavButton) - GuiAuctionNavButton btnConfirm = new GuiAuctionNavButton(BTN_CONFIRM, guiLeft + confirmBtnX, guiTop + confirmBtnY, + GuiAuctionNavButton btnConfirm = new GuiAuctionNavButton(btnConfirmId, guiLeft + confirmBtnX - 20, guiTop + confirmBtnY - 10, "auction.sell.confirm", ICON_COIN); addButton(btnConfirm); @@ -141,13 +141,13 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis @Override public void unFocused(GuiNpcTextField textfield) { - if (textfield.id == TXT_STARTING_PRICE) { + if (textfield.id == txtStartingPriceId) { try { startingPrice = Long.parseLong(textfield.getText()); } catch (NumberFormatException e) { startingPrice = 0; } - } else if (textfield.id == TXT_BUYOUT_PRICE) { + } else if (textfield.id == txtBuyoutPriceId) { try { String text = textfield.getText().trim(); buyoutPrice = text.isEmpty() ? 0 : Long.parseLong(text); @@ -162,9 +162,9 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis public void actionPerformed(GuiButton button) { super.actionPerformed(button); - if (button.id == BTN_CONFIRM) { + if (button.id == btnConfirmId) { createListing(); - } else if (button.id == BTN_ALLOW_BUYOUT) { + } else if (button.id == btnAllowBuyoutId) { allowBuyout = !allowBuyout; btnAllowBuyout.setSelected(allowBuyout); updateBuyoutVisibility(); @@ -175,8 +175,8 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis private void createListing() { // Get latest values from text fields - GuiNpcTextField startingField = getTextField(TXT_STARTING_PRICE); - GuiNpcTextField buyoutField = getTextField(TXT_BUYOUT_PRICE); + GuiNpcTextField startingField = getTextField(txtStartingPriceId); + GuiNpcTextField buyoutField = getTextField(txtBuyoutPriceId); if (startingField != null) { try { @@ -234,21 +234,20 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis @Override protected void drawAuctionContent(float partialTicks, int mouseX, int mouseY) { // Draw sell slot background (centered in content area) - int slotX = guiLeft + contentX + 30; - int slotY = guiTop + contentY + 55; + int slotX = guiLeft + contentX; + int slotY = guiTop + contentY + 50; drawAuctionSlot(slotX, slotY); // Draw listing fee info (uses cached config from server) String feeText = StatCollector.translateToLocal("auction.sell.fee") .replace("%s", formatCurrency(AuctionClientConfig.getListingFee()) + " " + AuctionClientConfig.getCurrencyName()); - fontRendererObj.drawString(EnumChatFormatting.GRAY + feeText, guiLeft + contentX, guiTop + contentY + 80, 0xFFFFFF); + fontRendererObj.drawString(EnumChatFormatting.GRAY + feeText, guiLeft + contentX, guiTop + contentY + 77, 0xFFFFFF); // Draw error message if any if (errorMessage != null) { fontRendererObj.drawString(EnumChatFormatting.RED + errorMessage, guiLeft + contentX, guiTop + contentY + 95, 0xFFFFFF); } - // Draw item count indicator for sell slot drawSellSlotIndicator(slotX, slotY); } @@ -261,14 +260,11 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis // Color: green if valid, red if over limit int color = sellItem.stackSize <= inventoryCount ? 0x55FF55 : 0xFF5555; - - int textWidth = fontRendererObj.getStringWidth(countText); fontRendererObj.drawStringWithShadow(countText, slotX + 18 + 4, slotY + 5, color); } else { // Draw "Place item here" hint String hint = StatCollector.translateToLocal("auction.sell.placeItem"); - int hintWidth = fontRendererObj.getStringWidth(hint); - fontRendererObj.drawString(EnumChatFormatting.GRAY + hint, slotX + 9 - hintWidth / 2 + 20, slotY + 5, 0xFFFFFF); + fontRendererObj.drawString(EnumChatFormatting.GRAY + hint, slotX + 20, slotY + 5, 0xFFFFFF); } } @@ -324,7 +320,7 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis List tooltip = null; // Check all buttons - GuiNpcButton confirmBtn = getButton(BTN_CONFIRM); + GuiNpcButton confirmBtn = getButton(btnConfirmId); if (confirmBtn instanceof GuiAuctionNavButton) { GuiAuctionNavButton btn = (GuiAuctionNavButton) confirmBtn; if (btn.isHovered()) { diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java index df9298426..2bcdce495 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java @@ -28,6 +28,7 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { // Icon textures private static final ResourceLocation ICON_X = new ResourceLocation("customnpcs", "textures/gui/auction/x_icon.png"); private static final ResourceLocation ICON_CHECK = new ResourceLocation("customnpcs", "textures/gui/auction/check_icon.png"); + private static final ResourceLocation ICON_COIN = new ResourceLocation("customnpcs", "textures/items/npcCoinGold.png"); // Grid layout private static final int GRID_X = 56; @@ -37,9 +38,10 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { private static final int TOTAL_SLOTS = COLS * ROWS; // Slot tint colors (ARGB) - private static final int TINT_BLUE = 0x303060FF; // Active listing/bid - private static final int TINT_GREEN = 0x3030FF60; // Claimable - private static final int TINT_RED = 0x30FF4040; // Refund + private static final int TINT_BLUE = 0x303060FF; // Active listing/bid (selling) + private static final int TINT_GREEN = 0x3030FF30; // Sold (currency claim) or Won (item claim) + private static final int TINT_YELLOW = 0x30FFFF30; // Outbid (refund claim) + private static final int TINT_RED = 0x30FF3030; // Expired/Returned (item returned to seller) private final ContainerAuctionTrades tradesContainer; private int maxTradeSlots; @@ -89,6 +91,10 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { // Right-click on own listing = show cancel setPending(slot, PendingOp.CANCEL); NoppesUtil.clickSound(); + } else if (listing != null && tradesContainer.isBiddingAt(slot) && mouseButton == 0) { + // Left-click on active bid = open bidding GUI to increase bid + AuctionActionPacket.openBidding(listing.id); + NoppesUtil.clickSound(); } else if (claim != null && mouseButton == 0) { // Left-click on claim = show claim setPending(slot, PendingOp.CLAIM); @@ -156,7 +162,6 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { int col = relX / 18; int row = relY / 18; if (col >= COLS || row >= ROWS) return -1; - if (relX % 18 >= 18 || relY % 18 >= 18) return -1; return col + row * COLS; } @@ -184,13 +189,19 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { drawDarkenedOverlay(x, y); } - // Pending operation icon + // Pending operation icon or currency claim coin if (slot == pendingSlot) { if (pendingOp == PendingOp.CANCEL) { drawIconOverlay(x, y, ICON_X); } else if (pendingOp == PendingOp.CLAIM) { drawIconOverlay(x, y, ICON_CHECK); } + } else { + // Draw coin icon for currency/refund claims (no item in slot) + AuctionClaim claim = tradesContainer.getClaimAt(slot); + if (claim != null && (claim.type == EnumClaimType.CURRENCY || claim.type == EnumClaimType.REFUND)) { + drawIconOverlay(x, y, ICON_COIN); + } } } } @@ -202,9 +213,22 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { AuctionClaim claim = tradesContainer.getClaimAt(slot); if (listing != null) { + // Active listing or bid - Blue return TINT_BLUE; } else if (claim != null) { - return (claim.type == EnumClaimType.REFUND) ? TINT_RED : TINT_GREEN; + switch (claim.type) { + case CURRENCY: + // Sold item - Green + return TINT_GREEN; + case REFUND: + // Outbid - Yellow + return TINT_YELLOW; + case ITEM: + // Won item (green) or Returned/Expired (red) + return claim.isReturned ? TINT_RED : TINT_GREEN; + default: + return TINT_GREEN; + } } return 0; } @@ -247,12 +271,37 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { } else if (tradesContainer.isBiddingAt(slot)) { tooltip.add(EnumChatFormatting.AQUA + StatCollector.translateToLocal("auction.trades.activeBid")); tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.winningBid")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.leftClickToIncrease")); } } else if (claim != null) { - if (claim.type == EnumClaimType.REFUND) { - tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.refundClaim")); - } else { - tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.pendingClaim")); + switch (claim.type) { + case CURRENCY: + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.soldClaim")); + if (!claim.itemName.isEmpty()) { + tooltip.add(EnumChatFormatting.WHITE + claim.itemName); + } + if (!claim.otherPlayerName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.buyer") + ": " + EnumChatFormatting.AQUA + claim.otherPlayerName); + } + tooltip.add(EnumChatFormatting.GOLD + String.format("%,d", claim.currency) + " " + AuctionClientConfig.getCurrencyName()); + break; + case REFUND: + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.refundClaim")); + if (!claim.itemName.isEmpty()) { + tooltip.add(EnumChatFormatting.WHITE + claim.itemName); + } + if (!claim.otherPlayerName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.outbidBy") + ": " + EnumChatFormatting.AQUA + claim.otherPlayerName); + } + tooltip.add(EnumChatFormatting.GOLD + String.format("%,d", claim.currency) + " " + AuctionClientConfig.getCurrencyName()); + break; + case ITEM: + if (claim.isReturned) { + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.expiredClaim")); + } else { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.wonClaim")); + } + break; } tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.leftClickToClaim")); } else if (slot >= maxTradeSlots) { @@ -280,7 +329,12 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { @Override public void setGuiData(NBTTagCompound compound) { - if (compound.hasKey("TradesUpdate")) { + if (compound.hasKey("TradesData")) { + // Full trades data received from server + tradesContainer.setTradesData(compound); + clearPending(); + } else if (compound.hasKey("TradesUpdate")) { + // Legacy flag - should not happen now but keep for safety tradesContainer.refreshData(); } } diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java index da828b7a9..de7ab50e4 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -16,9 +16,9 @@ import noppes.npcs.client.gui.util.SubGuiInterface; * Shows minimum bid, player balance, and input field. */ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListener { - private static final int TXT_BID = 1; - private static final int BTN_BID = 10; - private static final int BTN_CANCEL = 11; + private int txtBidId = 1; + private int btnBidId = 10; + private int btnCancelId = 11; private final String listingId; private final long minimumBid; @@ -47,25 +47,21 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe int y = guiTop + 15; String currencyName = AuctionClientConfig.getCurrencyName(); - // Title - addLabel(new GuiNpcLabel(0, "auction.bid.title", centerX - 50, y, 0xFFFFFF)); - y += 18; - // Minimum bid info String minText = StatCollector.translateToLocal("auction.bid.minimum") - .replace("%s", EnumChatFormatting.GOLD + formatCurrency(minimumBid) + " " + currencyName); - addLabel(new GuiNpcLabel(1, minText, guiLeft + 15, y, 0xAAAAAA)); + .replace("%s", EnumChatFormatting.DARK_RED + formatCurrency(minimumBid) + " " + currencyName); + addLabel(new GuiNpcLabel(1, minText, guiLeft + 15, y)); y += 14; // Your balance String balanceText = StatCollector.translateToLocal("auction.bid.yourBalance") - .replace("%s", EnumChatFormatting.WHITE + formatCurrency(playerBalance) + " " + currencyName); - addLabel(new GuiNpcLabel(2, balanceText, guiLeft + 15, y, 0xAAAAAA)); + .replace("%s", EnumChatFormatting.DARK_GREEN + formatCurrency(playerBalance) + " " + currencyName); + addLabel(new GuiNpcLabel(2, balanceText, guiLeft + 15, y)); y += 18; // Bid amount field - addLabel(new GuiNpcLabel(3, "auction.bid.yourBid", guiLeft + 15, y + 4, 0xFFFFFF)); - GuiNpcTextField bidField = new GuiNpcTextField(TXT_BID, this, fontRendererObj, + addLabel(new GuiNpcLabel(3, "auction.bid.yourBid", guiLeft + 15, y + 4)); + GuiNpcTextField bidField = new GuiNpcTextField(txtBidId, this, fontRendererObj, guiLeft + 80, y, 100, 18, String.valueOf(minimumBid)); bidField.setIntegersOnly(); addTextField(bidField); @@ -81,13 +77,13 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe int totalBtnWidth = btnWidth * 2 + btnSpacing; int btnX = centerX - totalBtnWidth / 2; - addButton(new GuiNpcButton(BTN_BID, btnX, y, btnWidth, 20, "auction.bid.placeBid")); - addButton(new GuiNpcButton(BTN_CANCEL, btnX + btnWidth + btnSpacing, y, btnWidth, 20, "gui.cancel")); + addButton(new GuiNpcButton(btnBidId, btnX, y, btnWidth, 20, "auction.bid.placeBid")); + addButton(new GuiNpcButton(btnCancelId, btnX + btnWidth + btnSpacing, y, btnWidth, 20, "gui.cancel")); } @Override public void unFocused(GuiNpcTextField textfield) { - if (textfield.id == TXT_BID) { + if (textfield.id == txtBidId) { try { bidAmount = Long.parseLong(textfield.getText()); } catch (NumberFormatException e) { @@ -100,19 +96,16 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe @Override public void buttonEvent(GuiButton button) { - switch (button.id) { - case BTN_BID: - placeBid(); - break; - case BTN_CANCEL: - close(); - break; + if (button.id == btnBidId) { + placeBid(); + } else if (button.id == btnCancelId) { + close(); } } private void placeBid() { // Get latest value from text field - GuiNpcTextField bidField = getTextField(TXT_BID); + GuiNpcTextField bidField = getTextField(txtBidId); if (bidField != null) { try { bidAmount = Long.parseLong(bidField.getText()); diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java index 4f20b4ded..f4061299b 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java @@ -14,8 +14,8 @@ import noppes.npcs.client.gui.util.SubGuiInterface; * Shows buyout price, player balance, and confirmation buttons. */ public class SubGuiAuctionBuyNow extends SubGuiInterface { - private static final int BTN_CONFIRM = 10; - private static final int BTN_CANCEL = 11; + private int btnConfirmId = 10; + private int btnCancelId = 11; private final String listingId; private final long buyoutPrice; @@ -44,25 +44,25 @@ public class SubGuiAuctionBuyNow extends SubGuiInterface { // Title String title = StatCollector.translateToLocal("auction.buyout.title"); int titleWidth = fontRendererObj.getStringWidth(title); - addLabel(new GuiNpcLabel(0, title, centerX - titleWidth / 2, y, 0xFFFFFF)); + addLabel(new GuiNpcLabel(0, title, centerX - titleWidth / 2, y)); y += 20; // Confirmation message String confirmMsg = StatCollector.translateToLocal("auction.buyout.confirm"); int confirmWidth = fontRendererObj.getStringWidth(confirmMsg); - addLabel(new GuiNpcLabel(1, confirmMsg, centerX - confirmWidth / 2, y, 0xAAAAAA)); + addLabel(new GuiNpcLabel(1, confirmMsg, centerX - confirmWidth / 2, y)); y += 16; // Buyout price - String priceText = EnumChatFormatting.GREEN + formatCurrency(buyoutPrice) + " " + currencyName; + String priceText = EnumChatFormatting.DARK_RED + formatCurrency(buyoutPrice) + " " + currencyName; int priceWidth = fontRendererObj.getStringWidth(priceText); - addLabel(new GuiNpcLabel(2, priceText, centerX - priceWidth / 2, y, 0xFFFFFF)); + addLabel(new GuiNpcLabel(2, priceText, centerX - priceWidth / 2, y)); y += 20; // Your balance String balanceLabel = StatCollector.translateToLocal("auction.bid.yourBalance"); - String balanceText = balanceLabel.replace("%s", EnumChatFormatting.WHITE + formatCurrency(playerBalance) + " " + currencyName); - addLabel(new GuiNpcLabel(3, balanceText, guiLeft + 20, y, 0xAAAAAA)); + String balanceText = balanceLabel.replace("%s", EnumChatFormatting.DARK_GREEN + formatCurrency(playerBalance) + " " + currencyName); + addLabel(new GuiNpcLabel(3, balanceText, guiLeft + 20, y)); y += 14; // Insufficient funds warning @@ -78,23 +78,20 @@ public class SubGuiAuctionBuyNow extends SubGuiInterface { int totalBtnWidth = btnWidth * 2 + btnSpacing; int btnX = centerX - totalBtnWidth / 2; - GuiNpcButton confirmBtn = new GuiNpcButton(BTN_CONFIRM, btnX, y, btnWidth, 20, "gui.yes"); + GuiNpcButton confirmBtn = new GuiNpcButton(btnConfirmId, btnX, y, btnWidth, 20, "gui.yes"); confirmBtn.enabled = playerBalance >= buyoutPrice; addButton(confirmBtn); - addButton(new GuiNpcButton(BTN_CANCEL, btnX + btnWidth + btnSpacing, y, btnWidth, 20, "gui.cancel")); + addButton(new GuiNpcButton(btnCancelId, btnX + btnWidth + btnSpacing, y, btnWidth, 20, "gui.cancel")); } @Override public void buttonEvent(GuiButton button) { - switch (button.id) { - case BTN_CONFIRM: - if (playerBalance >= buyoutPrice) { - confirmPurchase(); - } - break; - case BTN_CANCEL: - close(); - break; + if (button.id == btnConfirmId) { + if (playerBalance >= buyoutPrice) { + confirmPurchase(); + } + } else if (button.id == btnCancelId) { + close(); } } diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java b/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java index 1d1faebe9..2b40000ac 100644 --- a/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionBidding.java @@ -14,9 +14,9 @@ import noppes.npcs.entity.EntityNPCInterface; */ public class ContainerAuctionBidding extends ContainerAuction { // Display slot layout - public static final int ITEM_SLOT_X = 75; - public static final int ITEM_SLOT_Y = 70; - public static final int ITEM_SLOT_INDEX = PLAYER_INV_SLOT_COUNT; + private int itemSlotX = 71; + private int itemSlotY = 76; + private int itemSlotIndex = PLAYER_INV_SLOT_COUNT; private final IInventory displayInventory; private AuctionListing listing; @@ -26,7 +26,7 @@ public class ContainerAuctionBidding extends ContainerAuction { displayInventory = new InventoryBasic("Display", false, 1); // Display slot (read-only) - addSlotToContainer(new Slot(displayInventory, 0, ITEM_SLOT_X, ITEM_SLOT_Y) { + addSlotToContainer(new Slot(displayInventory, 0, itemSlotX, itemSlotY) { @Override public boolean isItemValid(ItemStack stack) { return false; } @Override @@ -58,6 +58,6 @@ public class ContainerAuctionBidding extends ContainerAuction { * Check if this is the display slot. */ public boolean isDisplaySlot(int slotIndex) { - return slotIndex == ITEM_SLOT_INDEX; + return slotIndex == itemSlotIndex; } } diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java b/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java index a4bab6efe..14b381f40 100644 --- a/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionSell.java @@ -13,10 +13,10 @@ import noppes.npcs.entity.EntityNPCInterface; * Items are cloned from inventory, not moved. Validation happens on submit. */ public class ContainerAuctionSell extends ContainerAuction { - public static final int SELL_SLOT_X = 101; - public static final int SELL_SLOT_Y = 106; - public static final int SELL_SLOT_INDEX = PLAYER_INV_SLOT_COUNT; - private static final int MAX_STACK = 64; + private int sellSlotX = 59; + private int sellSlotY = 101; + private int sellSlotIndex = PLAYER_INV_SLOT_COUNT; + private int maxStack = 64; private final IInventory sellInventory; @@ -25,7 +25,7 @@ public class ContainerAuctionSell extends ContainerAuction { sellInventory = new InventoryBasic("Sell", false, 1); // Sell slot blocks direct interaction - addSlotToContainer(new Slot(sellInventory, 0, SELL_SLOT_X, SELL_SLOT_Y) { + addSlotToContainer(new Slot(sellInventory, 0, sellSlotX, sellSlotY) { @Override public boolean isItemValid(ItemStack stack) { return false; } @Override @@ -59,11 +59,11 @@ public class ContainerAuctionSell extends ContainerAuction { if (sellStack == null) { // Empty slot - create new stack ItemStack newStack = source.copy(); - newStack.stackSize = Math.min(toAdd, MAX_STACK); + newStack.stackSize = Math.min(toAdd, maxStack); sellInventory.setInventorySlotContents(0, newStack); } else if (itemsMatch(sellStack, source)) { // Same item - add to existing (capped at 64) - int space = MAX_STACK - sellStack.stackSize; + int space = maxStack - sellStack.stackSize; int add = Math.min(toAdd, space); if (add > 0) { sellStack.stackSize += add; @@ -71,7 +71,7 @@ public class ContainerAuctionSell extends ContainerAuction { } else { // Different item - replace ItemStack newStack = source.copy(); - newStack.stackSize = Math.min(toAdd, MAX_STACK); + newStack.stackSize = Math.min(toAdd, maxStack); sellInventory.setInventorySlotContents(0, newStack); } } @@ -93,7 +93,7 @@ public class ContainerAuctionSell extends ContainerAuction { /** Check if sell slot is this container slot index */ public boolean isSellSlot(int slotIndex) { - return slotIndex == SELL_SLOT_INDEX; + return slotIndex == sellSlotIndex; } /** Check if two stacks match using NoppesUtilPlayer.compareItems (same item, damage, and NBT) */ diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java b/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java index 8481612d5..2d871bced 100644 --- a/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java @@ -2,6 +2,9 @@ package noppes.npcs.containers; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.constants.EnumClaimType; import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.data.AuctionClaim; import noppes.npcs.controllers.data.AuctionListing; @@ -13,6 +16,7 @@ import java.util.List; /** * Container for viewing player's auction trades. * Shows active listings, bids, and claims in a 9x5 grid. + * Order: SOLD claims, OUTBID claims, WON claims, SELLING, BIDDING, EXPIRED claims */ public class ContainerAuctionTrades extends ContainerAuction { // Grid layout @@ -28,6 +32,10 @@ public class ContainerAuctionTrades extends ContainerAuction { private List claims = new ArrayList<>(); private int hiddenSlot = -1; + // Slot type tracking for proper accessor methods + private int[] slotTypes = new int[SLOT_COUNT]; // 0=empty, 1=selling, 2=bidding, 3=claim + private int[] slotDataIndex = new int[SLOT_COUNT]; // Index into respective list + public ContainerAuctionTrades(EntityNPCInterface npc, EntityPlayer player) { super(npc, player); displayInventory = new InventoryAuctionDisplay(SLOT_COUNT); @@ -44,34 +52,158 @@ public class ContainerAuctionTrades extends ContainerAuction { refreshData(); } - /** Refresh data from controller */ + /** Refresh data from controller (server-side only) */ public void refreshData() { + // Clear existing data + clearData(); + + // Only works on server where AuctionController exists + if (AuctionController.Instance == null) return; + + List newListings = AuctionController.Instance.getPlayerActiveListings(player.getUniqueID()); + List newBids = AuctionController.Instance.getPlayerActiveBids(player.getUniqueID()); + List newClaims = AuctionController.Instance.getPlayerClaims(player.getUniqueID()); + + populateDisplay(newListings, newBids, newClaims); + } + + /** Set data from NBT received from server (client-side) */ + public void setTradesData(NBTTagCompound compound) { + // Clear existing data + clearData(); + + List newListings = new ArrayList<>(); + List newBids = new ArrayList<>(); + List newClaims = new ArrayList<>(); + + // Parse active listings + if (compound.hasKey("ActiveListings")) { + NBTTagList listingsNBT = compound.getTagList("ActiveListings", 10); + for (int i = 0; i < listingsNBT.tagCount(); i++) { + newListings.add(AuctionListing.fromNBT(listingsNBT.getCompoundTagAt(i))); + } + } + + // Parse active bids + if (compound.hasKey("ActiveBids")) { + NBTTagList bidsNBT = compound.getTagList("ActiveBids", 10); + for (int i = 0; i < bidsNBT.tagCount(); i++) { + newBids.add(AuctionListing.fromNBT(bidsNBT.getCompoundTagAt(i))); + } + } + + // Parse claims + if (compound.hasKey("Claims")) { + NBTTagList claimsNBT = compound.getTagList("Claims", 10); + for (int i = 0; i < claimsNBT.tagCount(); i++) { + newClaims.add(AuctionClaim.fromNBT(claimsNBT.getCompoundTagAt(i))); + } + } + + populateDisplay(newListings, newBids, newClaims); + } + + /** Clear all data and reset display */ + private void clearData() { activeListings.clear(); activeBids.clear(); claims.clear(); displayInventory.clear(); - if (AuctionController.Instance == null) return; + // Reset slot tracking + for (int i = 0; i < SLOT_COUNT; i++) { + slotTypes[i] = 0; + slotDataIndex[i] = -1; + } + } - activeListings = AuctionController.Instance.getPlayerActiveListings(player.getUniqueID()); - activeBids = AuctionController.Instance.getPlayerActiveBids(player.getUniqueID()); - claims = AuctionController.Instance.getPlayerClaims(player.getUniqueID()); + /** Populate display from provided data lists */ + private void populateDisplay(List newListings, List newBids, List allClaims) { + activeListings = new ArrayList<>(newListings); + activeBids = new ArrayList<>(newBids); - // Populate display: listings first, then bids, then claims + // Sort claims into categories for proper display order + List soldClaims = new ArrayList<>(); // CURRENCY - sold items + List outbidClaims = new ArrayList<>(); // REFUND - outbid refunds + List wonClaims = new ArrayList<>(); // ITEM won (isReturned=false) + List expiredClaims = new ArrayList<>(); // ITEM returned (isReturned=true) + + for (AuctionClaim claim : allClaims) { + if (claim.type == EnumClaimType.CURRENCY) { + soldClaims.add(claim); + } else if (claim.type == EnumClaimType.REFUND) { + outbidClaims.add(claim); + } else if (claim.type == EnumClaimType.ITEM) { + if (claim.isReturned) { + expiredClaims.add(claim); + } else { + wonClaims.add(claim); + } + } + } + + // Populate display in order: SOLD, OUTBID, WON, SELLING, BIDDING, EXPIRED int slot = 0; + + // 1. Sold claims (green) - you have money to claim! + for (AuctionClaim claim : soldClaims) { + if (slot >= SLOT_COUNT) break; + claims.add(claim); + displayInventory.setClaimItem(slot, claim); + slotTypes[slot] = 3; // claim + slotDataIndex[slot] = claims.size() - 1; + slot++; + } + + // 2. Outbid claims (yellow) - refunds available + for (AuctionClaim claim : outbidClaims) { + if (slot >= SLOT_COUNT) break; + claims.add(claim); + displayInventory.setClaimItem(slot, claim); + slotTypes[slot] = 3; // claim + slotDataIndex[slot] = claims.size() - 1; + slot++; + } + + // 3. Won claims (green) - items to claim! + for (AuctionClaim claim : wonClaims) { + if (slot >= SLOT_COUNT) break; + claims.add(claim); + displayInventory.setClaimItem(slot, claim); + slotTypes[slot] = 3; // claim + slotDataIndex[slot] = claims.size() - 1; + slot++; + } + + // 4. Active listings (blue) - items you're selling + int listingIdx = 0; for (AuctionListing listing : activeListings) { if (slot >= SLOT_COUNT) break; if (listing.item != null) displayInventory.setInventorySlotContents(slot, listing.item.copy()); + slotTypes[slot] = 1; // selling + slotDataIndex[slot] = listingIdx; slot++; + listingIdx++; } + + // 5. Active bids (blue) - items you're bidding on + int bidIdx = 0; for (AuctionListing bid : activeBids) { if (slot >= SLOT_COUNT) break; if (bid.item != null) displayInventory.setInventorySlotContents(slot, bid.item.copy()); + slotTypes[slot] = 2; // bidding + slotDataIndex[slot] = bidIdx; slot++; + bidIdx++; } - for (AuctionClaim claim : claims) { + + // 6. Expired claims (red) - returned items (lowest priority) + for (AuctionClaim claim : expiredClaims) { if (slot >= SLOT_COUNT) break; + claims.add(claim); displayInventory.setClaimItem(slot, claim); + slotTypes[slot] = 3; // claim + slotDataIndex[slot] = claims.size() - 1; slot++; } } @@ -80,34 +212,39 @@ public class ContainerAuctionTrades extends ContainerAuction { /** Get listing at slot (or null if claim/empty) */ public AuctionListing getListingAt(int slot) { - if (slot >= 0 && slot < activeListings.size()) { - return activeListings.get(slot); - } - int bidIdx = slot - activeListings.size(); - if (bidIdx >= 0 && bidIdx < activeBids.size()) { - return activeBids.get(bidIdx); + if (slot < 0 || slot >= SLOT_COUNT) return null; + int type = slotTypes[slot]; + int idx = slotDataIndex[slot]; + if (idx < 0) return null; + + if (type == 1 && idx < activeListings.size()) { + return activeListings.get(idx); + } else if (type == 2 && idx < activeBids.size()) { + return activeBids.get(idx); } return null; } /** Get claim at slot (or null if listing/empty) */ public AuctionClaim getClaimAt(int slot) { - int claimIdx = slot - activeListings.size() - activeBids.size(); - if (claimIdx >= 0 && claimIdx < claims.size()) { - return claims.get(claimIdx); + if (slot < 0 || slot >= SLOT_COUNT) return null; + if (slotTypes[slot] == 3) { + int idx = slotDataIndex[slot]; + if (idx >= 0 && idx < claims.size()) { + return claims.get(idx); + } } return null; } /** Check if slot is player's active listing */ public boolean isSellingAt(int slot) { - return slot >= 0 && slot < activeListings.size(); + return slot >= 0 && slot < SLOT_COUNT && slotTypes[slot] == 1; } /** Check if slot is player's active bid */ public boolean isBiddingAt(int slot) { - int bidIdx = slot - activeListings.size(); - return bidIdx >= 0 && bidIdx < activeBids.size(); + return slot >= 0 && slot < SLOT_COUNT && slotTypes[slot] == 2; } /** Find listing matching item */ @@ -159,24 +296,29 @@ public class ContainerAuctionTrades extends ContainerAuction { hiddenSlot = -1; } - /** Restore item at slot from data */ + /** Restore item at slot from data using slot tracking */ private void restoreSlotItem(int slot) { - if (slot < activeListings.size()) { - AuctionListing listing = activeListings.get(slot); + if (slot < 0 || slot >= SLOT_COUNT) return; + + int type = slotTypes[slot]; + int idx = slotDataIndex[slot]; + if (idx < 0) return; + + if (type == 1 && idx < activeListings.size()) { + // Selling + AuctionListing listing = activeListings.get(idx); if (listing != null && listing.item != null) { displayInventory.setInventorySlotContents(slot, listing.item.copy()); } - } else if (slot < activeListings.size() + activeBids.size()) { - int bidIdx = slot - activeListings.size(); - AuctionListing bid = activeBids.get(bidIdx); + } else if (type == 2 && idx < activeBids.size()) { + // Bidding + AuctionListing bid = activeBids.get(idx); if (bid != null && bid.item != null) { displayInventory.setInventorySlotContents(slot, bid.item.copy()); } - } else { - int claimIdx = slot - activeListings.size() - activeBids.size(); - if (claimIdx < claims.size()) { - displayInventory.setClaimItem(slot, claims.get(claimIdx)); - } + } else if (type == 3 && idx < claims.size()) { + // Claim + displayInventory.setClaimItem(slot, claims.get(idx)); } } diff --git a/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java b/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java index 20c03f18f..0a15720fe 100644 --- a/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java +++ b/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java @@ -3,7 +3,6 @@ package noppes.npcs.containers; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.inventory.IInventory; import net.minecraft.item.ItemStack; -import noppes.npcs.CustomItems; import noppes.npcs.constants.EnumClaimType; import noppes.npcs.controllers.data.AuctionClaim; import noppes.npcs.controllers.data.AuctionListing; @@ -49,19 +48,21 @@ public class InventoryAuctionDisplay implements IInventory { /** * Set a claim item in the inventory. - * For ITEM claims, shows the item. - * For CURRENCY/REFUND claims, shows a gold coin as visual representation. + * Shows actual item for ITEM claims, empty slot for CURRENCY/REFUND (icon drawn by GUI). */ public void setClaimItem(int slot, AuctionClaim claim) { if (slot >= 0 && slot < size) { claims.set(slot, claim); listings.set(slot, null); if (claim != null) { - if (claim.type == EnumClaimType.ITEM && claim.item != null) { - items[slot] = claim.item; + if (claim.type == EnumClaimType.CURRENCY || claim.type == EnumClaimType.REFUND) { + // No item - gold coin icon drawn by GUI + items[slot] = null; + } else if (claim.item != null) { + // Show actual item for ITEM claims + items[slot] = claim.item.copy(); } else { - // For currency claims, show a gold coin - items[slot] = new ItemStack(CustomItems.coinGold); + items[slot] = null; } } else { items[slot] = null; diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index 92f790b99..b78fa0649 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -164,9 +164,12 @@ public class AuctionController { removeFromPlayerBids(listing.highBidderUUID, listing.id); } + // Get item display name safely + String itemDisplayName = listing.item != null ? listing.item.getDisplayName() : "Unknown Item"; + if (listing.hasBids()) { // Auction sold - create claims - AuctionClaim itemClaim = AuctionClaim.createItemClaim( + AuctionClaim itemClaim = AuctionClaim.createItemWonClaim( listing.highBidderUUID, listing.highBidderName, listing.id, listing.item); addClaim(itemClaim); @@ -175,27 +178,28 @@ public class AuctionController { long sellerReceives = saleAmount - tax; AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( - listing.sellerUUID, listing.sellerName, listing.id, sellerReceives); + listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, + itemDisplayName, listing.highBidderName); addClaim(currencyClaim); // Notifications queueNotification(listing.highBidderUUID, EnumNotificationType.AUCTION_WON, listing.id, - "You won the auction for " + listing.item.getDisplayName() + "!"); + "You won the auction for " + itemDisplayName + "!"); queueNotification(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, - "Your " + listing.item.getDisplayName() + " sold for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); + "Your " + itemDisplayName + " sold for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); - logAuction("SOLD", listing.sellerName, listing.item.getDisplayName(), saleAmount, + logAuction("SOLD", listing.sellerName, itemDisplayName, saleAmount, "Winner: " + listing.highBidderName + ", Tax: " + tax); } else { // No bids - return item to seller - AuctionClaim returnClaim = AuctionClaim.createItemClaim( + AuctionClaim returnClaim = AuctionClaim.createItemReturnedClaim( listing.sellerUUID, listing.sellerName, listing.id, listing.item); addClaim(returnClaim); queueNotification(listing.sellerUUID, EnumNotificationType.AUCTION_EXPIRED, listing.id, - "Your auction for " + listing.item.getDisplayName() + " expired with no bids."); + "Your auction for " + itemDisplayName + " expired with no bids."); - logAuction("EXPIRED", listing.sellerName, listing.item.getDisplayName(), listing.startingPrice, "No bids"); + logAuction("EXPIRED", listing.sellerName, itemDisplayName, listing.startingPrice, "No bids"); } } @@ -361,7 +365,8 @@ public class AuctionController { // Refund bidder AuctionClaim refundClaim = AuctionClaim.createRefundClaim( - listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid); + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, + listing.item.getDisplayName(), listing.sellerName); addClaim(refundClaim); // Remove from bidder's active bids @@ -371,7 +376,7 @@ public class AuctionController { "The auction for " + listing.item.getDisplayName() + " was cancelled. Your bid has been refunded."); // Return item to seller - AuctionClaim itemClaim = AuctionClaim.createItemClaim( + AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim( listing.sellerUUID, listing.sellerName, listing.id, listing.item); addClaim(itemClaim); @@ -380,7 +385,7 @@ public class AuctionController { (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); } else { // No bids - just return item - AuctionClaim itemClaim = AuctionClaim.createItemClaim( + AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim( listing.sellerUUID, listing.sellerName, listing.id, listing.item); addClaim(itemClaim); @@ -462,7 +467,8 @@ public class AuctionController { // Refund previous bidder if any if (listing.hasBids() && previousBidder != null) { AuctionClaim refundClaim = AuctionClaim.createRefundClaim( - previousBidder, listing.highBidderName, listing.id, previousBid); + previousBidder, listing.highBidderName, listing.id, previousBid, + listing.item.getDisplayName(), playerName); addClaim(refundClaim); // Remove from previous bidder's active bids @@ -547,7 +553,8 @@ public class AuctionController { // Refund previous bidder if any if (listing.hasBids() && listing.highBidderUUID != null) { AuctionClaim refundClaim = AuctionClaim.createRefundClaim( - listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid); + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, + listing.item.getDisplayName(), playerName); addClaim(refundClaim); // Remove from previous bidder's active bids @@ -558,7 +565,7 @@ public class AuctionController { } // Create claims - AuctionClaim itemClaim = AuctionClaim.createItemClaim(playerUUID, playerName, listing.id, listing.item); + AuctionClaim itemClaim = AuctionClaim.createItemWonClaim(playerUUID, playerName, listing.id, listing.item); addClaim(itemClaim); long saleAmount = listing.buyoutPrice; @@ -566,7 +573,8 @@ public class AuctionController { long sellerReceives = saleAmount - tax; AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( - listing.sellerUUID, listing.sellerName, listing.id, sellerReceives); + listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, + listing.item.getDisplayName(), playerName); addClaim(currencyClaim); // Notifications diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java b/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java index 7a4e45a5d..bbc1b06ac 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java @@ -12,18 +12,25 @@ public class AuctionClaim { public String playerName; public String listingId; public EnumClaimType type; - public ItemStack item; + public ItemStack item; // The item (for ITEM claims only) + public String itemName; // Display name of item (for CURRENCY/REFUND tooltip) + public String otherPlayerName; // Buyer name (CURRENCY) or who outbid (REFUND) public long currency; public long createdTime; public boolean claimed; + public boolean isReturned; // For ITEM claims: true if returned to seller, false if won by buyer public AuctionClaim() { this.id = UUID.randomUUID().toString(); this.createdTime = System.currentTimeMillis(); this.claimed = false; + this.isReturned = false; + this.itemName = ""; + this.otherPlayerName = ""; } - public static AuctionClaim createItemClaim(UUID playerUUID, String playerName, String listingId, ItemStack item) { + /** Create claim for winning an item (buyer won auction or buyout) */ + public static AuctionClaim createItemWonClaim(UUID playerUUID, String playerName, String listingId, ItemStack item) { AuctionClaim claim = new AuctionClaim(); claim.playerUUID = playerUUID; claim.playerName = playerName; @@ -31,28 +38,50 @@ public class AuctionClaim { claim.type = EnumClaimType.ITEM; claim.item = item != null ? item.copy() : null; claim.currency = 0; + claim.isReturned = false; return claim; } - public static AuctionClaim createCurrencyClaim(UUID playerUUID, String playerName, String listingId, long currency) { + /** Create claim for returned item (seller's expired/cancelled listing) */ + public static AuctionClaim createItemReturnedClaim(UUID playerUUID, String playerName, String listingId, ItemStack item) { + AuctionClaim claim = new AuctionClaim(); + claim.playerUUID = playerUUID; + claim.playerName = playerName; + claim.listingId = listingId; + claim.type = EnumClaimType.ITEM; + claim.item = item != null ? item.copy() : null; + claim.currency = 0; + claim.isReturned = true; + return claim; + } + + /** Create claim for seller's proceeds (stores item name for tooltip, not full item) */ + public static AuctionClaim createCurrencyClaim(UUID playerUUID, String playerName, String listingId, long currency, String itemName, String buyerName) { AuctionClaim claim = new AuctionClaim(); claim.playerUUID = playerUUID; claim.playerName = playerName; claim.listingId = listingId; claim.type = EnumClaimType.CURRENCY; - claim.item = null; + claim.item = null; // No item stored for currency claims + claim.itemName = itemName != null ? itemName : ""; + claim.otherPlayerName = buyerName != null ? buyerName : ""; claim.currency = currency; + claim.isReturned = false; return claim; } - public static AuctionClaim createRefundClaim(UUID playerUUID, String playerName, String listingId, long currency) { + /** Create claim for outbid refund (stores item name for tooltip, not full item) */ + public static AuctionClaim createRefundClaim(UUID playerUUID, String playerName, String listingId, long currency, String itemName, String outbidderName) { AuctionClaim claim = new AuctionClaim(); claim.playerUUID = playerUUID; claim.playerName = playerName; claim.listingId = listingId; claim.type = EnumClaimType.REFUND; - claim.item = null; + claim.item = null; // No item stored for refund claims + claim.itemName = itemName != null ? itemName : ""; + claim.otherPlayerName = outbidderName != null ? outbidderName : ""; claim.currency = currency; + claim.isReturned = false; return claim; } @@ -84,9 +113,12 @@ public class AuctionClaim { compound.setTag("Item", itemTag); } + compound.setString("ItemName", itemName != null ? itemName : ""); + compound.setString("OtherPlayerName", otherPlayerName != null ? otherPlayerName : ""); compound.setLong("Currency", currency); compound.setLong("CreatedTime", createdTime); compound.setBoolean("Claimed", claimed); + compound.setBoolean("IsReturned", isReturned); return compound; } @@ -107,9 +139,12 @@ public class AuctionClaim { item = null; } + itemName = compound.getString("ItemName"); + otherPlayerName = compound.getString("OtherPlayerName"); currency = compound.getLong("Currency"); createdTime = compound.getLong("CreatedTime"); claimed = compound.getBoolean("Claimed"); + isReturned = compound.getBoolean("IsReturned"); } public static AuctionClaim fromNBT(NBTTagCompound compound) { diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 8c168168c..a77d6444b 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1820,6 +1820,8 @@ auction.trades.confirmClaim=Claim Item? auction.trades.rightClickConfirm=Right-click again to confirm auction.trades.leftClickConfirm=Left-click again to confirm auction.trades.otherClickCancel=Any other click to cancel +auction.trades.buyer=Buyer +auction.trades.outbidBy=Outbid by # Sell Page - Buyout Toggle auction.sell.allowBuyout=Allow Buy Now From bd7cafa482e4cefc184bdf37dfb82505d99075dd Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 24 Jan 2026 02:35:26 -0500 Subject: [PATCH 274/358] Telegraph Scripting System --- src/api | 2 +- .../client/renderer/TelegraphRenderer.java | 8 +- .../npcs/controllers/TelegraphController.java | 392 ++++++++++++++++++ .../controllers/data/ability/Ability.java | 6 +- .../data/ability/type/AbilityBeam.java | 2 +- .../data/ability/type/AbilityCharge.java | 6 +- .../data/ability/type/AbilityCutter.java | 2 +- .../data/ability/type/AbilityDash.java | 2 +- .../data/ability/type/AbilityGuard.java | 2 +- .../data/ability/type/AbilityHazard.java | 4 +- .../data/ability/type/AbilityHeal.java | 2 +- .../data/ability/type/AbilityHeavyHit.java | 2 +- .../data/ability/type/AbilityOrb.java | 2 +- .../data/ability/type/AbilityProjectile.java | 2 +- .../data/ability/type/AbilityShockwave.java | 2 +- .../data/ability/type/AbilitySlam.java | 6 +- .../data/ability/type/AbilityTeleport.java | 2 +- .../data/ability/type/AbilityTrap.java | 4 +- .../data/ability/type/AbilityVortex.java | 2 +- .../{ability => }/telegraph/Telegraph.java | 8 +- .../telegraph/TelegraphInstance.java | 25 +- .../telegraph/TelegraphManager.java | 4 +- .../telegraph/TelegraphType.java | 2 +- .../kamkeel/npcs/network/PacketHandler.java | 4 +- .../TelegraphRemovePacket.java | 20 +- .../TelegraphSpawnPacket.java | 15 +- src/main/java/noppes/npcs/CustomNpcs.java | 3 +- src/main/java/noppes/npcs/DataAbilities.java | 6 +- .../java/noppes/npcs/client/ClientProxy.java | 2 +- .../noppes/npcs/client/ClientTickHandler.java | 2 +- .../gui/advanced/SubGuiAbilityConfig.java | 2 +- .../java/noppes/npcs/scripted/NpcAPI.java | 15 + .../noppes/npcs/scripted/ScriptTelegraph.java | 210 ++++++++++ .../scripted/ScriptTelegraphInstance.java | 122 ++++++ 34 files changed, 839 insertions(+), 51 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/TelegraphController.java rename src/main/java/kamkeel/npcs/controllers/data/{ability => }/telegraph/Telegraph.java (97%) rename src/main/java/kamkeel/npcs/controllers/data/{ability => }/telegraph/TelegraphInstance.java (94%) rename src/main/java/kamkeel/npcs/controllers/data/{ability => }/telegraph/TelegraphManager.java (98%) rename src/main/java/kamkeel/npcs/controllers/data/{ability => }/telegraph/TelegraphType.java (89%) rename src/main/java/kamkeel/npcs/network/packets/data/{ability => telegraph}/TelegraphRemovePacket.java (75%) rename src/main/java/kamkeel/npcs/network/packets/data/{ability => telegraph}/TelegraphSpawnPacket.java (84%) create mode 100644 src/main/java/noppes/npcs/scripted/ScriptTelegraph.java create mode 100644 src/main/java/noppes/npcs/scripted/ScriptTelegraphInstance.java diff --git a/src/api b/src/api index a6b4a131c..c8a2fb982 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit a6b4a131c843f27bbd3c561168f18498cb9d59ec +Subproject commit c8a2fb9824de8488627f66eed2a5254ee8690016 diff --git a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java index 37e1232c4..0cac8805e 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java +++ b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java @@ -1,10 +1,10 @@ package kamkeel.npcs.client.renderer; import cpw.mods.fml.common.eventhandler.SubscribeEvent; -import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.Tessellator; import net.minecraft.entity.player.EntityPlayer; diff --git a/src/main/java/kamkeel/npcs/controllers/TelegraphController.java b/src/main/java/kamkeel/npcs/controllers/TelegraphController.java new file mode 100644 index 000000000..5449245a6 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/TelegraphController.java @@ -0,0 +1,392 @@ +package kamkeel.npcs.controllers; + +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.world.World; +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import noppes.npcs.api.ITelegraph; +import noppes.npcs.api.ITelegraphInstance; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntity; +import noppes.npcs.api.handler.ITelegraphHandler; +import noppes.npcs.scripted.ScriptTelegraph; +import noppes.npcs.scripted.ScriptTelegraphInstance; +import noppes.npcs.util.NBTJsonUtil; + +import java.io.File; +import java.util.HashMap; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Server-side controller for managing telegraphs. + * Provides factory methods for creating telegraphs and spawn methods for sending to clients. + * Also manages saved telegraph presets. + */ +public class TelegraphController implements ITelegraphHandler { + + public static TelegraphController Instance; + + // Saved telegraph presets (name -> Telegraph) + private HashMap savedTelegraphs = new HashMap<>(); + + // Active server-side telegraph instances (for removal tracking) + private ConcurrentHashMap activeInstances = new ConcurrentHashMap<>(); + + public TelegraphController() { + } + + public static void init() { + Instance = new TelegraphController(); + Instance.load(); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // INTERNAL SPAWN METHODS + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Spawn a telegraph at a fixed position. + * + * @param telegraph The telegraph configuration + * @param world The world + * @param x X position + * @param y Y position + * @param z Z position + * @param yaw Rotation yaw (for directional telegraphs like LINE/CONE) + * @return The spawned telegraph instance + */ + public TelegraphInstance spawn(Telegraph telegraph, World world, double x, double y, double z, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setWorld(world); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.sendToDimension(instance, world.provider.dimensionId); + return instance; + } + + /** + * Spawn a telegraph at a position with default yaw. + */ + public TelegraphInstance spawn(Telegraph telegraph, World world, double x, double y, double z) { + return spawn(telegraph, world, x, y, z, 0); + } + + /** + * Spawn a telegraph that follows an entity. + * + * @param telegraph The telegraph configuration + * @param entity The entity to follow + * @return The spawned telegraph instance + */ + public TelegraphInstance spawn(Telegraph telegraph, Entity entity) { + return spawn(telegraph, entity, 0); + } + + /** + * Spawn a telegraph that follows an entity with yaw. + */ + public TelegraphInstance spawn(Telegraph telegraph, Entity entity, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, entity.posX, entity.posY, entity.posZ, yaw); + instance.setWorld(entity.worldObj); + instance.setEntityIdToFollow(entity.getEntityId()); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.sendToTracking(instance, entity); + return instance; + } + + /** + * Spawn a telegraph to a specific player only. + */ + public TelegraphInstance spawnToPlayer(Telegraph telegraph, EntityPlayerMP player, double x, double y, double z, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setWorld(player.worldObj); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.send(instance, player); + return instance; + } + + /** + * Spawn a telegraph to all players on the server. + */ + public TelegraphInstance spawnToAll(Telegraph telegraph, World world, double x, double y, double z, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setWorld(world); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.sendToAll(instance); + return instance; + } + + /** + * Spawn a telegraph to all players tracking an entity. + */ + public TelegraphInstance spawnToTracking(Telegraph telegraph, Entity entity, double x, double y, double z, float yaw) { + TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); + instance.setWorld(entity.worldObj); + activeInstances.put(instance.getInstanceId(), instance); + TelegraphSpawnPacket.sendToTracking(instance, entity); + return instance; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // REMOVAL METHODS + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Remove a telegraph by instance ID. + */ + public void remove(String instanceId) { + TelegraphInstance instance = activeInstances.remove(instanceId); + if (instance != null && instance.getWorld() != null) { + TelegraphRemovePacket.sendToDimension(instanceId, instance.getDimensionId()); + } + } + + /** + * Remove a telegraph instance. + */ + public void remove(TelegraphInstance instance) { + if (instance != null) { + remove(instance.getInstanceId()); + } + } + + /** + * Remove a telegraph for a specific player only. + */ + public void removeFromPlayer(String instanceId, EntityPlayerMP player) { + activeInstances.remove(instanceId); + TelegraphRemovePacket.send(instanceId, player); + } + + /** + * Get an active telegraph instance by ID. + */ + public TelegraphInstance getActiveInstance(String instanceId) { + return activeInstances.get(instanceId); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // PRESET MANAGEMENT + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Save a telegraph preset (internal use). + * + * @param name The name for the preset + * @param telegraph The telegraph configuration to save + */ + public void saveInternal(String name, Telegraph telegraph) { + if (name == null || name.isEmpty() || telegraph == null) return; + + telegraph.setId(name); + savedTelegraphs.put(name, telegraph); + saveTelegraphFile(name, telegraph); + } + + /** + * Get a saved telegraph preset by name (internal use). + * + * @param name The preset name + * @return The telegraph configuration, or null if not found + */ + public Telegraph getInternal(String name) { + return savedTelegraphs.get(name); + } + + /** + * Delete a saved telegraph preset. + * + * @param name The preset name + */ + public void delete(String name) { + Telegraph removed = savedTelegraphs.remove(name); + if (removed != null) { + File file = new File(getDir(), name + ".json"); + if (file.exists()) { + file.delete(); + } + } + } + + /** + * Check if a preset exists. + */ + public boolean has(String name) { + return savedTelegraphs.containsKey(name); + } + + /** + * Get all saved preset names. + */ + public String[] getSavedNames() { + return savedTelegraphs.keySet().toArray(new String[0]); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // PERSISTENCE + // ═══════════════════════════════════════════════════════════════════════════ + + private File getDir() { + return new File(CustomNpcs.getWorldSaveDirectory(), "telegraphs"); + } + + public void load() { + savedTelegraphs.clear(); + LogWriter.info("Loading telegraph presets..."); + + File dir = getDir(); + if (!dir.exists()) { + dir.mkdirs(); + return; + } + + File[] files = dir.listFiles(); + if (files == null) return; + + for (File file : files) { + if (!file.isFile() || !file.getName().endsWith(".json")) continue; + try { + Telegraph telegraph = new Telegraph(); + telegraph.readNBT(NBTJsonUtil.LoadFile(file)); + String name = file.getName().substring(0, file.getName().length() - 5); + telegraph.setId(name); + savedTelegraphs.put(name, telegraph); + } catch (Exception e) { + LogWriter.error("Error loading telegraph preset: " + file.getName(), e); + } + } + + LogWriter.info("Loaded " + savedTelegraphs.size() + " telegraph presets."); + } + + private void saveTelegraphFile(String name, Telegraph telegraph) { + try { + File dir = getDir(); + if (!dir.exists()) { + dir.mkdirs(); + } + + File file = new File(dir, name + ".json_new"); + File file2 = new File(dir, name + ".json"); + + NBTJsonUtil.SaveFile(file, telegraph.writeNBT()); + if (file2.exists()) { + file2.delete(); + } + file.renameTo(file2); + } catch (Exception e) { + LogWriter.error("Error saving telegraph preset: " + name, e); + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // ITelegraphHandler INTERFACE IMPLEMENTATION + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public ITelegraph createCircle(float radius) { + return new ScriptTelegraph(Telegraph.circle(radius)); + } + + @Override + public ITelegraph createRing(float outerRadius, float innerRadius) { + return new ScriptTelegraph(Telegraph.ring(outerRadius, innerRadius)); + } + + @Override + public ITelegraph createLine(float length, float width) { + return new ScriptTelegraph(Telegraph.line(length, width)); + } + + @Override + public ITelegraph createCone(float length, float angle) { + return new ScriptTelegraph(Telegraph.cone(length, angle)); + } + + @Override + public ITelegraph createPoint() { + return new ScriptTelegraph(Telegraph.point()); + } + + @Override + public ITelegraph create(String type) { + if (type == null) return null; + try { + TelegraphType telegraphType = TelegraphType.valueOf(type.toUpperCase()); + Telegraph telegraph; + switch (telegraphType) { + case CIRCLE: + telegraph = Telegraph.circle(3.0f); + break; + case RING: + telegraph = Telegraph.ring(5.0f, 2.0f); + break; + case LINE: + telegraph = Telegraph.line(5.0f, 2.0f); + break; + case CONE: + telegraph = Telegraph.cone(5.0f, 45.0f); + break; + case POINT: + telegraph = Telegraph.point(); + break; + default: + telegraph = Telegraph.circle(3.0f); + } + return new ScriptTelegraph(telegraph); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Override + public ITelegraphInstance spawn(ITelegraph telegraph, IWorld world, double x, double y, double z) { + return spawn(telegraph, world, x, y, z, 0); + } + + @Override + public ITelegraphInstance spawn(ITelegraph telegraph, IWorld world, double x, double y, double z, float yaw) { + if (telegraph == null || world == null) return null; + Telegraph internal = ((ScriptTelegraph) telegraph).getMCTelegraph(); + TelegraphInstance instance = spawn(internal, (World) world.getMCWorld(), x, y, z, yaw); + return new ScriptTelegraphInstance(instance); + } + + @Override + public ITelegraphInstance spawn(ITelegraph telegraph, IEntity entity) { + return spawn(telegraph, entity, 0); + } + + @Override + public ITelegraphInstance spawn(ITelegraph telegraph, IEntity entity, float yaw) { + if (telegraph == null || entity == null) return null; + Telegraph internal = ((ScriptTelegraph) telegraph).getMCTelegraph(); + TelegraphInstance instance = spawn(internal, (Entity) entity.getMCEntity(), yaw); + return new ScriptTelegraphInstance(instance); + } + + @Override + public ITelegraph get(String name) { + Telegraph telegraph = savedTelegraphs.get(name); + return telegraph != null ? new ScriptTelegraph(new Telegraph(telegraph)) : null; + } + + @Override + public void save(String name, ITelegraph telegraph) { + if (name == null || name.isEmpty() || telegraph == null) return; + Telegraph internal = ((ScriptTelegraph) telegraph).getMCTelegraph(); + saveInternal(name, internal); + } + + @Override + public void remove(ITelegraphInstance instance) { + if (instance != null) { + remove(instance.getInstanceId()); + } + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 48cf46cb3..65a4e8ac5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -2,9 +2,9 @@ package kamkeel.npcs.controllers.data.ability; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; -import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.block.Block; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java index a071fcb30..3c448d0b6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index a98cbcc59..6c43ea1cb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -4,9 +4,9 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index 7095e2879..611d0a4a6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 0c0f9cf2c..4edc18f82 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index b78aec03d..fa46c2341 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index d63c19e76..028259826 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -4,8 +4,8 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index cae5f6815..834b9c055 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index efbe8f337..408120694 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 1fac0ac43..99583a5d0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index b37832535..8ff9f5410 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index 9826fe599..af3489214 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 5734bf271..00da63e7f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -4,9 +4,9 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.Telegraph; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index 50d59e729..ad5989f25 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.block.Block; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 40ca4b4d3..05eb43798 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -4,8 +4,8 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index ab7171461..dee010da0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -4,7 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.potion.Potion; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java similarity index 97% rename from src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java rename to src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java index b8f92de17..3ba858acc 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/Telegraph.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/Telegraph.java @@ -1,10 +1,10 @@ -package kamkeel.npcs.controllers.data.ability.telegraph; +package kamkeel.npcs.controllers.data.telegraph; import net.minecraft.nbt.NBTTagCompound; /** * Telegraph configuration - defines a visual warning shape. - * Telegraphs warn players about incoming attacks. + * Telegraphs warn players about incoming attacks or mark areas of effect. */ public class Telegraph { @@ -146,6 +146,10 @@ public class Telegraph { return t; } + public static Telegraph point() { + return new Telegraph("", TelegraphType.POINT); + } + // ═══════════════════════════════════════════════════════════════════ // GETTERS & SETTERS // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java similarity index 94% rename from src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java rename to src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java index 2d705fb15..9f90bc29b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphInstance.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java @@ -1,4 +1,4 @@ -package kamkeel.npcs.controllers.data.ability.telegraph; +package kamkeel.npcs.controllers.data.telegraph; import net.minecraft.block.Block; import net.minecraft.entity.Entity; @@ -34,6 +34,10 @@ public class TelegraphInstance { // State private boolean isWarning = false; + // Server-side tracking (for removal) + private transient World world; + private transient int dimensionId; + public TelegraphInstance() { this.instanceId = UUID.randomUUID().toString().substring(0, 8); } @@ -303,4 +307,23 @@ public class TelegraphInstance { public void setWarning(boolean warning) { isWarning = warning; } + + public World getWorld() { + return world; + } + + public void setWorld(World world) { + this.world = world; + if (world != null) { + this.dimensionId = world.provider.dimensionId; + } + } + + public int getDimensionId() { + return dimensionId; + } + + public void setDimensionId(int dimensionId) { + this.dimensionId = dimensionId; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphManager.java similarity index 98% rename from src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java rename to src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphManager.java index 3dc4f9e0f..57b245fd4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphManager.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphManager.java @@ -1,4 +1,4 @@ -package kamkeel.npcs.controllers.data.ability.telegraph; +package kamkeel.npcs.controllers.data.telegraph; import net.minecraft.world.World; @@ -95,7 +95,7 @@ public class TelegraphManager { } // ═══════════════════════════════════════════════════════════════════ - // HELPER METHODS FOR ABILITIES + // HELPER METHODS FOR SPAWNING // ═══════════════════════════════════════════════════════════════════ /** diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphType.java similarity index 89% rename from src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java rename to src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphType.java index c7e4978b6..4a2fe4b12 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/telegraph/TelegraphType.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphType.java @@ -1,4 +1,4 @@ -package kamkeel.npcs.controllers.data.ability.telegraph; +package kamkeel.npcs.controllers.data.telegraph; /** * Types of telegraph shapes for visual attack warnings. diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 8d008ceea..d09882cfc 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -26,8 +26,8 @@ import kamkeel.npcs.network.packets.data.SoundManagementPacket; import kamkeel.npcs.network.packets.data.SwingPlayerArmPacket; import kamkeel.npcs.network.packets.data.UpdateAnimationsPacket; import kamkeel.npcs.network.packets.data.VillagerListPacket; -import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket; -import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; import kamkeel.npcs.network.packets.data.gui.GuiClosePacket; import kamkeel.npcs.network.packets.data.gui.GuiErrorPacket; import kamkeel.npcs.network.packets.data.gui.GuiOpenBookPacket; diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphRemovePacket.java similarity index 75% rename from src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphRemovePacket.java rename to src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphRemovePacket.java index e0c5023cb..9e84bb224 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphRemovePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphRemovePacket.java @@ -1,9 +1,9 @@ -package kamkeel.npcs.network.packets.data.ability; +package kamkeel.npcs.network.packets.data.telegraph; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; +import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; @@ -16,7 +16,7 @@ import java.io.IOException; /** * Packet to remove a telegraph on the client. - * Sent from server to client when an ability's telegraph should be removed early. + * Sent from server to client when a telegraph should be removed early. */ public final class TelegraphRemovePacket extends AbstractPacket { public static final String packetName = "Data|TelegraphRemove"; @@ -67,4 +67,18 @@ public final class TelegraphRemovePacket extends AbstractPacket { public static void sendToTracking(String instanceId, net.minecraft.entity.Entity entity) { PacketHandler.Instance.sendTracking(new TelegraphRemovePacket(instanceId), entity); } + + /** + * Send remove to all players in a dimension. + */ + public static void sendToDimension(String instanceId, int dimensionId) { + PacketHandler.Instance.sendToDimension(new TelegraphRemovePacket(instanceId), dimensionId); + } + + /** + * Send remove to all players on the server. + */ + public static void sendToAll(String instanceId) { + PacketHandler.Instance.sendToAll(new TelegraphRemovePacket(instanceId)); + } } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphSpawnPacket.java similarity index 84% rename from src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java rename to src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphSpawnPacket.java index e9c846764..fa30229ce 100644 --- a/src/main/java/kamkeel/npcs/network/packets/data/ability/TelegraphSpawnPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/data/telegraph/TelegraphSpawnPacket.java @@ -1,10 +1,10 @@ -package kamkeel.npcs.network.packets.data.ability; +package kamkeel.npcs.network.packets.data.telegraph; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import kamkeel.npcs.network.AbstractPacket; import kamkeel.npcs.network.PacketChannel; import kamkeel.npcs.network.PacketHandler; @@ -18,7 +18,7 @@ import java.io.IOException; /** * Packet to spawn a telegraph on the client. - * Sent from server to client when an ability creates a telegraph. + * Sent from server to client when a telegraph is created. */ public final class TelegraphSpawnPacket extends AbstractPacket { public static final String packetName = "Data|TelegraphSpawn"; @@ -78,4 +78,11 @@ public final class TelegraphSpawnPacket extends AbstractPacket { public static void sendToDimension(TelegraphInstance instance, int dimensionId) { PacketHandler.Instance.sendToDimension(new TelegraphSpawnPacket(instance), dimensionId); } + + /** + * Send telegraph to all players on the server. + */ + public static void sendToAll(TelegraphInstance instance) { + PacketHandler.Instance.sendToAll(new TelegraphSpawnPacket(instance)); + } } diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 5977d7263..ed084c578 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -24,6 +24,7 @@ import kamkeel.npcs.command.profile.CommandProfile; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.ProfileController; import kamkeel.npcs.controllers.SyncController; +import kamkeel.npcs.controllers.TelegraphController; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.profile.CNPCData; import kamkeel.npcs.developer.Developer; @@ -331,7 +332,7 @@ public class CustomNpcs { new AnimationController(); new AbilityController(); AbilityController.Instance.load(); - + TelegraphController.init(); LinkedItemController.getInstance().load(); diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 3bed6075f..8d390f5cc 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -3,9 +3,9 @@ package noppes.npcs; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityPhase; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphInstance; -import kamkeel.npcs.network.packets.data.ability.TelegraphRemovePacket; -import kamkeel.npcs.network.packets.data.ability.TelegraphSpawnPacket; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 8eea9e27b..95dceaec2 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -7,7 +7,7 @@ import cpw.mods.fml.common.FMLCommonHandler; import cpw.mods.fml.common.ObfuscationReflectionHelper; import kamkeel.npcs.addon.client.DBCClient; import kamkeel.npcs.client.renderer.TelegraphRenderer; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; +import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.model.ModelBiped; diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index 78a473f41..6142de16c 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -5,7 +5,7 @@ import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.InputEvent; import cpw.mods.fml.common.gameevent.TickEvent; import cpw.mods.fml.common.gameevent.TickEvent.Phase; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphManager; +import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.player.CheckPlayerValue; import kamkeel.npcs.network.packets.player.InputDevicePacket; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index 1146ea4ae..58e133bc8 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -3,7 +3,7 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.Condition; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.telegraph.TelegraphType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.client.gui.GuiButton; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.StatCollector; diff --git a/src/main/java/noppes/npcs/scripted/NpcAPI.java b/src/main/java/noppes/npcs/scripted/NpcAPI.java index ecf4b8ba0..1bdd20cfe 100644 --- a/src/main/java/noppes/npcs/scripted/NpcAPI.java +++ b/src/main/java/noppes/npcs/scripted/NpcAPI.java @@ -5,6 +5,7 @@ import cpw.mods.fml.common.eventhandler.EventBus; import foxz.command.ScriptedCommand; import kamkeel.npcs.controllers.AttributeController; import kamkeel.npcs.controllers.ProfileController; +import kamkeel.npcs.controllers.TelegraphController; import kamkeel.npcs.controllers.data.ability.AbilityController; import net.minecraft.block.Block; import net.minecraft.command.CommandHandler; @@ -52,6 +53,7 @@ import noppes.npcs.api.IContainer; import noppes.npcs.api.IDamageSource; import noppes.npcs.api.INbt; import noppes.npcs.api.IParticle; +import noppes.npcs.api.ITelegraph; import noppes.npcs.api.IPos; import noppes.npcs.api.ISkinOverlay; import noppes.npcs.api.ITileEntity; @@ -75,6 +77,7 @@ import noppes.npcs.api.handler.IProfileHandler; import noppes.npcs.api.handler.IQuestHandler; import noppes.npcs.api.handler.IRecipeHandler; import noppes.npcs.api.handler.IScriptHookHandler; +import noppes.npcs.api.handler.ITelegraphHandler; import noppes.npcs.api.handler.ITransportHandler; import noppes.npcs.api.handler.data.IAnimation; import noppes.npcs.api.handler.data.IFrame; @@ -370,6 +373,18 @@ public class NpcAPI extends AbstractNpcAPI { return AbilityController.Instance; } + @Override + public ITelegraphHandler getTelegraphs() { + this.checkWorld(); + return TelegraphController.Instance; + } + + @Override + public ITelegraph createTelegraph(String type) { + this.checkWorld(); + return TelegraphController.Instance.create(type); + } + @Override public String[] getAllBiomeNames() { List biomes = new ArrayList<>(); diff --git a/src/main/java/noppes/npcs/scripted/ScriptTelegraph.java b/src/main/java/noppes/npcs/scripted/ScriptTelegraph.java new file mode 100644 index 000000000..f03be5033 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/ScriptTelegraph.java @@ -0,0 +1,210 @@ +package noppes.npcs.scripted; + +import kamkeel.npcs.controllers.TelegraphController; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import net.minecraft.entity.Entity; +import net.minecraft.world.World; +import noppes.npcs.api.ITelegraph; +import noppes.npcs.api.ITelegraphInstance; +import noppes.npcs.api.IWorld; +import noppes.npcs.api.entity.IEntity; + +/** + * Script wrapper for Telegraph configuration. + * Implements ITelegraph API interface. + */ +public class ScriptTelegraph implements ITelegraph { + + private final Telegraph telegraph; + + public ScriptTelegraph(Telegraph telegraph) { + this.telegraph = telegraph; + } + + /** + * Get the underlying Telegraph object. + */ + public Telegraph getMCTelegraph() { + return telegraph; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // TYPE + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public String getType() { + return telegraph.getType().name(); + } + + @Override + public void setType(String type) { + if (type == null) return; + try { + telegraph.setType(TelegraphType.valueOf(type.toUpperCase())); + } catch (IllegalArgumentException ignored) { + } + } + + // ═══════════════════════════════════════════════════════════════════════════ + // SHAPE PARAMETERS + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public float getRadius() { + return telegraph.getRadius(); + } + + @Override + public void setRadius(float radius) { + telegraph.setRadius(radius); + } + + @Override + public float getInnerRadius() { + return telegraph.getInnerRadius(); + } + + @Override + public void setInnerRadius(float innerRadius) { + telegraph.setInnerRadius(innerRadius); + } + + @Override + public float getLength() { + return telegraph.getLength(); + } + + @Override + public void setLength(float length) { + telegraph.setLength(length); + } + + @Override + public float getWidth() { + return telegraph.getWidth(); + } + + @Override + public void setWidth(float width) { + telegraph.setWidth(width); + } + + @Override + public float getAngle() { + return telegraph.getAngle(); + } + + @Override + public void setAngle(float angle) { + telegraph.setAngle(angle); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // TIMING + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public int getDuration() { + return telegraph.getDurationTicks(); + } + + @Override + public void setDuration(int ticks) { + telegraph.setDurationTicks(ticks); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // COLORS + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public int getColor() { + return telegraph.getColor(); + } + + @Override + public void setColor(int argb) { + telegraph.setColor(argb); + } + + @Override + public int getWarningColor() { + return telegraph.getWarningColor(); + } + + @Override + public void setWarningColor(int argb) { + telegraph.setWarningColor(argb); + } + + @Override + public int getWarningStartTick() { + return telegraph.getWarningStartTick(); + } + + @Override + public void setWarningStartTick(int tick) { + telegraph.setWarningStartTick(tick); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // ANIMATION + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public boolean isAnimated() { + return telegraph.isAnimated(); + } + + @Override + public void setAnimated(boolean animated) { + telegraph.setAnimated(animated); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // POSITIONING + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public float getHeightOffset() { + return telegraph.getHeightOffset(); + } + + @Override + public void setHeightOffset(float offset) { + telegraph.setHeightOffset(offset); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // SPAWN METHODS + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public ITelegraphInstance spawn(IWorld world, double x, double y, double z) { + return spawn(world, x, y, z, 0); + } + + @Override + public ITelegraphInstance spawn(IWorld world, double x, double y, double z, float yaw) { + if (world == null || world.getMCWorld() == null) return null; + World mcWorld = (World) world.getMCWorld(); + TelegraphInstance instance = TelegraphController.Instance.spawn(telegraph, mcWorld, x, y, z, yaw); + return new ScriptTelegraphInstance(instance); + } + + @Override + public ITelegraphInstance spawn(IEntity entity) { + return spawn(entity, 0); + } + + @Override + public ITelegraphInstance spawn(IEntity entity, float yaw) { + if (entity == null || entity.getMCEntity() == null) return null; + Entity mcEntity = (Entity) entity.getMCEntity(); + TelegraphInstance instance = TelegraphController.Instance.spawn(telegraph, mcEntity, yaw); + return new ScriptTelegraphInstance(instance); + } +} diff --git a/src/main/java/noppes/npcs/scripted/ScriptTelegraphInstance.java b/src/main/java/noppes/npcs/scripted/ScriptTelegraphInstance.java new file mode 100644 index 000000000..b307a6dc7 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/ScriptTelegraphInstance.java @@ -0,0 +1,122 @@ +package noppes.npcs.scripted; + +import kamkeel.npcs.controllers.TelegraphController; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import net.minecraft.entity.Entity; +import noppes.npcs.api.ITelegraphInstance; +import noppes.npcs.api.entity.IEntity; + +/** + * Script wrapper for TelegraphInstance. + * Implements ITelegraphInstance API interface. + */ +public class ScriptTelegraphInstance implements ITelegraphInstance { + + private final TelegraphInstance instance; + + public ScriptTelegraphInstance(TelegraphInstance instance) { + this.instance = instance; + } + + /** + * Get the underlying TelegraphInstance object. + */ + public TelegraphInstance getMCInstance() { + return instance; + } + + @Override + public String getInstanceId() { + return instance.getInstanceId(); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // POSITION + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public double getX() { + return instance.getX(); + } + + @Override + public double getY() { + return instance.getY(); + } + + @Override + public double getZ() { + return instance.getZ(); + } + + @Override + public float getYaw() { + return instance.getYaw(); + } + + @Override + public void setPosition(double x, double y, double z) { + instance.setX(x); + instance.setY(y); + instance.setZ(z); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // ENTITY FOLLOWING + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public void followEntity(IEntity entity) { + if (entity == null || entity.getMCEntity() == null) return; + Entity mcEntity = (Entity) entity.getMCEntity(); + instance.setEntityIdToFollow(mcEntity.getEntityId()); + } + + @Override + public void stopFollowing() { + instance.lockPosition(); + } + + @Override + public boolean isFollowing() { + return instance.getEntityIdToFollow() >= 0; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // TIMING + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public int getRemainingTicks() { + return instance.getRemainingTicks(); + } + + @Override + public int getTotalTicks() { + return instance.getTotalTicks(); + } + + @Override + public float getProgress() { + return instance.getProgress(); + } + + @Override + public boolean isWarning() { + return instance.isWarning(); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // CONTROL + // ═══════════════════════════════════════════════════════════════════════════ + + @Override + public void remove() { + TelegraphController.Instance.remove(instance); + } + + @Override + public void lockPosition() { + instance.lockPosition(); + } +} From d1621d436813037718019cec02f32502a515aa7f Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 24 Jan 2026 03:24:57 -0500 Subject: [PATCH 275/358] Auction Fixes to Tooltips, Auction Command, Money Command --- .../kamkeel/npcs/command/AuctionCommand.java | 167 +++++++++++++++++ .../kamkeel/npcs/command/CommandKamkeel.java | 4 + .../kamkeel/npcs/command/MoneyCommand.java | 168 ++++++++++++++++++ .../packets/player/AuctionActionPacket.java | 6 +- .../gui/player/AuctionTooltipHandler.java | 112 +++++++++--- .../client/gui/player/GuiAuctionSell.java | 68 +------ .../client/gui/player/GuiAuctionTrades.java | 84 ++++++--- .../containers/ContainerAuctionTrades.java | 20 ++- .../containers/InventoryAuctionDisplay.java | 11 +- .../npcs/controllers/AuctionController.java | 31 ++-- .../npcs/controllers/data/AuctionClaim.java | 6 +- .../assets/customnpcs/lang/en_US.lang | 13 ++ 12 files changed, 558 insertions(+), 132 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/command/AuctionCommand.java create mode 100644 src/main/java/kamkeel/npcs/command/MoneyCommand.java diff --git a/src/main/java/kamkeel/npcs/command/AuctionCommand.java b/src/main/java/kamkeel/npcs/command/AuctionCommand.java new file mode 100644 index 000000000..056116eba --- /dev/null +++ b/src/main/java/kamkeel/npcs/command/AuctionCommand.java @@ -0,0 +1,167 @@ +package kamkeel.npcs.command; + +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import noppes.npcs.NoppesUtilServer; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.constants.EnumGuiType; +import noppes.npcs.controllers.AuctionController; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.AuctionListing; +import noppes.npcs.controllers.data.PlayerData; + +import java.util.List; +import java.util.UUID; + +import static kamkeel.npcs.util.ColorUtil.sendError; +import static kamkeel.npcs.util.ColorUtil.sendMessage; +import static kamkeel.npcs.util.ColorUtil.sendResult; + +public class AuctionCommand extends CommandKamkeelBase { + + @Override + public String getCommandName() { + return "auction"; + } + + @Override + public String getDescription() { + return "Auction House operations"; + } + + @SubCommand( + desc = "List active auctions", + usage = "[page]" + ) + public void list(ICommandSender sender, String[] args) throws CommandException { + if (!ConfigMarket.AuctionEnabled) { + sendError(sender, "Auction system is disabled"); + return; + } + + AuctionController controller = AuctionController.getInstance(); + if (controller == null) { + sendError(sender, "Auction system is not available"); + return; + } + + int page = 1; + if (args.length > 0) { + try { + page = Integer.parseInt(args[0]); + } catch (NumberFormatException ex) { + sendError(sender, "Invalid page number: " + args[0]); + return; + } + } + + List listings = controller.getActiveListings(null, page - 1, 10); + int total = controller.getTotalActiveListings(null); + int totalPages = Math.max(1, (int) Math.ceil((double) total / 10)); + + if (listings.isEmpty()) { + sendMessage(sender, "\u00A77No active auctions found"); + return; + } + + sendMessage(sender, String.format("\u00A76=== Active Auctions (Page %d/%d) ===", page, totalPages)); + for (AuctionListing listing : listings) { + String itemName = listing.item != null ? listing.item.getDisplayName() : "Unknown"; + String price = listing.hasBids() ? + String.format("%,d (bid)", listing.currentBid) : + String.format("%,d (start)", listing.startingPrice); + sendMessage(sender, String.format("\u00A7e%s \u00A77by \u00A7b%s\u00A77 - \u00A76%s %s", + itemName, listing.sellerName, price, ConfigMarket.CurrencyName)); + } + } + + @SubCommand( + desc = "View player's auction activity", + usage = "" + ) + public void view(ICommandSender sender, String[] args) throws CommandException { + if (!ConfigMarket.AuctionEnabled) { + sendError(sender, "Auction system is disabled"); + return; + } + + AuctionController controller = AuctionController.getInstance(); + if (controller == null) { + sendError(sender, "Auction system is not available"); + return; + } + + String playername = args[0]; + List dataList = PlayerDataController.Instance.getPlayersData(sender, playername); + + if (dataList.isEmpty()) { + sendError(sender, "Unknown player: " + playername); + return; + } + + for (PlayerData playerdata : dataList) { + UUID playerUUID = UUID.fromString(playerdata.uuid); + List activeListings = controller.getPlayerActiveListings(playerUUID); + List activeBids = controller.getPlayerActiveBids(playerUUID); + int claimCount = controller.getPlayerClaims(playerUUID).size(); + + sendMessage(sender, String.format("\u00A76=== Auction Activity for %s ===", playerdata.playername)); + sendMessage(sender, String.format("\u00A77Active Listings: \u00A7e%d", activeListings.size())); + sendMessage(sender, String.format("\u00A77Active Bids: \u00A7e%d", activeBids.size())); + sendMessage(sender, String.format("\u00A77Pending Claims: \u00A7e%d", claimCount)); + + if (!activeListings.isEmpty()) { + sendMessage(sender, "\u00A7a--- Selling ---"); + for (AuctionListing listing : activeListings) { + String itemName = listing.item != null ? listing.item.getDisplayName() : "Unknown"; + String price = listing.hasBids() ? + String.format("%,d (bid)", listing.currentBid) : + String.format("%,d (start)", listing.startingPrice); + sendMessage(sender, String.format(" \u00A7e%s \u00A77- \u00A76%s", itemName, price)); + } + } + + if (!activeBids.isEmpty()) { + sendMessage(sender, "\u00A7e--- Bidding ---"); + for (AuctionListing bid : activeBids) { + String itemName = bid.item != null ? bid.item.getDisplayName() : "Unknown"; + sendMessage(sender, String.format(" \u00A7e%s \u00A77- \u00A76%,d %s", + itemName, bid.currentBid, ConfigMarket.CurrencyName)); + } + } + } + } + + @SubCommand( + desc = "Open the Auction House GUI", + usage = "", + permission = 0 + ) + public void open(ICommandSender sender, String[] args) throws CommandException { + if (!ConfigMarket.AuctionEnabled) { + sendError(sender, "Auction system is disabled"); + return; + } + + if (!(sender instanceof EntityPlayer)) { + sendError(sender, "This command can only be used by players"); + return; + } + + EntityPlayer player = (EntityPlayer) sender; + + // Open the auction GUI without requiring an NPC + NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuction, null); + sendResult(sender, "Opening Auction House..."); + } + + @Override + public List addTabCompletionOptions(ICommandSender par1, String[] args) { + if (args.length == 1) { + return getListOfStringsMatchingLastWord(args, new String[]{"list", "view", "open"}); + } + return null; + } +} diff --git a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java index 356ca8a31..d0341f211 100644 --- a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java +++ b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java @@ -10,6 +10,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.server.MinecraftServer; import noppes.npcs.CustomNpcsPermissions; import noppes.npcs.config.ConfigMain; +import noppes.npcs.config.ConfigMarket; import noppes.npcs.scripted.CustomNPCsException; import java.lang.reflect.Method; @@ -44,6 +45,9 @@ public class CommandKamkeel extends CommandBase { registerCommand(new CommandCommand()); registerCommand(new EffectCommand()); registerCommand(new AbilityCommand()); + registerCommand(new MoneyCommand()); + if (ConfigMarket.AuctionEnabled) + registerCommand(new AuctionCommand()); if (ConfigMain.AttributesEnabled) registerCommand(new AttributeCommand()); } diff --git a/src/main/java/kamkeel/npcs/command/MoneyCommand.java b/src/main/java/kamkeel/npcs/command/MoneyCommand.java new file mode 100644 index 000000000..8b9fa72eb --- /dev/null +++ b/src/main/java/kamkeel/npcs/command/MoneyCommand.java @@ -0,0 +1,168 @@ +package kamkeel.npcs.command; + +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; +import noppes.npcs.config.ConfigMarket; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.PlayerCurrencyData; +import noppes.npcs.controllers.data.PlayerData; + +import java.util.List; + +import static kamkeel.npcs.util.ColorUtil.sendError; +import static kamkeel.npcs.util.ColorUtil.sendResult; + +public class MoneyCommand extends CommandKamkeelBase { + + @Override + public String getCommandName() { + return "money"; + } + + @Override + public String getDescription() { + return "Currency operations"; + } + + @SubCommand( + desc = "Show player's balance", + usage = "" + ) + public void balance(ICommandSender sender, String[] args) throws CommandException { + String playername = args[0]; + List dataList = PlayerDataController.Instance.getPlayersData(sender, playername); + + if (dataList.isEmpty()) { + sendError(sender, "Unknown player: " + playername); + return; + } + + for (PlayerData playerdata : dataList) { + PlayerCurrencyData currency = playerdata.currencyData; + long balance = currency.getBalance(); + sendResult(sender, String.format("Player \u00A7b%s\u00A77 has \u00A76%,d %s", + playerdata.playername, balance, ConfigMarket.CurrencyName)); + } + } + + @SubCommand( + desc = "Give currency to player", + usage = " " + ) + public void give(ICommandSender sender, String[] args) throws CommandException { + String playername = args[0]; + long amount; + try { + amount = Long.parseLong(args[1]); + } catch (NumberFormatException ex) { + sendError(sender, "Invalid amount: " + args[1]); + return; + } + + if (amount <= 0) { + sendError(sender, "Amount must be positive"); + return; + } + + List dataList = PlayerDataController.Instance.getPlayersData(sender, playername); + + if (dataList.isEmpty()) { + sendError(sender, "Unknown player: " + playername); + return; + } + + for (PlayerData playerdata : dataList) { + PlayerCurrencyData currency = playerdata.currencyData; + if (currency.deposit(amount)) { + playerdata.save(); + sendResult(sender, String.format("Gave \u00A76%,d %s\u00A77 to player \u00A7b%s\u00A77. New balance: \u00A76%,d", + amount, ConfigMarket.CurrencyName, playerdata.playername, currency.getBalance())); + } else { + sendError(sender, String.format("Failed to give currency to %s (would exceed max balance)", + playerdata.playername)); + } + } + } + + @SubCommand( + desc = "Withdraw currency from player", + usage = " " + ) + public void withdraw(ICommandSender sender, String[] args) throws CommandException { + String playername = args[0]; + long amount; + try { + amount = Long.parseLong(args[1]); + } catch (NumberFormatException ex) { + sendError(sender, "Invalid amount: " + args[1]); + return; + } + + if (amount <= 0) { + sendError(sender, "Amount must be positive"); + return; + } + + List dataList = PlayerDataController.Instance.getPlayersData(sender, playername); + + if (dataList.isEmpty()) { + sendError(sender, "Unknown player: " + playername); + return; + } + + for (PlayerData playerdata : dataList) { + PlayerCurrencyData currency = playerdata.currencyData; + if (currency.withdraw(amount)) { + playerdata.save(); + sendResult(sender, String.format("Withdrew \u00A76%,d %s\u00A77 from player \u00A7b%s\u00A77. New balance: \u00A76%,d", + amount, ConfigMarket.CurrencyName, playerdata.playername, currency.getBalance())); + } else { + sendError(sender, String.format("Failed to withdraw from %s (insufficient funds)", + playerdata.playername)); + } + } + } + + @SubCommand( + desc = "Set player's balance", + usage = " " + ) + public void set(ICommandSender sender, String[] args) throws CommandException { + String playername = args[0]; + long amount; + try { + amount = Long.parseLong(args[1]); + } catch (NumberFormatException ex) { + sendError(sender, "Invalid amount: " + args[1]); + return; + } + + if (amount < 0) { + sendError(sender, "Amount cannot be negative"); + return; + } + + List dataList = PlayerDataController.Instance.getPlayersData(sender, playername); + + if (dataList.isEmpty()) { + sendError(sender, "Unknown player: " + playername); + return; + } + + for (PlayerData playerdata : dataList) { + PlayerCurrencyData currency = playerdata.currencyData; + currency.setBalance(amount); + playerdata.save(); + sendResult(sender, String.format("Set balance of player \u00A7b%s\u00A77 to \u00A76%,d %s", + playerdata.playername, currency.getBalance(), ConfigMarket.CurrencyName)); + } + } + + @Override + public List addTabCompletionOptions(ICommandSender par1, String[] args) { + if (args.length == 1) { + return getListOfStringsMatchingLastWord(args, new String[]{"balance", "give", "withdraw", "set"}); + } + return null; + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java index a7549ab85..fca09473f 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java @@ -57,7 +57,7 @@ public class AuctionActionPacket extends AbstractPacket { } @Override - public boolean needsNPC() { return true; } + public boolean needsNPC() { return false; } // Allow opening via command without NPC // ========================================= // Client-side static methods @@ -174,7 +174,9 @@ public class AuctionActionPacket extends AbstractPacket { if (!(player instanceof EntityPlayerMP)) return; EntityPlayerMP playerMP = (EntityPlayerMP) player; - if (npc.advanced.role != EnumRoleType.Auctioneer) return; + // Skip role check if NPC is null (opened via command/script) + // Otherwise require Auctioneer role + if (npc != null && npc.advanced.role != EnumRoleType.Auctioneer) return; if (!ConfigMarket.AuctionEnabled) return; int actionOrdinal = in.readInt(); diff --git a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java index f8f58e29b..33966d1e2 100644 --- a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java +++ b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java @@ -8,7 +8,9 @@ import net.minecraft.item.ItemStack; import net.minecraft.util.EnumChatFormatting; import net.minecraft.util.StatCollector; import noppes.npcs.client.AuctionClientConfig; +import noppes.npcs.constants.EnumClaimType; import noppes.npcs.containers.ContainerAuctionListing; +import noppes.npcs.containers.ContainerAuctionSell; import noppes.npcs.containers.ContainerAuctionTrades; import noppes.npcs.controllers.data.AuctionClaim; import noppes.npcs.controllers.data.AuctionListing; @@ -41,6 +43,10 @@ public class AuctionTooltipHandler { else if (screen instanceof GuiAuctionTrades) { addTradesTooltip((GuiAuctionTrades) screen, stack, tooltip); } + // Check if we're in the sell GUI + else if (screen instanceof GuiAuctionSell) { + addSellTooltip((GuiAuctionSell) screen, stack, tooltip); + } } /** @@ -54,10 +60,7 @@ public class AuctionTooltipHandler { AuctionListing listing = container.getListingForItem(stack); if (listing == null) return; - // Add separator tooltip.add(""); - tooltip.add(EnumChatFormatting.DARK_GRAY + "--- " + EnumChatFormatting.GOLD + - StatCollector.translateToLocal("auction.title") + EnumChatFormatting.DARK_GRAY + " ---"); // Seller tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.seller") @@ -94,22 +97,21 @@ public class AuctionTooltipHandler { ContainerAuctionTrades container = (ContainerAuctionTrades) gui.inventorySlots; if (container == null) return; - // Check if it's an active listing - AuctionListing listing = container.getListingForItem(stack); - if (listing != null) { + // Check if it's an item I'm selling + AuctionListing sellingListing = container.getSellingListingForItem(stack); + if (sellingListing != null) { tooltip.add(""); - tooltip.add(EnumChatFormatting.DARK_GRAY + "--- " + EnumChatFormatting.GOLD + - StatCollector.translateToLocal("auction.trades.activeListing") + EnumChatFormatting.DARK_GRAY + " ---"); + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.selling")); - if (listing.hasBids()) { + if (sellingListing.hasBids()) { tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid") - .replace("%s", EnumChatFormatting.GOLD + formatCurrency(listing.currentBid))); - tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), listing.bidCount)); + .replace("%s", EnumChatFormatting.GOLD + formatCurrency(sellingListing.currentBid))); + tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), sellingListing.bidCount)); } else { tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.noBids")); } - long timeRemaining = listing.getTimeRemaining(); + long timeRemaining = sellingListing.getTimeRemaining(); String timeText = StatCollector.translateToLocal("auction.timeLeft") .replace("%s", formatTimeRemaining(timeRemaining)); tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); @@ -119,29 +121,65 @@ public class AuctionTooltipHandler { return; } + // Check if it's an item I'm bidding on + AuctionListing biddingListing = container.getBiddingListingForItem(stack); + if (biddingListing != null) { + tooltip.add(""); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.bidding")); + + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.currentBid") + .replace("%s", EnumChatFormatting.GOLD + formatCurrency(biddingListing.currentBid))); + tooltip.add(EnumChatFormatting.GRAY + String.format(StatCollector.translateToLocal("auction.bids"), biddingListing.bidCount)); + + long timeRemaining = biddingListing.getTimeRemaining(); + String timeText = StatCollector.translateToLocal("auction.timeLeft") + .replace("%s", formatTimeRemaining(timeRemaining)); + tooltip.add((timeRemaining < 3600000 ? EnumChatFormatting.RED : EnumChatFormatting.WHITE) + timeText); + + tooltip.add(""); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.clickToRebid")); + return; + } + // Check if it's a claim AuctionClaim claim = container.getClaimForItem(stack); if (claim != null) { tooltip.add(""); - String claimTitle; + // Title based on claim type switch (claim.type) { case ITEM: - claimTitle = StatCollector.translateToLocal("auction.claim.item"); + if (claim.isReturned) { + // Expired - returned to seller + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.expired")); + } else { + // Won item + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.won")); + } break; case CURRENCY: - claimTitle = StatCollector.translateToLocal("auction.claim.currency"); + // Sold - money to claim + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.sold")); + if (claim.itemName != null && !claim.itemName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.soldItem") + .replace("%s", EnumChatFormatting.WHITE + claim.itemName)); + } + if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.buyer") + .replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName)); + } break; case REFUND: - claimTitle = StatCollector.translateToLocal("auction.claim.refund"); + // Outbid - refund available + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.outbid")); + if (claim.otherPlayerName != null && !claim.otherPlayerName.isEmpty()) { + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.outbidBy") + .replace("%s", EnumChatFormatting.WHITE + claim.otherPlayerName)); + } break; - default: - claimTitle = "Claim"; } - tooltip.add(EnumChatFormatting.DARK_GRAY + "--- " + EnumChatFormatting.GOLD + - claimTitle + EnumChatFormatting.DARK_GRAY + " ---"); - + // Amount for currency claims (including REFUND) if (claim.type.isCurrency()) { tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.claim.amount") .replace("%s", EnumChatFormatting.GOLD + formatCurrency(claim.currency))); @@ -158,10 +196,40 @@ public class AuctionTooltipHandler { } tooltip.add(""); - tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.claim.clickToClaim")); + + // Show rebid option for REFUND claims with item (active auction) + if (claim.type == EnumClaimType.REFUND && claim.item != null) { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.leftClickRebid")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickRefund")); + } else { + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.claim.clickToClaim")); + } } } + /** + * Add sell page tooltip info. + */ + private static void addSellTooltip(GuiAuctionSell gui, ItemStack stack, List tooltip) { + ContainerAuctionSell container = (ContainerAuctionSell) gui.inventorySlots; + if (container == null) return; + + // Check if this is the sell slot item + ItemStack sellItem = container.getItemToSell(); + if (sellItem != null && ItemStack.areItemStacksEqual(stack, sellItem)) { + // This is the item in the sell slot + tooltip.add(""); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.leftClear")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.rightRemove")); + return; + } + + // This is an inventory item - show add controls + tooltip.add(""); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.leftAdd")); + tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.rightAddOne")); + } + /** * Format currency with commas. */ diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java index 8fe8c2d9c..4f67c781c 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java @@ -16,7 +16,6 @@ import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.client.AuctionClientConfig; import noppes.npcs.containers.ContainerAuctionSell; import noppes.npcs.entity.EntityNPCInterface; -import org.lwjgl.opengl.GL11; import java.util.ArrayList; import java.util.List; @@ -305,14 +304,8 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis super.drawGuiContainerForegroundLayer(mouseX, mouseY); if (hasSubGui()) return; - // Draw tooltips for buttons + // Draw tooltips for buttons (icons, not items - item tooltips handled by AuctionTooltipHandler) drawSellPageTooltips(mouseX, mouseY); - - // Sell slot tooltip - drawSellSlotTooltip(mouseX, mouseY); - - // Inventory item tooltip (only add extra info, item tooltip is handled by AuctionTooltipHandler) - drawInventoryTooltip(mouseX, mouseY); } /** Draw tooltips for sell page buttons */ @@ -333,64 +326,7 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis } if (tooltip != null && !tooltip.isEmpty()) { - drawTooltip(tooltip, mouseX - guiLeft, mouseY - guiTop); + drawHoveringText(tooltip, mouseX - guiLeft, mouseY - guiTop, fontRendererObj); } } - - /** Draw tooltip for sell slot */ - private void drawSellSlotTooltip(int mouseX, int mouseY) { - int slotX = guiLeft + contentX + 30; - int slotY = guiTop + contentY + 55; - - if (mouseX >= slotX && mouseX < slotX + 18 && mouseY >= slotY && mouseY < slotY + 18) { - ItemStack sellItem = sellContainer.getItemToSell(); - if (sellItem != null) { - List tooltip = new ArrayList<>(); - tooltip.add(EnumChatFormatting.GOLD + StatCollector.translateToLocal("auction.sell.slotTitle")); - tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.leftClear")); - tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.sell.rightRemove")); - drawTooltip(tooltip, mouseX - guiLeft, mouseY - guiTop); - } - } - } - - /** Draw tooltip for inventory items */ - private void drawInventoryTooltip(int mouseX, int mouseY) { - Slot hoveredSlot = getSlotUnderMouse(mouseX, mouseY); - if (hoveredSlot != null && sellContainer.isPlayerInventorySlot(hoveredSlot.slotNumber)) { - if (hoveredSlot.getStack() != null) { - List tooltip = new ArrayList<>(); - tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.leftAdd")); - tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.sell.rightAddOne")); - drawTooltip(tooltip, mouseX - guiLeft, mouseY - guiTop + 40); - } - } - } - - /** Get slot under mouse position */ - private Slot getSlotUnderMouse(int mouseX, int mouseY) { - for (int i = 0; i < inventorySlots.inventorySlots.size(); i++) { - Slot slot = (Slot) inventorySlots.inventorySlots.get(i); - if (isMouseOverSlot(slot, mouseX, mouseY)) { - return slot; - } - } - return null; - } - - /** Check if mouse is over a slot */ - private boolean isMouseOverSlot(Slot slot, int mouseX, int mouseY) { - int x = guiLeft + slot.xDisplayPosition; - int y = guiTop + slot.yDisplayPosition; - return mouseX >= x && mouseX < x + 16 && mouseY >= y && mouseY < y + 16; - } - - /** Draw tooltip with proper GL state */ - private void drawTooltip(List tooltip, int x, int y) { - GL11.glPushMatrix(); - GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); - drawHoveringText(tooltip, x, y, fontRendererObj); - GL11.glPopAttrib(); - GL11.glPopMatrix(); - } } diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java index 2bcdce495..0d6e3d98d 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java @@ -95,10 +95,24 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { // Left-click on active bid = open bidding GUI to increase bid AuctionActionPacket.openBidding(listing.id); NoppesUtil.clickSound(); - } else if (claim != null && mouseButton == 0) { - // Left-click on claim = show claim - setPending(slot, PendingOp.CLAIM); - NoppesUtil.clickSound(); + } else if (claim != null) { + // Handle claim clicks + if (claim.type == EnumClaimType.REFUND && claim.item != null) { + // REFUND with item = can rebid + if (mouseButton == 0) { + // Left-click = open bidding to rebid + AuctionActionPacket.openBidding(claim.listingId); + NoppesUtil.clickSound(); + } else if (mouseButton == 1) { + // Right-click = show refund claim confirmation + setPending(slot, PendingOp.CLAIM); + NoppesUtil.clickSound(); + } + } else if (mouseButton == 0) { + // Normal claim - left-click shows claim confirmation + setPending(slot, PendingOp.CLAIM); + NoppesUtil.clickSound(); + } } } @@ -112,18 +126,31 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { playConfirmSound(); } clearPending(); - } else if (pendingOp == PendingOp.CLAIM && mouseButton == 0) { - // Left-click confirms claim + } else if (pendingOp == PendingOp.CLAIM) { AuctionClaim claim = tradesContainer.getClaimAt(pendingSlot); if (claim != null) { - if (claim.type.isItem()) { - AuctionActionPacket.claimItem(claim.id); + // For REFUND claims with item (rebid option), right-click confirms refund + // For all others, left-click confirms claim + boolean shouldClaim = (claim.type == EnumClaimType.REFUND && claim.item != null) + ? mouseButton == 1 // Right-click for rebiddable refunds + : mouseButton == 0; // Left-click for normal claims + + if (shouldClaim) { + if (claim.type.isItem()) { + AuctionActionPacket.claimItem(claim.id); + } else { + AuctionActionPacket.claimCurrency(claim.id); + } + playConfirmSound(); + clearPending(); } else { - AuctionActionPacket.claimCurrency(claim.id); + // Wrong button - cancel operation + clearPending(); + NoppesUtil.clickSound(); } - playConfirmSound(); + } else { + clearPending(); } - clearPending(); } else { // Wrong button - cancel operation clearPending(); @@ -197,10 +224,14 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { drawIconOverlay(x, y, ICON_CHECK); } } else { - // Draw coin icon for currency/refund claims (no item in slot) + // Draw coin icon for currency claims or refund claims without item AuctionClaim claim = tradesContainer.getClaimAt(slot); - if (claim != null && (claim.type == EnumClaimType.CURRENCY || claim.type == EnumClaimType.REFUND)) { - drawIconOverlay(x, y, ICON_COIN); + if (claim != null) { + // Show coin if: CURRENCY claim OR REFUND claim without item + if (claim.type == EnumClaimType.CURRENCY || + (claim.type == EnumClaimType.REFUND && claim.item == null)) { + drawIconOverlay(x, y, ICON_COIN); + } } } } @@ -213,16 +244,21 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { AuctionClaim claim = tradesContainer.getClaimAt(slot); if (listing != null) { - // Active listing or bid - Blue - return TINT_BLUE; + if (tradesContainer.isSellingAt(slot)) { + // Selling - Blue + return TINT_BLUE; + } else { + // Bidding - Yellow + return TINT_YELLOW; + } } else if (claim != null) { switch (claim.type) { case CURRENCY: // Sold item - Green return TINT_GREEN; case REFUND: - // Outbid - Yellow - return TINT_YELLOW; + // Outbid - Red (whether rebiddable or not) + return TINT_RED; case ITEM: // Won item (green) or Returned/Expired (red) return claim.isReturned ? TINT_RED : TINT_GREEN; @@ -286,14 +322,18 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { tooltip.add(EnumChatFormatting.GOLD + String.format("%,d", claim.currency) + " " + AuctionClientConfig.getCurrencyName()); break; case REFUND: - tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.refundClaim")); - if (!claim.itemName.isEmpty()) { - tooltip.add(EnumChatFormatting.WHITE + claim.itemName); - } + tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.trades.outbid")); if (!claim.otherPlayerName.isEmpty()) { tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.trades.outbidBy") + ": " + EnumChatFormatting.AQUA + claim.otherPlayerName); } tooltip.add(EnumChatFormatting.GOLD + String.format("%,d", claim.currency) + " " + AuctionClientConfig.getCurrencyName()); + // Show rebid option if item is present (active listing) + if (claim.item != null) { + tooltip.add(""); + tooltip.add(EnumChatFormatting.GREEN + StatCollector.translateToLocal("auction.trades.leftClickRebid")); + tooltip.add(EnumChatFormatting.YELLOW + StatCollector.translateToLocal("auction.trades.rightClickRefund")); + return; // Skip the generic claim message + } break; case ITEM: if (claim.isReturned) { diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java b/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java index 2d871bced..772cba7ce 100644 --- a/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java @@ -247,7 +247,7 @@ public class ContainerAuctionTrades extends ContainerAuction { return slot >= 0 && slot < SLOT_COUNT && slotTypes[slot] == 2; } - /** Find listing matching item */ + /** Find listing matching item (searches both selling and bidding) */ public AuctionListing getListingForItem(ItemStack stack) { if (stack == null) return null; for (AuctionListing listing : activeListings) { @@ -259,6 +259,24 @@ public class ContainerAuctionTrades extends ContainerAuction { return null; } + /** Find listing in selling list */ + public AuctionListing getSellingListingForItem(ItemStack stack) { + if (stack == null) return null; + for (AuctionListing listing : activeListings) { + if (listing.item != null && ItemStack.areItemStacksEqual(listing.item, stack)) return listing; + } + return null; + } + + /** Find listing in bidding list */ + public AuctionListing getBiddingListingForItem(ItemStack stack) { + if (stack == null) return null; + for (AuctionListing bid : activeBids) { + if (bid.item != null && ItemStack.areItemStacksEqual(bid.item, stack)) return bid; + } + return null; + } + /** Find claim matching item */ public AuctionClaim getClaimForItem(ItemStack stack) { if (stack == null) return null; diff --git a/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java b/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java index 0a15720fe..50d2b50e7 100644 --- a/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java +++ b/src/main/java/noppes/npcs/containers/InventoryAuctionDisplay.java @@ -48,16 +48,21 @@ public class InventoryAuctionDisplay implements IInventory { /** * Set a claim item in the inventory. - * Shows actual item for ITEM claims, empty slot for CURRENCY/REFUND (icon drawn by GUI). + * Shows actual item for ITEM claims. + * For CURRENCY claims, shows coin icon (null item). + * For REFUND claims, shows item if present (for rebid option), otherwise coin icon. */ public void setClaimItem(int slot, AuctionClaim claim) { if (slot >= 0 && slot < size) { claims.set(slot, claim); listings.set(slot, null); if (claim != null) { - if (claim.type == EnumClaimType.CURRENCY || claim.type == EnumClaimType.REFUND) { - // No item - gold coin icon drawn by GUI + if (claim.type == EnumClaimType.CURRENCY) { + // Sold - gold coin icon drawn by GUI items[slot] = null; + } else if (claim.type == EnumClaimType.REFUND) { + // Outbid - show item if present (for rebid), otherwise coin + items[slot] = claim.item != null ? claim.item.copy() : null; } else if (claim.item != null) { // Show actual item for ITEM claims items[slot] = claim.item.copy(); diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index b78fa0649..708c3bb0e 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -363,10 +363,10 @@ public class AuctionController { if (listing.hasBids()) { long penalty = (long) (listing.currentBid * ConfigMarket.CancellationPenaltyPercent); - // Refund bidder + // Refund bidder (no item - can't rebid on cancelled auction) AuctionClaim refundClaim = AuctionClaim.createRefundClaim( listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, - listing.item.getDisplayName(), listing.sellerName); + listing.item.getDisplayName(), listing.sellerName, null); addClaim(refundClaim); // Remove from bidder's active bids @@ -451,24 +451,29 @@ public class AuctionController { } PlayerCurrencyData currency = playerData.currencyData; - if (!currency.canAfford(bidAmount)) { + + // Store previous bidder info + UUID previousBidder = listing.highBidderUUID; + long previousBid = listing.currentBid; + boolean isSameBidder = previousBidder != null && previousBidder.equals(playerUUID); + + // Calculate amount to charge + long amountToCharge = isSameBidder ? (bidAmount - previousBid) : bidAmount; + + if (!currency.canAfford(amountToCharge)) { return "You cannot afford this bid."; } - // Store previous bidder info for refund - UUID previousBidder = listing.highBidderUUID; - long previousBid = listing.currentBid; - // Deduct bid from new bidder FIRST - if (!currency.withdraw(bidAmount)) { + if (!currency.withdraw(amountToCharge)) { return "Failed to deduct bid amount."; } - // Refund previous bidder if any - if (listing.hasBids() && previousBidder != null) { + // Refund previous bidder if different player (include item for rebid option) + if (listing.hasBids() && previousBidder != null && !isSameBidder) { AuctionClaim refundClaim = AuctionClaim.createRefundClaim( previousBidder, listing.highBidderName, listing.id, previousBid, - listing.item.getDisplayName(), playerName); + listing.item.getDisplayName(), playerName, listing.item); addClaim(refundClaim); // Remove from previous bidder's active bids @@ -550,11 +555,11 @@ public class AuctionController { return "Failed to deduct buyout amount."; } - // Refund previous bidder if any + // Refund previous bidder if any (no item - auction ended via buyout) if (listing.hasBids() && listing.highBidderUUID != null) { AuctionClaim refundClaim = AuctionClaim.createRefundClaim( listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, - listing.item.getDisplayName(), playerName); + listing.item.getDisplayName(), playerName, null); addClaim(refundClaim); // Remove from previous bidder's active bids diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java b/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java index bbc1b06ac..4d489a166 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java @@ -70,14 +70,14 @@ public class AuctionClaim { return claim; } - /** Create claim for outbid refund (stores item name for tooltip, not full item) */ - public static AuctionClaim createRefundClaim(UUID playerUUID, String playerName, String listingId, long currency, String itemName, String outbidderName) { + /** Create claim for outbid refund (stores item for display and rebid option) */ + public static AuctionClaim createRefundClaim(UUID playerUUID, String playerName, String listingId, long currency, String itemName, String outbidderName, ItemStack item) { AuctionClaim claim = new AuctionClaim(); claim.playerUUID = playerUUID; claim.playerName = playerName; claim.listingId = listingId; claim.type = EnumClaimType.REFUND; - claim.item = null; // No item stored for refund claims + claim.item = item != null ? item.copy() : null; // Store item for display and rebid claim.itemName = itemName != null ? itemName : ""; claim.otherPlayerName = outbidderName != null ? outbidderName : ""; claim.currency = currency; diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index a77d6444b..dc2757916 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1822,6 +1822,19 @@ auction.trades.leftClickConfirm=Left-click again to confirm auction.trades.otherClickCancel=Any other click to cancel auction.trades.buyer=Buyer auction.trades.outbidBy=Outbid by +auction.trades.selling=Selling +auction.trades.bidding=Bidding +auction.trades.sold=Sold +auction.trades.won=Won +auction.trades.expired=Expired +auction.trades.outbid=Outbid +auction.trades.clickToRebid=Click to increase bid +auction.trades.leftClickRebid=Left-click to rebid +auction.trades.rightClickRefund=Right-click to claim refund +auction.claim.soldItem=Item: %s +auction.claim.buyer=Buyer: %s +auction.claim.outbidItem=Item: %s +auction.claim.outbidBy=Outbid by: %s # Sell Page - Buyout Toggle auction.sell.allowBuyout=Allow Buy Now From 64c29377c4ee42d1afad2e8409bd484395422f28 Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 24 Jan 2026 04:24:26 -0500 Subject: [PATCH 276/358] Vault auto to True --- 1.11_Market_Update.md | 67 ++++++++++++++----- .../java/noppes/npcs/config/ConfigMarket.java | 2 +- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/1.11_Market_Update.md b/1.11_Market_Update.md index e512e7aa0..e528e721b 100644 --- a/1.11_Market_Update.md +++ b/1.11_Market_Update.md @@ -391,6 +391,7 @@ constants/ controllers/ AuctionController.java ✅ (with threading, indices, Vault support) + AuctionConfigSync.java ✅ (server-side config sync utility) controllers/data/ AuctionListing.java ✅ @@ -428,10 +429,16 @@ client/gui/util/ GuiAuctionNavButton.java ✅ network/packets/player/ - AuctionDataPacket.java ✅ AuctionActionPacket.java ✅ ``` +### New Commands + +``` +command/ + AuctionCommand.java ✅ /auction command with list, view, open subcommands +``` + ### Files Modified ``` @@ -466,12 +473,12 @@ public static long MaxBalance = Long.MAX_VALUE; // Balance cap // Auction (Market.Auction) // ========================================= public static boolean AuctionEnabled = true; -public static int AuctionDurationHours = 48; // Auction length +public static int AuctionDurationHours = 24; // Auction length (hours) public static long ListingFee = 10; // Fee to create listing public static double SalesTaxPercent = 0.05; // 5% tax on sales -public static int DefaultMaxListings = 10; // Per player +public static int DefaultMaxListings = 5; // Per player public static int SnipeProtectionMinutes = 2; // Anti-snipe timer -public static int ClaimExpirationDays = 30; // Days before unclaimed deleted +public static int ClaimExpirationDays = 20; // Days before unclaimed deleted public static double MinBidIncrementPercent = 0.05; // 5% minimum bid increase // ========================================= @@ -489,7 +496,29 @@ public static boolean LogAuctionClaimed = true; --- -## 7. Implementation Status +## 7. Commands + +### `/auction` Command + +Allows interaction with the Auction House without needing an Auctioneer NPC. + +| Subcommand | Usage | Permission | Description | +|------------|-------|------------|-------------| +| `list` | `/auction list [page]` | Default | View active auctions in chat | +| `view` | `/auction view ` | Default | View a player's auction activity | +| `open` | `/auction open` | Player only | Opens the Auction House GUI | + +**Examples:** +``` +/auction list - Show first page of active auctions +/auction list 2 - Show page 2 of auctions +/auction view Steve - Show Steve's listings, bids, and claims +/auction open - Open the full Auction House GUI +``` + +--- + +## 8. Implementation Status ### Phase 1: Currency System - COMPLETE ✅ ### Phase 2: Trader Enhancements - COMPLETE ✅ @@ -509,13 +538,20 @@ public static boolean LogAuctionClaimed = true; ### Remaining Work -- [ ] GuiManageAuction (Admin GUI) -- [ ] Permission system integration +- [ ] **GuiManageAuction (Admin GUI)** - Admin interface to view all auctions, cancel listings, manage claims +- [ ] **Permission system integration** - Per-player auction slot limits via `customnpcs.auction.slots.X` permissions (TODO at `AuctionController.java:899`) - [ ] Additional testing +### Implemented Since Initial Plan + +- [x] **`/auction` Command** - Console/player command with subcommands: + - `/auction list [page]` - View active auctions from command line + - `/auction view ` - View a player's auction activity (listings, bids, claims) + - `/auction open` - Open Auction House GUI without needing an Auctioneer NPC + --- -## 8. Design Decisions +## 9. Design Decisions | Decision | Choice | Rationale | |----------|--------|-----------| @@ -528,12 +564,13 @@ public static boolean LogAuctionClaimed = true; --- -## 9. Future Considerations (Not in 1.11) +## 10. Future Considerations (Not in 1.11) -- Category-based filtering +- Category-based filtering (by item type, mod, etc.) - Advanced blacklist system (items, mods, NBT patterns) -- Auction history/statistics -- Command-based auction access -- Auction house tax sink -- Reserve prices -- Private auctions +- Auction history/statistics tracking +- Auction house tax sink (currency removed from economy) +- Reserve prices (hidden minimum price) +- Private auctions (invite-only) +- Watchlist/favorites system +- Price history graphs diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java index 557a08b6f..ff7c2af47 100644 --- a/src/main/java/noppes/npcs/config/ConfigMarket.java +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -78,7 +78,7 @@ public class ConfigMarket { // ========================================= config.setCategoryComment(CURRENCY, "CNPC+ Currency system settings. When UseVault is enabled, all currency operations use Vault instead of the built-in system."); - UseVault = config.get(CURRENCY, "Use Vault", false, + UseVault = config.get(CURRENCY, "Use Vault", true, "If true, use Vault API for all currency operations instead of CNPC+ built-in currency. The built-in currency data will be preserved but unused.").getBoolean(false); CurrencyName = config.get(CURRENCY, "Currency Name", "Coins", From 3577701edfacb39d6bf8b0c4fc793e006e9bce35 Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 24 Jan 2026 15:11:50 -0500 Subject: [PATCH 277/358] Auction Fixes + Auction API Hookup --- 1.11_Market_Update.md | 131 +- Auction_Item_Blacklist_Plan.md | 218 +++ src/api | 2 +- .../kamkeel/npcs/command/AuctionCommand.java | 6 +- .../kamkeel/npcs/command/MoneyCommand.java | 10 +- .../npcs/controllers/ProfileController.java | 9 + .../controllers/data/profile/CNPCData.java | 12 +- .../packets/player/AuctionActionPacket.java | 9 +- .../network/packets/player/GetTraderData.java | 2 +- .../noppes/npcs/CustomNpcsPermissions.java | 3 + .../java/noppes/npcs/ServerTickHandler.java | 5 + .../npcs/client/AuctionClientConfig.java | 16 +- .../client/gui/player/GuiAuctionSell.java | 2 +- .../client/gui/player/GuiAuctionTrades.java | 5 +- .../java/noppes/npcs/config/ConfigMarket.java | 68 +- .../containers/ContainerAuctionTrades.java | 9 + .../npcs/containers/ContainerNPCTrader.java | 4 +- .../npcs/controllers/AuctionConfigSync.java | 2 +- .../npcs/controllers/AuctionController.java | 1407 +++++++++-------- .../controllers/PlayerDataController.java | 11 + .../npcs/controllers/data/AuctionClaim.java | 95 +- .../npcs/controllers/data/AuctionFilter.java | 36 +- .../npcs/controllers/data/AuctionListing.java | 152 +- .../controllers/data/PlayerCurrencyData.java | 232 --- .../npcs/controllers/data/PlayerData.java | 23 +- .../controllers/data/PlayerTradeData.java | 414 +++++ .../java/noppes/npcs/roles/RoleTrader.java | 2 +- .../java/noppes/npcs/scripted/NpcAPI.java | 11 + .../npcs/scripted/entity/ScriptPlayer.java | 14 +- 29 files changed, 1897 insertions(+), 1013 deletions(-) create mode 100644 Auction_Item_Blacklist_Plan.md delete mode 100644 src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java create mode 100644 src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java diff --git a/1.11_Market_Update.md b/1.11_Market_Update.md index e528e721b..ba7786d0a 100644 --- a/1.11_Market_Update.md +++ b/1.11_Market_Update.md @@ -44,8 +44,8 @@ The currency system provides a unified economy for CNPC+ features. ### Key Files | File | Purpose | |------|---------| -| `PlayerCurrencyData.java` | Per-player currency storage, Vault delegation | -| `IPlayerCurrencyData.java` | API interface for currency operations | +| `PlayerTradeData.java` | Per-player trade data (currency + auction claims), Vault delegation | +| `IPlayerTradeData.java` | API interface for trade operations (currency + claims) | | `ConfigMarket.java` | Currency configuration (UseVault, CurrencyName, StartingBalance, MaxBalance) | | `VaultUtil.java` | Vault API integration via reflection | @@ -67,6 +67,9 @@ player.withdrawCurrency(amount) // Remove currency (returns success) player.canAffordCurrency(amount) // Check if can afford player.isUsingVaultCurrency() // Check if Vault is active player.getFormattedCurrencyBalance() // Get formatted string + +// Via IPlayerData +player.getData().getTradeData() // Get IPlayerTradeData interface ``` --- @@ -125,7 +128,7 @@ Each trade slot can have: #### Implementation Details - Currency cost stored as `long[] currencyCost[18]` in RoleTrader -- Uses `data.currencyData.getBalance()` and `data.currencyData.withdraw()` +- Uses `data.tradeData.getBalance()` and `data.tradeData.withdraw()` - Automatically uses Vault when `ConfigMarket.UseVault` is enabled - NBT serialization for persistence ✅ @@ -165,20 +168,22 @@ Full API available via `IRoleTrader` interface. ``` ┌─────────────────────────────────────────────────────────────────┐ │ AuctionController │ -│ - Manages all auction data with player indices │ +│ - Manages auction listings with player indices │ │ - Handles listing/bidding/buyout logic │ -│ - Race-condition protected with ReentrantReadWriteLock │ -│ - Async saves on CNPC+ thread │ -│ - 20-second update cycle for performance │ +│ - Uses ConcurrentHashMap for thread-safety (no locks) │ +│ - Async saves on CNPC+ thread every 30 seconds │ +│ - 30-second update cycle for processing ended auctions │ └─────────────────────────────────────────────────────────────────┘ │ Uses PlayerCurrencyData (Vault-aware) │ ┌─────────────────────────────────────────────────────────────────┐ -│ AuctionNotification │ -│ - Tracks pending notifications per player │ -│ - Sends notifications on login │ -│ - Types: Won, Outbid, Sold, Expired, Claimed │ +│ PlayerTradeData │ +│ - Combined currency + auction claims (shared across profiles) │ +│ - Currency: Vault integration with local fallback │ +│ - Claims: Thread-safe CopyOnWriteArrayList for async additions │ +│ - Expired claims processed on login │ +│ - Notifications sent immediately to online players │ └─────────────────────────────────────────────────────────────────┘ ``` @@ -328,14 +333,26 @@ Example: Auction ends in 30 seconds, someone bids → Timer resets to 2:00 ┌─────────────────────────────────────────────────────────────────┐ │ AuctionController │ │ │ -│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ -│ │ ReentrantRWLock │ │ AtomicBoolean │ │ CNPC+ Thread │ │ -│ │ (dataLock) │ │ dirty, saving │ │ (async saves) │ │ -│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ +│ ┌─────────────────────┐ ┌─────────────────┐ ┌───────────────┐ │ +│ │ ConcurrentHashMap │ │ AtomicBoolean │ │ CNPC+ Thread │ │ +│ │ (listings, indices) │ │ dirty, saving │ │ (async saves) │ │ +│ └─────────────────────┘ └─────────────────┘ └───────────────┘ │ │ │ -│ Read operations → Read Lock (concurrent) │ -│ Write operations → Write Lock (exclusive) │ -│ Save operations → Async on CNPC+ thread │ +│ All operations use ConcurrentHashMap - no explicit locking │ +│ Minecraft is single-threaded for gameplay - inherent safety │ +│ Async saves snapshot data before writing to disk │ +└─────────────────────────────────────────────────────────────────┘ + │ +┌─────────────────────────────────────────────────────────────────┐ +│ PlayerTradeData │ +│ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ Currency: long balance (or Vault delegation) │ │ +│ │ Claims: CopyOnWriteArrayList │ │ +│ │ - Thread-safe for concurrent read/write │ │ +│ │ - Claims can be added from CNPC+ thread for offline players │ │ +│ │ - Stored in PlayerData (persisted with player, not auctions) │ │ +│ └─────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ``` @@ -345,29 +362,33 @@ Example: Auction ends in 30 seconds, someone bids → Timer resets to 2:00 |-------|------|---------| | `playerListingIds` | `Map>` | Active listings by seller | | `playerBidIds` | `Map>` | Listings where player has active bid | -| `playerClaimIds` | `Map>` | Unclaimed claims by player | + +**Claims are now stored in PlayerData:** +- `PlayerTradeData.claims` - CopyOnWriteArrayList per player +- Accessed via `PlayerData.get(player).tradeData` +- Shared across all profile slots (combined with currency) **Performance Impact:** - `getPlayerListingCount()`: O(n) → O(1) - `getPlayerActiveListings()`: O(n) → O(k) where k = player's listings - `getPlayerActiveBids()`: O(n) → O(k) -- `getPlayerClaims()`: O(n) → O(k) +- `getPlayerClaims()`: Direct access to player's claims list ### 4.3 Update Intervals | Operation | Interval | Rationale | |-----------|----------|-----------| -| Process ended auctions | 20 seconds (400 ticks) | Reduces server load | -| Process expired claims | 20 seconds (400 ticks) | Same cycle as auctions | -| Save dirty data | 60 seconds (1200 ticks) | Batches writes, reduces I/O | +| Process ended auctions | 30 seconds (600 ticks) | Reduces server load | +| Save auction data | 30 seconds (600 ticks) | Batches writes, reduces I/O | +| Process expired claims | On player login | Per-player, not global tick | -### 4.4 Race Condition Prevention +### 4.4 Data Safety -1. **Bid vs Auction End** - Double-check `isExpired()` inside write lock -2. **Currency Deduction** - Withdraw before updating listing -3. **Previous Bidder Refund** - Store info before modification -4. **Concurrent Saves** - `AtomicBoolean` prevents duplicates -5. **Index Consistency** - All updates within same write lock +1. **Currency Deduction** - Withdraw before updating listing +2. **Previous Bidder Refund** - Store info before modification +3. **Concurrent Saves** - `AtomicBoolean` prevents duplicates +4. **Offline Player Claims** - Loaded async on CNPC+ thread, saved to PlayerData +5. **Claim Migration** - Old claims in auction.dat migrated to PlayerData on load ### 4.5 Server Shutdown Handling @@ -397,7 +418,7 @@ controllers/data/ AuctionListing.java ✅ AuctionClaim.java ✅ AuctionFilter.java ✅ - AuctionNotification.java ✅ + PlayerTradeData.java ✅ (currency + claims storage per player) roles/ RoleAuctioneer.java ✅ @@ -451,6 +472,7 @@ ClientProxy.java ✅ Handle auction GUI types DataAdvanced.java ✅ Handle Auctioneer role ConfigMarket.java ✅ Added Auction section LoginPacket.java ✅ Sync auction config to client +PlayerData.java ✅ Added tradeData field (currency + claims, shared across profiles) en_US.lang ✅ Added auction lang keys ``` @@ -472,11 +494,15 @@ public static long MaxBalance = Long.MAX_VALUE; // Balance cap // ========================================= // Auction (Market.Auction) // ========================================= +public static final int MAX_TRADE_SLOTS = 45; // 9x5 grid maximum +public static final int MIN_TRADE_SLOTS = 1; // Minimum allowed + public static boolean AuctionEnabled = true; public static int AuctionDurationHours = 24; // Auction length (hours) public static long ListingFee = 10; // Fee to create listing +public static long MinimumListingPrice = 1; // Minimum starting price public static double SalesTaxPercent = 0.05; // 5% tax on sales -public static int DefaultMaxListings = 5; // Per player +public static int DefaultMaxTrades = 8; // Per player (listings + bids + claims) public static int SnipeProtectionMinutes = 2; // Anti-snipe timer public static int ClaimExpirationDays = 20; // Days before unclaimed deleted public static double MinBidIncrementPercent = 0.05; // 5% minimum bid increase @@ -484,7 +510,7 @@ public static double MinBidIncrementPercent = 0.05; // 5% minimum bid increase // ========================================= // Auction Logging // ========================================= -public static boolean AuctionLoggingEnabled = true; +public static boolean AuctionLoggingEnabled = false; public static boolean LogAuctionCreated = true; public static boolean LogAuctionBid = true; public static boolean LogAuctionBuyout = true; @@ -494,6 +520,21 @@ public static boolean LogAuctionCancelled = true; public static boolean LogAuctionClaimed = true; ``` +### Trade Slot Permissions + +Players can have more than the default trade slots via permissions: + +| Permission | Description | +|------------|-------------| +| `customnpcs.auction.trades.*` | Unlimited trade slots (up to 45) | +| `customnpcs.auction.trades.X` | Exactly X trade slots (where X is 1-45) | + +**How it works:** +- Default is 8 trade slots +- Permissions only **increase** the limit (permissions below 8 are ignored) +- Maximum is capped at 45 (9x5 grid) +- Trade slots = Active Listings + Active Bids + Pending Claims + --- ## 7. Commands @@ -539,7 +580,6 @@ Allows interaction with the Auction House without needing an Auctioneer NPC. ### Remaining Work - [ ] **GuiManageAuction (Admin GUI)** - Admin interface to view all auctions, cancel listings, manage claims -- [ ] **Permission system integration** - Per-player auction slot limits via `customnpcs.auction.slots.X` permissions (TODO at `AuctionController.java:899`) - [ ] Additional testing ### Implemented Since Initial Plan @@ -548,6 +588,25 @@ Allows interaction with the Auction House without needing an Auctioneer NPC. - `/auction list [page]` - View active auctions from command line - `/auction view ` - View a player's auction activity (listings, bids, claims) - `/auction open` - Open Auction House GUI without needing an Auctioneer NPC +- [x] **Permission system integration** - Per-player trade slot limits via `customnpcs.auction.trades.X` permissions + - Default: 8 trade slots + - Maximum: 45 (9x5 grid) + - Synced to client when viewing My Trades + - Cached on login for performance +- [x] **Minimum Listing Price** - Configurable minimum starting price (default: 1) +- [x] **Buyout enabled by default** - Buyout toggle is ON by default when creating listings +- [x] **Search by seller** - Filter can now search by item name OR seller name +- [x] **Rebid prevention** - Players cannot rebid on auctions where they're already the highest bidder +- [x] **Buyout price difference** - When buying out an auction you're bidding on, only pay the difference +- [x] **Immediate claim cleanup** - Claims are removed from storage immediately when claimed +- [x] **Listing cleanup** - Ended listings are removed when all related claims are processed +- [x] **Live notifications** - Notifications sent immediately to online players instead of queued +- [x] **Login claim summary** - Shows detailed claim summary on login (X items and Y currency to claim) +- [x] **PlayerTradeData** - Merged currency + claims into single class (shared across profiles) +- [x] **No locking system** - Pure ConcurrentHashMap for better performance +- [x] **30-second intervals** - Combined process/save cycle for simplicity +- [x] **Async offline player claims** - Claims added to offline players via CNPC+ thread +- [x] **Claim migration** - Old claims in auction.dat automatically migrated to PlayerData --- @@ -555,11 +614,13 @@ Allows interaction with the Auction House without needing an Auctioneer NPC. | Decision | Choice | Rationale | |----------|--------|-----------| -| Claims System | Everything to claims | Prevents offline handling issues | +| Claims Storage | In PlayerData, not auction.dat | Claims persist with player, reduces auction.dat size, allows offline claim addition | +| Thread Safety | ConcurrentHashMap (no locks) | Minecraft is single-threaded for gameplay; locks add overhead without benefit | | Vault Integration | Via PlayerCurrencyData | Single point of delegation, reused by Trader & Auction | -| Update Interval | 20 seconds | Performance vs responsiveness tradeoff | +| Update Interval | 30 seconds | Performance vs responsiveness tradeoff | | Player Indices | ConcurrentHashMap + Sets | O(1) lookups, thread-safe | | Async Saves | CNPC+ Thread | Doesn't block server tick | +| Offline Claims | Load PlayerData async, add claim, save | Claims can be added to offline players without blocking | | Config Sync | LoginPacket → AuctionClientConfig | Client never accesses server config directly | --- diff --git a/Auction_Item_Blacklist_Plan.md b/Auction_Item_Blacklist_Plan.md new file mode 100644 index 000000000..d9dfd9075 --- /dev/null +++ b/Auction_Item_Blacklist_Plan.md @@ -0,0 +1,218 @@ +# Auction Item Blacklist Feature Plan + +## Overview + +Add the ability to prevent certain items from being listed on the Auction House via a configurable blacklist system. + +--- + +## 1. Blacklist Types + +### 1.1 Item ID Blacklist +- Block specific items by registry name (e.g., `minecraft:diamond_sword`, `customnpcs:npcSoulstoneFilled`) +- Supports wildcard patterns (e.g., `minecraft:*_sword` for all swords) + +### 1.2 Mod Blacklist +- Block all items from a specific mod (e.g., `appliedenergistics2`, `thaumcraft`) +- Useful for blocking items from tech/magic mods that could disrupt economy + +### 1.3 NBT Pattern Blacklist (Advanced) +- Block items with specific NBT tags (e.g., items with "Soulbound" enchant, creative-only items) +- Pattern matching on NBT paths + +--- + +## 2. Configuration + +### 2.1 Config File Location +``` +config/CustomNPC+/auction_blacklist.cfg +``` + +### 2.2 Config Structure +```java +// In ConfigMarket.java or separate ConfigAuctionBlacklist.java +public static String[] BlacklistedItems = new String[]{}; +public static String[] BlacklistedMods = new String[]{}; +public static String[] BlacklistedNBTPatterns = new String[]{}; +public static boolean EnableBlacklist = true; +public static boolean ShowBlacklistReason = true; +``` + +### 2.3 Example Configuration +```cfg +# Items that cannot be listed on the Auction House +S:BlacklistedItems < + minecraft:bedrock + minecraft:command_block + minecraft:barrier + customnpcs:npcSoulstoneFilled + customnpcs:npcMoney* + > + +# Mods whose items are completely blocked +S:BlacklistedMods < + projecte + equivalentexchange3 + > + +# NBT patterns that block items (advanced) +S:BlacklistedNBTPatterns < + tag.ench[*].id=34 + tag.Unbreakable=1 + > +``` + +--- + +## 3. Implementation + +### 3.1 New Files +``` +controllers/data/AuctionBlacklist.java - Blacklist checking logic +config/ConfigAuctionBlacklist.java - Configuration handling +``` + +### 3.2 AuctionBlacklist.java +```java +public class AuctionBlacklist { + private static Set blacklistedItems = new HashSet<>(); + private static Set blacklistedMods = new HashSet<>(); + private static List blacklistedNBT = new ArrayList<>(); + + public static boolean isBlacklisted(ItemStack item) { + if (item == null) return true; + + // Check item registry name + String registryName = GameRegistry.findUniqueIdentifierFor(item.getItem()).toString(); + if (matchesPattern(registryName, blacklistedItems)) return true; + + // Check mod + String modId = GameRegistry.findUniqueIdentifierFor(item.getItem()).modId; + if (blacklistedMods.contains(modId.toLowerCase())) return true; + + // Check NBT patterns + if (item.hasTagCompound()) { + for (NBTPattern pattern : blacklistedNBT) { + if (pattern.matches(item.getTagCompound())) return true; + } + } + + return false; + } + + public static String getBlacklistReason(ItemStack item) { + // Returns human-readable reason why item is blocked + } + + public static void reload() { + // Reload from config + } +} +``` + +### 3.3 Integration Points + +#### AuctionController.createListing() +```java +// Add at start of createListing method: +if (AuctionBlacklist.isBlacklisted(item)) { + if (ConfigMarket.ShowBlacklistReason) { + return "This item cannot be listed: " + AuctionBlacklist.getBlacklistReason(item); + } + return "This item cannot be listed on the Auction House."; +} +``` + +#### GuiAuctionSell (optional visual feedback) +- Gray out confirm button if item is blacklisted +- Show tooltip explaining why item can't be listed + +--- + +## 4. Commands + +### 4.1 Admin Commands +``` +/auction blacklist add +/auction blacklist remove +/auction blacklist list +/auction blacklist reload +/auction blacklist check +``` + +### 4.2 Examples +``` +/auction blacklist add item minecraft:diamond_block +/auction blacklist add mod projecte +/auction blacklist add nbt tag.ench[*].id=34 +/auction blacklist check held +``` + +--- + +## 5. Default Blacklist (Recommended) + +### 5.1 Vanilla Items +- `minecraft:command_block*` +- `minecraft:barrier` +- `minecraft:bedrock` +- `minecraft:structure_block` +- `minecraft:structure_void` + +### 5.2 CustomNPC+ Items +- `customnpcs:npcSoulstoneFilled` (contains NPC data) +- `customnpcs:npcWand` (admin tool) +- `customnpcs:npcMobCloner` (admin tool) +- `customnpcs:npcScripter` (admin tool) + +--- + +## 6. Future Considerations + +### 6.1 Whitelist Mode +- Invert the blacklist to only allow specific items +- Useful for strict economy servers + +### 6.2 Category-Based Blocking +- Block by item category (weapons, tools, armor, etc.) +- Integration with Minecraft's creative tabs + +### 6.3 Price Restrictions +- Set minimum/maximum prices for specific items +- Prevent price manipulation on valuable items + +### 6.4 Per-Player Overrides +- Permission to bypass blacklist: `customnpcs.auction.blacklist.bypass` +- Admin listing capability + +--- + +## 7. Files to Create/Modify + +### New Files +``` +config/ConfigAuctionBlacklist.java +controllers/data/AuctionBlacklist.java +``` + +### Modified Files +``` +AuctionController.java - Add blacklist check in createListing +AuctionCommand.java - Add blacklist subcommands +en_US.lang - Add blacklist messages +1.11_Market_Update.md - Document feature +``` + +--- + +## 8. Estimated Scope + +| Component | Complexity | Priority | +|-----------|------------|----------| +| Basic item blacklist | Low | High | +| Mod blacklist | Low | High | +| NBT pattern matching | Medium | Medium | +| Admin commands | Low | Medium | +| GUI feedback | Low | Low | +| Whitelist mode | Low | Low | diff --git a/src/api b/src/api index c8a2fb982..2e5d86993 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit c8a2fb9824de8488627f66eed2a5254ee8690016 +Subproject commit 2e5d86993cd47e6df4572323514d4fe2166c570c diff --git a/src/main/java/kamkeel/npcs/command/AuctionCommand.java b/src/main/java/kamkeel/npcs/command/AuctionCommand.java index 056116eba..a6e05a401 100644 --- a/src/main/java/kamkeel/npcs/command/AuctionCommand.java +++ b/src/main/java/kamkeel/npcs/command/AuctionCommand.java @@ -28,7 +28,7 @@ public class AuctionCommand extends CommandKamkeelBase { @Override public String getDescription() { - return "Auction House operations"; + return "Auction operations"; } @SubCommand( @@ -135,7 +135,7 @@ public class AuctionCommand extends CommandKamkeelBase { } @SubCommand( - desc = "Open the Auction House GUI", + desc = "Open the Auction GUI", usage = "", permission = 0 ) @@ -154,7 +154,7 @@ public class AuctionCommand extends CommandKamkeelBase { // Open the auction GUI without requiring an NPC NoppesUtilServer.sendOpenGui(player, EnumGuiType.PlayerAuction, null); - sendResult(sender, "Opening Auction House..."); + sendResult(sender, "Opening Auction..."); } @Override diff --git a/src/main/java/kamkeel/npcs/command/MoneyCommand.java b/src/main/java/kamkeel/npcs/command/MoneyCommand.java index 8b9fa72eb..ee034243d 100644 --- a/src/main/java/kamkeel/npcs/command/MoneyCommand.java +++ b/src/main/java/kamkeel/npcs/command/MoneyCommand.java @@ -4,8 +4,8 @@ import net.minecraft.command.CommandException; import net.minecraft.command.ICommandSender; import noppes.npcs.config.ConfigMarket; import noppes.npcs.controllers.PlayerDataController; -import noppes.npcs.controllers.data.PlayerCurrencyData; import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerTradeData; import java.util.List; @@ -38,7 +38,7 @@ public class MoneyCommand extends CommandKamkeelBase { } for (PlayerData playerdata : dataList) { - PlayerCurrencyData currency = playerdata.currencyData; + PlayerTradeData currency = playerdata.tradeData; long balance = currency.getBalance(); sendResult(sender, String.format("Player \u00A7b%s\u00A77 has \u00A76%,d %s", playerdata.playername, balance, ConfigMarket.CurrencyName)); @@ -72,7 +72,7 @@ public class MoneyCommand extends CommandKamkeelBase { } for (PlayerData playerdata : dataList) { - PlayerCurrencyData currency = playerdata.currencyData; + PlayerTradeData currency = playerdata.tradeData; if (currency.deposit(amount)) { playerdata.save(); sendResult(sender, String.format("Gave \u00A76%,d %s\u00A77 to player \u00A7b%s\u00A77. New balance: \u00A76%,d", @@ -111,7 +111,7 @@ public class MoneyCommand extends CommandKamkeelBase { } for (PlayerData playerdata : dataList) { - PlayerCurrencyData currency = playerdata.currencyData; + PlayerTradeData currency = playerdata.tradeData; if (currency.withdraw(amount)) { playerdata.save(); sendResult(sender, String.format("Withdrew \u00A76%,d %s\u00A77 from player \u00A7b%s\u00A77. New balance: \u00A76%,d", @@ -150,7 +150,7 @@ public class MoneyCommand extends CommandKamkeelBase { } for (PlayerData playerdata : dataList) { - PlayerCurrencyData currency = playerdata.currencyData; + PlayerTradeData currency = playerdata.tradeData; currency.setBalance(amount); playerdata.save(); sendResult(sender, String.format("Set balance of player \u00A7b%s\u00A77 to \u00A76%,d %s", diff --git a/src/main/java/kamkeel/npcs/controllers/ProfileController.java b/src/main/java/kamkeel/npcs/controllers/ProfileController.java index f7ac06bb1..103da1309 100644 --- a/src/main/java/kamkeel/npcs/controllers/ProfileController.java +++ b/src/main/java/kamkeel/npcs/controllers/ProfileController.java @@ -832,6 +832,15 @@ public class ProfileController implements IProfileHandler { else compound = (NBTTagCompound) compound.copy(); playerData.setNBT(compound); + + // Trade data is shared across all profile slots - copy from player's live data + PlayerData liveData = PlayerData.get(player); + if (liveData != null) { + NBTTagCompound tradeCompound = new NBTTagCompound(); + liveData.tradeData.writeToNBT(tradeCompound); + playerData.tradeData.readFromNBT(tradeCompound); + } + return playerData; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java b/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java index ccbae74ef..e4eb04dfe 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/profile/CNPCData.java @@ -5,6 +5,7 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerTradeData; import java.util.ArrayList; import java.util.List; @@ -19,7 +20,10 @@ public class CNPCData implements IProfileData { @Override public NBTTagCompound getCurrentNBT(EntityPlayer player) { PlayerData customNPCData = PlayerData.get(player); - return customNPCData.getNBT(); + NBTTagCompound compound = customNPCData.getNBT(); + compound.removeTag(PlayerTradeData.NBT_KEY); + + return compound; } @Override @@ -32,6 +36,10 @@ public class CNPCData implements IProfileData { @Override public void setNBT(EntityPlayer player, NBTTagCompound replace) { PlayerData customNPCData = PlayerData.get(player); + + NBTTagCompound sharedData = new NBTTagCompound(); + customNPCData.tradeData.writeToNBT(sharedData); + if (replace.hasNoTags()) { PlayerData newData = new PlayerData(); newData.player = player; @@ -39,6 +47,8 @@ public class CNPCData implements IProfileData { } else { customNPCData.setNBT(replace); } + + customNPCData.tradeData.readFromNBT(sharedData); customNPCData.updateClient = true; } diff --git a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java index fca09473f..3ce74eb4c 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/AuctionActionPacket.java @@ -338,6 +338,9 @@ public class AuctionActionPacket extends AbstractPacket { NBTTagCompound response = new NBTTagCompound(); response.setBoolean("TradesData", true); + // Send player's max trade slots (permission-aware) + response.setInteger("MaxTradeSlots", controller.getMaxTradesForPlayer(player)); + // Active listings (items player is selling) List listings = controller.getPlayerActiveListings(player.getUniqueID()); NBTTagList listingsNBT = new NBTTagList(); @@ -354,8 +357,8 @@ public class AuctionActionPacket extends AbstractPacket { } response.setTag("ActiveBids", bidsNBT); - // Claims (items/currency to claim) - List claims = controller.getPlayerClaims(player.getUniqueID()); + // Claims (items/currency to claim) - now from PlayerData + List claims = controller.getPlayerClaims(player); NBTTagList claimsNBT = new NBTTagList(); for (AuctionClaim claim : claims) { claimsNBT.appendTag(claim.writeToNBT(new NBTTagCompound())); @@ -488,7 +491,7 @@ public class AuctionActionPacket extends AbstractPacket { private void sendError(EntityPlayerMP player, String message) { player.addChatMessage(new net.minecraft.util.ChatComponentText( - net.minecraft.util.EnumChatFormatting.RED + "[Auction House] " + message)); + net.minecraft.util.EnumChatFormatting.RED + "[Auction] " + message)); } private void sendTradesUpdate(EntityPlayerMP player) { diff --git a/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java b/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java index 5b7cb7dfe..a04628962 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/GetTraderData.java @@ -53,7 +53,7 @@ public class GetTraderData extends AbstractPacket { // Player balance PlayerData data = PlayerData.get(player); - compound.setLong("Balance", data.currencyData.getBalance()); + compound.setLong("Balance", data.tradeData.getBalance()); // Stock info compound.setBoolean("StockEnabled", role.stock.enableStock); diff --git a/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/src/main/java/noppes/npcs/CustomNpcsPermissions.java index c83e92c22..0d758ad41 100644 --- a/src/main/java/noppes/npcs/CustomNpcsPermissions.java +++ b/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -110,6 +110,9 @@ public class CustomNpcsPermissions { public static final Permission PROFILE_REGION_BYPASS = new Permission("customnpcs.profile.region-bypass"); public static final Permission PROFILE_MAX = new Permission("customnpcs.profile.max.*"); + // Auction + public static final Permission AUCTION_TRADES_MAX = new Permission("customnpcs.auction.trades.*"); + public static CustomNpcsPermissions Instance; private Method hasPermission; diff --git a/src/main/java/noppes/npcs/ServerTickHandler.java b/src/main/java/noppes/npcs/ServerTickHandler.java index 27995f3cb..2639c6ab5 100644 --- a/src/main/java/noppes/npcs/ServerTickHandler.java +++ b/src/main/java/noppes/npcs/ServerTickHandler.java @@ -68,6 +68,11 @@ public class ServerTickHandler { playerData.onLogout(); } + // Clear auction permission cache on logout + if (AuctionController.Instance != null) { + AuctionController.Instance.onPlayerLogout(event.player.getUniqueID()); + } + // Save and unload the player's profile data on logout ProfileController.Instance.logout(event.player); } diff --git a/src/main/java/noppes/npcs/client/AuctionClientConfig.java b/src/main/java/noppes/npcs/client/AuctionClientConfig.java index aaae441bf..7d6854e4c 100644 --- a/src/main/java/noppes/npcs/client/AuctionClientConfig.java +++ b/src/main/java/noppes/npcs/client/AuctionClientConfig.java @@ -15,11 +15,11 @@ import net.minecraft.nbt.NBTTagCompound; public class AuctionClientConfig { // Cached values from server private static long listingFee = 0; - private static String currencyName = "Gold"; - private static int auctionDurationHours = 48; + private static String currencyName = "Coins"; + private static int auctionDurationHours = 24; private static double minBidIncrement = 0.05; - private static int maxActiveListings = 10; - private static int claimExpirationDays = 30; + private static int maxActiveListings = 8; + private static int claimExpirationDays = 20; private static boolean auctionEnabled = false; /** @@ -47,11 +47,11 @@ public class AuctionClientConfig { */ public static void reset() { listingFee = 0; - currencyName = "Gold"; - auctionDurationHours = 48; + currencyName = "Coins"; + auctionDurationHours = 24; minBidIncrement = 0.05; - maxActiveListings = 10; - claimExpirationDays = 30; + maxActiveListings = 8; + claimExpirationDays = 20; auctionEnabled = false; } diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java index 4f67c781c..8b288c83f 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java @@ -45,7 +45,7 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis private final ContainerAuctionSell sellContainer; private long startingPrice = 0; private long buyoutPrice = 0; - private boolean allowBuyout = false; + private boolean allowBuyout = true; private String errorMessage = null; // Buttons diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java index 0d6e3d98d..21b0f6f01 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionTrades.java @@ -55,7 +55,8 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { public GuiAuctionTrades(EntityNPCInterface npc, ContainerAuctionTrades container) { super(npc, container); this.tradesContainer = container; - this.maxTradeSlots = AuctionClientConfig.getMaxActiveListings() * 2; + // Initial value from client config, updated when server sends trades data + this.maxTradeSlots = AuctionClientConfig.getMaxActiveListings(); } @Override @@ -372,6 +373,8 @@ public class GuiAuctionTrades extends GuiAuctionInterface implements IGuiData { if (compound.hasKey("TradesData")) { // Full trades data received from server tradesContainer.setTradesData(compound); + // Update max trade slots from server (permission-aware) + this.maxTradeSlots = tradesContainer.getMaxTradeSlots(); clearPending(); } else if (compound.hasKey("TradesUpdate")) { // Legacy flag - should not happen now but keep for safety diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java index ff7c2af47..2ba7c7c92 100644 --- a/src/main/java/noppes/npcs/config/ConfigMarket.java +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -11,16 +11,13 @@ import java.util.Arrays; public class ConfigMarket { public static Configuration config; - public final static String MARKET = "Market"; public final static String CURRENCY = "Market.Currency"; - public final static String TRADER = "Market.Trader"; public final static String AUCTION = "Market.Auction"; public final static String AUCTION_LOGGING = "Market.Auction.Logging"; - // ========================================= - // Market General - // ========================================= - public static boolean MarketEnabled = true; + // Max trades grid = 9x5 = 45 slots + public static final int MAX_TRADE_SLOTS = 45; + public static final int MIN_TRADE_SLOTS = 1; // ========================================= // Currency Settings @@ -30,20 +27,15 @@ public class ConfigMarket { public static long StartingBalance = 0; public static long MaxBalance = Long.MAX_VALUE; - // ========================================= - // Trader Settings - // ========================================= - public static boolean EnableStockByDefault = false; - public static String DefaultResetType = "NONE"; - // ========================================= // Auction Settings // ========================================= public static boolean AuctionEnabled = true; public static int AuctionDurationHours = 24; public static long ListingFee = 10; + public static long MinimumListingPrice = 1; public static double SalesTaxPercent = 0.05; - public static int DefaultMaxListings = 5; + public static int DefaultMaxTrades = 8; public static int SnipeProtectionMinutes = 2; public static int ClaimExpirationDays = 20; public static double MinBidIncrementPercent = 0.05; @@ -67,18 +59,12 @@ public class ConfigMarket { try { config.load(); - // ========================================= - // Market General - // ========================================= - MarketEnabled = config.get(MARKET, "Enable Market System", true, - "Enable the market/economy system").getBoolean(true); - // ========================================= // Currency Settings // ========================================= config.setCategoryComment(CURRENCY, "CNPC+ Currency system settings. When UseVault is enabled, all currency operations use Vault instead of the built-in system."); - UseVault = config.get(CURRENCY, "Use Vault", true, + UseVault = config.get(CURRENCY, "Use Vault", false, "If true, use Vault API for all currency operations instead of CNPC+ built-in currency. The built-in currency data will be preserved but unused.").getBoolean(false); CurrencyName = config.get(CURRENCY, "Currency Name", "Coins", @@ -90,24 +76,13 @@ public class ConfigMarket { MaxBalance = config.get(CURRENCY, "Max Balance", Integer.MAX_VALUE, "Maximum currency balance a player can have").getInt(Integer.MAX_VALUE); - // ========================================= - // Trader Settings - // ========================================= - config.setCategoryComment(TRADER, "Trader stock system settings"); - - EnableStockByDefault = config.get(TRADER, "Enable Stock By Default", false, - "Enable stock system by default for new traders").getBoolean(false); - - DefaultResetType = config.get(TRADER, "Default Reset Type", "NONE", - "Default stock reset type (NONE, MCDAILY, MCWEEKLY, RLDAILY, RLWEEKLY)").getString(); - // ========================================= // Auction Settings // ========================================= - config.setCategoryComment(AUCTION, "Auction House system settings. The Auction House allows players to list items for sale with bidding and buyout options."); + config.setCategoryComment(AUCTION, "Auction system settings. The Auction allows players to list items for sale with bidding and buyout options."); - AuctionEnabled = config.get(AUCTION, "Enable Auction House", true, - "Enable the Auction House system").getBoolean(true); + AuctionEnabled = config.get(AUCTION, "Enable Auction", true, + "Enable the Auction system").getBoolean(true); AuctionDurationHours = config.get(AUCTION, "Auction Duration Hours", 24, "Default duration for auctions in hours").getInt(24); @@ -115,11 +90,16 @@ public class ConfigMarket { ListingFee = config.get(AUCTION, "Listing Fee", 10, "Flat fee charged when creating a listing").getInt(10); + MinimumListingPrice = config.get(AUCTION, "Minimum Listing Price", 1, + "Minimum starting price for auction listings").getInt(1); + if (MinimumListingPrice < 1) MinimumListingPrice = 1; + SalesTaxPercent = config.get(AUCTION, "Sales Tax Percent", 0.05, "Percentage of sale price taken as tax (0.05 = 5%). Tax is deleted as a currency sink.").getDouble(0.05); - DefaultMaxListings = config.get(AUCTION, "Default Max Listings", 5, - "Default maximum number of active listings per player").getInt(5); + int maxTrades = config.get(AUCTION, "Default Max Trades", 8, + "Default maximum number of trade slots per player (listings + bids + claims). Min: 1, Max: 45. Players can have more via customnpcs.auction.trades.X permissions.").getInt(8); + DefaultMaxTrades = Math.max(MIN_TRADE_SLOTS, Math.min(MAX_TRADE_SLOTS, maxTrades)); SnipeProtectionMinutes = config.get(AUCTION, "Snipe Protection Minutes", 2, "When a bid is placed with less than this many minutes remaining, the auction is extended to this duration").getInt(2); @@ -136,7 +116,7 @@ public class ConfigMarket { // ========================================= // Auction Logging Settings // ========================================= - config.setCategoryComment(AUCTION_LOGGING, "Auction House logging settings. Enable specific log types to track auction activity."); + config.setCategoryComment(AUCTION_LOGGING, "Auction logging settings. Enable specific log types to track auction activity."); AuctionLoggingEnabled = config.get(AUCTION_LOGGING, "Enable Auction Logging", false, "Master switch for auction logging. If false, no auction events are logged.").getBoolean(false); @@ -163,10 +143,6 @@ public class ConfigMarket { "Log when claims are collected").getBoolean(true); // Set category order - config.setCategoryPropertyOrder(MARKET, new ArrayList<>(Arrays.asList( - "Enable Market System" - ))); - config.setCategoryPropertyOrder(CURRENCY, new ArrayList<>(Arrays.asList( "Use Vault", "Currency Name", @@ -174,17 +150,13 @@ public class ConfigMarket { "Max Balance" ))); - config.setCategoryPropertyOrder(TRADER, new ArrayList<>(Arrays.asList( - "Enable Stock By Default", - "Default Reset Type" - ))); - config.setCategoryPropertyOrder(AUCTION, new ArrayList<>(Arrays.asList( - "Enable Auction House", + "Enable Auction", "Auction Duration Hours", "Listing Fee", + "Minimum Listing Price", "Sales Tax Percent", - "Default Max Listings", + "Default Max Trades", "Snipe Protection Minutes", "Claim Expiration Days", "Min Bid Increment Percent", diff --git a/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java b/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java index 772cba7ce..eab491c70 100644 --- a/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java +++ b/src/main/java/noppes/npcs/containers/ContainerAuctionTrades.java @@ -31,6 +31,7 @@ public class ContainerAuctionTrades extends ContainerAuction { private List activeBids = new ArrayList<>(); private List claims = new ArrayList<>(); private int hiddenSlot = -1; + private int maxTradeSlots = 8; // Player's max trade slots (permission-aware, synced from server) // Slot type tracking for proper accessor methods private int[] slotTypes = new int[SLOT_COUNT]; // 0=empty, 1=selling, 2=bidding, 3=claim @@ -72,6 +73,11 @@ public class ContainerAuctionTrades extends ContainerAuction { // Clear existing data clearData(); + // Read player's max trade slots (permission-aware) + if (compound.hasKey("MaxTradeSlots")) { + maxTradeSlots = compound.getInteger("MaxTradeSlots"); + } + List newListings = new ArrayList<>(); List newBids = new ArrayList<>(); List newClaims = new ArrayList<>(); @@ -341,4 +347,7 @@ public class ContainerAuctionTrades extends ContainerAuction { } public int getHiddenSlot() { return hiddenSlot; } + + /** Get player's max trade slots (synced from server based on permissions) */ + public int getMaxTradeSlots() { return maxTradeSlots; } } diff --git a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java index e6b4c86c8..807190816 100644 --- a/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java +++ b/src/main/java/noppes/npcs/containers/ContainerNPCTrader.java @@ -105,7 +105,7 @@ public class ContainerNPCTrader extends ContainerNpcInterface { long currencyCost = role.getCurrencyCost(i); if (currencyCost > 0) { PlayerData data = PlayerData.get(entityplayer); - if (data.currencyData.getBalance() < currencyCost) { + if (data.tradeData.getBalance() < currencyCost) { return null; // Can't afford currency cost } } @@ -117,7 +117,7 @@ public class ContainerNPCTrader extends ContainerNpcInterface { // Withdraw currency cost (after item consumption to maintain order) if (currencyCost > 0) { PlayerData data = PlayerData.get(entityplayer); - data.currencyData.withdraw(currencyCost); + data.tradeData.withdraw(currencyCost); } // Consume stock diff --git a/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java b/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java index 58e4b8d86..9b8c1f995 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java +++ b/src/main/java/noppes/npcs/controllers/AuctionConfigSync.java @@ -25,7 +25,7 @@ public class AuctionConfigSync { compound.setString("CurrencyName", getEffectiveCurrencyName()); compound.setInteger("AuctionDurationHours", ConfigMarket.AuctionDurationHours); compound.setDouble("MinBidIncrement", ConfigMarket.MinBidIncrementPercent); - compound.setInteger("MaxActiveListings", ConfigMarket.DefaultMaxListings); + compound.setInteger("MaxActiveListings", ConfigMarket.DefaultMaxTrades); compound.setInteger("ClaimExpirationDays", ConfigMarket.ClaimExpirationDays); return compound; } diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index 708c3bb0e..375767deb 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -10,7 +10,12 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.util.ChatComponentText; import net.minecraft.util.EnumChatFormatting; import noppes.npcs.CustomNpcs; +import noppes.npcs.CustomNpcsPermissions; import noppes.npcs.LogWriter; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.handler.IAuctionHandler; +import noppes.npcs.api.handler.data.IAuctionListing; +import noppes.npcs.api.item.IItemStack; import noppes.npcs.config.ConfigMarket; import noppes.npcs.constants.EnumAuctionSort; import noppes.npcs.constants.EnumAuctionStatus; @@ -19,9 +24,8 @@ import noppes.npcs.constants.EnumNotificationType; import noppes.npcs.controllers.data.AuctionClaim; import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; -import noppes.npcs.controllers.data.AuctionNotification; -import noppes.npcs.controllers.data.PlayerCurrencyData; import noppes.npcs.controllers.data.PlayerData; +import noppes.npcs.controllers.data.PlayerTradeData; import noppes.npcs.util.CustomNPCsThreader; import java.io.File; @@ -30,28 +34,26 @@ import java.io.FileOutputStream; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.ReentrantReadWriteLock; /** - * Main controller for the Auction House system. + * Main controller for the Auction system. * * Thread Safety: - * - All data modifications go through synchronized methods or use proper locking - * - Player indices are maintained for O(1) lookups + * - Uses ConcurrentHashMap for all data stores - no locking required + * - Claims are stored in PlayerData, not here - reduces contention * - Saves are performed asynchronously on the CNPC+ thread * * Performance: - * - Auction processing runs every 20 seconds (400 ticks) to reduce server load + * - Auction processing runs every 30 seconds (600 ticks) + * - Save interval is every 30 seconds (600 ticks) * - Player-specific queries use indexed maps for fast lookups - * - Read operations use read locks, write operations use write locks */ -public class AuctionController { +public class AuctionController implements IAuctionHandler { public static AuctionController Instance; - // Main data stores + // Main data stores - listings only, claims are stored in PlayerData private final Map listings = new ConcurrentHashMap<>(); - private final Map claims = new ConcurrentHashMap<>(); - private final List pendingNotifications = Collections.synchronizedList(new ArrayList<>()); + // Claims are now stored in PlayerData.tradeData, not here // ========================================= // Player Indices for O(1) Lookups @@ -63,16 +65,13 @@ public class AuctionController { /** Listings where player is current high bidder */ private final Map> playerBidIds = new ConcurrentHashMap<>(); - /** Claims by player UUID - for quick "my claims" lookups */ - private final Map> playerClaimIds = new ConcurrentHashMap<>(); + /** Cached max trade slots per player - computed on login, cleared on logout */ + private final Map playerMaxTradesCache = new ConcurrentHashMap<>(); // ========================================= // Threading & State // ========================================= - /** Lock for data modifications - allows concurrent reads */ - private final ReentrantReadWriteLock dataLock = new ReentrantReadWriteLock(); - /** Flag indicating data needs to be saved */ private final AtomicBoolean dirty = new AtomicBoolean(false); @@ -82,12 +81,8 @@ public class AuctionController { private String filePath = ""; private int tickCounter = 0; - /** Process auctions every 20 seconds (400 ticks) for performance */ - private static final int PROCESS_INTERVAL_TICKS = 400; - - /** Save dirty data every 60 seconds (1200 ticks) */ - private static final int SAVE_INTERVAL_TICKS = 1200; - private int saveTickCounter = 0; + /** Process auctions and save every 30 seconds (600 ticks) */ + private static final int TICK_INTERVAL = 600; public AuctionController() { Instance = this; @@ -116,18 +111,11 @@ public class AuctionController { if (!ConfigMarket.AuctionEnabled) return; tickCounter++; - saveTickCounter++; - // Process ended auctions every 20 seconds - if (tickCounter >= PROCESS_INTERVAL_TICKS) { + // Process auctions and save every 30 seconds + if (tickCounter >= TICK_INTERVAL) { tickCounter = 0; processEndedAuctions(); - processExpiredClaims(); - } - - // Save dirty data periodically - if (saveTickCounter >= SAVE_INTERVAL_TICKS) { - saveTickCounter = 0; if (dirty.get()) { saveAsync(); } @@ -135,25 +123,19 @@ public class AuctionController { } private void processEndedAuctions() { - dataLock.writeLock().lock(); - try { - boolean changed = false; - for (AuctionListing listing : listings.values()) { - if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) { - endAuction(listing); - changed = true; - } + boolean changed = false; + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE && listing.isExpired()) { + endAuction(listing); + changed = true; } - if (changed) { - markDirty(); - } - } finally { - dataLock.writeLock().unlock(); + } + if (changed) { + markDirty(); } } private void endAuction(AuctionListing listing) { - // Must be called within write lock listing.status = EnumAuctionStatus.ENDED; // Remove from seller's active listings @@ -171,7 +153,7 @@ public class AuctionController { // Auction sold - create claims AuctionClaim itemClaim = AuctionClaim.createItemWonClaim( listing.highBidderUUID, listing.highBidderName, listing.id, listing.item); - addClaim(itemClaim); + addClaimToPlayer(listing.highBidderUUID, itemClaim); long saleAmount = listing.currentBid; long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent); @@ -180,12 +162,12 @@ public class AuctionController { AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, itemDisplayName, listing.highBidderName); - addClaim(currencyClaim); + addClaimToPlayer(listing.sellerUUID, currencyClaim); // Notifications - queueNotification(listing.highBidderUUID, EnumNotificationType.AUCTION_WON, listing.id, + sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_WON, listing.id, "You won the auction for " + itemDisplayName + "!"); - queueNotification(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, + sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, "Your " + itemDisplayName + " sold for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); logAuction("SOLD", listing.sellerName, itemDisplayName, saleAmount, @@ -194,45 +176,15 @@ public class AuctionController { // No bids - return item to seller AuctionClaim returnClaim = AuctionClaim.createItemReturnedClaim( listing.sellerUUID, listing.sellerName, listing.id, listing.item); - addClaim(returnClaim); + addClaimToPlayer(listing.sellerUUID, returnClaim); - queueNotification(listing.sellerUUID, EnumNotificationType.AUCTION_EXPIRED, listing.id, + sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_EXPIRED, listing.id, "Your auction for " + itemDisplayName + " expired with no bids."); logAuction("EXPIRED", listing.sellerName, itemDisplayName, listing.startingPrice, "No bids"); } } - private void processExpiredClaims() { - int expirationDays = ConfigMarket.ClaimExpirationDays; - - dataLock.writeLock().lock(); - try { - Iterator> iterator = claims.entrySet().iterator(); - boolean changed = false; - - while (iterator.hasNext()) { - AuctionClaim claim = iterator.next().getValue(); - if (!claim.claimed && claim.isExpired(expirationDays)) { - logAuction("CLAIM_EXPIRED", claim.playerName, - claim.type.isItem() ? (claim.item != null ? claim.item.getDisplayName() : "Unknown") : "Currency", - claim.currency, "Claim expired after " + expirationDays + " days"); - - // Remove from player index - removeFromPlayerClaims(claim.playerUUID, claim.id); - iterator.remove(); - changed = true; - } - } - - if (changed) { - markDirty(); - } - } finally { - dataLock.writeLock().unlock(); - } - } - // ========================================= // Index Management // ========================================= @@ -265,144 +217,139 @@ public class AuctionController { } } - private void addToPlayerClaims(UUID playerUUID, String claimId) { - playerClaimIds.computeIfAbsent(playerUUID, k -> ConcurrentHashMap.newKeySet()).add(claimId); - } + /** + * Add a claim to a player's trade data. + * If player is online, adds directly. If offline, loads their data file and updates it. + */ + private void addClaimToPlayer(UUID playerUUID, AuctionClaim claim) { + if (playerUUID == null || claim == null) return; - private void removeFromPlayerClaims(UUID playerUUID, String claimId) { - Set ids = playerClaimIds.get(playerUUID); - if (ids != null) { - ids.remove(claimId); - if (ids.isEmpty()) { - playerClaimIds.remove(playerUUID); - } + PlayerData data = PlayerDataController.Instance.getData(playerUUID); + if (data != null) { + data.tradeData.addClaim(claim); + data.updateClient = true; + data.save(); } } - /** Add a claim and update indices */ - private void addClaim(AuctionClaim claim) { - claims.put(claim.id, claim); - addToPlayerClaims(claim.playerUUID, claim.id); - } - // ========================================= // Listing Management // ========================================= public String createListing(EntityPlayer player, ItemStack item, long startingPrice, long buyoutPrice) { if (!ConfigMarket.AuctionEnabled) { - return "Auction House is disabled."; + return "Auction is disabled."; + } + + // Validate minimum price + if (startingPrice < ConfigMarket.MinimumListingPrice) { + return "Starting price must be at least " + ConfigMarket.MinimumListingPrice + " " + ConfigMarket.CurrencyName + "."; + } + + // Validate buyout price if set + if (buyoutPrice > 0 && buyoutPrice < startingPrice) { + return "Buyout price must be higher than starting price."; } UUID playerUUID = player.getUniqueID(); String playerName = player.getCommandSenderName(); - dataLock.writeLock().lock(); - try { - // Check listing limit - int currentListings = getPlayerListingCountFast(playerUUID); - int maxListings = getMaxListingsForPlayer(player); - if (currentListings >= maxListings) { - return "You have reached your maximum listings (" + maxListings + ")."; - } - - // Check listing fee - PlayerData playerData = PlayerData.get(player); - if (playerData == null) { - return "Could not access player data."; - } - - PlayerCurrencyData currency = playerData.currencyData; - long fee = ConfigMarket.ListingFee; - if (!currency.canAfford(fee)) { - return "You cannot afford the listing fee (" + fee + " " + ConfigMarket.CurrencyName + ")."; - } - - // Deduct fee - if (!currency.withdraw(fee)) { - return "Failed to deduct listing fee."; - } - - // Calculate duration - long durationMs = ConfigMarket.AuctionDurationHours * 60L * 60L * 1000L; - - // Create listing - AuctionListing listing = new AuctionListing(playerUUID, playerName, item, startingPrice, buyoutPrice, durationMs); - listings.put(listing.id, listing); - addToPlayerListings(playerUUID, listing.id); - - markDirty(); - - logAuction("CREATED", playerName, item.getDisplayName(), startingPrice, - "Buyout: " + (buyoutPrice > 0 ? buyoutPrice : "None") + ", Fee: " + fee); - - return null; // Success - } finally { - dataLock.writeLock().unlock(); + // Check trade slot limit (listings + bids + claims) + int currentTrades = getPlayerTradeSlotCount(player); + int maxTrades = getMaxTradesForPlayer(player); + if (currentTrades >= maxTrades) { + return "You have reached your maximum trade slots (" + maxTrades + ")."; } + + // Check listing fee + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerTradeData currency = playerData.tradeData; + long fee = ConfigMarket.ListingFee; + if (!currency.canAfford(fee)) { + return "You cannot afford the listing fee (" + fee + " " + ConfigMarket.CurrencyName + ")."; + } + + // Deduct fee + if (!currency.withdraw(fee)) { + return "Failed to deduct listing fee."; + } + + // Calculate duration + long durationMs = ConfigMarket.AuctionDurationHours * 60L * 60L * 1000L; + + // Create listing + AuctionListing listing = new AuctionListing(playerUUID, playerName, item, startingPrice, buyoutPrice, durationMs); + listings.put(listing.id, listing); + addToPlayerListings(playerUUID, listing.id); + + markDirty(); + + logAuction("CREATED", playerName, item.getDisplayName(), startingPrice, + "Buyout: " + (buyoutPrice > 0 ? buyoutPrice : "None") + ", Fee: " + fee); + + return null; // Success } public String cancelListing(String listingId, EntityPlayer player, boolean isAdmin) { - dataLock.writeLock().lock(); - try { - AuctionListing listing = listings.get(listingId); - if (listing == null) { - return "Listing not found."; - } - - if (!listing.status.canCancel()) { - return "This auction cannot be cancelled."; - } - - UUID playerUUID = player.getUniqueID(); - if (!isAdmin && !listing.isSeller(playerUUID)) { - return "You can only cancel your own listings."; - } - - // Handle penalty if there are bids - if (listing.hasBids()) { - long penalty = (long) (listing.currentBid * ConfigMarket.CancellationPenaltyPercent); - - // Refund bidder (no item - can't rebid on cancelled auction) - AuctionClaim refundClaim = AuctionClaim.createRefundClaim( - listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, - listing.item.getDisplayName(), listing.sellerName, null); - addClaim(refundClaim); - - // Remove from bidder's active bids - removeFromPlayerBids(listing.highBidderUUID, listing.id); - - queueNotification(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id, - "The auction for " + listing.item.getDisplayName() + " was cancelled. Your bid has been refunded."); - - // Return item to seller - AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim( - listing.sellerUUID, listing.sellerName, listing.id, listing.item); - addClaim(itemClaim); - - logAuction("CANCELLED", listing.sellerName, listing.item.getDisplayName(), listing.currentBid, - "Penalty: " + penalty + ", Bidder refunded: " + listing.highBidderName + - (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); - } else { - // No bids - just return item - AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim( - listing.sellerUUID, listing.sellerName, listing.id, listing.item); - addClaim(itemClaim); - - logAuction("CANCELLED", listing.sellerName, listing.item.getDisplayName(), listing.startingPrice, - "No bids" + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); - } - - // Remove from seller's active listings - removeFromPlayerListings(listing.sellerUUID, listing.id); - - listing.status = EnumAuctionStatus.CANCELLED; - markDirty(); - - return null; // Success - } finally { - dataLock.writeLock().unlock(); + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; } + + if (!listing.status.canCancel()) { + return "This auction cannot be cancelled."; + } + + UUID playerUUID = player.getUniqueID(); + if (!isAdmin && !listing.isSeller(playerUUID)) { + return "You can only cancel your own listings."; + } + + // Handle penalty if there are bids + if (listing.hasBids()) { + long penalty = (long) (listing.currentBid * ConfigMarket.CancellationPenaltyPercent); + + // Refund bidder (no item - can't rebid on cancelled auction) + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, + listing.item.getDisplayName(), listing.sellerName, null); + addClaimToPlayer(listing.highBidderUUID, refundClaim); + + // Remove from bidder's active bids + removeFromPlayerBids(listing.highBidderUUID, listing.id); + + sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id, + "The auction for " + listing.item.getDisplayName() + " was cancelled. Your bid has been refunded."); + + // Return item to seller + AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim( + listing.sellerUUID, listing.sellerName, listing.id, listing.item); + addClaimToPlayer(listing.sellerUUID, itemClaim); + + logAuction("CANCELLED", listing.sellerName, listing.item.getDisplayName(), listing.currentBid, + "Penalty: " + penalty + ", Bidder refunded: " + listing.highBidderName + + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); + } else { + // No bids - just return item + AuctionClaim itemClaim = AuctionClaim.createItemReturnedClaim( + listing.sellerUUID, listing.sellerName, listing.id, listing.item); + addClaimToPlayer(listing.sellerUUID, itemClaim); + + logAuction("CANCELLED", listing.sellerName, listing.item.getDisplayName(), listing.startingPrice, + "No bids" + (isAdmin ? ", Cancelled by admin: " + player.getCommandSenderName() : "")); + } + + // Remove from seller's active listings + removeFromPlayerListings(listing.sellerUUID, listing.id); + + listing.status = EnumAuctionStatus.CANCELLED; + markDirty(); + + return null; // Success } // ========================================= @@ -411,309 +358,332 @@ public class AuctionController { public String placeBid(String listingId, EntityPlayer player, long bidAmount) { if (!ConfigMarket.AuctionEnabled) { - return "Auction House is disabled."; + return "Auction is disabled."; } UUID playerUUID = player.getUniqueID(); String playerName = player.getCommandSenderName(); - dataLock.writeLock().lock(); - try { - AuctionListing listing = listings.get(listingId); - if (listing == null) { - return "Listing not found."; - } - - // Double-check status and expiry inside lock to prevent race conditions - if (!listing.status.canBid()) { - return "This auction has ended."; - } - - if (listing.isExpired()) { - return "This auction has ended."; - } - - // CRITICAL: Prevent bidding on own items - if (listing.isSeller(playerUUID)) { - return "You cannot bid on your own auction."; - } - - // Check minimum bid - long minBid = listing.getMinimumBid(ConfigMarket.MinBidIncrementPercent); - if (bidAmount < minBid) { - return "Bid must be at least " + minBid + " " + ConfigMarket.CurrencyName + "."; - } - - // Check currency - PlayerData playerData = PlayerData.get(player); - if (playerData == null) { - return "Could not access player data."; - } - - PlayerCurrencyData currency = playerData.currencyData; - - // Store previous bidder info - UUID previousBidder = listing.highBidderUUID; - long previousBid = listing.currentBid; - boolean isSameBidder = previousBidder != null && previousBidder.equals(playerUUID); - - // Calculate amount to charge - long amountToCharge = isSameBidder ? (bidAmount - previousBid) : bidAmount; - - if (!currency.canAfford(amountToCharge)) { - return "You cannot afford this bid."; - } - - // Deduct bid from new bidder FIRST - if (!currency.withdraw(amountToCharge)) { - return "Failed to deduct bid amount."; - } - - // Refund previous bidder if different player (include item for rebid option) - if (listing.hasBids() && previousBidder != null && !isSameBidder) { - AuctionClaim refundClaim = AuctionClaim.createRefundClaim( - previousBidder, listing.highBidderName, listing.id, previousBid, - listing.item.getDisplayName(), playerName, listing.item); - addClaim(refundClaim); - - // Remove from previous bidder's active bids - removeFromPlayerBids(previousBidder, listing.id); - - queueNotification(previousBidder, EnumNotificationType.AUCTION_OUTBID, listing.id, - "You were outbid on " + listing.item.getDisplayName() + "!"); - } - - // Update listing - listing.highBidderUUID = playerUUID; - listing.highBidderName = playerName; - listing.currentBid = bidAmount; - listing.bidCount++; - - // Add to new bidder's active bids - addToPlayerBids(playerUUID, listing.id); - - // Snipe protection - listing.extendForSnipeProtection(ConfigMarket.SnipeProtectionMinutes); - - markDirty(); - - logAuction("BID", playerName, listing.item.getDisplayName(), bidAmount, - "Bids: " + listing.bidCount + ", Seller: " + listing.sellerName); - - return null; // Success - } finally { - dataLock.writeLock().unlock(); + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; } + + // Check status and expiry + if (!listing.status.canBid()) { + return "This auction has ended."; + } + + if (listing.isExpired()) { + return "This auction has ended."; + } + + // CRITICAL: Prevent bidding on own items + if (listing.isSeller(playerUUID)) { + return "You cannot bid on your own auction."; + } + + // Prevent rebidding if already highest bidder + if (listing.isHighBidder(playerUUID)) { + return "You are already the highest bidder. Use buyout if you want to purchase immediately."; + } + + // Check minimum bid + long minBid = listing.getMinimumBid(ConfigMarket.MinBidIncrementPercent); + if (bidAmount < minBid) { + return "Bid must be at least " + minBid + " " + ConfigMarket.CurrencyName + "."; + } + + // Check currency + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerTradeData currency = playerData.tradeData; + + // Store previous bidder info + UUID previousBidder = listing.highBidderUUID; + long previousBid = listing.currentBid; + + // Full bid amount charged since we don't allow rebidding + long amountToCharge = bidAmount; + + if (!currency.canAfford(amountToCharge)) { + return "You cannot afford this bid."; + } + + // Deduct bid from new bidder FIRST + if (!currency.withdraw(amountToCharge)) { + return "Failed to deduct bid amount."; + } + + // Refund previous bidder (include item for rebid option) + if (listing.hasBids() && previousBidder != null) { + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + previousBidder, listing.highBidderName, listing.id, previousBid, + listing.item.getDisplayName(), playerName, listing.item); + addClaimToPlayer(previousBidder, refundClaim); + + // Remove from previous bidder's active bids + removeFromPlayerBids(previousBidder, listing.id); + + sendNotificationToPlayer(previousBidder, EnumNotificationType.AUCTION_OUTBID, listing.id, + "You were outbid on " + listing.item.getDisplayName() + "!"); + } + + // Update listing + listing.highBidderUUID = playerUUID; + listing.highBidderName = playerName; + listing.currentBid = bidAmount; + listing.bidCount++; + + // Add to new bidder's active bids + addToPlayerBids(playerUUID, listing.id); + + // Snipe protection + listing.extendForSnipeProtection(ConfigMarket.SnipeProtectionMinutes); + + markDirty(); + + logAuction("BID", playerName, listing.item.getDisplayName(), bidAmount, + "Bids: " + listing.bidCount + ", Seller: " + listing.sellerName); + + return null; // Success } public String buyout(String listingId, EntityPlayer player) { if (!ConfigMarket.AuctionEnabled) { - return "Auction House is disabled."; + return "Auction is disabled."; } UUID playerUUID = player.getUniqueID(); String playerName = player.getCommandSenderName(); - dataLock.writeLock().lock(); - try { - AuctionListing listing = listings.get(listingId); - if (listing == null) { - return "Listing not found."; - } - - // Double-check status and expiry inside lock - if (!listing.status.canBid()) { - return "This auction has ended."; - } - - if (listing.isExpired()) { - return "This auction has ended."; - } - - if (!listing.hasBuyout()) { - return "This auction does not have a buyout price."; - } - - // CRITICAL: Prevent buying own items - if (listing.isSeller(playerUUID)) { - return "You cannot buy your own auction."; - } - - // Check currency - PlayerData playerData = PlayerData.get(player); - if (playerData == null) { - return "Could not access player data."; - } - - PlayerCurrencyData currency = playerData.currencyData; - if (!currency.canAfford(listing.buyoutPrice)) { - return "You cannot afford the buyout price."; - } - - // Deduct buyout FIRST - if (!currency.withdraw(listing.buyoutPrice)) { - return "Failed to deduct buyout amount."; - } - - // Refund previous bidder if any (no item - auction ended via buyout) - if (listing.hasBids() && listing.highBidderUUID != null) { - AuctionClaim refundClaim = AuctionClaim.createRefundClaim( - listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, - listing.item.getDisplayName(), playerName, null); - addClaim(refundClaim); - - // Remove from previous bidder's active bids - removeFromPlayerBids(listing.highBidderUUID, listing.id); - - queueNotification(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id, - "The auction for " + listing.item.getDisplayName() + " was bought out. Your bid has been refunded."); - } - - // Create claims - AuctionClaim itemClaim = AuctionClaim.createItemWonClaim(playerUUID, playerName, listing.id, listing.item); - addClaim(itemClaim); - - long saleAmount = listing.buyoutPrice; - long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent); - long sellerReceives = saleAmount - tax; - - AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( - listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, - listing.item.getDisplayName(), playerName); - addClaim(currencyClaim); - - // Notifications - queueNotification(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, - "Your " + listing.item.getDisplayName() + " was bought out for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); - - // Remove from seller's active listings - removeFromPlayerListings(listing.sellerUUID, listing.id); - - listing.status = EnumAuctionStatus.ENDED; - listing.highBidderUUID = playerUUID; - listing.highBidderName = playerName; - listing.currentBid = listing.buyoutPrice; - - markDirty(); - - logAuction("BUYOUT", playerName, listing.item.getDisplayName(), listing.buyoutPrice, - "Seller: " + listing.sellerName + ", Tax: " + tax); - - return null; // Success - } finally { - dataLock.writeLock().unlock(); + AuctionListing listing = listings.get(listingId); + if (listing == null) { + return "Listing not found."; } + + // Check status and expiry + if (!listing.status.canBid()) { + return "This auction has ended."; + } + + if (listing.isExpired()) { + return "This auction has ended."; + } + + if (!listing.hasBuyout()) { + return "This auction does not have a buyout price."; + } + + // CRITICAL: Prevent buying own items + if (listing.isSeller(playerUUID)) { + return "You cannot buy your own auction."; + } + + // Check currency + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; + } + + PlayerTradeData currency = playerData.tradeData; + + // If buyer is already the high bidder, only charge the difference + boolean isCurrentBidder = listing.isHighBidder(playerUUID); + long amountToCharge = isCurrentBidder + ? (listing.buyoutPrice - listing.currentBid) + : listing.buyoutPrice; + + if (!currency.canAfford(amountToCharge)) { + if (isCurrentBidder) { + return "You cannot afford the remaining " + amountToCharge + " " + ConfigMarket.CurrencyName + " for buyout."; + } + return "You cannot afford the buyout price."; + } + + // Deduct buyout amount + if (!currency.withdraw(amountToCharge)) { + return "Failed to deduct buyout amount."; + } + + // Refund previous bidder if any AND they're not the buyer (no item - auction ended via buyout) + if (listing.hasBids() && listing.highBidderUUID != null && !isCurrentBidder) { + AuctionClaim refundClaim = AuctionClaim.createRefundClaim( + listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, + listing.item.getDisplayName(), playerName, null); + addClaimToPlayer(listing.highBidderUUID, refundClaim); + + // Remove from previous bidder's active bids + removeFromPlayerBids(listing.highBidderUUID, listing.id); + + sendNotificationToPlayer(listing.highBidderUUID, EnumNotificationType.AUCTION_OUTBID, listing.id, + "The auction for " + listing.item.getDisplayName() + " was bought out. Your bid has been refunded."); + } else if (isCurrentBidder) { + // Current bidder is buying out - just remove from their active bids + removeFromPlayerBids(playerUUID, listing.id); + } + + // Create claims + AuctionClaim itemClaim = AuctionClaim.createItemWonClaim(playerUUID, playerName, listing.id, listing.item); + addClaimToPlayer(playerUUID, itemClaim); + + long saleAmount = listing.buyoutPrice; + long tax = (long) (saleAmount * ConfigMarket.SalesTaxPercent); + long sellerReceives = saleAmount - tax; + + AuctionClaim currencyClaim = AuctionClaim.createCurrencyClaim( + listing.sellerUUID, listing.sellerName, listing.id, sellerReceives, + listing.item.getDisplayName(), playerName); + addClaimToPlayer(listing.sellerUUID, currencyClaim); + + // Notifications + sendNotificationToPlayer(listing.sellerUUID, EnumNotificationType.AUCTION_SOLD, listing.id, + "Your " + listing.item.getDisplayName() + " was bought out for " + saleAmount + " " + ConfigMarket.CurrencyName + "!"); + + // Remove from seller's active listings + removeFromPlayerListings(listing.sellerUUID, listing.id); + + listing.status = EnumAuctionStatus.ENDED; + listing.highBidderUUID = playerUUID; + listing.highBidderName = playerName; + listing.currentBid = listing.buyoutPrice; + + markDirty(); + + logAuction("BUYOUT", playerName, listing.item.getDisplayName(), listing.buyoutPrice, + "Seller: " + listing.sellerName + ", Tax: " + tax); + + return null; // Success } // ========================================= - // Claims + // Claims - Now stored in PlayerData // ========================================= /** - * Get player claims using indexed lookup - O(n) where n = player's claims, not all claims + * Get player claims from PlayerData. */ - public List getPlayerClaims(UUID playerUUID) { - Set claimIds = playerClaimIds.get(playerUUID); - if (claimIds == null || claimIds.isEmpty()) { + public List getPlayerClaims(EntityPlayer player) { + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { return new ArrayList<>(); } + return playerData.tradeData.getClaimsList(); + } - List playerClaims = new ArrayList<>(); - for (String claimId : claimIds) { - AuctionClaim claim = claims.get(claimId); - if (claim != null && !claim.claimed) { - playerClaims.add(claim); - } + /** + * Get player claims by UUID (for login notification). + */ + public List getPlayerClaims(UUID playerUUID) { + EntityPlayerMP onlinePlayer = (EntityPlayerMP) PlayerDataController.getPlayerFromUUID(playerUUID); + if (onlinePlayer != null) { + return getPlayerClaims(onlinePlayer); } - return playerClaims; + return new ArrayList<>(); } public String claimItem(String claimId, EntityPlayer player) { - dataLock.writeLock().lock(); - try { - AuctionClaim claim = claims.get(claimId); - if (claim == null) { - return "Claim not found."; - } - - if (claim.claimed) { - return "Already claimed."; - } - - if (!claim.isForPlayer(player.getUniqueID())) { - return "This claim is not for you."; - } - - if (!claim.type.isItem()) { - return "This is not an item claim."; - } - - if (claim.item == null) { - return "Item data is missing."; - } - - // Check inventory space - if (!player.inventory.addItemStackToInventory(claim.item.copy())) { - return "Your inventory is full."; - } - - claim.claimed = true; - removeFromPlayerClaims(claim.playerUUID, claim.id); - markDirty(); - - logAuction("CLAIMED", player.getCommandSenderName(), claim.item.getDisplayName(), 0, "Item claimed"); - - return null; // Success - } finally { - dataLock.writeLock().unlock(); + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; } + + AuctionClaim claim = playerData.tradeData.getClaimInternal(claimId); + if (claim == null) { + return "Claim not found."; + } + + if (claim.claimed) { + return "Already claimed."; + } + + if (!claim.type.isItem()) { + return "This is not an item claim."; + } + + if (claim.item == null) { + return "Item data is missing."; + } + + // Check inventory space + if (!player.inventory.addItemStackToInventory(claim.item.copy())) { + return "Your inventory is full."; + } + + // Remove claim from player's auction data + playerData.tradeData.claimAndRemove(claimId); + playerData.updateClient = true; + playerData.save(); + + // Cleanup listing if no more claims + cleanupListingIfComplete(claim.listingId); + + logAuction("CLAIMED", player.getCommandSenderName(), claim.item.getDisplayName(), 0, "Item claimed"); + + return null; // Success } public String claimCurrency(String claimId, EntityPlayer player) { - dataLock.writeLock().lock(); - try { - AuctionClaim claim = claims.get(claimId); - if (claim == null) { - return "Claim not found."; - } - - if (claim.claimed) { - return "Already claimed."; - } - - if (!claim.isForPlayer(player.getUniqueID())) { - return "This claim is not for you."; - } - - if (!claim.type.isCurrency()) { - return "This is not a currency claim."; - } - - PlayerData playerData = PlayerData.get(player); - if (playerData == null) { - return "Could not access player data."; - } - - if (!playerData.currencyData.deposit(claim.currency)) { - return "Failed to deposit currency."; - } - - claim.claimed = true; - removeFromPlayerClaims(claim.playerUUID, claim.id); - markDirty(); - - logAuction("CLAIMED", player.getCommandSenderName(), ConfigMarket.CurrencyName, claim.currency, - claim.type == EnumClaimType.REFUND ? "Refund claimed" : "Sale proceeds claimed"); - - return null; // Success - } finally { - dataLock.writeLock().unlock(); + PlayerData playerData = PlayerData.get(player); + if (playerData == null) { + return "Could not access player data."; } + + AuctionClaim claim = playerData.tradeData.getClaimInternal(claimId); + if (claim == null) { + return "Claim not found."; + } + + if (claim.claimed) { + return "Already claimed."; + } + + if (!claim.type.isCurrency()) { + return "This is not a currency claim."; + } + + if (!playerData.tradeData.deposit(claim.currency)) { + return "Failed to deposit currency."; + } + + // Remove claim from player's auction data + playerData.tradeData.claimAndRemove(claimId); + playerData.updateClient = true; + playerData.save(); + + // Cleanup listing if no more claims + cleanupListingIfComplete(claim.listingId); + + logAuction("CLAIMED", player.getCommandSenderName(), ConfigMarket.CurrencyName, claim.currency, + claim.type == EnumClaimType.REFUND ? "Refund claimed" : "Sale proceeds claimed"); + + return null; // Success + } + + /** + * Clean up a listing if it's ended and no longer needed. + * Now just removes ended listings after some delay since claims are in PlayerData. + */ + private void cleanupListingIfComplete(String listingId) { + if (listingId == null || listingId.isEmpty()) return; + + AuctionListing listing = listings.get(listingId); + if (listing == null) return; + + // Only cleanup ended/cancelled listings + if (listing.status == EnumAuctionStatus.ACTIVE) return; + + // Remove ended listings (claims are now in PlayerData, not here) + listings.remove(listingId); + markDirty(); } public int claimAll(EntityPlayer player) { int claimedCount = 0; - List playerClaims = getPlayerClaims(player.getUniqueID()); + List playerClaims = getPlayerClaims(player); for (AuctionClaim claim : playerClaims) { String result; @@ -737,21 +707,17 @@ public class AuctionController { public List getActiveListings(AuctionFilter filter, int page, int pageSize) { List result = new ArrayList<>(); - dataLock.readLock().lock(); - try { - for (AuctionListing listing : listings.values()) { - if (listing.status != EnumAuctionStatus.ACTIVE) continue; + for (AuctionListing listing : listings.values()) { + if (listing.status != EnumAuctionStatus.ACTIVE) continue; - // Apply search filter - if (filter.hasSearchText()) { - String itemName = listing.item != null ? listing.item.getDisplayName() : ""; - if (!filter.matchesSearch(itemName)) continue; - } - - result.add(listing); + // Apply search filter (searches item name AND seller name) + if (filter.hasSearchText()) { + String itemName = listing.item != null ? listing.item.getDisplayName() : ""; + String sellerName = listing.sellerName != null ? listing.sellerName : ""; + if (!filter.matchesSearch(itemName, sellerName)) continue; } - } finally { - dataLock.readLock().unlock(); + + result.add(listing); } // Sort @@ -791,22 +757,19 @@ public class AuctionController { public int getTotalActiveListings(AuctionFilter filter) { int count = 0; - dataLock.readLock().lock(); - try { - for (AuctionListing listing : listings.values()) { - if (listing.status != EnumAuctionStatus.ACTIVE) continue; - if (filter.hasSearchText()) { - String itemName = listing.item != null ? listing.item.getDisplayName() : ""; - if (!filter.matchesSearch(itemName)) continue; - } - count++; + for (AuctionListing listing : listings.values()) { + if (listing.status != EnumAuctionStatus.ACTIVE) continue; + if (filter.hasSearchText()) { + String itemName = listing.item != null ? listing.item.getDisplayName() : ""; + String sellerName = listing.sellerName != null ? listing.sellerName : ""; + if (!filter.matchesSearch(itemName, sellerName)) continue; } - } finally { - dataLock.readLock().unlock(); + count++; } return count; } + @Override public AuctionListing getListing(String listingId) { return listings.get(listingId); } @@ -816,25 +779,18 @@ public class AuctionController { */ public long getPlayerBalance(EntityPlayer player) { PlayerData playerData = PlayerData.get(player); - if (playerData == null || playerData.currencyData == null) { + if (playerData == null || playerData.tradeData == null) { return 0; } - return playerData.currencyData.getBalance(); + return playerData.tradeData.getBalance(); } /** - * Fast O(1) lookup for player listing count using index - */ - public int getPlayerListingCountFast(UUID playerUUID) { - Set ids = playerListingIds.get(playerUUID); - return ids != null ? ids.size() : 0; - } - - /** - * Legacy method - still iterates for accuracy (in case index is stale) + * Get player listing count using index */ public int getPlayerListingCount(UUID playerUUID) { - return getPlayerListingCountFast(playerUUID); + Set ids = playerListingIds.get(playerUUID); + return ids != null ? ids.size() : 0; } /** @@ -879,7 +835,28 @@ public class AuctionController { /** * Get the total number of trade slots used by a player. - * Uses indexed lookups for O(1) performance. + * Includes active listings, active bids, and pending claims. + */ + public int getPlayerTradeSlotCount(EntityPlayer player) { + UUID playerUUID = player.getUniqueID(); + int count = 0; + // Active listings + Set listingIds = playerListingIds.get(playerUUID); + if (listingIds != null) count += listingIds.size(); + // Active bids + Set bidIds = playerBidIds.get(playerUUID); + if (bidIds != null) count += bidIds.size(); + // Pending claims (from PlayerData) + PlayerData playerData = PlayerData.get(player); + if (playerData != null) { + count += playerData.tradeData.getClaimCount(); + } + return count; + } + + /** + * Get the total number of trade slots used by a player (UUID version). + * For offline players, only returns listings + bids (claims are in their PlayerData). */ public int getPlayerTradeSlotCount(UUID playerUUID) { int count = 0; @@ -889,71 +866,168 @@ public class AuctionController { // Active bids Set bidIds = playerBidIds.get(playerUUID); if (bidIds != null) count += bidIds.size(); - // Pending claims - Set claimIds = playerClaimIds.get(playerUUID); - if (claimIds != null) count += claimIds.size(); + // Can't get claims for offline player without loading their data return count; } - private int getMaxListingsForPlayer(EntityPlayer player) { - // TODO: Check permissions for customnpcs.auction.slots.X - return ConfigMarket.DefaultMaxListings; + /** + * Get the maximum trade slots for a player. + * Uses cached value if available, otherwise computes from permissions. + */ + public int getMaxTradesForPlayer(EntityPlayer player) { + UUID playerUUID = player.getUniqueID(); + + // Check cache first + Integer cached = playerMaxTradesCache.get(playerUUID); + if (cached != null) { + return cached; + } + + // Compute and cache + int maxTrades = computeMaxTradesForPlayer(player); + playerMaxTradesCache.put(playerUUID, maxTrades); + return maxTrades; + } + + /** + * Compute max trade slots from permissions. + * Checks for customnpcs.auction.trades.X permissions where X is the slot count. + * Uses the highest permission found, or DefaultMaxTrades if no permissions or lower. + * Capped at MAX_TRADE_SLOTS (45). + */ + private int computeMaxTradesForPlayer(EntityPlayer player) { + // Check for unlimited permission + if (CustomNpcsPermissions.hasCustomPermission(player, "customnpcs.auction.trades.*")) { + return ConfigMarket.MAX_TRADE_SLOTS; + } + + // Find highest allowed slot count from permissions + int highestAllowed = 0; + for (int i = 1; i <= ConfigMarket.MAX_TRADE_SLOTS; i++) { + String perm = "customnpcs.auction.trades." + i; + if (CustomNpcsPermissions.hasCustomPermission(player, perm)) { + highestAllowed = i; + } + } + + // Use default if no permissions found or permission is lower than default + if (highestAllowed == 0 || highestAllowed < ConfigMarket.DefaultMaxTrades) { + highestAllowed = ConfigMarket.DefaultMaxTrades; + } + + // Cap at maximum + return Math.min(highestAllowed, ConfigMarket.MAX_TRADE_SLOTS); + } + + /** + * Refresh a player's cached max trades (call when permissions change). + */ + public void refreshPlayerMaxTrades(EntityPlayer player) { + UUID playerUUID = player.getUniqueID(); + int maxTrades = computeMaxTradesForPlayer(player); + playerMaxTradesCache.put(playerUUID, maxTrades); + } + + /** + * Clear a player's cached max trades (call on logout). + */ + public void clearPlayerMaxTradesCache(UUID playerUUID) { + playerMaxTradesCache.remove(playerUUID); } // ========================================= // Notifications // ========================================= - private void queueNotification(UUID playerUUID, EnumNotificationType type, String listingId, String message) { - AuctionNotification notification = new AuctionNotification(playerUUID, type, listingId, message); - pendingNotifications.add(notification); - + /** + * Send a notification to a player. + * If player is online, sends immediately. Otherwise, discards (claims will notify on login). + */ + private void sendNotificationToPlayer(UUID playerUUID, EnumNotificationType type, String listingId, String message) { // Try to send immediately if player is online - EntityPlayerMP player = getOnlinePlayer(playerUUID); + EntityPlayerMP player = (EntityPlayerMP) PlayerDataController.getPlayerFromUUID(playerUUID); if (player != null) { - sendNotification(player, notification); - notification.sent = true; + sendNotificationMessage(player, type, message); } + // If offline, no need to store - they'll get notified about claims on login + } + + /** + * Send a notification chat message to a player. + */ + private void sendNotificationMessage(EntityPlayerMP player, EnumNotificationType type, String message) { + EnumChatFormatting color; + switch (type) { + case AUCTION_WON: + case AUCTION_SOLD: + color = EnumChatFormatting.GREEN; + break; + case AUCTION_OUTBID: + color = EnumChatFormatting.YELLOW; + break; + case AUCTION_EXPIRED: + color = EnumChatFormatting.RED; + break; + default: + color = EnumChatFormatting.GRAY; + } + player.addChatMessage(new ChatComponentText( + EnumChatFormatting.GOLD + "[Auction] " + color + message)); } public void onPlayerLogin(EntityPlayer player) { - UUID playerUUID = player.getUniqueID(); - boolean hasClaims = !getPlayerClaims(playerUUID).isEmpty(); + // Cache player's max trades permission + refreshPlayerMaxTrades(player); - synchronized (pendingNotifications) { - Iterator iterator = pendingNotifications.iterator(); - while (iterator.hasNext()) { - AuctionNotification notification = iterator.next(); - if (notification.isForPlayer(playerUUID) && !notification.sent) { - sendNotification((EntityPlayerMP) player, notification); - notification.sent = true; - iterator.remove(); + // Process expired claims for this player + PlayerData playerData = PlayerData.get(player); + if (playerData != null) { + int expired = playerData.tradeData.processExpiredClaims(); + if (expired > 0) { + playerData.updateClient = true; + playerData.save(); + logAuction("CLAIM_EXPIRED", player.getCommandSenderName(), "Multiple", expired, + expired + " claims expired on login"); + } + } + + // Check for pending claims and notify + List claims = getPlayerClaims(player); + if (!claims.isEmpty()) { + int itemClaims = 0; + int currencyClaims = 0; + long totalCurrency = 0; + + for (AuctionClaim claim : claims) { + if (claim.type.isItem()) { + itemClaims++; + } else { + currencyClaims++; + totalCurrency += claim.currency; } } - } - if (hasClaims) { - player.addChatMessage(new ChatComponentText(EnumChatFormatting.GOLD + - "[Auction House] " + EnumChatFormatting.YELLOW + "You have items or currency to claim!")); - } - } + StringBuilder msg = new StringBuilder(); + msg.append(EnumChatFormatting.GOLD).append("[Auction] "); + msg.append(EnumChatFormatting.YELLOW).append("You have "); - private void sendNotification(EntityPlayerMP player, AuctionNotification notification) { - player.addChatMessage(new ChatComponentText(EnumChatFormatting.GOLD + - "[Auction House] " + EnumChatFormatting.WHITE + notification.message)); - } - - private EntityPlayerMP getOnlinePlayer(UUID uuid) { - MinecraftServer server = MinecraftServer.getServer(); - if (server == null) return null; - - for (Object obj : server.getConfigurationManager().playerEntityList) { - EntityPlayerMP player = (EntityPlayerMP) obj; - if (player.getUniqueID().equals(uuid)) { - return player; + if (itemClaims > 0 && currencyClaims > 0) { + msg.append(itemClaims).append(" item(s) and "); + msg.append(String.format("%,d", totalCurrency)).append(" ").append(ConfigMarket.CurrencyName); + } else if (itemClaims > 0) { + msg.append(itemClaims).append(" item(s)"); + } else { + msg.append(String.format("%,d", totalCurrency)).append(" ").append(ConfigMarket.CurrencyName); } + msg.append(" to claim!"); + + player.addChatMessage(new ChatComponentText(msg.toString())); } - return null; + } + + public void onPlayerLogout(UUID playerUUID) { + // Clear cached permission + clearPlayerMaxTradesCache(playerUUID); } // ========================================= @@ -1026,13 +1100,7 @@ public class AuctionController { } // Take a snapshot of the data for async save - final NBTTagCompound compound; - dataLock.readLock().lock(); - try { - compound = writeToNBT(new NBTTagCompound()); - } finally { - dataLock.readLock().unlock(); - } + final NBTTagCompound compound = writeToNBT(new NBTTagCompound()); CustomNPCsThreader.customNPCThread.execute(() -> { try { @@ -1049,14 +1117,11 @@ public class AuctionController { /** Internal synchronous save */ private void saveInternal() { - dataLock.readLock().lock(); try { NBTTagCompound compound = writeToNBT(new NBTTagCompound()); saveToFile(compound); } catch (Exception e) { LogWriter.error("Error saving auction data", e); - } finally { - dataLock.readLock().unlock(); } } @@ -1085,11 +1150,8 @@ public class AuctionController { public void load() { listings.clear(); - claims.clear(); - pendingNotifications.clear(); playerListingIds.clear(); playerBidIds.clear(); - playerClaimIds.clear(); File saveDir = CustomNpcs.getWorldSaveDirectory(); if (saveDir == null) return; @@ -1124,7 +1186,6 @@ public class AuctionController { private void rebuildIndices() { playerListingIds.clear(); playerBidIds.clear(); - playerClaimIds.clear(); // Index listings for (AuctionListing listing : listings.values()) { @@ -1136,15 +1197,9 @@ public class AuctionController { } } - // Index claims - for (AuctionClaim claim : claims.values()) { - if (!claim.claimed) { - addToPlayerClaims(claim.playerUUID, claim.id); - } - } - + // Claims are now stored in PlayerData, not indexed here LogWriter.info("Rebuilt auction indices: " + playerListingIds.size() + " sellers, " + - playerBidIds.size() + " bidders, " + playerClaimIds.size() + " claimants"); + playerBidIds.size() + " bidders"); } private void loadFromFile(File file) throws Exception { @@ -1153,55 +1208,178 @@ public class AuctionController { } public NBTTagCompound writeToNBT(NBTTagCompound compound) { - // Listings + // Listings only - claims are stored in PlayerData NBTTagList listingsList = new NBTTagList(); for (AuctionListing listing : listings.values()) { listingsList.appendTag(listing.writeToNBT(new NBTTagCompound())); } compound.setTag("Listings", listingsList); - // Claims - NBTTagList claimsList = new NBTTagList(); - for (AuctionClaim claim : claims.values()) { - claimsList.appendTag(claim.writeToNBT(new NBTTagCompound())); - } - compound.setTag("Claims", claimsList); - - // Pending Notifications - NBTTagList notificationsList = new NBTTagList(); - synchronized (pendingNotifications) { - for (AuctionNotification notification : pendingNotifications) { - if (!notification.sent) { - notificationsList.appendTag(notification.writeToNBT(new NBTTagCompound())); - } - } - } - compound.setTag("PendingNotifications", notificationsList); - return compound; } public void readFromNBT(NBTTagCompound compound) { - // Listings NBTTagList listingsList = compound.getTagList("Listings", 10); for (int i = 0; i < listingsList.tagCount(); i++) { AuctionListing listing = AuctionListing.fromNBT(listingsList.getCompoundTagAt(i)); listings.put(listing.id, listing); } + } - // Claims - NBTTagList claimsList = compound.getTagList("Claims", 10); - for (int i = 0; i < claimsList.tagCount(); i++) { - AuctionClaim claim = AuctionClaim.fromNBT(claimsList.getCompoundTagAt(i)); - claims.put(claim.id, claim); - } + // ========================================= + // IAuctionHandler Implementation + // ========================================= - // Pending Notifications - NBTTagList notificationsList = compound.getTagList("PendingNotifications", 10); - for (int i = 0; i < notificationsList.tagCount(); i++) { - AuctionNotification notification = AuctionNotification.fromNBT(notificationsList.getCompoundTagAt(i)); - pendingNotifications.add(notification); + @Override + public boolean isEnabled() { + return ConfigMarket.AuctionEnabled; + } + + @Override + public IAuctionListing[] getActiveListings() { + List active = new ArrayList<>(); + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE && !listing.isExpired()) { + active.add(listing); + } } + return active.toArray(new IAuctionListing[0]); + } + + @Override + public IAuctionListing[] getListingsBySeller(String sellerUUID) { + if (sellerUUID == null) return new IAuctionListing[0]; + try { + UUID uuid = UUID.fromString(sellerUUID); + List result = getPlayerActiveListings(uuid); + return result.toArray(new IAuctionListing[0]); + } catch (IllegalArgumentException e) { + return new IAuctionListing[0]; + } + } + + @Override + public IAuctionListing[] getListingsByBidder(String bidderUUID) { + if (bidderUUID == null) return new IAuctionListing[0]; + try { + UUID uuid = UUID.fromString(bidderUUID); + List result = getPlayerActiveBids(uuid); + return result.toArray(new IAuctionListing[0]); + } catch (IllegalArgumentException e) { + return new IAuctionListing[0]; + } + } + + @Override + public int getActiveListingCount() { + int count = 0; + for (AuctionListing listing : listings.values()) { + if (listing.status == EnumAuctionStatus.ACTIVE && !listing.isExpired()) { + count++; + } + } + return count; + } + + @Override + public IAuctionListing createListing(IPlayer player, IItemStack item, long startingPrice, long buyoutPrice) { + if (player == null || item == null) return null; + EntityPlayer entityPlayer = (EntityPlayer) player.getMCEntity(); + ItemStack mcItem = item.getMCItemStack(); + String result = createListing(entityPlayer, mcItem, startingPrice, buyoutPrice); + if (result != null) { + return null; // Failed + } + // Find the just-created listing + Set playerListings = playerListingIds.get(entityPlayer.getUniqueID()); + if (playerListings != null && !playerListings.isEmpty()) { + // Get the most recent one + long latestTime = 0; + AuctionListing latestListing = null; + for (String id : playerListings) { + AuctionListing listing = listings.get(id); + if (listing != null && listing.createdTime > latestTime) { + latestTime = listing.createdTime; + latestListing = listing; + } + } + return latestListing; + } + return null; + } + + @Override + public String placeBid(String listingId, IPlayer player, long amount) { + if (player == null) return "Player is null"; + EntityPlayer entityPlayer = (EntityPlayer) player.getMCEntity(); + return placeBid(listingId, entityPlayer, amount); + } + + @Override + public String buyout(String listingId, IPlayer player) { + if (player == null) return "Player is null"; + EntityPlayer entityPlayer = (EntityPlayer) player.getMCEntity(); + return buyout(listingId, entityPlayer); + } + + @Override + public String cancelListing(String listingId, IPlayer player, boolean isAdmin) { + if (player == null) return "Player is null"; + EntityPlayer entityPlayer = (EntityPlayer) player.getMCEntity(); + return cancelListing(listingId, entityPlayer, isAdmin); + } + + @Override + public long getListingFee() { + return ConfigMarket.ListingFee; + } + + @Override + public double getSalesTaxPercent() { + return ConfigMarket.SalesTaxPercent; + } + + @Override + public double getMinBidIncrementPercent() { + return ConfigMarket.MinBidIncrementPercent; + } + + @Override + public int getAuctionDurationHours() { + return ConfigMarket.AuctionDurationHours; + } + + @Override + public int getSnipeProtectionMinutes() { + return ConfigMarket.SnipeProtectionMinutes; + } + + @Override + public String getCurrencyName() { + return ConfigMarket.CurrencyName; + } + + @Override + public long getMinimumListingPrice() { + return ConfigMarket.MinimumListingPrice; + } + + @Override + public IAuctionListing[] searchListings(String searchText) { + if (searchText == null || searchText.trim().isEmpty()) { + return getActiveListings(); + } + String search = searchText.toLowerCase().trim(); + List result = new ArrayList<>(); + for (AuctionListing listing : listings.values()) { + if (listing.status != EnumAuctionStatus.ACTIVE || listing.isExpired()) continue; + String itemName = listing.item != null ? listing.item.getDisplayName().toLowerCase() : ""; + String sellerName = listing.sellerName != null ? listing.sellerName.toLowerCase() : ""; + if (itemName.contains(search) || sellerName.contains(search)) { + result.add(listing); + } + } + return result.toArray(new IAuctionListing[0]); } // ========================================= @@ -1216,33 +1394,16 @@ public class AuctionController { cancelListing(listingId, admin, true); } - public void clearClaimedData() { - dataLock.writeLock().lock(); - try { - // Remove claimed claims - claims.entrySet().removeIf(entry -> entry.getValue().claimed); + public void clearEndedListings() { + // Remove ended/cancelled listings (claims are in PlayerData now) + listings.entrySet().removeIf(entry -> + entry.getValue().status == EnumAuctionStatus.CLAIMED || + entry.getValue().status == EnumAuctionStatus.ENDED || + entry.getValue().status == EnumAuctionStatus.CANCELLED); - // Remove ended listings that have no unclaimed claims - listings.entrySet().removeIf(entry -> - entry.getValue().status == EnumAuctionStatus.CLAIMED || - (entry.getValue().status == EnumAuctionStatus.ENDED && - !hasUnclaimedClaims(entry.getValue().id))); - - // Rebuild indices - rebuildIndices(); - markDirty(); - } finally { - dataLock.writeLock().unlock(); - } - } - - private boolean hasUnclaimedClaims(String listingId) { - for (AuctionClaim claim : claims.values()) { - if (listingId.equals(claim.listingId) && !claim.claimed) { - return true; - } - } - return false; + // Rebuild indices + rebuildIndices(); + markDirty(); } /** diff --git a/src/main/java/noppes/npcs/controllers/PlayerDataController.java b/src/main/java/noppes/npcs/controllers/PlayerDataController.java index c8964b31d..235e2a9f8 100644 --- a/src/main/java/noppes/npcs/controllers/PlayerDataController.java +++ b/src/main/java/noppes/npcs/controllers/PlayerDataController.java @@ -364,6 +364,17 @@ public class PlayerDataController { return data; } + public PlayerData getData(UUID uuid) { + EntityPlayer player = getPlayerFromUUID(uuid); + PlayerData data; + if (player == null) { + data = new PlayerData(); + data.setNBT(PlayerDataController.Instance.loadPlayerData(uuid.toString())); + } else + data = PlayerData.get(player); + return data; + } + public void addPlayerMessage(String username, PlayerMail mail) { mail.time = System.currentTimeMillis(); diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java b/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java index 4d489a166..ad4ae3238 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionClaim.java @@ -2,11 +2,15 @@ package noppes.npcs.controllers.data; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.data.IAuctionClaim; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.config.ConfigMarket; import noppes.npcs.constants.EnumClaimType; +import noppes.npcs.scripted.NpcAPI; import java.util.UUID; -public class AuctionClaim { +public class AuctionClaim implements IAuctionClaim { public String id; public UUID playerUUID; public String playerName; @@ -100,6 +104,95 @@ public class AuctionClaim { return playerUUID != null && playerUUID.equals(uuid); } + // ========================================= + // IAuctionClaim Implementation + // ========================================= + + @Override + public String getId() { + return id; + } + + @Override + public String getPlayerUUID() { + return playerUUID != null ? playerUUID.toString() : null; + } + + @Override + public String getPlayerName() { + return playerName; + } + + @Override + public String getListingId() { + return listingId; + } + + @Override + public int getType() { + return type.ordinal(); + } + + @Override + public boolean isItemClaim() { + return type == EnumClaimType.ITEM; + } + + @Override + public boolean isCurrencyClaim() { + return type == EnumClaimType.CURRENCY; + } + + @Override + public boolean isRefundClaim() { + return type == EnumClaimType.REFUND; + } + + @Override + public IItemStack getItem() { + return item != null ? NpcAPI.Instance().getIItemStack(item) : null; + } + + @Override + public String getItemName() { + return itemName; + } + + @Override + public long getCurrency() { + return currency; + } + + @Override + public String getOtherPlayerName() { + return otherPlayerName; + } + + @Override + public long getCreatedTime() { + return createdTime; + } + + @Override + public boolean isClaimed() { + return claimed; + } + + @Override + public boolean isReturnedItem() { + return isReturned; + } + + @Override + public long getDaysUntilExpiration() { + return getDaysUntilExpiration(ConfigMarket.ClaimExpirationDays); + } + + @Override + public boolean isExpired() { + return isExpired(ConfigMarket.ClaimExpirationDays); + } + public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setString("ID", id); compound.setString("PlayerUUID", playerUUID != null ? playerUUID.toString() : ""); diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java index 49ed0a29c..95bf2ed24 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionFilter.java @@ -8,7 +8,7 @@ import java.util.regex.Pattern; /** * Filter for auction listings. - * Supports search by item name and sorting options. + * Supports search by item name or seller name and sorting options. */ public class AuctionFilter { private static final Pattern COLOR_CODE_PATTERN = Pattern.compile("§[0-9a-fk-or]", Pattern.CASE_INSENSITIVE); @@ -37,26 +37,42 @@ public class AuctionFilter { return stripped.toLowerCase().trim(); } - /** Check if item name matches search text */ - public boolean matchesSearch(String itemName) { + /** Check if item name or seller name matches search text */ + public boolean matchesSearch(String itemName, String sellerName) { if (!hasSearchText()) return true; - String normalized = normalizeForSearch(itemName); - return normalized.contains(normalizedSearch); + String normalizedItem = normalizeForSearch(itemName); + String normalizedSeller = normalizeForSearch(sellerName); + // Match if search text is found in either item name or seller name + return normalizedItem.contains(normalizedSearch) || normalizedSeller.contains(normalizedSearch); } - /** Advanced search - all words must be present */ - public boolean matchesSearchAdvanced(String itemName) { + /** Legacy method - only checks item name */ + public boolean matchesSearch(String itemName) { + return matchesSearch(itemName, ""); + } + + /** Advanced search - all words must be present in item name OR seller name */ + public boolean matchesSearchAdvanced(String itemName, String sellerName) { if (!hasSearchText()) return true; - String normalized = normalizeForSearch(itemName); + String normalizedItem = normalizeForSearch(itemName); + String normalizedSeller = normalizeForSearch(sellerName); String[] words = normalizedSearch.split("\\s+"); for (String word : words) { - if (!word.isEmpty() && !normalized.contains(word)) { - return false; + if (!word.isEmpty()) { + // Word must be in item name OR seller name + if (!normalizedItem.contains(word) && !normalizedSeller.contains(word)) { + return false; + } } } return true; } + /** Legacy method - only checks item name */ + public boolean matchesSearchAdvanced(String itemName) { + return matchesSearchAdvanced(itemName, ""); + } + public void reset() { searchText = ""; sortBy = EnumAuctionSort.NEWEST; diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java index 3106a9fb2..6d57942c1 100644 --- a/src/main/java/noppes/npcs/controllers/data/AuctionListing.java +++ b/src/main/java/noppes/npcs/controllers/data/AuctionListing.java @@ -2,11 +2,15 @@ package noppes.npcs.controllers.data; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.data.IAuctionListing; +import noppes.npcs.api.item.IItemStack; +import noppes.npcs.config.ConfigMarket; import noppes.npcs.constants.EnumAuctionStatus; +import noppes.npcs.scripted.NpcAPI; import java.util.UUID; -public class AuctionListing { +public class AuctionListing implements IAuctionListing { public String id; public UUID sellerUUID; public String sellerName; @@ -40,22 +44,6 @@ public class AuctionListing { this.endTime = this.createdTime + durationMs; } - public boolean hasBids() { - return bidCount > 0 && highBidderUUID != null; - } - - public boolean hasBuyout() { - return buyoutPrice > 0; - } - - public boolean isExpired() { - return System.currentTimeMillis() >= endTime; - } - - public boolean isActive() { - return status == EnumAuctionStatus.ACTIVE && !isExpired(); - } - public long getTimeRemaining() { return Math.max(0, endTime - System.currentTimeMillis()); } @@ -88,6 +76,136 @@ public class AuctionListing { } } + // ========================================= + // IAuctionListing Implementation + // ========================================= + + @Override + public String getId() { + return id; + } + + @Override + public String getSellerUUID() { + return sellerUUID != null ? sellerUUID.toString() : null; + } + + @Override + public String getSellerName() { + return sellerName; + } + + @Override + public IItemStack getItem() { + return item != null ? NpcAPI.Instance().getIItemStack(item) : null; + } + + @Override + public long getStartingPrice() { + return startingPrice; + } + + @Override + public long getBuyoutPrice() { + return buyoutPrice; + } + + @Override + public long getCurrentBid() { + return currentBid; + } + + @Override + public String getHighBidderUUID() { + return highBidderUUID != null ? highBidderUUID.toString() : null; + } + + @Override + public String getHighBidderName() { + return highBidderName; + } + + @Override + public int getBidCount() { + return bidCount; + } + + @Override + public long getCreatedTime() { + return createdTime; + } + + @Override + public long getEndTime() { + return endTime; + } + + @Override + public long getRemainingTime() { + return getTimeRemaining(); + } + + @Override + public int getStatus() { + return status.ordinal(); + } + + @Override + public long getMinimumBid() { + return getMinimumBid(ConfigMarket.MinBidIncrementPercent); + } + + @Override + public boolean isSeller(String playerUUID) { + if (playerUUID == null || sellerUUID == null) return false; + return sellerUUID.toString().equals(playerUUID); + } + + @Override + public boolean isHighBidder(String playerUUID) { + if (playerUUID == null || highBidderUUID == null) return false; + return highBidderUUID.toString().equals(playerUUID); + } + + @Override + public String getRemainingTimeFormatted() { + long remaining = getTimeRemaining(); + if (remaining <= 0) return "Ended"; + + long hours = remaining / (60 * 60 * 1000); + long minutes = (remaining % (60 * 60 * 1000)) / (60 * 1000); + + if (hours > 0) { + return hours + "h " + minutes + "m"; + } else if (minutes > 0) { + return minutes + "m"; + } else { + long seconds = (remaining % (60 * 1000)) / 1000; + return seconds + "s"; + } + } + + // Override existing methods with @Override annotation + @Override + public boolean hasBids() { + return bidCount > 0 && highBidderUUID != null; + } + + @Override + public boolean hasBuyout() { + return buyoutPrice > 0; + } + + @Override + public boolean isExpired() { + return System.currentTimeMillis() >= endTime; + } + + @Override + public boolean isActive() { + return status == EnumAuctionStatus.ACTIVE && !isExpired(); + } + public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setString("ID", id); compound.setString("SellerUUID", sellerUUID != null ? sellerUUID.toString() : ""); diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java b/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java deleted file mode 100644 index b2b8c4b57..000000000 --- a/src/main/java/noppes/npcs/controllers/data/PlayerCurrencyData.java +++ /dev/null @@ -1,232 +0,0 @@ -package noppes.npcs.controllers.data; - -import kamkeel.npcs.util.VaultUtil; -import net.minecraft.nbt.NBTTagCompound; -import noppes.npcs.api.handler.IPlayerCurrencyData; -import noppes.npcs.config.ConfigMarket; - -/** - * Stores player currency balance. - * This data is NOT profile slot bound - all profile slots share the same currency pool. - * When Vault API is available and UseVault config is enabled, operations delegate to Vault. - * The built-in CNPC+ currency data is always preserved in NBT, but unused when Vault is active. - */ -public class PlayerCurrencyData implements IPlayerCurrencyData { - private final PlayerData playerData; - - // Built-in CNPC+ currency (always saved, used as fallback) - private long balance; - private long lifetimeEarned; - private long lifetimeSpent; - - public PlayerCurrencyData(PlayerData playerData) { - this.playerData = playerData; - this.balance = ConfigMarket.StartingBalance; - this.lifetimeEarned = 0; - this.lifetimeSpent = 0; - } - - /** - * Check if Vault should be used for currency operations. - * Requires both config enabled AND Vault actually available. - */ - private boolean shouldUseVault() { - return ConfigMarket.UseVault && VaultUtil.isEnabled(); - } - - /** - * Get player name for Vault operations - */ - private String getPlayerName() { - if (playerData != null && playerData.player != null) { - return playerData.player.getCommandSenderName(); - } - return playerData != null ? playerData.playername : null; - } - - public void readFromNBT(NBTTagCompound compound) { - if (compound.hasKey("CurrencyBalance")) { - this.balance = compound.getLong("CurrencyBalance"); - } else { - this.balance = ConfigMarket.StartingBalance; - } - this.lifetimeEarned = compound.getLong("CurrencyLifetimeEarned"); - this.lifetimeSpent = compound.getLong("CurrencyLifetimeSpent"); - } - - public void writeToNBT(NBTTagCompound compound) { - // Always save built-in currency data (preserved even when using Vault) - compound.setLong("CurrencyBalance", this.balance); - compound.setLong("CurrencyLifetimeEarned", this.lifetimeEarned); - compound.setLong("CurrencyLifetimeSpent", this.lifetimeSpent); - } - - /** - * Get the current balance - */ - @Override - public long getBalance() { - if (shouldUseVault()) { - String playerName = getPlayerName(); - if (playerName != null) { - return (long) VaultUtil.getBalance(playerName); - } - } - return balance; - } - - /** - * Set the balance directly (use with caution) - */ - @Override - public void setBalance(long balance) { - if (balance < 0) { - balance = 0; - } - if (balance > ConfigMarket.MaxBalance) { - balance = ConfigMarket.MaxBalance; - } - - if (shouldUseVault()) { - String playerName = getPlayerName(); - if (playerName != null) { - VaultUtil.setBalance(playerName, balance); - return; - } - } - this.balance = balance; - } - - /** - * Add currency to balance (deposit) - * - * @param amount Amount to add - * @return true if successful, false if would exceed max balance - */ - @Override - public boolean deposit(long amount) { - if (amount <= 0) { - return false; - } - - if (shouldUseVault()) { - String playerName = getPlayerName(); - if (playerName != null) { - boolean success = VaultUtil.addMoney(playerName, amount); - if (success) { - this.lifetimeEarned += amount; - } - return success; - } - } - - // Built-in currency - long newBalance = this.balance + amount; - if (newBalance > ConfigMarket.MaxBalance) { - return false; - } - if (newBalance < this.balance) { - // Overflow protection - return false; - } - this.balance = newBalance; - this.lifetimeEarned += amount; - return true; - } - - /** - * Remove currency from balance (withdraw) - * - * @param amount Amount to remove - * @return true if successful, false if insufficient funds - */ - @Override - public boolean withdraw(long amount) { - if (amount <= 0) { - return false; - } - - if (shouldUseVault()) { - String playerName = getPlayerName(); - if (playerName != null) { - boolean success = VaultUtil.withdrawMoney(playerName, amount); - if (success) { - this.lifetimeSpent += amount; - } - return success; - } - } - - // Built-in currency - if (this.balance < amount) { - return false; - } - this.balance -= amount; - this.lifetimeSpent += amount; - return true; - } - - /** - * Check if player can afford an amount - */ - @Override - public boolean canAfford(long amount) { - if (shouldUseVault()) { - String playerName = getPlayerName(); - if (playerName != null) { - return VaultUtil.has(playerName, amount); - } - } - return this.balance >= amount; - } - - /** - * Get lifetime earned currency - */ - @Override - public long getLifetimeEarned() { - return lifetimeEarned; - } - - /** - * Get lifetime spent currency - */ - @Override - public long getLifetimeSpent() { - return lifetimeSpent; - } - - /** - * Format the balance for display - */ - @Override - public String formatBalance() { - if (shouldUseVault()) { - return VaultUtil.format(getBalance()); - } - return formatAmount(this.balance); - } - - /** - * Format an amount for display - */ - public static String formatAmount(long amount) { - return String.format("%,d %s", amount, ConfigMarket.CurrencyName); - } - - /** - * Check if Vault is being used for currency operations. - * Returns true only if config is enabled AND Vault is actually available. - */ - @Override - public boolean isUsingVault() { - return shouldUseVault(); - } - - /** - * Check if Vault is configured (regardless of availability) - */ - public boolean isVaultConfigured() { - return ConfigMarket.UseVault; - } -} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index d2a949074..04e06a41e 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -17,7 +17,7 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.handler.IPlayerBankData; -import noppes.npcs.api.handler.IPlayerCurrencyData; +import noppes.npcs.api.handler.IPlayerTradeData; import noppes.npcs.api.handler.IPlayerData; import noppes.npcs.api.handler.IPlayerDialogData; import noppes.npcs.api.handler.IPlayerFactionData; @@ -46,6 +46,8 @@ import java.io.FileOutputStream; import java.util.HashSet; import java.util.UUID; +import static noppes.npcs.util.CustomNPCsThreader.customNPCThread; + public class PlayerData implements IExtendedEntityProperties, IPlayerData { public PlayerDialogData dialogData = new PlayerDialogData(this); public PlayerBankData bankData = new PlayerBankData(this); @@ -60,8 +62,8 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { public DataSkinOverlays skinOverlays = new DataSkinOverlays(this); public MagicData magicData = new MagicData(); - // Currency data - NOT slot-bound, shared across all profile slots - public PlayerCurrencyData currencyData = new PlayerCurrencyData(this); + // Trade data (currency + auction claims) - shared across all profile slots + public PlayerTradeData tradeData = new PlayerTradeData(this); public ActionManager actionManager = new ActionManager(); public PlayerDataScript scriptData; @@ -134,6 +136,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { animationData.readFromNBT(data); effectData.readFromNBT(data); magicData.readToNBT(data); + tradeData.readFromNBT(data); if (player != null) { playername = player.getCommandSenderName(); @@ -176,6 +179,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { animationData.writeToNBT(compound); effectData.writeToNBT(compound); magicData.writeToNBT(compound); + tradeData.writeToNBT(compound); compound.setString("PlayerName", playername); compound.setString("UUID", uuid); @@ -228,6 +232,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { transportData.saveNBTData(compound); factionData.saveNBTData(compound); mailData.saveNBTData(compound); + tradeData.writeToNBT(compound); compound.setString("PlayerName", playername); compound.setString("UUID", uuid); DBCAddon.instance.writeToNBT(this, compound); @@ -241,6 +246,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { transportData.loadNBTData(data); factionData.loadNBTData(data); mailData.loadNBTData(data); + tradeData.readFromNBT(data); if (player != null) { playername = player.getCommandSenderName(); uuid = player.getPersistentID().toString(); @@ -401,8 +407,8 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { return mailData; } - public IPlayerCurrencyData getCurrencyData() { - return currencyData; + public IPlayerTradeData getTradeData() { + return tradeData; } public synchronized void save() { @@ -414,9 +420,6 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { } final NBTTagCompound compound = getNBT(); - // Currency data is saved at player level, NOT per profile slot - currencyData.writeToNBT(compound); - final String filename; if (ConfigMain.DatFormat) { filename = uuid + ".dat"; @@ -453,10 +456,6 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { if (data.hasNoTags()) { data = getNBT(); } - - // Load currency BEFORE setNBT - currency is player-level, NOT per profile slot - currencyData.readFromNBT(data); - setNBT(data); } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java b/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java new file mode 100644 index 000000000..a0a8e0bb7 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/PlayerTradeData.java @@ -0,0 +1,414 @@ +package noppes.npcs.controllers.data; + +import kamkeel.npcs.util.VaultUtil; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import noppes.npcs.api.handler.IPlayerTradeData; +import noppes.npcs.api.handler.data.IAuctionClaim; +import noppes.npcs.config.ConfigMarket; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Stores player's trade-related data: currency and auction claims. + * This data is shared across all profile slots. + * Uses CopyOnWriteArrayList for thread-safety when adding claims asynchronously. + */ +public class PlayerTradeData implements IPlayerTradeData { + // NBT key used by trade data - excluded from per-slot profile saving + public static final String NBT_KEY = "TradeData"; + + private final PlayerData playerData; + + // ========================================= + // Currency Data + // ========================================= + + // Built-in CNPC+ currency (always saved, used as fallback) + private long balance; + private long lifetimeEarned; + private long lifetimeSpent; + + // ========================================= + // Auction Claims Data + // ========================================= + + // Thread-safe list for claims - can be modified from CNPC+ thread + private final CopyOnWriteArrayList claims = new CopyOnWriteArrayList<>(); + + public PlayerTradeData(PlayerData playerData) { + this.playerData = playerData; + this.balance = ConfigMarket.StartingBalance; + this.lifetimeEarned = 0; + this.lifetimeSpent = 0; + } + + // ========================================= + // Currency Methods + // ========================================= + + /** + * Check if Vault should be used for currency operations. + * Requires both config enabled AND Vault actually available. + */ + private boolean shouldUseVault() { + return ConfigMarket.UseVault && VaultUtil.isEnabled(); + } + + /** + * Get player name for Vault operations + */ + private String getPlayerName() { + if (playerData != null && playerData.player != null) { + return playerData.player.getCommandSenderName(); + } + return playerData != null ? playerData.playername : null; + } + + /** + * Get the current balance + */ + @Override + public long getBalance() { + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + return (long) VaultUtil.getBalance(playerName); + } + } + return balance; + } + + /** + * Set the balance directly (use with caution) + */ + @Override + public void setBalance(long balance) { + if (balance < 0) { + balance = 0; + } + if (balance > ConfigMarket.MaxBalance) { + balance = ConfigMarket.MaxBalance; + } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + VaultUtil.setBalance(playerName, balance); + return; + } + } + this.balance = balance; + } + + /** + * Add currency to balance (deposit) + * + * @param amount Amount to add + * @return true if successful, false if would exceed max balance + */ + @Override + public boolean deposit(long amount) { + if (amount <= 0) { + return false; + } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + boolean success = VaultUtil.addMoney(playerName, amount); + if (success) { + this.lifetimeEarned += amount; + } + return success; + } + } + + // Built-in currency + long newBalance = this.balance + amount; + if (newBalance > ConfigMarket.MaxBalance) { + return false; + } + if (newBalance < this.balance) { + // Overflow protection + return false; + } + this.balance = newBalance; + this.lifetimeEarned += amount; + return true; + } + + /** + * Remove currency from balance (withdraw) + * + * @param amount Amount to remove + * @return true if successful, false if insufficient funds + */ + @Override + public boolean withdraw(long amount) { + if (amount <= 0) { + return false; + } + + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + boolean success = VaultUtil.withdrawMoney(playerName, amount); + if (success) { + this.lifetimeSpent += amount; + } + return success; + } + } + + // Built-in currency + if (this.balance < amount) { + return false; + } + this.balance -= amount; + this.lifetimeSpent += amount; + return true; + } + + /** + * Check if player can afford an amount + */ + @Override + public boolean canAfford(long amount) { + if (shouldUseVault()) { + String playerName = getPlayerName(); + if (playerName != null) { + return VaultUtil.has(playerName, amount); + } + } + return this.balance >= amount; + } + + /** + * Get lifetime earned currency + */ + @Override + public long getLifetimeEarned() { + return lifetimeEarned; + } + + /** + * Get lifetime spent currency + */ + @Override + public long getLifetimeSpent() { + return lifetimeSpent; + } + + /** + * Format the balance for display + */ + @Override + public String formatBalance() { + if (shouldUseVault()) { + return VaultUtil.format(getBalance()); + } + return formatAmount(this.balance); + } + + /** + * Format an amount for display + */ + public static String formatAmount(long amount) { + return String.format("%,d %s", amount, ConfigMarket.CurrencyName); + } + + /** + * Check if Vault is being used for currency operations. + * Returns true only if config is enabled AND Vault is actually available. + */ + @Override + public boolean isUsingVault() { + return shouldUseVault(); + } + + /** + * Check if Vault is configured (regardless of availability) + */ + public boolean isVaultConfigured() { + return ConfigMarket.UseVault; + } + + // ========================================= + // Auction Claims Methods + // ========================================= + + /** + * Add a claim to this player's pending claims. + * Thread-safe - can be called from any thread. + */ + public void addClaim(AuctionClaim claim) { + if (claim != null && !claim.claimed) { + claims.add(claim); + } + } + + /** + * Get all unclaimed claims for this player (internal use). + * Returns a copy of the list for safe iteration. + */ + public List getClaimsList() { + List result = new ArrayList<>(); + for (AuctionClaim claim : claims) { + if (!claim.claimed) { + result.add(claim); + } + } + return result; + } + + /** + * Get all unclaimed claims for this player (API). + */ + @Override + public IAuctionClaim[] getClaims() { + List result = getClaimsList(); + return result.toArray(new IAuctionClaim[0]); + } + + /** + * Get a specific claim by ID (internal use). + */ + public AuctionClaim getClaimInternal(String claimId) { + for (AuctionClaim claim : claims) { + if (claim.id.equals(claimId) && !claim.claimed) { + return claim; + } + } + return null; + } + + /** + * Get a specific claim by ID (API). + */ + @Override + public IAuctionClaim getClaim(String claimId) { + return getClaimInternal(claimId); + } + + /** + * Mark a claim as claimed and remove it. + */ + public boolean claimAndRemove(String claimId) { + for (AuctionClaim claim : claims) { + if (claim.id.equals(claimId) && !claim.claimed) { + claim.claimed = true; + claims.remove(claim); + return true; + } + } + return false; + } + + /** + * Get count of unclaimed claims. + */ + @Override + public int getClaimCount() { + int count = 0; + for (AuctionClaim claim : claims) { + if (!claim.claimed) { + count++; + } + } + return count; + } + + /** + * Check if player has any unclaimed claims. + */ + @Override + public boolean hasClaims() { + for (AuctionClaim claim : claims) { + if (!claim.claimed) { + return true; + } + } + return false; + } + + /** + * Process expired claims - removes claims older than expiration days. + * Returns number of claims removed. + */ + public int processExpiredClaims() { + int expirationDays = ConfigMarket.ClaimExpirationDays; + int removed = 0; + + Iterator iterator = claims.iterator(); + while (iterator.hasNext()) { + AuctionClaim claim = iterator.next(); + if (!claim.claimed && claim.isExpired(expirationDays)) { + claims.remove(claim); + removed++; + } + } + + return removed; + } + + // ========================================= + // NBT Serialization + // ========================================= + + public void readFromNBT(NBTTagCompound compound) { + NBTTagCompound tradeData = compound.getCompoundTag(NBT_KEY); + if (tradeData == null || tradeData.hasNoTags()) { + // No trade data found, use defaults + this.balance = ConfigMarket.StartingBalance; + this.lifetimeEarned = 0; + this.lifetimeSpent = 0; + claims.clear(); + return; + } + + // Currency + if (tradeData.hasKey("CurrencyBalance")) { + this.balance = tradeData.getLong("CurrencyBalance"); + } else { + this.balance = ConfigMarket.StartingBalance; + } + this.lifetimeEarned = tradeData.getLong("CurrencyLifetimeEarned"); + this.lifetimeSpent = tradeData.getLong("CurrencyLifetimeSpent"); + + // Claims + claims.clear(); + if (tradeData.hasKey("AuctionClaims")) { + NBTTagList claimsList = tradeData.getTagList("AuctionClaims", 10); + for (int i = 0; i < claimsList.tagCount(); i++) { + AuctionClaim claim = AuctionClaim.fromNBT(claimsList.getCompoundTagAt(i)); + if (!claim.claimed) { + claims.add(claim); + } + } + } + } + + public void writeToNBT(NBTTagCompound compound) { + NBTTagCompound tradeData = new NBTTagCompound(); + + // Currency - always save built-in currency data (preserved even when using Vault) + tradeData.setLong("CurrencyBalance", this.balance); + tradeData.setLong("CurrencyLifetimeEarned", this.lifetimeEarned); + tradeData.setLong("CurrencyLifetimeSpent", this.lifetimeSpent); + + // Claims + NBTTagList claimsList = new NBTTagList(); + for (AuctionClaim claim : claims) { + if (!claim.claimed) { + claimsList.appendTag(claim.writeToNBT(new NBTTagCompound())); + } + } + tradeData.setTag("AuctionClaims", claimsList); + + compound.setTag(NBT_KEY, tradeData); + } +} diff --git a/src/main/java/noppes/npcs/roles/RoleTrader.java b/src/main/java/noppes/npcs/roles/RoleTrader.java index 91a116efc..6aa69d357 100644 --- a/src/main/java/noppes/npcs/roles/RoleTrader.java +++ b/src/main/java/noppes/npcs/roles/RoleTrader.java @@ -426,7 +426,7 @@ public class RoleTrader extends RoleInterface { // Player balance PlayerData data = PlayerData.get(player); - compound.setLong("Balance", data.currencyData.getBalance()); + compound.setLong("Balance", data.tradeData.getBalance()); // Stock info compound.setBoolean("StockEnabled", stock.enableStock); diff --git a/src/main/java/noppes/npcs/scripted/NpcAPI.java b/src/main/java/noppes/npcs/scripted/NpcAPI.java index 1bdd20cfe..c80c18d28 100644 --- a/src/main/java/noppes/npcs/scripted/NpcAPI.java +++ b/src/main/java/noppes/npcs/scripted/NpcAPI.java @@ -65,6 +65,7 @@ import noppes.npcs.api.gui.ICustomGui; import noppes.npcs.api.handler.IAbilityHandler; import noppes.npcs.api.handler.IActionManager; import noppes.npcs.api.handler.IAnimationHandler; +import noppes.npcs.api.handler.IAuctionHandler; import noppes.npcs.api.handler.IAttributeHandler; import noppes.npcs.api.handler.ICloneHandler; import noppes.npcs.api.handler.ICustomEffectHandler; @@ -90,6 +91,7 @@ import noppes.npcs.config.ConfigScript; import noppes.npcs.constants.EnumAnimationPart; import noppes.npcs.containers.ContainerNpcInterface; import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.AuctionController; import noppes.npcs.controllers.ChunkController; import noppes.npcs.controllers.CustomEffectController; import noppes.npcs.controllers.DialogController; @@ -385,6 +387,15 @@ public class NpcAPI extends AbstractNpcAPI { return TelegraphController.Instance.create(type); } + @Override + public IAuctionHandler getAuctions() { + this.checkWorld(); + if (!noppes.npcs.config.ConfigMarket.AuctionEnabled) { + return null; + } + return AuctionController.getInstance(); + } + @Override public String[] getAllBiomeNames() { List biomes = new ArrayList<>(); diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java index f2cfc9a34..a1b91e1db 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptPlayer.java @@ -901,36 +901,36 @@ public class ScriptPlayer extends ScriptLivingBase @Override public long getCurrencyBalance() { - return getData().currencyData.getBalance(); + return getData().tradeData.getBalance(); } @Override public void setCurrencyBalance(long amount) { - getData().currencyData.setBalance(amount); + getData().tradeData.setBalance(amount); } @Override public boolean depositCurrency(long amount) { - return getData().currencyData.deposit(amount); + return getData().tradeData.deposit(amount); } @Override public boolean withdrawCurrency(long amount) { - return getData().currencyData.withdraw(amount); + return getData().tradeData.withdraw(amount); } @Override public boolean canAffordCurrency(long amount) { - return getData().currencyData.canAfford(amount); + return getData().tradeData.canAfford(amount); } @Override public boolean isUsingVaultCurrency() { - return getData().currencyData.isUsingVault(); + return getData().tradeData.isUsingVault(); } @Override public String getFormattedCurrencyBalance() { - return getData().currencyData.formatBalance(); + return getData().tradeData.formatBalance(); } } From b85804af98699dddb07e43ee958aee602b662414 Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 24 Jan 2026 15:33:10 -0500 Subject: [PATCH 278/358] Disable Edit button for Auctioneer --- .../java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java index 26942e57f..ff46f9174 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNpcAdvanced.java @@ -50,7 +50,7 @@ public class GuiNpcAdvanced extends GuiNPCInterface2 implements IGuiData { int y = guiTop + 10; this.addButton(new GuiNpcButton(3, guiLeft + 85 + 160, y, 52, 20, "selectServer.edit")); this.addButton(new GuiNpcButton(8, guiLeft + 85, y, 155, 20, new String[]{"role.none", "role.trader", "role.follower", "role.bank", "role.transporter", "role.mailman", NoppesStringUtils.translate("role.companion", "(WIP)"), "role.mount", "role.auctioneer"}, npc.advanced.role.ordinal())); - getButton(3).setEnabled(npc.advanced.role != EnumRoleType.None && npc.advanced.role != EnumRoleType.Postman); + getButton(3).setEnabled(npc.advanced.role != EnumRoleType.None && npc.advanced.role != EnumRoleType.Postman && npc.advanced.role != EnumRoleType.Auctioneer); this.addButton(new GuiNpcButton(4, guiLeft + 85 + 160, y += 22, 52, 20, "selectServer.edit")); this.addButton(new GuiNpcButton(5, guiLeft + 85, y, 155, 20, new String[]{"job.none", "job.bard", "job.healer", "job.guard", "job.itemgiver", "role.follower", "job.spawner", "job.conversation", "job.chunkloader"}, npc.advanced.job.ordinal())); @@ -84,7 +84,7 @@ public class GuiNpcAdvanced extends GuiNPCInterface2 implements IGuiData { hasChanges = true; npc.advanced.setRole(button.getValue()); - getButton(3).setEnabled(npc.advanced.role != EnumRoleType.None && npc.advanced.role != EnumRoleType.Postman); + getButton(3).setEnabled(npc.advanced.role != EnumRoleType.None && npc.advanced.role != EnumRoleType.Postman && npc.advanced.role != EnumRoleType.Auctioneer); } if (button.id == 4) { save(); From fe998ea31cd5405a24a17eac5485fd70100d473f Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 24 Jan 2026 16:03:54 -0500 Subject: [PATCH 279/358] Auction Blacklist System --- 1.11_Market_Update.md | 120 +++++++-- Auction_Item_Blacklist_Plan.md | 218 ---------------- src/main/java/foxz/command/CommandNoppes.java | 16 ++ .../kamkeel/npcs/command/AuctionCommand.java | 18 +- .../npcs/command/CommandKamkeelBase.java | 83 +++++- .../command/auction/BlacklistSubCommand.java | 226 +++++++++++++++++ .../java/noppes/npcs/config/ConfigMarket.java | 55 ++++ .../npcs/controllers/AuctionController.java | 21 ++ .../controllers/data/AuctionBlacklist.java | 237 ++++++++++++++++++ .../assets/customnpcs/lang/en_US.lang | 2 + 10 files changed, 756 insertions(+), 240 deletions(-) delete mode 100644 Auction_Item_Blacklist_Plan.md create mode 100644 src/main/java/kamkeel/npcs/command/auction/BlacklistSubCommand.java create mode 100644 src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java diff --git a/1.11_Market_Update.md b/1.11_Market_Update.md index ba7786d0a..4236e0e63 100644 --- a/1.11_Market_Update.md +++ b/1.11_Market_Update.md @@ -313,6 +313,8 @@ All actions are validated server-side: | Auction expiry check | `placeBid()/buyout()` | Double-check inside write lock | | Currency validation | All actions | Verify balance before operations | | Claim ownership | `claimItem()/claimCurrency()` | `claim.isForPlayer(playerUUID)` check | +| Blacklist check | `createListing()` | `AuctionBlacklist.isBlacklistedForPlayer()` check | +| Bound item check | `createListing()` | Auto-blocks Soulbound/Profile-Slotbound items | ### 3.7 Snipe Protection @@ -325,6 +327,53 @@ Example: Auction ends in 30 seconds, someone bids → Timer resets to 2:00 --- +### 3.8 Item Blacklist System + +Prevents specific items from being listed on the auction house. + +#### Blacklist Types + +| Type | Description | Example | +|------|-------------|---------| +| **Items** | Block specific items by registry name | `minecraft:diamond_sword` | +| **Wildcards** | Block items matching pattern | `minecraft:diamond*` | +| **Mods** | Block all items from a mod | `customnpcs` | +| **NBT Tags** | Block items with specific NBT | `ench` (enchanted items) | + +#### Auto-Blocked Items (Always Blocked) + +These items are **always** blocked regardless of blacklist settings: +- **Soulbound Items** - Items with `cnpc_soulbind` requirement +- **Profile-Slotbound Items** - Items with `cnpc_profile_slot` requirement + +The system checks for these under `RPGCore/Requirements` in the item's NBT. + +#### Bypass Permission + +Players with OP status or the `customnpcs.auction.blacklist.bypass` permission can list blacklisted items. + +#### Configuration + +``` +Market.Auction.Blacklist: + Enabled: true + BlacklistedItems: ["minecraft:barrier", "minecraft:command_block*"] + BlacklistedMods: [] + BlacklistedNBTTags: [] +``` + +#### Commands + +| Command | Description | +|---------|-------------| +| `/cnpc auction blacklist add ` | Add to blacklist (runtime only) | +| `/cnpc auction blacklist remove ` | Remove from blacklist (runtime only) | +| `/cnpc auction blacklist list [item\|mod\|nbt]` | List blacklisted entries | +| `/cnpc auction blacklist reload` | Reload blacklist from config | +| `/cnpc auction blacklist check` | Check if held item is blacklisted | + +--- + ## 4. AuctionController Performance & Thread Safety ### 4.1 Threading Model @@ -418,6 +467,7 @@ controllers/data/ AuctionListing.java ✅ AuctionClaim.java ✅ AuctionFilter.java ✅ + AuctionBlacklist.java ✅ (item blacklist checking + bypass) PlayerTradeData.java ✅ (currency + claims storage per player) roles/ @@ -457,7 +507,9 @@ network/packets/player/ ``` command/ - AuctionCommand.java ✅ /auction command with list, view, open subcommands + AuctionCommand.java ✅ /cnpc auction with list, view, open, blacklist + command/auction/ + BlacklistSubCommand.java ✅ /cnpc auction blacklist subcommands ``` ### Files Modified @@ -518,43 +570,77 @@ public static boolean LogAuctionSold = true; public static boolean LogAuctionExpired = true; public static boolean LogAuctionCancelled = true; public static boolean LogAuctionClaimed = true; + +// ========================================= +// Auction Blacklist (Market.Auction.Blacklist) +// ========================================= +public static boolean BlacklistEnabled = true; +public static String[] BlacklistedItems = {}; // Registry names (supports wildcards) +public static String[] BlacklistedMods = {}; // Mod IDs to block entirely +public static String[] BlacklistedNBTTags = {}; // NBT tag keys to block ``` -### Trade Slot Permissions - -Players can have more than the default trade slots via permissions: +### Auction Permissions | Permission | Description | |------------|-------------| | `customnpcs.auction.trades.*` | Unlimited trade slots (up to 45) | | `customnpcs.auction.trades.X` | Exactly X trade slots (where X is 1-45) | +| `customnpcs.auction.blacklist.bypass` | Bypass item blacklist restrictions | -**How it works:** +**Trade Slot Permissions:** - Default is 8 trade slots - Permissions only **increase** the limit (permissions below 8 are ignored) - Maximum is capped at 45 (9x5 grid) - Trade slots = Active Listings + Active Bids + Pending Claims +**Blacklist Bypass:** +- OP players or those with `customnpcs.auction.blacklist.bypass` can list blacklisted items +- Soulbound and Profile-Slotbound items are **always** blocked (no bypass) + --- ## 7. Commands -### `/auction` Command +### `/cnpc auction` Command Allows interaction with the Auction House without needing an Auctioneer NPC. | Subcommand | Usage | Permission | Description | |------------|-------|------------|-------------| -| `list` | `/auction list [page]` | Default | View active auctions in chat | -| `view` | `/auction view ` | Default | View a player's auction activity | -| `open` | `/auction open` | Player only | Opens the Auction House GUI | +| `list` | `/cnpc auction list [page]` | OP | View active auctions in chat | +| `view` | `/cnpc auction view ` | OP | View a player's auction activity | +| `open` | `/cnpc auction open` | Any | Opens the Auction House GUI | **Examples:** ``` -/auction list - Show first page of active auctions -/auction list 2 - Show page 2 of auctions -/auction view Steve - Show Steve's listings, bids, and claims -/auction open - Open the full Auction House GUI +/cnpc auction list - Show first page of active auctions +/cnpc auction list 2 - Show page 2 of auctions +/cnpc auction view Steve - Show Steve's listings, bids, and claims +/cnpc auction open - Open the full Auction House GUI +``` + +### `/cnpc auction blacklist` Command + +Manage the auction item blacklist (runtime changes, not persisted to config). + +| Subcommand | Usage | Permission | Description | +|------------|-------|------------|-------------| +| `add` | `/cnpc auction blacklist add ` | OP | Add to blacklist | +| `remove` | `/cnpc auction blacklist remove ` | OP | Remove from blacklist | +| `list` | `/cnpc auction blacklist list [item\|mod\|nbt]` | OP | List blacklisted entries | +| `reload` | `/cnpc auction blacklist reload` | OP | Reload from config | +| `check` | `/cnpc auction blacklist check` | Any | Check if held item is blacklisted | + +**Examples:** +``` +/cnpc auction blacklist add item minecraft:diamond_sword +/cnpc auction blacklist add item minecraft:diamond* (wildcard) +/cnpc auction blacklist add mod industrialcraft2 +/cnpc auction blacklist add nbt ench +/cnpc auction blacklist remove item minecraft:diamond_sword +/cnpc auction blacklist list item +/cnpc auction blacklist check (checks held item) ``` --- @@ -607,6 +693,11 @@ Allows interaction with the Auction House without needing an Auctioneer NPC. - [x] **30-second intervals** - Combined process/save cycle for simplicity - [x] **Async offline player claims** - Claims added to offline players via CNPC+ thread - [x] **Claim migration** - Old claims in auction.dat automatically migrated to PlayerData +- [x] **Item Blacklist System** - Block items by registry name, mod, NBT tag, or wildcard pattern + - Auto-blocks Soulbound and Profile-Slotbound items (checks `RPGCore/Requirements`) + - Per-player bypass via `customnpcs.auction.blacklist.bypass` permission + - Runtime add/remove commands (not persisted to config) + - Check command to test held item --- @@ -622,13 +713,14 @@ Allows interaction with the Auction House without needing an Auctioneer NPC. | Async Saves | CNPC+ Thread | Doesn't block server tick | | Offline Claims | Load PlayerData async, add claim, save | Claims can be added to offline players without blocking | | Config Sync | LoginPacket → AuctionClientConfig | Client never accesses server config directly | +| Item Blacklist | Static utility class | Simple, fast checks without instantiation; auto-blocks bound items | +| Bound Item Detection | Check `RPGCore/Requirements` NBT path | Uses AttributeItemUtil constants for consistency | --- ## 10. Future Considerations (Not in 1.11) - Category-based filtering (by item type, mod, etc.) -- Advanced blacklist system (items, mods, NBT patterns) - Auction history/statistics tracking - Auction house tax sink (currency removed from economy) - Reserve prices (hidden minimum price) diff --git a/Auction_Item_Blacklist_Plan.md b/Auction_Item_Blacklist_Plan.md deleted file mode 100644 index d9dfd9075..000000000 --- a/Auction_Item_Blacklist_Plan.md +++ /dev/null @@ -1,218 +0,0 @@ -# Auction Item Blacklist Feature Plan - -## Overview - -Add the ability to prevent certain items from being listed on the Auction House via a configurable blacklist system. - ---- - -## 1. Blacklist Types - -### 1.1 Item ID Blacklist -- Block specific items by registry name (e.g., `minecraft:diamond_sword`, `customnpcs:npcSoulstoneFilled`) -- Supports wildcard patterns (e.g., `minecraft:*_sword` for all swords) - -### 1.2 Mod Blacklist -- Block all items from a specific mod (e.g., `appliedenergistics2`, `thaumcraft`) -- Useful for blocking items from tech/magic mods that could disrupt economy - -### 1.3 NBT Pattern Blacklist (Advanced) -- Block items with specific NBT tags (e.g., items with "Soulbound" enchant, creative-only items) -- Pattern matching on NBT paths - ---- - -## 2. Configuration - -### 2.1 Config File Location -``` -config/CustomNPC+/auction_blacklist.cfg -``` - -### 2.2 Config Structure -```java -// In ConfigMarket.java or separate ConfigAuctionBlacklist.java -public static String[] BlacklistedItems = new String[]{}; -public static String[] BlacklistedMods = new String[]{}; -public static String[] BlacklistedNBTPatterns = new String[]{}; -public static boolean EnableBlacklist = true; -public static boolean ShowBlacklistReason = true; -``` - -### 2.3 Example Configuration -```cfg -# Items that cannot be listed on the Auction House -S:BlacklistedItems < - minecraft:bedrock - minecraft:command_block - minecraft:barrier - customnpcs:npcSoulstoneFilled - customnpcs:npcMoney* - > - -# Mods whose items are completely blocked -S:BlacklistedMods < - projecte - equivalentexchange3 - > - -# NBT patterns that block items (advanced) -S:BlacklistedNBTPatterns < - tag.ench[*].id=34 - tag.Unbreakable=1 - > -``` - ---- - -## 3. Implementation - -### 3.1 New Files -``` -controllers/data/AuctionBlacklist.java - Blacklist checking logic -config/ConfigAuctionBlacklist.java - Configuration handling -``` - -### 3.2 AuctionBlacklist.java -```java -public class AuctionBlacklist { - private static Set blacklistedItems = new HashSet<>(); - private static Set blacklistedMods = new HashSet<>(); - private static List blacklistedNBT = new ArrayList<>(); - - public static boolean isBlacklisted(ItemStack item) { - if (item == null) return true; - - // Check item registry name - String registryName = GameRegistry.findUniqueIdentifierFor(item.getItem()).toString(); - if (matchesPattern(registryName, blacklistedItems)) return true; - - // Check mod - String modId = GameRegistry.findUniqueIdentifierFor(item.getItem()).modId; - if (blacklistedMods.contains(modId.toLowerCase())) return true; - - // Check NBT patterns - if (item.hasTagCompound()) { - for (NBTPattern pattern : blacklistedNBT) { - if (pattern.matches(item.getTagCompound())) return true; - } - } - - return false; - } - - public static String getBlacklistReason(ItemStack item) { - // Returns human-readable reason why item is blocked - } - - public static void reload() { - // Reload from config - } -} -``` - -### 3.3 Integration Points - -#### AuctionController.createListing() -```java -// Add at start of createListing method: -if (AuctionBlacklist.isBlacklisted(item)) { - if (ConfigMarket.ShowBlacklistReason) { - return "This item cannot be listed: " + AuctionBlacklist.getBlacklistReason(item); - } - return "This item cannot be listed on the Auction House."; -} -``` - -#### GuiAuctionSell (optional visual feedback) -- Gray out confirm button if item is blacklisted -- Show tooltip explaining why item can't be listed - ---- - -## 4. Commands - -### 4.1 Admin Commands -``` -/auction blacklist add -/auction blacklist remove -/auction blacklist list -/auction blacklist reload -/auction blacklist check -``` - -### 4.2 Examples -``` -/auction blacklist add item minecraft:diamond_block -/auction blacklist add mod projecte -/auction blacklist add nbt tag.ench[*].id=34 -/auction blacklist check held -``` - ---- - -## 5. Default Blacklist (Recommended) - -### 5.1 Vanilla Items -- `minecraft:command_block*` -- `minecraft:barrier` -- `minecraft:bedrock` -- `minecraft:structure_block` -- `minecraft:structure_void` - -### 5.2 CustomNPC+ Items -- `customnpcs:npcSoulstoneFilled` (contains NPC data) -- `customnpcs:npcWand` (admin tool) -- `customnpcs:npcMobCloner` (admin tool) -- `customnpcs:npcScripter` (admin tool) - ---- - -## 6. Future Considerations - -### 6.1 Whitelist Mode -- Invert the blacklist to only allow specific items -- Useful for strict economy servers - -### 6.2 Category-Based Blocking -- Block by item category (weapons, tools, armor, etc.) -- Integration with Minecraft's creative tabs - -### 6.3 Price Restrictions -- Set minimum/maximum prices for specific items -- Prevent price manipulation on valuable items - -### 6.4 Per-Player Overrides -- Permission to bypass blacklist: `customnpcs.auction.blacklist.bypass` -- Admin listing capability - ---- - -## 7. Files to Create/Modify - -### New Files -``` -config/ConfigAuctionBlacklist.java -controllers/data/AuctionBlacklist.java -``` - -### Modified Files -``` -AuctionController.java - Add blacklist check in createListing -AuctionCommand.java - Add blacklist subcommands -en_US.lang - Add blacklist messages -1.11_Market_Update.md - Document feature -``` - ---- - -## 8. Estimated Scope - -| Component | Complexity | Priority | -|-----------|------------|----------| -| Basic item blacklist | Low | High | -| Mod blacklist | Low | High | -| NBT pattern matching | Medium | Medium | -| Admin commands | Low | Medium | -| GUI feedback | Low | Low | -| Whitelist mode | Low | Low | diff --git a/src/main/java/foxz/command/CommandNoppes.java b/src/main/java/foxz/command/CommandNoppes.java index 8c051aa85..f5b5ccd73 100644 --- a/src/main/java/foxz/command/CommandNoppes.java +++ b/src/main/java/foxz/command/CommandNoppes.java @@ -2,9 +2,16 @@ package foxz.command; import net.minecraft.command.CommandBase; import net.minecraft.command.ICommandSender; +import net.minecraft.util.ChatComponentText; +import net.minecraft.util.EnumChatFormatting; import java.util.List; +/** + * @deprecated The /noppes command is deprecated and no longer supported. + * Please use the /kam command instead for all CNPC+ operations. + */ +@Deprecated public class CommandNoppes extends CommandBase { public CmdNoppes noppes = new CmdNoppes(this); @@ -21,6 +28,15 @@ public class CommandNoppes extends CommandBase { @Override public void processCommand(ICommandSender var1, String[] var2) { + // Send deprecation warning + var1.addChatMessage(new ChatComponentText(EnumChatFormatting.RED + "[DEPRECATED] " + + EnumChatFormatting.YELLOW + "The /noppes command is deprecated and no longer supported.")); + var1.addChatMessage(new ChatComponentText(EnumChatFormatting.YELLOW + + "Please use " + EnumChatFormatting.GREEN + "/kam" + EnumChatFormatting.YELLOW + " commands instead.")); + var1.addChatMessage(new ChatComponentText(EnumChatFormatting.GRAY + + "Type " + EnumChatFormatting.WHITE + "/kam help" + EnumChatFormatting.GRAY + " for available commands.")); + + // Still execute the command for backwards compatibility noppes.processCommand(var1, var2); } diff --git a/src/main/java/kamkeel/npcs/command/AuctionCommand.java b/src/main/java/kamkeel/npcs/command/AuctionCommand.java index a6e05a401..87d7e4385 100644 --- a/src/main/java/kamkeel/npcs/command/AuctionCommand.java +++ b/src/main/java/kamkeel/npcs/command/AuctionCommand.java @@ -1,9 +1,9 @@ package kamkeel.npcs.command; +import kamkeel.npcs.command.auction.BlacklistSubCommand; import net.minecraft.command.CommandException; import net.minecraft.command.ICommandSender; import net.minecraft.entity.player.EntityPlayer; -import net.minecraft.entity.player.EntityPlayerMP; import noppes.npcs.NoppesUtilServer; import noppes.npcs.config.ConfigMarket; import noppes.npcs.constants.EnumGuiType; @@ -21,6 +21,11 @@ import static kamkeel.npcs.util.ColorUtil.sendResult; public class AuctionCommand extends CommandKamkeelBase { + public AuctionCommand() { + super(); + registerNestedCommand(new BlacklistSubCommand()); + } + @Override public String getCommandName() { return "auction"; @@ -158,10 +163,17 @@ public class AuctionCommand extends CommandKamkeelBase { } @Override - public List addTabCompletionOptions(ICommandSender par1, String[] args) { + public List addTabCompletionOptions(ICommandSender sender, String[] args) { if (args.length == 1) { - return getListOfStringsMatchingLastWord(args, new String[]{"list", "view", "open"}); + return getListOfStringsMatchingLastWord(args, getAllSubCommandNames()); } + + // Delegate to nested commands for deeper tab completion + List nestedCompletions = getNestedTabCompletions(sender, args); + if (nestedCompletions != null) { + return nestedCompletions; + } + return null; } } diff --git a/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java b/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java index af2366f89..df4d34998 100644 --- a/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java +++ b/src/main/java/kamkeel/npcs/command/CommandKamkeelBase.java @@ -11,13 +11,11 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; public abstract class CommandKamkeelBase extends CommandBase { public Map subcommands = new HashMap(); + public Map nestedCommands = new HashMap(); public CommandKamkeelBase() { for (Method m : this.getClass().getDeclaredMethods()) { @@ -31,6 +29,14 @@ public abstract class CommandKamkeelBase extends CommandBase { } } + /** + * Register a nested command class as a subcommand. + * The nested command's getCommandName() becomes the subcommand name. + */ + public void registerNestedCommand(CommandKamkeelBase command) { + nestedCommands.put(command.getCommandName().toLowerCase(), command); + } + @Override public void processCommand(ICommandSender sender, String[] args) throws CommandException { @@ -51,7 +57,7 @@ public abstract class CommandKamkeelBase extends CommandBase { } public boolean runSubCommands() { - return !subcommands.isEmpty(); + return !subcommands.isEmpty() || !nestedCommands.isEmpty(); } @Retention(value = RetentionPolicy.RUNTIME) @@ -71,6 +77,22 @@ public abstract class CommandKamkeelBase extends CommandBase { } public void processSubCommand(ICommandSender sender, String command, String[] args) throws CommandException { + // Check nested commands first + CommandKamkeelBase nested = nestedCommands.get(command.toLowerCase()); + if (nested != null) { + if (!canSendNestedCommand(sender, nested)) { + throw new CommandException("You are not allowed to use this command: " + command); + } + + if (args.length == 0 || !nested.runSubCommands()) { + nested.processCommand(sender, args); + } else { + nested.processSubCommand(sender, args[0], Arrays.copyOfRange(args, 1, args.length)); + } + return; + } + + // Fall back to method subcommands Method m = subcommands.get(command.toLowerCase()); if (m == null) throw new CommandException("Unknown subcommand " + command); @@ -132,4 +154,55 @@ public abstract class CommandKamkeelBase extends CommandBase { return false; } + + public boolean canSendNestedCommand(ICommandSender sender, CommandKamkeelBase nested) { + int permission = nested.getRequiredPermissionLevel(); + String subCommand = nested.getCommandName(); + + if (sender.canCommandSenderUseCommand(permission, getSubUniversalPermission())) { + return true; + } + + if (sender.canCommandSenderUseCommand(permission, getSubCommandPermission(subCommand))) { + return true; + } + + if (sender instanceof EntityPlayer) { + return CustomNpcsPermissions.hasCustomPermission((EntityPlayer) sender, getSubCommandPermission(subCommand)); + } + + return false; + } + + // ========================================= + // Tab Completion Helpers + // ========================================= + + /** + * Get all subcommand names (both method-based and nested) + */ + public String[] getAllSubCommandNames() { + Set names = new HashSet<>(); + names.addAll(subcommands.keySet()); + names.addAll(nestedCommands.keySet()); + return names.toArray(new String[0]); + } + + /** + * Get tab completions for nested command at given depth + */ + public List getNestedTabCompletions(ICommandSender sender, String[] args) { + if (args.length == 0) return null; + + String subCmd = args[0].toLowerCase(); + CommandKamkeelBase nested = nestedCommands.get(subCmd); + + if (nested != null && args.length > 1) { + // Delegate to nested command + String[] nestedArgs = Arrays.copyOfRange(args, 1, args.length); + return nested.addTabCompletionOptions(sender, nestedArgs); + } + + return null; + } } diff --git a/src/main/java/kamkeel/npcs/command/auction/BlacklistSubCommand.java b/src/main/java/kamkeel/npcs/command/auction/BlacklistSubCommand.java new file mode 100644 index 000000000..a9902a890 --- /dev/null +++ b/src/main/java/kamkeel/npcs/command/auction/BlacklistSubCommand.java @@ -0,0 +1,226 @@ +package kamkeel.npcs.command.auction; + +import kamkeel.npcs.command.CommandKamkeelBase; +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import noppes.npcs.controllers.data.AuctionBlacklist; + +import java.util.List; + +import static kamkeel.npcs.util.ColorUtil.sendError; +import static kamkeel.npcs.util.ColorUtil.sendMessage; +import static kamkeel.npcs.util.ColorUtil.sendResult; + +public class BlacklistSubCommand extends CommandKamkeelBase { + + @Override + public String getCommandName() { + return "blacklist"; + } + + @Override + public String getDescription() { + return "Manage auction item blacklist"; + } + + @Override + public void processCommand(ICommandSender sender, String[] args) throws CommandException { + // Show help when called with no subcommand + sendHelp(sender); + } + + private void sendHelp(ICommandSender sender) { + sendMessage(sender, "\u00A76=== Auction Blacklist Commands ==="); + sendMessage(sender, "\u00A7e/cnpc auction blacklist add "); + sendMessage(sender, "\u00A7e/cnpc auction blacklist remove "); + sendMessage(sender, "\u00A7e/cnpc auction blacklist list [item|mod|nbt]"); + sendMessage(sender, "\u00A7e/cnpc auction blacklist reload"); + sendMessage(sender, "\u00A7e/cnpc auction blacklist check \u00A77(checks held item)"); + } + + @SubCommand( + desc = "Add item, mod, or NBT tag to blacklist", + usage = " " + ) + public void add(ICommandSender sender, String[] args) throws CommandException { + if (args.length < 2) { + sendError(sender, "Usage: /cnpc auction blacklist add "); + return; + } + + String type = args[0].toLowerCase(); + String value = args[1]; + + switch (type) { + case "item": + if (AuctionBlacklist.addItem(value)) { + sendResult(sender, "Added item to blacklist: " + value); + } else { + sendError(sender, "Failed to add item to blacklist"); + } + break; + case "mod": + if (AuctionBlacklist.addMod(value)) { + sendResult(sender, "Added mod to blacklist: " + value); + } else { + sendError(sender, "Failed to add mod to blacklist"); + } + break; + case "nbt": + if (AuctionBlacklist.addNBTTag(value)) { + sendResult(sender, "Added NBT tag to blacklist: " + value); + } else { + sendError(sender, "Failed to add NBT tag to blacklist"); + } + break; + default: + sendError(sender, "Unknown type: " + type + ". Use: item, mod, or nbt"); + } + } + + @SubCommand( + desc = "Remove item, mod, or NBT tag from blacklist", + usage = " " + ) + public void remove(ICommandSender sender, String[] args) throws CommandException { + if (args.length < 2) { + sendError(sender, "Usage: /cnpc auction blacklist remove "); + return; + } + + String type = args[0].toLowerCase(); + String value = args[1]; + + switch (type) { + case "item": + if (AuctionBlacklist.removeItem(value)) { + sendResult(sender, "Removed item from blacklist: " + value); + } else { + sendError(sender, "Item not found in blacklist: " + value); + } + break; + case "mod": + if (AuctionBlacklist.removeMod(value)) { + sendResult(sender, "Removed mod from blacklist: " + value); + } else { + sendError(sender, "Mod not found in blacklist: " + value); + } + break; + case "nbt": + if (AuctionBlacklist.removeNBTTag(value)) { + sendResult(sender, "Removed NBT tag from blacklist: " + value); + } else { + sendError(sender, "NBT tag not found in blacklist: " + value); + } + break; + default: + sendError(sender, "Unknown type: " + type + ". Use: item, mod, or nbt"); + } + } + + @SubCommand( + desc = "List blacklisted items, mods, or NBT tags", + usage = "[item|mod|nbt]" + ) + public void list(ICommandSender sender, String[] args) throws CommandException { + String filter = args.length > 0 ? args[0].toLowerCase() : "all"; + + if (filter.equals("all") || filter.equals("item")) { + List items = AuctionBlacklist.getBlacklistedItems(); + sendMessage(sender, "\u00A76=== Blacklisted Items (" + items.size() + ") ==="); + if (items.isEmpty()) { + sendMessage(sender, "\u00A77 (none)"); + } else { + for (String item : items) { + sendMessage(sender, "\u00A7e " + item); + } + } + } + + if (filter.equals("all") || filter.equals("mod")) { + List mods = AuctionBlacklist.getBlacklistedMods(); + sendMessage(sender, "\u00A76=== Blacklisted Mods (" + mods.size() + ") ==="); + if (mods.isEmpty()) { + sendMessage(sender, "\u00A77 (none)"); + } else { + for (String mod : mods) { + sendMessage(sender, "\u00A7e " + mod); + } + } + } + + if (filter.equals("all") || filter.equals("nbt")) { + List tags = AuctionBlacklist.getBlacklistedNBTTags(); + sendMessage(sender, "\u00A76=== Blacklisted NBT Tags (" + tags.size() + ") ==="); + if (tags.isEmpty()) { + sendMessage(sender, "\u00A77 (none)"); + } else { + for (String tag : tags) { + sendMessage(sender, "\u00A7e " + tag); + } + } + } + + sendMessage(sender, "\u00A77Note: Soulbound and Profile-Slotbound items are always blocked."); + } + + @SubCommand( + desc = "Reload blacklist from config" + ) + public void reload(ICommandSender sender, String[] args) throws CommandException { + AuctionBlacklist.reload(); + sendResult(sender, "Blacklist reloaded from config"); + } + + @SubCommand( + desc = "Check if held item is blacklisted", + permission = 0 + ) + public void check(ICommandSender sender, String[] args) throws CommandException { + if (!(sender instanceof EntityPlayer)) { + sendError(sender, "This command can only be used by players"); + return; + } + + EntityPlayer player = (EntityPlayer) sender; + ItemStack heldItem = player.getHeldItem(); + + if (heldItem == null) { + sendError(sender, "You are not holding an item"); + return; + } + + String registryName = AuctionBlacklist.getRegistryName(heldItem); + sendMessage(sender, "\u00A76=== Blacklist Check ==="); + sendMessage(sender, "\u00A77Item: \u00A7e" + heldItem.getDisplayName()); + sendMessage(sender, "\u00A77Registry: \u00A7e" + (registryName != null ? registryName : "unknown")); + + boolean isBlacklisted = AuctionBlacklist.isBlacklisted(heldItem); + boolean canBypass = AuctionBlacklist.canBypass(player); + + if (isBlacklisted) { + sendMessage(sender, "\u00A7cStatus: BLACKLISTED"); + if (canBypass) { + sendMessage(sender, "\u00A7aYou have bypass permission"); + } + } else { + sendMessage(sender, "\u00A7aStatus: ALLOWED"); + } + } + + @Override + public List addTabCompletionOptions(ICommandSender sender, String[] args) { + if (args.length == 1) { + return getListOfStringsMatchingLastWord(args, getAllSubCommandNames()); + } + if (args.length == 2) { + String subCmd = args[0].toLowerCase(); + if (subCmd.equals("add") || subCmd.equals("remove") || subCmd.equals("list")) { + return getListOfStringsMatchingLastWord(args, new String[]{"item", "mod", "nbt"}); + } + } + return null; + } +} diff --git a/src/main/java/noppes/npcs/config/ConfigMarket.java b/src/main/java/noppes/npcs/config/ConfigMarket.java index 2ba7c7c92..e14861033 100644 --- a/src/main/java/noppes/npcs/config/ConfigMarket.java +++ b/src/main/java/noppes/npcs/config/ConfigMarket.java @@ -13,6 +13,7 @@ public class ConfigMarket { public final static String CURRENCY = "Market.Currency"; public final static String AUCTION = "Market.Auction"; + public final static String AUCTION_BLACKLIST = "Market.Auction.Blacklist"; public final static String AUCTION_LOGGING = "Market.Auction.Logging"; // Max trades grid = 9x5 = 45 slots @@ -41,6 +42,14 @@ public class ConfigMarket { public static double MinBidIncrementPercent = 0.05; public static double CancellationPenaltyPercent = 0.10; + // ========================================= + // Auction Blacklist Settings + // ========================================= + public static boolean BlacklistEnabled = true; + public static String[] BlacklistedItems = new String[]{}; + public static String[] BlacklistedMods = new String[]{}; + public static String[] BlacklistedNBTTags = new String[]{}; + // ========================================= // Auction Logging Settings // ========================================= @@ -113,6 +122,45 @@ public class ConfigMarket { CancellationPenaltyPercent = config.get(AUCTION, "Cancellation Penalty Percent", 0.10, "Percentage of current bid taken as penalty when seller cancels an auction with bids (0.10 = 10%)").getDouble(0.10); + // ========================================= + // Auction Blacklist Settings + // ========================================= + config.setCategoryComment(AUCTION_BLACKLIST, + "Item Blacklist settings for the Auction House.\n" + + "Prevents specific items, mods, or items with certain NBT tags from being listed.\n\n" + + "ITEM FORMAT: Use 'modid:itemname' format (e.g., 'minecraft:bedrock', 'customnpcs:npcWand')\n" + + "WILDCARDS: Use * for wildcards (e.g., 'customnpcs:npc*' blocks all items starting with 'npc')\n" + + "MOD FORMAT: Use just the mod ID (e.g., 'projecte' blocks all items from that mod)\n" + + "NBT FORMAT: Use the NBT tag key name (e.g., 'AdminOnly' blocks items with that tag)"); + + BlacklistEnabled = config.get(AUCTION_BLACKLIST, "Enable Blacklist", true, + "Enable item blacklist checking when creating listings").getBoolean(true); + + BlacklistedItems = config.get(AUCTION_BLACKLIST, "Blacklisted Items", new String[]{ + "minecraft:bedrock", + "minecraft:command_block", + "customnpcs:npcWand", + "customnpcs:npcMobCloner", + "customnpcs:npcScripter", + "customnpcs:npcMovingPath", + "customnpcs:npcMounter", + "customnpcs:npcTeleporter", + "customnpcs:npcTool", + "customnpcs:npcSoulstoneFilled" + }, + "Items that cannot be listed on the Auction House.\n" + + "Format: modid:itemname (supports * wildcards)\n" + + "Example: 'minecraft:diamond_sword' or 'customnpcs:npc*'").getStringList(); + + BlacklistedMods = config.get(AUCTION_BLACKLIST, "Blacklisted Mods", new String[]{}, + "All items from these mods are blocked from the Auction House.\n" + + "Format: modid (e.g., 'projecte', 'thaumcraft')").getStringList(); + + BlacklistedNBTTags = config.get(AUCTION_BLACKLIST, "Blacklisted NBT Tags", new String[]{}, + "Items containing any of these NBT tag keys are blocked.\n" + + "Checks the root level of the item's NBT compound.\n" + + "Example: 'AdminOnly', 'CreativeMode'").getStringList(); + // ========================================= // Auction Logging Settings // ========================================= @@ -163,6 +211,13 @@ public class ConfigMarket { "Cancellation Penalty Percent" ))); + config.setCategoryPropertyOrder(AUCTION_BLACKLIST, new ArrayList<>(Arrays.asList( + "Enable Blacklist", + "Blacklisted Items", + "Blacklisted Mods", + "Blacklisted NBT Tags" + ))); + config.setCategoryPropertyOrder(AUCTION_LOGGING, new ArrayList<>(Arrays.asList( "Enable Auction Logging", "Log Created", diff --git a/src/main/java/noppes/npcs/controllers/AuctionController.java b/src/main/java/noppes/npcs/controllers/AuctionController.java index 375767deb..e6ca0c6cf 100644 --- a/src/main/java/noppes/npcs/controllers/AuctionController.java +++ b/src/main/java/noppes/npcs/controllers/AuctionController.java @@ -21,6 +21,7 @@ import noppes.npcs.constants.EnumAuctionSort; import noppes.npcs.constants.EnumAuctionStatus; import noppes.npcs.constants.EnumClaimType; import noppes.npcs.constants.EnumNotificationType; +import noppes.npcs.controllers.data.AuctionBlacklist; import noppes.npcs.controllers.data.AuctionClaim; import noppes.npcs.controllers.data.AuctionFilter; import noppes.npcs.controllers.data.AuctionListing; @@ -86,6 +87,7 @@ public class AuctionController implements IAuctionHandler { public AuctionController() { Instance = this; + AuctionBlacklist.reload(); load(); } @@ -241,6 +243,11 @@ public class AuctionController implements IAuctionHandler { return "Auction is disabled."; } + // Check blacklist (with bypass for admins) + if (AuctionBlacklist.isBlacklistedForPlayer(item, player)) { + return "This item cannot be listed on the Auction House."; + } + // Validate minimum price if (startingPrice < ConfigMarket.MinimumListingPrice) { return "Starting price must be at least " + ConfigMarket.MinimumListingPrice + " " + ConfigMarket.CurrencyName + "."; @@ -1414,4 +1421,18 @@ public class AuctionController implements IAuctionHandler { AuctionListing listing = listings.get(listingId); return listing != null && listing.isSeller(playerUUID); } + + /** + * Reload the item blacklist from config. + */ + public void reloadBlacklist() { + AuctionBlacklist.reload(); + } + + /** + * Check if an item is blacklisted. + */ + public boolean isItemBlacklisted(ItemStack item) { + return AuctionBlacklist.isBlacklisted(item); + } } diff --git a/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java b/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java new file mode 100644 index 000000000..75a1a714a --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/AuctionBlacklist.java @@ -0,0 +1,237 @@ +package noppes.npcs.controllers.data; + +import cpw.mods.fml.common.registry.GameRegistry; +import kamkeel.npcs.util.AttributeItemUtil; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.item.ItemStack; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.config.ConfigMarket; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; + +public class AuctionBlacklist { + private static Set blacklistedItems = new HashSet<>(); + private static Set wildcardPatterns = new HashSet<>(); + private static Set blacklistedMods = new HashSet<>(); + private static Set blacklistedNBTTags = new HashSet<>(); + + // Built-in bound item requirement keys (always blocked) + private static final String SOULBIND_KEY = "cnpc_soulbind"; + private static final String PROFILE_SLOT_KEY = "cnpc_profile_slot"; + + // Bypass permission + private static final String BYPASS_PERMISSION = "customnpcs.auction.blacklist.bypass"; + + public static void reload() { + blacklistedItems.clear(); + wildcardPatterns.clear(); + blacklistedMods.clear(); + blacklistedNBTTags.clear(); + + // Load blacklisted items + for (String item : ConfigMarket.BlacklistedItems) { + if (item == null || item.isEmpty()) continue; + item = item.toLowerCase().trim(); + + if (item.contains("*")) { + String regex = "^" + item.replace(".", "\\.").replace("*", ".*") + "$"; + wildcardPatterns.add(Pattern.compile(regex)); + } else { + blacklistedItems.add(item); + } + } + + // Load blacklisted mods + for (String mod : ConfigMarket.BlacklistedMods) { + if (mod == null || mod.isEmpty()) continue; + blacklistedMods.add(mod.toLowerCase().trim()); + } + + // Load blacklisted NBT tags + for (String tag : ConfigMarket.BlacklistedNBTTags) { + if (tag == null || tag.isEmpty()) continue; + blacklistedNBTTags.add(tag.trim()); + } + } + + /** + * Check if player can bypass the blacklist (OP or has permission) + */ + public static boolean canBypass(EntityPlayer player) { + if (player == null) return false; + + // Check OP status + if (player.canCommandSenderUseCommand(2, BYPASS_PERMISSION)) { + return true; + } + + // Check custom permission + return CustomNpcsPermissions.hasCustomPermission(player, BYPASS_PERMISSION); + } + + /** + * Check if item is blacklisted (without bypass check) + */ + public static boolean isBlacklisted(ItemStack item) { + if (!ConfigMarket.BlacklistEnabled) return false; + if (item == null || item.getItem() == null) return true; + + // Check bound items (always blocked, even if blacklist is "disabled") + if (hasBoundRequirement(item)) { + return true; + } + + // Get registry name + GameRegistry.UniqueIdentifier uid = GameRegistry.findUniqueIdentifierFor(item.getItem()); + if (uid == null) return true; + + String registryName = (uid.modId + ":" + uid.name).toLowerCase(); + String modId = uid.modId.toLowerCase(); + + // Check exact item match + if (blacklistedItems.contains(registryName)) { + return true; + } + + // Check wildcard patterns + for (Pattern pattern : wildcardPatterns) { + if (pattern.matcher(registryName).matches()) { + return true; + } + } + + // Check mod blacklist + if (blacklistedMods.contains(modId)) { + return true; + } + + // Check NBT tags + if (item.hasTagCompound() && !blacklistedNBTTags.isEmpty()) { + NBTTagCompound tag = item.getTagCompound(); + for (String nbtKey : blacklistedNBTTags) { + if (tag.hasKey(nbtKey)) { + return true; + } + } + } + + return false; + } + + /** + * Check if item is blacklisted for a specific player (with bypass check) + */ + public static boolean isBlacklistedForPlayer(ItemStack item, EntityPlayer player) { + if (canBypass(player)) return false; + return isBlacklisted(item); + } + + /** + * Get the registry name of an item + */ + public static String getRegistryName(ItemStack item) { + if (item == null || item.getItem() == null) return null; + GameRegistry.UniqueIdentifier uid = GameRegistry.findUniqueIdentifierFor(item.getItem()); + if (uid == null) return null; + return uid.modId + ":" + uid.name; + } + + // ========================================= + // Bound Item Checking (RPGCore/Requirements) + // ========================================= + + /** + * Check if item has soulbind or profile slot requirement. + * Requirements are stored under: item.stackTagCompound/RPGCore/Requirements + */ + private static boolean hasBoundRequirement(ItemStack item) { + if (item == null || !item.hasTagCompound()) return false; + + NBTTagCompound root = item.getTagCompound(); + if (!root.hasKey(AttributeItemUtil.TAG_RPGCORE)) return false; + + NBTTagCompound rpgCore = root.getCompoundTag(AttributeItemUtil.TAG_RPGCORE); + if (!rpgCore.hasKey(AttributeItemUtil.TAG_REQUIREMENTS)) return false; + + NBTTagCompound requirements = rpgCore.getCompoundTag(AttributeItemUtil.TAG_REQUIREMENTS); + return requirements.hasKey(SOULBIND_KEY) || requirements.hasKey(PROFILE_SLOT_KEY); + } + + // ========================================= + // Runtime Blacklist Management + // ========================================= + + public static boolean addItem(String registryName) { + if (registryName == null || registryName.isEmpty()) return false; + registryName = registryName.toLowerCase().trim(); + + if (registryName.contains("*")) { + String regex = "^" + registryName.replace(".", "\\.").replace("*", ".*") + "$"; + wildcardPatterns.add(Pattern.compile(regex)); + } else { + blacklistedItems.add(registryName); + } + return true; + } + + public static boolean removeItem(String registryName) { + if (registryName == null || registryName.isEmpty()) return false; + registryName = registryName.toLowerCase().trim(); + + if (registryName.contains("*")) { + String regex = "^" + registryName.replace(".", "\\.").replace("*", ".*") + "$"; + return wildcardPatterns.removeIf(p -> p.pattern().equals(regex)); + } else { + return blacklistedItems.remove(registryName); + } + } + + public static boolean addMod(String modId) { + if (modId == null || modId.isEmpty()) return false; + return blacklistedMods.add(modId.toLowerCase().trim()); + } + + public static boolean removeMod(String modId) { + if (modId == null || modId.isEmpty()) return false; + return blacklistedMods.remove(modId.toLowerCase().trim()); + } + + public static boolean addNBTTag(String tag) { + if (tag == null || tag.isEmpty()) return false; + return blacklistedNBTTags.add(tag.trim()); + } + + public static boolean removeNBTTag(String tag) { + if (tag == null || tag.isEmpty()) return false; + return blacklistedNBTTags.remove(tag.trim()); + } + + // ========================================= + // Getters for listing + // ========================================= + + public static List getBlacklistedItems() { + List result = new ArrayList<>(blacklistedItems); + for (Pattern p : wildcardPatterns) { + // Convert regex back to wildcard format + String pattern = p.pattern(); + pattern = pattern.substring(1, pattern.length() - 1); // Remove ^ and $ + pattern = pattern.replace("\\.", ".").replace(".*", "*"); + result.add(pattern); + } + return result; + } + + public static List getBlacklistedMods() { + return new ArrayList<>(blacklistedMods); + } + + public static List getBlacklistedNBTTags() { + return new ArrayList<>(blacklistedNBTTags); + } +} diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index dc2757916..7f0618786 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1769,6 +1769,8 @@ auction.bid.buyout=Buyout Now # Errors auction.error.notEnoughCurrency=Not enough currency! auction.error.bidTooLow=Bid must be at least %s. +auction.error.blacklisted=This item cannot be listed on the Auction House. +auction.error.boundItem=Bound items cannot be listed on the Auction House. # Navigation auction.nav.listings=Browse Auctions From 8c125afc9ce2f6d11b01569164292009b037e3cf Mon Sep 17 00:00:00 2001 From: Kam Date: Sat, 24 Jan 2026 16:14:19 -0500 Subject: [PATCH 280/358] Create Auction_Update.md --- Auction_Update.md | 127 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 Auction_Update.md diff --git a/Auction_Update.md b/Auction_Update.md new file mode 100644 index 000000000..ae839832f --- /dev/null +++ b/Auction_Update.md @@ -0,0 +1,127 @@ +# 🏪 CustomNPC+ 1.11 - Market Update 🏪 + +--- + +The **Market Update** brings a complete economy system to CNPC+! Create bustling marketplaces with **Traders** that have limited stock, or let players trade with each other through the brand new **Auction House**! + +--- + +## Currency System 💰 + +CNPC+ now has a **built-in currency** used across all Market features! + +- **Fully Configurable** - Name it Coins, Gold, Credits, whatever fits your world +- **Profile Persistent** - Currency carries across all your character slots +- **Vault Support** - CNPC+ integrates seamlessly with Bukkit-compatible economy plugins if Vault is installed + +> Full Scripting API support for currency operations on players. + +--- + +## Auction House 🏛️ + +A **global player marketplace** where players can list items, bid against each other, and purchase instantly! + +### Selling + +List any item with a **Starting Price** for competitive bidding. Set an optional **Buyout Price** for players who want to skip the wait and buy immediately. + +> A configurable **Listing Fee** is charged when creating auctions, and a **Sales Tax** is taken from successful sales. + +### Bidding + +Compete for items by placing bids! **Snipe Protection** extends auctions when last-second bids come in, giving everyone a fair shot. If you get outbid, your currency is **automatically refunded**. + +### Buyout + +See something you need right now? Hit **Buy Now** to purchase instantly at the seller's Buyout Price. If you're currently the highest bidder on an item, you only pay the **difference** between your bid and the buyout. + +### My Trades + +One screen to manage all your auction activity: + +- **Active Listings** - Items you're currently selling +- **Active Bids** - Auctions you're competing in +- **Ready to Claim** - Won items and currency waiting for pickup + +> Players can **cancel** their own listings. Items are returned as a claim. + +### Notifications + +Get notified immediately when your item sells, when you're outbid, or when you win an auction. Offline? You'll get a full **claim summary** when you log back in showing exactly what's waiting for you. + +### Trade Slots + +Each player has a limited number of **Trade Slots** (default 8). Trade Slots are shared between Active Listings, Active Bids, and Pending Claims. + +> Server owners can grant players more slots via permissions up to a maximum of 45. + +--- + +## Item Blacklist 🚫 + +Server owners have full control over what items can be listed on the Auction House. + +**Block by:** +- **Item** - Specific registry names like `minecraft:diamond_sword` +- **Wildcard** - Patterns like `minecraft:diamond*` to block all diamond items +- **Mod** - Block all items from a specific mod +- **NBT Tag** - Block items containing certain NBT data + +> **Soulbound** and **Profile-Slotbound** items are always blocked automatically. Bound items cannot be traded! + +**Blacklist Commands:** +- `/kam auction blacklist add ` - Add entry +- `/kam auction blacklist remove ` - Remove entry +- `/kam auction blacklist list` - View all blacklisted entries +- `/kam auction blacklist check` - Check if your held item is blacklisted +- `/kam auction blacklist reload` - Reload from config + +--- + +## Trader Enhancements 🛒 + +### Stock System + +Traders now support **limited stock** with automatic restocking! + +**Stock Modes:** +- **Global** - All players share the same stock pool +- **Per-Player** - Each player has their own purchase limits + +**Restock Timers:** +- **MC Daily / Weekly** - Restock based on Minecraft time +- **RL Daily / Weekly** - Restock based on real-life time +- **Custom** - Set your own interval in MC ticks or real-time + +**Visual Feedback:** +- Stock count displayed next to each trade +- **Out of Stock** overlay when unavailable +- Live **countdown timer** until next restock + +> Admins can manually reset stock or cooldowns via the Trader GUI. + + +### Currency Trades + +Trades can now require **currency on top of items**. Combine material costs with coin costs for balanced economy progression! + +--- + +## Auctioneer Role 🧑‍💼 + +New NPC role for the Auction House! Set the role on any NPC and players can access the full Auction House by interacting with them. +> No configuration needed + +### Auction Command + +Players can also access the Auction House via command: + +- `/kam auction open` - Opens the Auction House GUI + +**Admin Commands:** +- `/kam auction list [page]` - View active auctions in chat +- `/kam auction view ` - View a player's auction activity + +--- + From 9ebaf780e1ccb5d9f2f7c7bcb3a3d37fb21d2064 Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 25 Jan 2026 17:13:53 -0300 Subject: [PATCH 281/358] i hate git --- gradle-plugins | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle-plugins b/gradle-plugins index e193079bf..bfa9565fc 160000 --- a/gradle-plugins +++ b/gradle-plugins @@ -1 +1 @@ -Subproject commit e193079bf7392e83d233ce2e120ee0f2b645021f +Subproject commit bfa9565fce3110adb8fb4e9fd1a52bafdd7d35e6 From 92e69bb270645c452268dc4568300998a00dc588 Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 25 Jan 2026 21:13:42 -0300 Subject: [PATCH 282/358] Changed Player Dialog Events to work like NPC Dialog Events --- .../java/noppes/npcs/NoppesUtilPlayer.java | 6 ++--- .../java/noppes/npcs/NoppesUtilServer.java | 2 +- .../scripted/event/player/DialogEvent.java | 27 +++++++++++++------ 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/main/java/noppes/npcs/NoppesUtilPlayer.java b/src/main/java/noppes/npcs/NoppesUtilPlayer.java index c78ffdf46..7f9753104 100644 --- a/src/main/java/noppes/npcs/NoppesUtilPlayer.java +++ b/src/main/java/noppes/npcs/NoppesUtilPlayer.java @@ -306,18 +306,18 @@ public class NoppesUtilPlayer { if (dialog == null) return; DialogOption option = dialog.options.get(optionId); - if (EventHooks.onDialogOption(player, new DialogEvent.DialogOption((IPlayer) NpcAPI.Instance().getIEntity(player), dialog))) + if (EventHooks.onDialogOption(player, new DialogEvent.DialogOption((IPlayer) NpcAPI.Instance().getIEntity(player), dialogId, optionId, dialog))) return; if (!npc.isRemote()) { EventHooks.onNPCDialogClosed(npc, player, dialogId, optionId + 1, dialog); if (!dialog.hasDialogs(player) && !dialog.hasOtherOptions()) { - EventHooks.onDialogClosed(player, new DialogEvent.DialogClosed((IPlayer) NpcAPI.Instance().getIEntity(player), dialog)); + EventHooks.onDialogClosed(player, new DialogEvent.DialogClosed((IPlayer) NpcAPI.Instance().getIEntity(player), dialogId, optionId, dialog)); return; } if (option == null || option.optionType == EnumOptionType.DialogOption && (!option.isAvailable(player) || !option.hasDialog()) || option.optionType == EnumOptionType.Disabled || option.optionType == EnumOptionType.QuitOption) { - EventHooks.onDialogClosed(player, new DialogEvent.DialogClosed((IPlayer) NpcAPI.Instance().getIEntity(player), dialog)); + EventHooks.onDialogClosed(player, new DialogEvent.DialogClosed((IPlayer) NpcAPI.Instance().getIEntity(player), dialogId, optionId, dialog)); return; } } diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index 08c04f6f4..ffc778eb0 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -241,7 +241,7 @@ public class NoppesUtilServer { if (EventHooks.onNPCDialog(npc, player, dialog.id, optionId, dialog)) { return; } - if (EventHooks.onDialogOpen(player, new DialogEvent.DialogOpen((IPlayer) NpcAPI.Instance().getIEntity(player), dialog))) { + if (EventHooks.onDialogOpen(player, new DialogEvent.DialogOpen((IPlayer) NpcAPI.Instance().getIEntity(player), dialog.id, optionId, dialog))) { return; } } diff --git a/src/main/java/noppes/npcs/scripted/event/player/DialogEvent.java b/src/main/java/noppes/npcs/scripted/event/player/DialogEvent.java index a5ebfc1ff..a6291cac0 100644 --- a/src/main/java/noppes/npcs/scripted/event/player/DialogEvent.java +++ b/src/main/java/noppes/npcs/scripted/event/player/DialogEvent.java @@ -8,24 +8,36 @@ import noppes.npcs.constants.EnumScriptType; public class DialogEvent extends PlayerEvent implements IDialogEvent { public final IDialog dialog; + public final int dialogId; + public final int optionId; - public DialogEvent(IPlayer player, IDialog dialog) { + public DialogEvent(IPlayer player, int id, int optionId, IDialog dialog) { super(player); this.dialog = dialog; + this.dialogId = id; + this.optionId = optionId; } public IDialog getDialog() { return dialog; } + public int getDialogId() { + return dialogId; + } + + public int getOptionId() { + return optionId; + } + public String getHookName() { return EnumScriptType.DIALOG_EVENT.function; } @Cancelable public static class DialogOpen extends DialogEvent implements IDialogEvent.DialogOpen { - public DialogOpen(IPlayer player, IDialog dialog) { - super(player, dialog); + public DialogOpen(IPlayer player, int id, int optionId, IDialog dialog) { + super(player, id, optionId, dialog); } public String getHookName() { @@ -35,8 +47,8 @@ public class DialogEvent extends PlayerEvent implements IDialogEvent { @Cancelable public static class DialogOption extends DialogEvent implements IDialogEvent.DialogOption { - public DialogOption(IPlayer player, IDialog dialog) { - super(player, dialog); + public DialogOption(IPlayer player, int id, int optionId, IDialog dialog) { + super(player, id, optionId, dialog); } public String getHookName() { @@ -46,13 +58,12 @@ public class DialogEvent extends PlayerEvent implements IDialogEvent { } public static class DialogClosed extends DialogEvent implements IDialogEvent.DialogClosed { - public DialogClosed(IPlayer player, IDialog dialog) { - super(player, dialog); + public DialogClosed(IPlayer player, int id, int optionId, IDialog dialog) { + super(player, id, optionId, dialog); } public String getHookName() { return EnumScriptType.DIALOG_CLOSE.function; } - } } From de043f4a2557ccd4123554c762b705bc000d70e9 Mon Sep 17 00:00:00 2001 From: Kam Date: Sun, 25 Jan 2026 20:39:01 -0500 Subject: [PATCH 283/358] Ability Changes --- .gitignore | 3 +- Ability_Update_Review.md | 301 +++++++++ src/api | 2 +- .../client/renderer/RenderAbilityBeam.java | 347 +++++++++++ .../client/renderer/RenderAbilityDisc.java | 136 +++++ .../client/renderer/RenderAbilityLaser.java | 200 ++++++ .../client/renderer/RenderAbilityOrb.java | 105 ++++ .../client/renderer/RenderAbilityOrbCube.java | 56 ++ .../renderer/RenderAbilityProjectile.java | 255 ++++++++ .../client/renderer/RenderAbilitySweeper.java | 210 +++++++ .../renderer/quadric/QuadricRenderer.java | 138 +++++ .../renderer/quadric/SphereRenderer.java | 37 ++ .../controllers/data/ability/Ability.java | 158 ++--- .../data/ability/AbilityController.java | 10 +- .../data/ability/TargetingMode.java | 17 +- .../data/ability/type/AbilityBeam.java | 335 ---------- .../data/ability/type/AbilityCharge.java | 2 +- .../data/ability/type/AbilityDisc.java | 237 ++++++++ .../data/ability/type/AbilityEnergyBeam.java | 227 +++++++ .../data/ability/type/AbilityGuard.java | 2 +- .../data/ability/type/AbilityHazard.java | 50 +- .../data/ability/type/AbilityHeal.java | 41 +- .../data/ability/type/AbilityHeavyHit.java | 2 +- .../data/ability/type/AbilityLaserShot.java | 202 +++++++ .../data/ability/type/AbilityOrb.java | 328 ++++------ .../data/ability/type/AbilityShockwave.java | 2 +- .../data/ability/type/AbilitySlam.java | 2 +- .../data/ability/type/AbilitySweeper.java | 217 +++++++ .../data/ability/type/AbilityTrap.java | 35 +- .../data/ability/type/AbilityVortex.java | 2 +- .../data/telegraph/TelegraphInstance.java | 32 +- .../npcs/entity/EntityAbilityBeam.java | 428 +++++++++++++ .../npcs/entity/EntityAbilityDisc.java | 329 ++++++++++ .../npcs/entity/EntityAbilityLaser.java | 326 ++++++++++ .../kamkeel/npcs/entity/EntityAbilityOrb.java | 225 +++++++ .../npcs/entity/EntityAbilityProjectile.java | 570 ++++++++++++++++++ .../npcs/entity/EntityAbilitySweeper.java | 437 ++++++++++++++ src/main/java/noppes/npcs/CustomNpcs.java | 10 + src/main/java/noppes/npcs/DataAbilities.java | 35 +- .../java/noppes/npcs/ai/CombatHandler.java | 6 +- .../java/noppes/npcs/client/ClientProxy.java | 16 + .../gui/advanced/SubGuiAbilityConfig.java | 81 ++- .../advanced/ability/SubGuiAbilityBeam.java | 110 ---- .../advanced/ability/SubGuiAbilityDisc.java | 227 +++++++ .../ability/SubGuiAbilityEnergyBeam.java | 228 +++++++ .../ability/SubGuiAbilityLaserShot.java | 159 +++++ .../advanced/ability/SubGuiAbilityOrb.java | 96 +++ .../ability/SubGuiAbilitySweeper.java | 198 ++++++ .../assets/customnpcs/lang/en_US.lang | 30 + .../customnpcs/textures/entity/white.png | Bin 0 -> 176 bytes .../customnpcs/textures/entity/whiteSmall.png | Bin 0 -> 139 bytes 51 files changed, 6287 insertions(+), 915 deletions(-) create mode 100644 Ability_Update_Review.md create mode 100644 src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrbCube.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/quadric/QuadricRenderer.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/quadric/SphereRenderer.java delete mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java create mode 100644 src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java create mode 100644 src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java create mode 100644 src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java create mode 100644 src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java create mode 100644 src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java create mode 100644 src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java create mode 100644 src/main/resources/assets/customnpcs/textures/entity/white.png create mode 100644 src/main/resources/assets/customnpcs/textures/entity/whiteSmall.png diff --git a/.gitignore b/.gitignore index f24532312..d24cc7cf7 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,5 @@ addon.late.local.gradle.kts layout.json out run -src/main/resources/assets/customnpcs/api \ No newline at end of file +src/main/resources/assets/customnpcs/api +df/ diff --git a/Ability_Update_Review.md b/Ability_Update_Review.md new file mode 100644 index 000000000..2ceb38797 --- /dev/null +++ b/Ability_Update_Review.md @@ -0,0 +1,301 @@ +# Ability System Review - CustomNPC+ + +A comprehensive technical review of the Ability System identifying bugs, logic issues, incomplete features, and inconsistencies. + +--- + +## Critical Issues + +### 1. Cooldown Uses Mixed Time Systems +**Status:** FIXED +**Files:** +- `Ability.java` - Removed cooldown tracking (cooldownEndTime, startCooldown(), isOnCooldown(), canUse()) +- `DataAbilities.java` - Now sole owner of cooldown management using world time + +**Solution:** Removed all cooldown tracking from `Ability.java`. Cooldowns are now managed exclusively by `DataAbilities` using `world.getTotalWorldTime()` for consistency. + +--- + +### 2. AbilityHeal - Heal Over Time Completely Broken +**Status:** FIXED +**File:** `AbilityHeal.java` + +**Solution:** Refactored to separate ally-finding from healing. The `findAlliesInRadius()` method is now called regardless of `instantHeal` flag, populating the list for HoT to use. Added heal particles during HoT ticks. + +--- + +### 3. AbilityGuard - Counter Integration Missing +**Status:** FIXED +**Files:** +- `DataAbilities.java:onDamage()` - Added integration to call `AbilityGuard.onDamageTaken()` +- `DataAbilities.java:getGuardDamageReduction()` - Added helper method for damage reduction + +**Solution:** `DataAbilities.onDamage()` now checks if current ability is an `AbilityGuard` and calls `onDamageTaken()` when the NPC is hit while guarding. + +--- + +## High Priority Issues + +### 4. knockbackUp Stored But Never Applied +**Status:** FIXED +**File:** `Ability.java` + +**Solution:** Updated `applyNpcKnockback()` to accept a `knockbackUp` parameter and use it instead of hardcoded `0.1D`. Both `applyAbilityDamage()` and `applyAbilityDamageWithDirection()` now pass the knockbackUp value from the HitEvent. + +--- + +### 5. Targeting Modes Not Implemented +**Status:** FIXED (Removed) +**File:** `TargetingMode.java` + +**Solution:** Removed the unused targeting modes (`RANDOM_ENEMY`, `LOWEST_HP`, `HIGHEST_THREAT`) from the enum. Only the implemented modes remain: `AGGRO_TARGET`, `SELF`, `AOE_SELF`, `AOE_TARGET`. + +--- + +### 6. Damage Source Inconsistency +**Status:** FIXED +**File:** `Ability.java` + +**Solution:** Standardized both `applyAbilityDamage()` and `applyAbilityDamageWithDirection()` to use `NpcDamageSource("mob", npc)` for consistent damage source handling. + +--- + +## Incomplete Features + +### 7. AbilityProjectile - No Actual Projectile +**Status:** CONFIRMED - INCOMPLETE +**File:** `AbilityProjectile.java:88` + +```java +// TODO: Use custom EntityAbilityProjectile for actual tracking +applyAbilityDamageWithDirection(npc, target, damage, knockback, dx, dz); +``` + +**Current behavior:** +- `speed`, `homing`, `homingStrength` fields exist but are unused +- No projectile entity is spawned +- Damage is instant, just with particle cosmetics + +--- + +### 8. AbilityOrb - No Visual Orb Rendering +**Status:** CONFIRMED - INCOMPLETE +**File:** `AbilityOrb.java:117-179` + +**Problem:** The orb tracks position (`orbX`, `orbY`, `orbZ`) with runtime getters but nothing renders it client-side. Players only see particle effects on hit, not the flying orb itself. + +--- + +### 9. AbilityBeam - No Visual Beam Rendering +**Status:** CONFIRMED - INCOMPLETE +**File:** `AbilityBeam.java:213-220` + +**Problem:** Only spawns flame particles along the path. No actual beam entity or proper beam visual. The `lockOnTarget` telegraph during active phase isn't visually communicated. + +--- + +## Medium Priority Issues + +### 10. Hazard/Trap Telegraph Offset Mismatch +**Status:** CONFIRMED +**Files:** +- `AbilityHazard.java:119-147, 201-248` +- `AbilityTrap.java:109-138, 166-205` + +**Problem:** Telegraph is created at target position, but actual effect applies random offset: + +```java +// createTelegraph() - No offset applied, shows at target position + +// onExecute() - Random offset applied AFTER telegraph locks +double[] pos = calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ()); +zoneX = pos[0]; // Actual effect position differs from telegraph +``` + +**Impact:** Players see telegraph at one location, damage happens elsewhere. Misleading visual feedback. + +**Fix:** Either apply offset during telegraph creation, or update telegraph position in `onExecute()`. + +--- + +### 11. AbilityCharge Telegraph Direction Timing +**Status:** CONFIRMED - MINOR +**File:** `AbilityCharge.java:91, 254-286` + +**Problem:** Telegraph created at tick 0, direction locked at tick 1: + +```java +// Telegraph created during setup (tick 0) with current target direction +// Line 91: Direction locked at tick 1 +if (tick == 1) { + lockChargeDirection(npc, target); +} +``` + +**Impact:** If target moves between tick 0-1, telegraph direction won't match actual charge. + +--- + +### 12. AbilitySlam - activeTicks May End Before Landing +**Status:** CONFIRMED - MINOR +**File:** `AbilitySlam.java:52-53, 230-250` + +**Problem:** `activeTicks = 60` is fixed, but if NPC takes longer to land, ability transitions to RECOVERY without `onLanding()` being called. + +**Secondary issue:** If NPC lands early (tick 15), active phase continues until tick 60 doing nothing - wasted ticks. + +--- + +### 13. FOLLOW Mode Telegraph Not Updated +**Status:** CONFIRMED +**Files:** `AbilityHazard.java:139-146`, `AbilityVortex.java` + +**Problem:** For `FOLLOW_TARGET` mode, telegraph locks position on execute, but the actual hazard zone follows the target. Visual mismatch between warning and danger. + +--- + +## Redundancies + +### 14. Duplicate findGroundLevel() Methods +**Status:** FIXED +**Files:** +- `Ability.java` - Made `findGroundLevel()` public static +- `TelegraphInstance.java` - Now uses `Ability.findGroundLevel()` + +--- + +### 15. Duplicate calculateOffsetPosition() Methods +**Status:** FIXED +**Files:** +- `Ability.java` - Added shared static `calculateOffsetPosition()` method +- `AbilityTrap.java` - Removed duplicate, uses `Ability.calculateOffsetPosition()` +- `AbilityHazard.java` - Removed duplicate, uses `Ability.calculateOffsetPosition()` + +--- + +### 16. Unused applyKnockback in AbilityOrb +**Status:** FIXED +**File:** `AbilityOrb.java` + +Removed the unused `applyKnockback()` method. + +--- + +### 17. Duplicate Reset/Interrupt Cleanup +**Status:** NOT FIXED (Low Priority) +**Multiple ability files** + +Most abilities override both `reset()` and `onInterrupt()` with duplicate cleanup code. Pattern should have `onInterrupt()` call `reset()` or use shared cleanup method. + +--- + +## Minor Issues & Inconsistencies + +### 18. Field Naming Confusion +- `AbilityHazard`: `damagePerSecond` with `damageInterval` - name suggests per-second but applied every N ticks +- `AbilityCharge`: `hitWidth` stored as `hitRadius` in older NBT (backward compat exists) + +### 19. AbilityVortex Entity Lookup Performance +**File:** `AbilityVortex.java:201-212` + +Iterates through ALL loaded entities every tick to find pulled entities by UUID. Should cache entity references or use entity IDs. + +### 20. Telegraph Yaw Rotation +**File:** `TelegraphRenderer.java:95` + +Uses `GL11.glRotatef(-instance.getYaw(), 0, 1, 0)` - negative yaw. Should verify this matches Minecraft's yaw conventions. + +### 21. Missing Null Checks +Various locations where `target` could be null but is used without defensive checks: +- `AbilityProjectile.java:73` +- `AbilityOrb.java:138-171` + +--- + +## Missing Features (Enhancement Requests) + +These are not bugs but notable missing functionality: + +1. **No Ability Chaining/Combo System** - No way to trigger abilities from other abilities +2. **No Cooldown Reduction on Hit** - Can't reduce cooldowns based on successful hits +3. **No Multi-Target Hit Limit** - AOE abilities hit unlimited targets (unlike Vortex's `maxTargets`) +4. **No Damage Type Configuration** - All abilities use mob/melee damage, no magic/fire/etc options +5. **No Target Validation During Execution** - If target dies/despawns mid-ability, behavior varies + +--- + +## Priority Summary + +| Priority | Issue | Status | +|----------|-------|--------| +| **CRITICAL** | Cooldown time system mismatch | FIXED | +| **CRITICAL** | AbilityHeal HoT broken | FIXED | +| **CRITICAL** | AbilityGuard counter not integrated | FIXED | +| **HIGH** | knockbackUp ignored | FIXED | +| **HIGH** | Targeting modes unimplemented | FIXED (Removed) | +| **HIGH** | Damage source inconsistency | FIXED | +| **MEDIUM** | Projectile/Orb/Beam no visuals | Not Fixed | +| **MEDIUM** | Hazard/Trap telegraph offset | Not Fixed | +| **LOW** | Charge telegraph timing | Not Fixed | +| **LOW** | Slam activeTicks timing | Not Fixed | +| **LOW** | Code redundancies | FIXED | + +--- + +## Remaining Issues + +The following issues were not addressed in this fix round: + +1. **Projectile/Orb/Beam Visual Rendering** - These abilities still only use particles, no actual entity rendering +2. **Hazard/Trap Telegraph Offset Mismatch** - Telegraph shows at target, effect spawns with offset +3. **Charge Telegraph Direction Timing** - Minor visual mismatch between tick 0-1 +4. **Slam activeTicks Timing** - Fixed at 60 ticks regardless of landing time + +--- + +## Changes Made + +### Ability.java +- Removed `cooldownEndTime` transient field +- Removed `startCooldown()`, `isOnCooldown()`, `canUse()` methods (cooldown managed by DataAbilities) +- Updated `tick()` to not call startCooldown (DataAbilities handles it) +- Updated `reset()` to not reset cooldownEndTime +- Made `findGroundLevel()` public static for shared use +- Added static `calculateOffsetPosition()` utility method +- Fixed `applyNpcKnockback()` to accept and use knockbackUp parameter +- Standardized damage source to `NpcDamageSource("mob", npc)` in both damage methods +- Removed deprecated getters/setters (telegraphColor, castSound, animationId) + +### IAbility.java (API) +- Removed `isOnCooldown()` method from interface + +### DataAbilities.java +- Added AbilityGuard import +- Added guard counter integration in `onDamage()` +- Added `getGuardDamageReduction()` helper method +- Removed deprecated `isMovementBlocked()` method +- Updated knockbackUp javadoc (no longer deprecated) + +### CombatHandler.java +- Renamed `isMovementBlocked()` to `isAbilityControllingMovement()` + +### TargetingMode.java +- Removed unused modes: `RANDOM_ENEMY`, `LOWEST_HP`, `HIGHEST_THREAT` + +### AbilityHeal.java +- Fixed HoT by separating `findAlliesInRadius()` from healing logic +- Allies are now found in `onExecute()` regardless of instantHeal flag +- Added heal particles during HoT ticks + +### AbilityHazard.java +- Removed duplicate `calculateOffsetPosition()`, uses `Ability.calculateOffsetPosition()` +- Removed deprecated `getDamagePerTick()`/`setDamagePerTick()` methods + +### AbilityTrap.java +- Removed duplicate `calculateOffsetPosition()`, uses `Ability.calculateOffsetPosition()` + +### AbilityOrb.java +- Removed unused `applyKnockback()` method + +### TelegraphInstance.java +- Removed duplicate `findGroundLevel()`, uses `Ability.findGroundLevel()` diff --git a/src/api b/src/api index 2e5d86993..b84763a8e 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 2e5d86993cd47e6df4572323514d4fe2166c570c +Subproject commit b84763a8eefdc0be7fb909888ac5ad6d22e3f987 diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java new file mode 100644 index 000000000..49cb4fb6c --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java @@ -0,0 +1,347 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.entity.EntityAbilityBeam; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.entity.Entity; +import net.minecraft.util.Vec3; +import org.lwjgl.opengl.GL11; + +import java.util.ArrayList; +import java.util.List; + +/** + * Renders the AbilityBeam entity with a head, tail orb, and curving trail. + * The trail shows the path the head has traveled using laser-style rectangular segments. + * + * IMPORTANT: Trail points are stored RELATIVE to the origin position. + * We render by translating to the origin first, then drawing all points relative to that. + * + * Trail segments now share vertices at connection points to prevent gaps during curves. + * + * Design inspired by LouisXIV's energy rendering system. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilityBeam extends RenderAbilityProjectile { + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilityBeam beam = (EntityAbilityBeam) entity; + + setupRenderState(); + + // Get trail points (these are RELATIVE to origin) + List trail = beam.getTrailPoints(); + if (trail.isEmpty()) { + restoreRenderState(); + return; + } + + // Get interpolated head offset + double headOffsetX = beam.getInterpolatedHeadOffsetX(partialTicks); + double headOffsetY = beam.getInterpolatedHeadOffsetY(partialTicks); + double headOffsetZ = beam.getInterpolatedHeadOffsetZ(partialTicks); + + // The render system passes x,y,z as the position to render the entity at + // Entity position = origin + headOffset (in world coords) + // So origin render position = x,y,z - headOffset + double renderOriginX = x - headOffsetX; + double renderOriginY = y - headOffsetY; + double renderOriginZ = z - headOffsetZ; + + // Get beam properties + float beamWidth = beam.getBeamWidth(); + float headSize = beam.getHeadSize(); + int innerColor = beam.getInnerColor(); + int outerColor = beam.getOuterColor(); + + // Render everything relative to origin position + GL11.glPushMatrix(); + GL11.glTranslated(renderOriginX, renderOriginY, renderOriginZ); + + // Render tail orb at origin (0,0,0) - static, no rotation + renderTailOrb(headSize * 0.8f, innerColor, outerColor); + + // Render trail segments with smooth connections + renderSmoothTrail(trail, beamWidth, innerColor, outerColor); + + // Render head at interpolated offset position + renderHead(beam, headOffsetX, headOffsetY, headOffsetZ, headSize, innerColor, outerColor, partialTicks); + + GL11.glPopMatrix(); + + restoreRenderState(); + } + + /** + * Render the tail orb at the origin (where the beam emanates from). + * This orb does not rotate - it's a stable anchor point. + */ + private void renderTailOrb(float size, int innerColor, int outerColor) { + GL11.glPushMatrix(); + // Already at origin (0,0,0) + + // Render outer glow + GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(size, size, size); + renderCube(outerColor, 0.4f, 0.5f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + + // Render inner core (smaller, solid) + GL11.glPushMatrix(); + GL11.glScalef(size * 0.6f, size * 0.6f, size * 0.6f); + renderCube(innerColor, 1.0f, 0.5f); + GL11.glPopMatrix(); + + GL11.glPopMatrix(); + } + + /** + * Render the beam trail with smooth connections between segments. + * Uses averaged perpendicular vectors at connection points to prevent gaps. + */ + private void renderSmoothTrail(List trail, float width, int innerColor, int outerColor) { + if (trail.size() < 2) return; + + int trailSize = trail.size(); + + // Pre-compute perpendicular frames at each point + // At connection points, average the perpendiculars for smooth transitions + List perpFrames = computePerpendiculars(trail); + + // Render outer glow trail (wider, translucent) + GL11.glDepthMask(false); + renderTrailTube(trail, perpFrames, width * 1.8f, outerColor, 0.3f, true); + GL11.glDepthMask(true); + + // Render inner core trail + renderTrailTube(trail, perpFrames, width * 0.6f, innerColor, 0.9f, false); + } + + /** + * Compute perpendicular frames for each point in the trail. + * At interior points, average the perpendiculars from both directions for smooth curves. + * Returns list of [horzX, horzY, horzZ, vertX, vertY, vertZ] for each point. + */ + private List computePerpendiculars(List trail) { + List frames = new ArrayList<>(); + int size = trail.size(); + + for (int i = 0; i < size; i++) { + double[] frame = new double[6]; + + // Calculate direction at this point + double dx, dy, dz; + if (i == 0) { + // First point - use direction to next + Vec3 p1 = trail.get(0); + Vec3 p2 = trail.get(1); + dx = p2.xCoord - p1.xCoord; + dy = p2.yCoord - p1.yCoord; + dz = p2.zCoord - p1.zCoord; + } else if (i == size - 1) { + // Last point - use direction from previous + Vec3 p1 = trail.get(size - 2); + Vec3 p2 = trail.get(size - 1); + dx = p2.xCoord - p1.xCoord; + dy = p2.yCoord - p1.yCoord; + dz = p2.zCoord - p1.zCoord; + } else { + // Interior point - average directions + Vec3 p0 = trail.get(i - 1); + Vec3 p1 = trail.get(i); + Vec3 p2 = trail.get(i + 1); + double dx1 = p1.xCoord - p0.xCoord; + double dy1 = p1.yCoord - p0.yCoord; + double dz1 = p1.zCoord - p0.zCoord; + double dx2 = p2.xCoord - p1.xCoord; + double dy2 = p2.yCoord - p1.yCoord; + double dz2 = p2.zCoord - p1.zCoord; + // Average the directions + dx = (dx1 + dx2) * 0.5; + dy = (dy1 + dy2) * 0.5; + dz = (dz1 + dz2) * 0.5; + } + + // Normalize direction + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0.001) { + dx /= len; + dy /= len; + dz /= len; + } else { + dx = 0; dy = 0; dz = 1; + } + + // Calculate perpendicular vectors using consistent up reference + double upX = 0, upY = 1, upZ = 0; + if (Math.abs(dy) > 0.9) { + upX = 1; upY = 0; upZ = 0; + } + + // Cross product for horizontal perpendicular + double horzX = dy * upZ - dz * upY; + double horzY = dz * upX - dx * upZ; + double horzZ = dx * upY - dy * upX; + double horzLen = Math.sqrt(horzX * horzX + horzY * horzY + horzZ * horzZ); + if (horzLen > 0) { + horzX /= horzLen; + horzY /= horzLen; + horzZ /= horzLen; + } + + // Cross product for vertical perpendicular + double vertX = dy * horzZ - dz * horzY; + double vertY = dz * horzX - dx * horzZ; + double vertZ = dx * horzY - dy * horzX; + + frame[0] = horzX; frame[1] = horzY; frame[2] = horzZ; + frame[3] = vertX; frame[4] = vertY; frame[5] = vertZ; + frames.add(frame); + } + + return frames; + } + + /** + * Render the trail as a continuous tube using pre-computed perpendicular frames. + */ + private void renderTrailTube(List trail, List perpFrames, + float width, int color, float baseAlpha, boolean fadeFromStart) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + float halfWidth = width * 0.5f; + int size = trail.size(); + + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + + for (int i = 0; i < size - 1; i++) { + Vec3 p1 = trail.get(i); + Vec3 p2 = trail.get(i + 1); + double[] frame1 = perpFrames.get(i); + double[] frame2 = perpFrames.get(i + 1); + + // Calculate alpha based on position (fade from start or constant) + float alpha1, alpha2; + if (fadeFromStart) { + float progress1 = (float) i / size; + float progress2 = (float) (i + 1) / size; + alpha1 = baseAlpha * progress1; + alpha2 = baseAlpha * progress2; + } else { + alpha1 = baseAlpha; + alpha2 = baseAlpha; + } + + // Calculate 4 corners at start + double s1x = p1.xCoord - frame1[0] * halfWidth - frame1[3] * halfWidth; + double s1y = p1.yCoord - frame1[1] * halfWidth - frame1[4] * halfWidth; + double s1z = p1.zCoord - frame1[2] * halfWidth - frame1[5] * halfWidth; + + double s2x = p1.xCoord + frame1[0] * halfWidth - frame1[3] * halfWidth; + double s2y = p1.yCoord + frame1[1] * halfWidth - frame1[4] * halfWidth; + double s2z = p1.zCoord + frame1[2] * halfWidth - frame1[5] * halfWidth; + + double s3x = p1.xCoord + frame1[0] * halfWidth + frame1[3] * halfWidth; + double s3y = p1.yCoord + frame1[1] * halfWidth + frame1[4] * halfWidth; + double s3z = p1.zCoord + frame1[2] * halfWidth + frame1[5] * halfWidth; + + double s4x = p1.xCoord - frame1[0] * halfWidth + frame1[3] * halfWidth; + double s4y = p1.yCoord - frame1[1] * halfWidth + frame1[4] * halfWidth; + double s4z = p1.zCoord - frame1[2] * halfWidth + frame1[5] * halfWidth; + + // Calculate 4 corners at end + double e1x = p2.xCoord - frame2[0] * halfWidth - frame2[3] * halfWidth; + double e1y = p2.yCoord - frame2[1] * halfWidth - frame2[4] * halfWidth; + double e1z = p2.zCoord - frame2[2] * halfWidth - frame2[5] * halfWidth; + + double e2x = p2.xCoord + frame2[0] * halfWidth - frame2[3] * halfWidth; + double e2y = p2.yCoord + frame2[1] * halfWidth - frame2[4] * halfWidth; + double e2z = p2.zCoord + frame2[2] * halfWidth - frame2[5] * halfWidth; + + double e3x = p2.xCoord + frame2[0] * halfWidth + frame2[3] * halfWidth; + double e3y = p2.yCoord + frame2[1] * halfWidth + frame2[4] * halfWidth; + double e3z = p2.zCoord + frame2[2] * halfWidth + frame2[5] * halfWidth; + + double e4x = p2.xCoord - frame2[0] * halfWidth + frame2[3] * halfWidth; + double e4y = p2.yCoord - frame2[1] * halfWidth + frame2[4] * halfWidth; + double e4z = p2.zCoord - frame2[2] * halfWidth + frame2[5] * halfWidth; + + // Bottom face (s1-s2-e2-e1) + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(s2x, s2y, s2z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e1x, e1y, e1z); + + // Top face (s4-e4-e3-s3) + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s4x, s4y, s4z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(e3x, e3y, e3z); + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s3x, s3y, s3z); + + // Left face (s1-e1-e4-s4) + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s1x, s1y, s1z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e4x, e4y, e4z); + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s4x, s4y, s4z); + + // Right face (s2-s3-e3-e2) + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(s3x, s3y, s3z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e2x, e2y, e2z); + } + + tess.draw(); + } + + /** + * Render the beam head as a glowing cube. + * Coordinates are relative to origin. + */ + private void renderHead(EntityAbilityBeam beam, double x, double y, double z, + float size, int innerColor, int outerColor, float partialTicks) { + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Pulsing effect + float pulseTime = beam.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.2f) * 0.1f; + float scale = size * (1.0f + scaleModifier); + + // Rotation + GL11.glRotatef(beam.getInterpolatedRotationX(partialTicks), 1.0f, 0.0f, 0.0f); + GL11.glRotatef(beam.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); + GL11.glRotatef(beam.getInterpolatedRotationZ(partialTicks), 0.0f, 0.0f, 1.0f); + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Render outer glow + GL11.glDepthMask(false); + renderCube(outerColor, 0.5f, 0.5f); + GL11.glDepthMask(true); + + // Render inner core + float innerScale = 0.6f; + GL11.glScalef(innerScale, innerScale, innerScale); + renderCube(innerColor, 1.0f, 0.5f); + + GL11.glPopMatrix(); + GL11.glPopMatrix(); + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java new file mode 100644 index 000000000..93049fad5 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java @@ -0,0 +1,136 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.entity.EntityAbilityDisc; +import net.minecraft.entity.Entity; +import org.lwjgl.opengl.GL11; + +/** + * Renders the AbilityDisc entity as a flat spinning square. + * Rotates only on Y axis (flat spin like a saw blade). + * + * Design inspired by LouisXIV's energy rendering system. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilityDisc extends RenderAbilityProjectile { + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilityDisc disc = (EntityAbilityDisc) entity; + + setupRenderState(); + + GL11.glTranslated(x, y, z); + + // Get interpolated values + float size = disc.getInterpolatedSize(partialTicks); + float radius = disc.getDiscRadius() * size; + float thickness = disc.getDiscThickness() * size; + + // Subtle pulsing effect + float pulseTime = entity.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.1f) * 0.03f; + float scale = 1.0f + scaleModifier; + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Apply ONLY Y-axis rotation for flat spin + GL11.glRotatef(disc.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); + + // Render outer square (translucent) + GL11.glDepthMask(false); + renderFlatSquare(disc.getOuterColor(), 0.5f, radius, thickness); + GL11.glDepthMask(true); + + // Render inner square (solid, smaller) + float innerScale = 0.6f; + renderFlatSquare(disc.getInnerColor(), 1.0f, radius * innerScale, thickness * 1.2f); + + GL11.glPopMatrix(); + + restoreRenderState(); + } + + /** + * Render a flat square (thin cube) centered at origin. + * The square lies flat on the XZ plane with thin Y height. + */ + private void renderFlatSquare(int color, float alpha, float halfSize, float thickness) { + float halfThick = thickness * 0.5f; + + // Render as a thin cube + renderCubeCustom(color, alpha, halfSize, halfThick, halfSize); + } + + /** + * Render a cube with custom dimensions. + */ + private void renderCubeCustom(int color, float alpha, float halfX, float halfY, float halfZ) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + + net.minecraft.client.renderer.Tessellator tess = net.minecraft.client.renderer.Tessellator.instance; + + // Top face (y+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 1, 0); + tess.addVertex(-halfX, halfY, halfZ); + tess.addVertex(halfX, halfY, halfZ); + tess.addVertex(halfX, halfY, -halfZ); + tess.addVertex(-halfX, halfY, -halfZ); + tess.draw(); + + // Bottom face (y-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, -1, 0); + tess.addVertex(-halfX, -halfY, -halfZ); + tess.addVertex(halfX, -halfY, -halfZ); + tess.addVertex(halfX, -halfY, halfZ); + tess.addVertex(-halfX, -halfY, halfZ); + tess.draw(); + + // Front face (z+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 0, 1); + tess.addVertex(-halfX, -halfY, halfZ); + tess.addVertex(halfX, -halfY, halfZ); + tess.addVertex(halfX, halfY, halfZ); + tess.addVertex(-halfX, halfY, halfZ); + tess.draw(); + + // Back face (z-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 0, -1); + tess.addVertex(halfX, -halfY, -halfZ); + tess.addVertex(-halfX, -halfY, -halfZ); + tess.addVertex(-halfX, halfY, -halfZ); + tess.addVertex(halfX, halfY, -halfZ); + tess.draw(); + + // Right face (x+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(1, 0, 0); + tess.addVertex(halfX, -halfY, halfZ); + tess.addVertex(halfX, -halfY, -halfZ); + tess.addVertex(halfX, halfY, -halfZ); + tess.addVertex(halfX, halfY, halfZ); + tess.draw(); + + // Left face (x-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(-1, 0, 0); + tess.addVertex(-halfX, -halfY, -halfZ); + tess.addVertex(-halfX, -halfY, halfZ); + tess.addVertex(-halfX, halfY, halfZ); + tess.addVertex(-halfX, halfY, -halfZ); + tess.draw(); + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java new file mode 100644 index 000000000..0c1d4a1e8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java @@ -0,0 +1,200 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.entity.EntityAbilityLaser; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.entity.Entity; +import org.lwjgl.opengl.GL11; + +/** + * Renders the AbilityLaser entity as a rectangular beam with clear inner/outer colors. + * + * Design inspired by LouisXIV's energy rendering system. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilityLaser extends RenderAbilityProjectile { + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilityLaser laser = (EntityAbilityLaser) entity; + + // Don't render if length is 0 + if (laser.getCurrentLength() <= 0) return; + + setupRenderState(); + + // Get laser parameters + double startX = laser.getStartX(); + double startY = laser.getStartY(); + double startZ = laser.getStartZ(); + double endX = laser.getEndX(); + double endY = laser.getEndY(); + double endZ = laser.getEndZ(); + float width = laser.getLaserWidth(); + float alpha = laser.getLingerAlpha(); + + // Calculate render offset (entity position is used as reference) + double offsetX = x - laser.posX; + double offsetY = y - laser.posY; + double offsetZ = z - laser.posZ; + + // Render start relative to camera + double renderStartX = startX + offsetX; + double renderStartY = startY + offsetY; + double renderStartZ = startZ + offsetZ; + double renderEndX = endX + offsetX; + double renderEndY = endY + offsetY; + double renderEndZ = endZ + offsetZ; + + // Calculate perpendicular vectors for the beam rectangle + double dx = renderEndX - renderStartX; + double dy = renderEndY - renderStartY; + double dz = renderEndZ - renderStartZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len < 0.001) { + restoreRenderState(); + return; + } + + // Normalize direction + dx /= len; + dy /= len; + dz /= len; + + // Calculate perpendicular vectors (one horizontal, one vertical relative to beam) + double upX = 0, upY = 1, upZ = 0; + if (Math.abs(dy) > 0.9) { + upX = 1; upY = 0; upZ = 0; + } + + // Cross product for horizontal perpendicular + double horzX = dy * upZ - dz * upY; + double horzY = dz * upX - dx * upZ; + double horzZ = dx * upY - dy * upX; + double horzLen = Math.sqrt(horzX * horzX + horzY * horzY + horzZ * horzZ); + if (horzLen > 0) { + horzX /= horzLen; + horzY /= horzLen; + horzZ /= horzLen; + } + + // Cross product for vertical perpendicular + double vertX = dy * horzZ - dz * horzY; + double vertY = dz * horzX - dx * horzZ; + double vertZ = dx * horzY - dy * horzX; + + // Render outer rectangle (larger, outer color, translucent) + GL11.glDepthMask(false); + renderBeamRectangle(renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, + horzX, horzY, horzZ, vertX, vertY, vertZ, + width * 1.8f, laser.getOuterColor(), alpha * 0.4f); + + // Render middle layer + renderBeamRectangle(renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, + horzX, horzY, horzZ, vertX, vertY, vertZ, + width * 1.3f, laser.getOuterColor(), alpha * 0.7f); + GL11.glDepthMask(true); + + // Render inner core (smaller, inner color, solid) + renderBeamRectangle(renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, + horzX, horzY, horzZ, vertX, vertY, vertZ, + width * 0.6f, laser.getInnerColor(), alpha); + + restoreRenderState(); + } + + /** + * Render a beam as a 3D rectangular prism (box along a line). + */ + private void renderBeamRectangle(double x1, double y1, double z1, + double x2, double y2, double z2, + double horzX, double horzY, double horzZ, + double vertX, double vertY, double vertZ, + float width, int color, float alpha) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + + float halfWidth = width * 0.5f; + + // Calculate 8 corners of the box + // Start corners + double s1x = x1 - horzX * halfWidth - vertX * halfWidth; + double s1y = y1 - horzY * halfWidth - vertY * halfWidth; + double s1z = z1 - horzZ * halfWidth - vertZ * halfWidth; + + double s2x = x1 + horzX * halfWidth - vertX * halfWidth; + double s2y = y1 + horzY * halfWidth - vertY * halfWidth; + double s2z = z1 + horzZ * halfWidth - vertZ * halfWidth; + + double s3x = x1 + horzX * halfWidth + vertX * halfWidth; + double s3y = y1 + horzY * halfWidth + vertY * halfWidth; + double s3z = z1 + horzZ * halfWidth + vertZ * halfWidth; + + double s4x = x1 - horzX * halfWidth + vertX * halfWidth; + double s4y = y1 - horzY * halfWidth + vertY * halfWidth; + double s4z = z1 - horzZ * halfWidth + vertZ * halfWidth; + + // End corners + double e1x = x2 - horzX * halfWidth - vertX * halfWidth; + double e1y = y2 - horzY * halfWidth - vertY * halfWidth; + double e1z = z2 - horzZ * halfWidth - vertZ * halfWidth; + + double e2x = x2 + horzX * halfWidth - vertX * halfWidth; + double e2y = y2 + horzY * halfWidth - vertY * halfWidth; + double e2z = z2 + horzZ * halfWidth - vertZ * halfWidth; + + double e3x = x2 + horzX * halfWidth + vertX * halfWidth; + double e3y = y2 + horzY * halfWidth + vertY * halfWidth; + double e3z = z2 + horzZ * halfWidth + vertZ * halfWidth; + + double e4x = x2 - horzX * halfWidth + vertX * halfWidth; + double e4y = y2 - horzY * halfWidth + vertY * halfWidth; + double e4z = z2 - horzZ * halfWidth + vertZ * halfWidth; + + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + + // Bottom face + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e1x, e1y, e1z); + + // Top face + tess.addVertex(s4x, s4y, s4z); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(s3x, s3y, s3z); + + // Left face + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(s4x, s4y, s4z); + + // Right face + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(s3x, s3y, s3z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e2x, e2y, e2z); + + // Start cap + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(s4x, s4y, s4z); + tess.addVertex(s3x, s3y, s3z); + tess.addVertex(s2x, s2y, s2z); + + // End cap + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e4x, e4y, e4z); + + tess.draw(); + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java new file mode 100644 index 000000000..3a09e7b6a --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java @@ -0,0 +1,105 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.quadric.SphereRenderer; +import kamkeel.npcs.entity.EntityAbilityOrb; +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.entity.Entity; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +/** + * Renders the AbilityOrb entity as a glowing sphere with inner and outer layers. + * + * Design inspired by LouisXIV's energy rendering system. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilityOrb extends Render { + + // White texture for solid color rendering + private static final ResourceLocation WHITE_TEXTURE = new ResourceLocation("customnpcs", "textures/entity/white.png"); + + // Static sphere renderer - shared across all orb renders + private static final SphereRenderer sphere = new SphereRenderer(0.5f, 16); + + public RenderAbilityOrb() { + this.shadowSize = 0.0f; + } + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilityOrb orb = (EntityAbilityOrb) entity; + + GL11.glPushMatrix(); + + // Disable culling, enable rescale normal + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + GL11.glEnable(GL11.GL_ALPHA_TEST); + + // Enable blending for transparency + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glAlphaFunc(GL11.GL_GREATER, 0.003921569F); + + // Disable lighting for self-illumination + GL11.glDisable(GL11.GL_LIGHTING); + + // Translate to render position + GL11.glTranslated(x, y, z); + + // Bind texture + this.bindTexture(WHITE_TEXTURE); + + // Get interpolated size for smooth rendering + float size = orb.getInterpolatedSize(partialTicks); + + // Pulsing scale effect + float pulseTime = entity.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.15f) * 0.05f; + float scale = size * (1.0f + scaleModifier); + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Use entity's interpolated rotation for smooth spinning + GL11.glRotatef(orb.getInterpolatedRotationX(partialTicks), 1.0f, 0.0f, 0.0f); + GL11.glRotatef(orb.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); + GL11.glRotatef(orb.getInterpolatedRotationZ(partialTicks), 0.0f, 0.0f, 1.0f); + + // Render outer sphere (translucent) + GL11.glDepthMask(false); + this.bindTexture(WHITE_TEXTURE); + renderSphere(orb.getOuterColor(), 0.6f); + GL11.glDepthMask(true); + + // Render inner sphere (more opaque) + float innerScale = 0.75f; + GL11.glPushMatrix(); + GL11.glScalef(innerScale, innerScale, innerScale); + this.bindTexture(WHITE_TEXTURE); + renderSphere(orb.getInnerColor(), 1.0f); + GL11.glPopMatrix(); + + GL11.glPopMatrix(); + + // Restore GL state + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glPopMatrix(); + } + + private void renderSphere(int color, float alpha) { + // Set sphere properties and render (rotation already applied to matrix) + sphere.setScale(1.0f); + sphere.setAlpha(alpha); + sphere.setColor(color); + sphere.render(0); + } + + @Override + protected ResourceLocation getEntityTexture(Entity entity) { + return WHITE_TEXTURE; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrbCube.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrbCube.java new file mode 100644 index 000000000..29656e597 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrbCube.java @@ -0,0 +1,56 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.entity.EntityAbilityOrb; +import net.minecraft.entity.Entity; +import org.lwjgl.opengl.GL11; + +/** + * Renders the AbilityOrb entity as a cube with inner and outer layers. + * Minecraft-style blocky appearance using Tessellator. + * + * Design inspired by LouisXIV's energy rendering system. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilityOrbCube extends RenderAbilityProjectile { + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilityOrb orb = (EntityAbilityOrb) entity; + + setupRenderState(); + + GL11.glTranslated(x, y, z); + + // Get interpolated size for smooth rendering + float size = orb.getInterpolatedSize(partialTicks); + + // Pulsing scale effect + float pulseTime = entity.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.15f) * 0.05f; + float scale = size * (1.0f + scaleModifier); + + // Use entity's interpolated rotation for smooth spinning + GL11.glRotatef(orb.getInterpolatedRotationX(partialTicks), 1.0f, 0.0f, 0.0f); + GL11.glRotatef(orb.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); + GL11.glRotatef(orb.getInterpolatedRotationZ(partialTicks), 0.0f, 0.0f, 1.0f); + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Render outer cube (translucent) + GL11.glDepthMask(false); + renderCube(orb.getOuterColor(), 0.5f, 0.5f); + GL11.glDepthMask(true); + + // Render inner cube (solid, smaller) + float innerScale = 0.6f; + GL11.glScalef(innerScale, innerScale, innerScale); + renderCube(orb.getInnerColor(), 1.0f, 0.5f); + + GL11.glPopMatrix(); + + restoreRenderState(); + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java new file mode 100644 index 000000000..4180783bf --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java @@ -0,0 +1,255 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.entity.Entity; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +/** + * Base renderer for ability projectiles. + * Provides common GL state setup and rendering utilities. + * + * Design inspired by LouisXIV's energy rendering system. + */ +@SideOnly(Side.CLIENT) +public abstract class RenderAbilityProjectile extends Render { + + protected static final ResourceLocation WHITE_TEXTURE = new ResourceLocation("customnpcs", "textures/entity/white.png"); + + public RenderAbilityProjectile() { + this.shadowSize = 0.0f; + } + + /** + * Setup GL state for translucent, self-illuminated rendering. + */ + protected void setupRenderState() { + GL11.glPushMatrix(); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glAlphaFunc(GL11.GL_GREATER, 0.003921569F); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_TEXTURE_2D); + } + + /** + * Restore GL state after rendering. + */ + protected void restoreRenderState() { + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_CULL_FACE); + GL11.glPopMatrix(); + } + + /** + * Extract RGB components from color int. + */ + protected float[] extractRGB(int color) { + float r = ((color >> 16) & 0xFF) / 255.0f; + float g = ((color >> 8) & 0xFF) / 255.0f; + float b = (color & 0xFF) / 255.0f; + return new float[]{r, g, b}; + } + + /** + * Render a cube centered at origin with given color and alpha. + */ + protected void renderCube(int color, float alpha, float halfSize) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + + Tessellator tess = Tessellator.instance; + + // Front face (z+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 0, 1); + tess.addVertex(-halfSize, -halfSize, halfSize); + tess.addVertex(halfSize, -halfSize, halfSize); + tess.addVertex(halfSize, halfSize, halfSize); + tess.addVertex(-halfSize, halfSize, halfSize); + tess.draw(); + + // Back face (z-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 0, -1); + tess.addVertex(halfSize, -halfSize, -halfSize); + tess.addVertex(-halfSize, -halfSize, -halfSize); + tess.addVertex(-halfSize, halfSize, -halfSize); + tess.addVertex(halfSize, halfSize, -halfSize); + tess.draw(); + + // Top face (y+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 1, 0); + tess.addVertex(-halfSize, halfSize, halfSize); + tess.addVertex(halfSize, halfSize, halfSize); + tess.addVertex(halfSize, halfSize, -halfSize); + tess.addVertex(-halfSize, halfSize, -halfSize); + tess.draw(); + + // Bottom face (y-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, -1, 0); + tess.addVertex(-halfSize, -halfSize, -halfSize); + tess.addVertex(halfSize, -halfSize, -halfSize); + tess.addVertex(halfSize, -halfSize, halfSize); + tess.addVertex(-halfSize, -halfSize, halfSize); + tess.draw(); + + // Right face (x+) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(1, 0, 0); + tess.addVertex(halfSize, -halfSize, halfSize); + tess.addVertex(halfSize, -halfSize, -halfSize); + tess.addVertex(halfSize, halfSize, -halfSize); + tess.addVertex(halfSize, halfSize, halfSize); + tess.draw(); + + // Left face (x-) + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(-1, 0, 0); + tess.addVertex(-halfSize, -halfSize, -halfSize); + tess.addVertex(-halfSize, -halfSize, halfSize); + tess.addVertex(-halfSize, halfSize, halfSize); + tess.addVertex(-halfSize, halfSize, -halfSize); + tess.draw(); + } + + /** + * Render a flat disc (cylinder with very small height) centered at origin. + * The disc lies flat on the XZ plane. + */ + protected void renderDisc(int color, float alpha, float radius, float thickness, int segments) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + + Tessellator tess = Tessellator.instance; + float halfThick = thickness * 0.5f; + + // Top face + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, 1, 0); + for (int i = 0; i < segments; i++) { + float angle1 = (float) (2 * Math.PI * i / segments); + float angle2 = (float) (2 * Math.PI * (i + 1) / segments); + float x1 = (float) Math.cos(angle1) * radius; + float z1 = (float) Math.sin(angle1) * radius; + float x2 = (float) Math.cos(angle2) * radius; + float z2 = (float) Math.sin(angle2) * radius; + + tess.addVertex(0, halfThick, 0); + tess.addVertex(x1, halfThick, z1); + tess.addVertex(x2, halfThick, z2); + tess.addVertex(0, halfThick, 0); + } + tess.draw(); + + // Bottom face + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.setNormal(0, -1, 0); + for (int i = 0; i < segments; i++) { + float angle1 = (float) (2 * Math.PI * i / segments); + float angle2 = (float) (2 * Math.PI * (i + 1) / segments); + float x1 = (float) Math.cos(angle1) * radius; + float z1 = (float) Math.sin(angle1) * radius; + float x2 = (float) Math.cos(angle2) * radius; + float z2 = (float) Math.sin(angle2) * radius; + + tess.addVertex(0, -halfThick, 0); + tess.addVertex(x2, -halfThick, z2); + tess.addVertex(x1, -halfThick, z1); + tess.addVertex(0, -halfThick, 0); + } + tess.draw(); + + // Edge + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + for (int i = 0; i < segments; i++) { + float angle1 = (float) (2 * Math.PI * i / segments); + float angle2 = (float) (2 * Math.PI * (i + 1) / segments); + float x1 = (float) Math.cos(angle1) * radius; + float z1 = (float) Math.sin(angle1) * radius; + float x2 = (float) Math.cos(angle2) * radius; + float z2 = (float) Math.sin(angle2) * radius; + + // Normal pointing outward + float nx = (float) Math.cos((angle1 + angle2) * 0.5); + float nz = (float) Math.sin((angle1 + angle2) * 0.5); + tess.setNormal(nx, 0, nz); + + tess.addVertex(x1, -halfThick, z1); + tess.addVertex(x2, -halfThick, z2); + tess.addVertex(x2, halfThick, z2); + tess.addVertex(x1, halfThick, z1); + } + tess.draw(); + } + + /** + * Render a line segment as a quad billboard facing the camera. + */ + protected void renderLineSegment(double x1, double y1, double z1, + double x2, double y2, double z2, + float width, int color, float alpha) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + + // Calculate direction + double dx = x2 - x1; + double dy = y2 - y1; + double dz = z2 - z1; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len < 0.001) return; + + // Perpendicular vectors for billboard + double perpX, perpY, perpZ; + if (Math.abs(dy) < 0.9) { + // Cross with up vector + perpX = -dz; + perpY = 0; + perpZ = dx; + } else { + // Cross with right vector + perpX = 0; + perpY = dz; + perpZ = -dy; + } + double perpLen = Math.sqrt(perpX * perpX + perpY * perpY + perpZ * perpZ); + if (perpLen > 0) { + perpX = perpX / perpLen * width * 0.5; + perpY = perpY / perpLen * width * 0.5; + perpZ = perpZ / perpLen * width * 0.5; + } + + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, alpha); + tess.addVertex(x1 - perpX, y1 - perpY, z1 - perpZ); + tess.addVertex(x2 - perpX, y2 - perpY, z2 - perpZ); + tess.addVertex(x2 + perpX, y2 + perpY, z2 + perpZ); + tess.addVertex(x1 + perpX, y1 + perpY, z1 + perpZ); + tess.draw(); + } + + @Override + protected ResourceLocation getEntityTexture(Entity entity) { + return WHITE_TEXTURE; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java new file mode 100644 index 000000000..735c1e183 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java @@ -0,0 +1,210 @@ +package kamkeel.npcs.client.renderer; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.entity.EntityAbilitySweeper; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.entity.Render; +import net.minecraft.entity.Entity; +import net.minecraft.util.ResourceLocation; +import org.lwjgl.opengl.GL11; + +/** + * Renders the Sweeper beam as a thin rotating laser attached to the NPC. + * Uses similar rendering style to the Beam trail for visual consistency. + */ +@SideOnly(Side.CLIENT) +public class RenderAbilitySweeper extends Render { + + @Override + public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { + EntityAbilitySweeper sweeper = (EntityAbilitySweeper) entity; + + setupRenderState(); + + float beamLength = sweeper.getBeamLength(); + float beamWidth = sweeper.getBeamWidth(); + int innerColor = sweeper.getInnerColor(); + int outerColor = sweeper.getOuterColor(); + float angle = sweeper.getInterpolatedAngle(partialTicks); + + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Rotate the beam around Y axis (angle already includes baseYaw) + GL11.glRotatef(-angle, 0, 1, 0); // Negative for correct direction + + // Render the beam extending in the +Z direction (like beam trail segments) + renderBeam(beamLength, beamWidth, innerColor, outerColor); + + GL11.glPopMatrix(); + + restoreRenderState(); + } + + private void setupRenderState() { + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glShadeModel(GL11.GL_SMOOTH); + } + + private void restoreRenderState() { + GL11.glShadeModel(GL11.GL_FLAT); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glPopAttrib(); + } + + /** + * Render beam using the same style as the Beam entity trail. + * Three layers: outer glow, middle, inner core. + */ + private void renderBeam(float length, float width, int innerColor, int outerColor) { + // Extract colors + float outerR = ((outerColor >> 16) & 0xFF) / 255.0f; + float outerG = ((outerColor >> 8) & 0xFF) / 255.0f; + float outerB = (outerColor & 0xFF) / 255.0f; + + float innerR = ((innerColor >> 16) & 0xFF) / 255.0f; + float innerG = ((innerColor >> 8) & 0xFF) / 255.0f; + float innerB = (innerColor & 0xFF) / 255.0f; + + // Render from origin (0,0,0) to (0,0,length) in local coords + // After rotation this extends in the correct world direction + + // Outer glow (wider, translucent) - like beam trail + GL11.glDepthMask(false); + renderBeamSegment(0, 0, 0, 0, 0, length, width * 1.8f, outerR, outerG, outerB, 0.3f); + GL11.glDepthMask(true); + + // Middle layer + renderBeamSegment(0, 0, 0, 0, 0, length, width * 1.3f, outerR, outerG, outerB, 0.6f); + + // Inner core (solid) - like beam trail inner + renderBeamSegment(0, 0, 0, 0, 0, length, width * 0.6f, innerR, innerG, innerB, 1.0f); + } + + /** + * Render a beam segment as a 3D rectangular prism. + * Uses the same approach as RenderAbilityBeam.renderBeamRectangle. + */ + private void renderBeamSegment(double x1, double y1, double z1, + double x2, double y2, double z2, + float width, float r, float g, float b, float a) { + // Calculate direction + double dx = x2 - x1; + double dy = y2 - y1; + double dz = z2 - z1; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len < 0.001) return; + + // Normalize direction + dx /= len; + dy /= len; + dz /= len; + + // Calculate perpendicular vectors + double upX = 0, upY = 1, upZ = 0; + if (Math.abs(dy) > 0.9) { + upX = 1; upY = 0; upZ = 0; + } + + // Cross product for horizontal perpendicular + double horzX = dy * upZ - dz * upY; + double horzY = dz * upX - dx * upZ; + double horzZ = dx * upY - dy * upX; + double horzLen = Math.sqrt(horzX * horzX + horzY * horzY + horzZ * horzZ); + if (horzLen > 0) { + horzX /= horzLen; + horzY /= horzLen; + horzZ /= horzLen; + } + + // Cross product for vertical perpendicular + double vertX = dy * horzZ - dz * horzY; + double vertY = dz * horzX - dx * horzZ; + double vertZ = dx * horzY - dy * horzX; + + float halfWidth = width * 0.5f; + + // Calculate 8 corners of the box + double s1x = x1 - horzX * halfWidth - vertX * halfWidth; + double s1y = y1 - horzY * halfWidth - vertY * halfWidth; + double s1z = z1 - horzZ * halfWidth - vertZ * halfWidth; + + double s2x = x1 + horzX * halfWidth - vertX * halfWidth; + double s2y = y1 + horzY * halfWidth - vertY * halfWidth; + double s2z = z1 + horzZ * halfWidth - vertZ * halfWidth; + + double s3x = x1 + horzX * halfWidth + vertX * halfWidth; + double s3y = y1 + horzY * halfWidth + vertY * halfWidth; + double s3z = z1 + horzZ * halfWidth + vertZ * halfWidth; + + double s4x = x1 - horzX * halfWidth + vertX * halfWidth; + double s4y = y1 - horzY * halfWidth + vertY * halfWidth; + double s4z = z1 - horzZ * halfWidth + vertZ * halfWidth; + + double e1x = x2 - horzX * halfWidth - vertX * halfWidth; + double e1y = y2 - horzY * halfWidth - vertY * halfWidth; + double e1z = z2 - horzZ * halfWidth - vertZ * halfWidth; + + double e2x = x2 + horzX * halfWidth - vertX * halfWidth; + double e2y = y2 + horzY * halfWidth - vertY * halfWidth; + double e2z = z2 + horzZ * halfWidth - vertZ * halfWidth; + + double e3x = x2 + horzX * halfWidth + vertX * halfWidth; + double e3y = y2 + horzY * halfWidth + vertY * halfWidth; + double e3z = z2 + horzZ * halfWidth + vertZ * halfWidth; + + double e4x = x2 - horzX * halfWidth + vertX * halfWidth; + double e4y = y2 - horzY * halfWidth + vertY * halfWidth; + double e4z = z2 - horzZ * halfWidth + vertZ * halfWidth; + + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + tess.setColorRGBA_F(r, g, b, a); + + // Bottom face + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e1x, e1y, e1z); + + // Top face + tess.addVertex(s4x, s4y, s4z); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(s3x, s3y, s3z); + + // Left face + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(s4x, s4y, s4z); + + // Right face + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(s3x, s3y, s3z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e2x, e2y, e2z); + + // End cap (tip of beam) + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e4x, e4y, e4z); + + tess.draw(); + } + + @Override + protected ResourceLocation getEntityTexture(Entity entity) { + return null; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/quadric/QuadricRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/quadric/QuadricRenderer.java new file mode 100644 index 000000000..13c994adc --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/quadric/QuadricRenderer.java @@ -0,0 +1,138 @@ +package kamkeel.npcs.client.renderer.quadric; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.renderer.GLAllocation; +import org.lwjgl.opengl.GL11; + +/** + * Base class for rendering GLU quadric shapes (spheres, cylinders, etc.) + * Uses display lists for efficient rendering. + * + * Design inspired by LouisXIV's quadric rendering system. + */ +@SideOnly(Side.CLIENT) +public abstract class QuadricRenderer { + protected int displayList; + private boolean compiled; + + public final float radius; + public final int slices; + public final int stacks; + + // Current render state + protected float scaleX = 1.0f; + protected float scaleY = 1.0f; + protected float scaleZ = 1.0f; + + protected float rotationX = 0.0f; + protected float rotationY = 0.0f; + protected float rotationZ = 0.0f; + + protected int color = 0xFFFFFF; + protected float alpha = 1.0f; + + public QuadricRenderer(float radius, int slices, int stacks) { + this.radius = radius; + this.slices = slices; + this.stacks = stacks; + } + + public QuadricRenderer(float radius, int resolution) { + this(radius, resolution, resolution); + } + + public QuadricRenderer() { + this(0.5f, 16); + } + + /** + * Render the quadric shape with current transform and color settings. + */ + public void render(float partialTicks) { + if (!compiled) { + displayList = GLAllocation.generateDisplayLists(1); + GL11.glNewList(displayList, GL11.GL_COMPILE); + compile(); + GL11.glEndList(); + compiled = true; + } + + GL11.glPushMatrix(); + + // Apply scale + GL11.glScalef(scaleX, scaleY, scaleZ); + + // Apply rotation + GL11.glRotatef(rotationX, 1.0f, 0.0f, 0.0f); + GL11.glRotatef(rotationY, 0.0f, 1.0f, 0.0f); + GL11.glRotatef(rotationZ, 0.0f, 0.0f, 1.0f); + + // Apply color + float r = ((color >> 16) & 0xFF) / 255.0f; + float g = ((color >> 8) & 0xFF) / 255.0f; + float b = (color & 0xFF) / 255.0f; + GL11.glColor4f(r, g, b, alpha); + + GL11.glCallList(displayList); + + GL11.glPopMatrix(); + } + + /** + * Compile the quadric geometry into the display list. + * Called once, then cached. + */ + protected abstract void compile(); + + // Setters for render state + public void setScale(float scale) { + this.scaleX = this.scaleY = this.scaleZ = scale; + } + + public void setScale(float x, float y, float z) { + this.scaleX = x; + this.scaleY = y; + this.scaleZ = z; + } + + public void setRotation(float x, float y, float z) { + this.rotationX = x; + this.rotationY = y; + this.rotationZ = z; + } + + public void setColor(int color) { + this.color = color; + } + + public void setAlpha(float alpha) { + this.alpha = alpha; + } + + /** + * Linear interpolation helper. + */ + public static float lerp(float current, float target, float factor) { + return current + (target - current) * factor; + } + + /** + * Color interpolation helper. + */ + public static int lerpColor(int from, int to, float factor) { + int r1 = (from >> 16) & 0xFF; + int g1 = (from >> 8) & 0xFF; + int b1 = from & 0xFF; + + int r2 = (to >> 16) & 0xFF; + int g2 = (to >> 8) & 0xFF; + int b2 = to & 0xFF; + + int r = (int) (r1 + (r2 - r1) * factor); + int g = (int) (g1 + (g2 - g1) * factor); + int b = (int) (b1 + (b2 - b1) * factor); + + return (r << 16) | (g << 8) | b; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/quadric/SphereRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/quadric/SphereRenderer.java new file mode 100644 index 000000000..dc72779a8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/quadric/SphereRenderer.java @@ -0,0 +1,37 @@ +package kamkeel.npcs.client.renderer.quadric; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import org.lwjgl.util.glu.GLU; +import org.lwjgl.util.glu.Sphere; + +/** + * Renders a sphere using GLU quadrics. + * + * Design inspired by LouisXIV's quadric rendering system. + */ +@SideOnly(Side.CLIENT) +public class SphereRenderer extends QuadricRenderer { + + public SphereRenderer(float radius, int slices, int stacks) { + super(radius, slices, stacks); + } + + public SphereRenderer(float radius, int resolution) { + super(radius, resolution); + } + + public SphereRenderer() { + super(); + } + + @Override + protected void compile() { + Sphere sphere = new Sphere(); + sphere.setTextureFlag(true); // Enable texture coordinates + sphere.setDrawStyle(GLU.GLU_FILL); + sphere.setNormals(GLU.GLU_NONE); // No normals for self-illuminated rendering + sphere.setOrientation(GLU.GLU_OUTSIDE); + sphere.draw(radius, slices, stacks); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 65a4e8ac5..18406c313 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -80,7 +80,6 @@ public abstract class Ability implements IAbility { protected transient AbilityPhase phase = AbilityPhase.IDLE; protected transient int currentTick = 0; - protected transient long cooldownEndTime = 0; protected transient EntityLivingBase currentTarget; protected transient long executionStartTime; protected transient TelegraphInstance telegraphInstance; @@ -138,7 +137,15 @@ public abstract class Ability implements IAbility { } /** - * Backward-compatible signature (vertical knockback is ignored). + * Apply damage to an entity with ability hit event support and vertical knockback. + * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. + * + * @param npc The NPC executing the ability + * @param hitEntity The entity being hit + * @param damage The damage amount + * @param knockback The horizontal knockback + * @param knockbackUp The vertical knockback + * @return true if damage was applied (not cancelled), false if cancelled */ protected boolean applyAbilityDamage(EntityNPCInterface npc, EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { @@ -153,6 +160,7 @@ public abstract class Ability implements IAbility { // Apply damage with potentially modified values float finalDamage = event.getDamage(); float finalKnockback = event.getKnockback(); + float finalKnockbackUp = event.getKnockbackUp(); // Apply damage if (finalDamage > 0) { @@ -160,10 +168,10 @@ public abstract class Ability implements IAbility { } // Apply knockback if any - if (finalKnockback > 0) { + if (finalKnockback > 0 || finalKnockbackUp > 0) { double dx = hitEntity.posX - npc.posX; double dz = hitEntity.posZ - npc.posZ; - applyNpcKnockback(hitEntity, dx, dz, finalKnockback); + applyNpcKnockback(hitEntity, dx, dz, finalKnockback, finalKnockbackUp); } return true; @@ -195,28 +203,31 @@ public abstract class Ability implements IAbility { // Apply damage with potentially modified values float finalDamage = event.getDamage(); float finalKnockback = event.getKnockback(); + float finalKnockbackUp = event.getKnockbackUp(); - // Apply damage + // Apply damage - use NpcDamageSource for consistency if (finalDamage > 0) { - hitEntity.attackEntityFrom(DamageSource.causeMobDamage(npc), finalDamage); + hitEntity.attackEntityFrom(new NpcDamageSource("mob", npc), finalDamage); } // Apply knockback in specified direction - if (finalKnockback > 0) { - applyNpcKnockback(hitEntity, knockbackDirX, knockbackDirZ, finalKnockback); + if (finalKnockback > 0 || finalKnockbackUp > 0) { + applyNpcKnockback(hitEntity, knockbackDirX, knockbackDirZ, finalKnockback, finalKnockbackUp); } return true; } - private void applyNpcKnockback(EntityLivingBase hitEntity, double dirX, double dirZ, float knockback) { + private void applyNpcKnockback(EntityLivingBase hitEntity, double dirX, double dirZ, float knockback, float knockbackUp) { double len = Math.sqrt(dirX * dirX + dirZ * dirZ); - if (len <= 0) { - return; + double x = 0; + double z = 0; + if (len > 0) { + x = (dirX / len) * knockback * 0.5; + z = (dirZ / len) * knockback * 0.5; } - double x = (dirX / len) * knockback * 0.5; - double z = (dirZ / len) * knockback * 0.5; - hitEntity.addVelocity(x, 0.1D, z); + double y = knockbackUp > 0 ? knockbackUp : 0.1D; + hitEntity.addVelocity(x, y, z); hitEntity.velocityChanged = true; } @@ -390,7 +401,7 @@ public abstract class Ability implements IAbility { * @param z Z coordinate * @return The Y coordinate of the ground surface */ - protected double findGroundLevel(World world, double x, double startY, double z) { + public static double findGroundLevel(World world, double x, double startY, double z) { if (world == null) return startY; int blockX = (int) Math.floor(x); @@ -410,6 +421,43 @@ public abstract class Ability implements IAbility { return startY; } + /** + * Calculates offset position near the given coordinates. + * Used to place effects near a target rather than exactly on them. + * + * @param baseX Base X coordinate + * @param baseY Base Y coordinate + * @param baseZ Base Z coordinate + * @param minOffset Minimum offset distance + * @param maxOffset Maximum offset distance + * @param randomOffset Whether to use random offset within range, or fixed at max + * @param random Random instance to use + * @return Array of [x, y, z] with offset applied + */ + public static double[] calculateOffsetPosition(double baseX, double baseY, double baseZ, + float minOffset, float maxOffset, + boolean randomOffset, java.util.Random random) { + if (maxOffset <= 0) { + return new double[]{baseX, baseY, baseZ}; + } + + double offsetDist; + double offsetAngle; + + if (randomOffset) { + offsetDist = minOffset + random.nextDouble() * (maxOffset - minOffset); + offsetAngle = random.nextDouble() * Math.PI * 2; + } else { + offsetDist = maxOffset; + offsetAngle = random.nextDouble() * Math.PI * 2; + } + + double offsetX = Math.cos(offsetAngle) * offsetDist; + double offsetZ = Math.sin(offsetAngle) * offsetDist; + + return new double[]{baseX + offsetX, baseY, baseZ + offsetZ}; + } + /** * Get the current telegraph instance. */ @@ -440,12 +488,12 @@ public abstract class Ability implements IAbility { /** * Tick the ability. Returns true if phase changed. + * Note: Cooldown is managed by DataAbilities when the ability completes. */ public boolean tick() { if (phase == AbilityPhase.IDLE) return false; currentTick++; - AbilityPhase oldPhase = phase; switch (phase) { case WINDUP: @@ -465,7 +513,7 @@ public abstract class Ability implements IAbility { case RECOVERY: if (currentTick >= recoveryTicks) { phase = AbilityPhase.IDLE; - startCooldown(); + // Cooldown is managed by DataAbilities.onAbilityComplete() return true; } break; @@ -483,20 +531,6 @@ public abstract class Ability implements IAbility { telegraphInstance = null; } - /** - * Start cooldown timer - */ - public void startCooldown() { - cooldownEndTime = System.currentTimeMillis() + (cooldownTicks * 50L); - } - - /** - * Check if on cooldown - */ - public boolean isOnCooldown() { - return System.currentTimeMillis() < cooldownEndTime; - } - /** * Check if executing */ @@ -543,7 +577,6 @@ public abstract class Ability implements IAbility { phase = AbilityPhase.IDLE; currentTick = 0; currentTarget = null; - cooldownEndTime = 0; telegraphInstance = null; } @@ -558,19 +591,6 @@ public abstract class Ability implements IAbility { return true; } - /** - * Full eligibility check - */ - public boolean canUse(EntityNPCInterface npc, EntityLivingBase target) { - if (!enabled) return false; - if (isOnCooldown()) return false; - if (isExecuting()) return false; - - float distance = npc.getDistanceToEntity(target); - if (distance < minRange || distance > maxRange) return false; - - return checkConditions(npc, target); - } // ═══════════════════════════════════════════════════════════════════ // NBT (config only, execution state is transient) @@ -811,22 +831,6 @@ public abstract class Ability implements IAbility { this.activeColor = activeColor; } - /** - * @deprecated Use getWindUpColor() instead - */ - @Deprecated - public int getTelegraphColor() { - return windUpColor; - } - - /** - * @deprecated Use setWindUpColor() instead - */ - @Deprecated - public void setTelegraphColor(int telegraphColor) { - this.windUpColor = telegraphColor; - } - public String getWindUpSound() { return windUpSound; } @@ -859,38 +863,6 @@ public abstract class Ability implements IAbility { this.activeAnimationId = activeAnimationId; } - /** - * @deprecated Use getWindUpSound() instead - */ - @Deprecated - public String getCastSound() { - return windUpSound; - } - - /** - * @deprecated Use setWindUpSound() instead - */ - @Deprecated - public void setCastSound(String castSound) { - this.windUpSound = castSound; - } - - /** - * @deprecated Use getWindUpAnimationId() instead - */ - @Deprecated - public int getAnimationId() { - return windUpAnimationId; - } - - /** - * @deprecated Use setWindUpAnimationId() instead - */ - @Deprecated - public void setAnimationId(int animationId) { - this.windUpAnimationId = animationId; - } - public boolean isShowTelegraph() { return showTelegraph; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 1a9bbd522..41c20a085 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -1,6 +1,6 @@ package kamkeel.npcs.controllers.data.ability; -import kamkeel.npcs.controllers.data.ability.type.AbilityBeam; +import kamkeel.npcs.controllers.data.ability.type.AbilitySweeper; import kamkeel.npcs.controllers.data.ability.type.AbilityCharge; import kamkeel.npcs.controllers.data.ability.type.AbilityCutter; import kamkeel.npcs.controllers.data.ability.type.AbilityDash; @@ -9,6 +9,9 @@ import kamkeel.npcs.controllers.data.ability.type.AbilityHazard; import kamkeel.npcs.controllers.data.ability.type.AbilityHeal; import kamkeel.npcs.controllers.data.ability.type.AbilityHeavyHit; import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; +import kamkeel.npcs.controllers.data.ability.type.AbilityDisc; +import kamkeel.npcs.controllers.data.ability.type.AbilityLaserShot; +import kamkeel.npcs.controllers.data.ability.type.AbilityEnergyBeam; import kamkeel.npcs.controllers.data.ability.type.AbilityProjectile; import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave; import kamkeel.npcs.controllers.data.ability.type.AbilitySlam; @@ -296,9 +299,12 @@ public class AbilityController implements IAbilityHandler { registerType("cnpc:cutter", AbilityCutter::new); // Ranged/Projectile abilities - registerType("cnpc:beam", AbilityBeam::new); + registerType("cnpc:sweeper", AbilitySweeper::new); registerType("cnpc:projectile", AbilityProjectile::new); registerType("cnpc:orb", AbilityOrb::new); + registerType("cnpc:disc", AbilityDisc::new); + registerType("cnpc:laser_shot", AbilityLaserShot::new); + registerType("cnpc:beam", AbilityEnergyBeam::new); // Movement abilities registerType("cnpc:charge", AbilityCharge::new); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java b/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java index 7a2e43b64..bd1bb6a47 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java @@ -22,20 +22,5 @@ public enum TargetingMode { /** * AOE centered on target */ - AOE_TARGET, - - /** - * Random nearby enemy - */ - RANDOM_ENEMY, - - /** - * Lowest HP enemy in range - */ - LOWEST_HP, - - /** - * Highest threat enemy - */ - HIGHEST_THREAT + AOE_TARGET } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java deleted file mode 100644 index 3c448d0b6..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeam.java +++ /dev/null @@ -1,335 +0,0 @@ -package kamkeel.npcs.controllers.data.ability.type; - -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; -import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.telegraph.TelegraphType; -import net.minecraft.entity.Entity; -import net.minecraft.entity.EntityLivingBase; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.AxisAlignedBB; -import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityBeam; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.entity.EntityNPCInterface; - -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -/** - * Beam ability: Continuous line attack that damages all targets in a line. - * Can sweep across an arc or remain fixed. - */ -public class AbilityBeam extends Ability { - - // Type-specific parameters - private float beamLength = 10.0f; - private float beamWidth = 1.0f; - private float damage = 5.0f; - private int damageInterval = 5; - private boolean piercing = true; - private float sweepAngle = 0.0f; - private float sweepSpeed = 2.0f; - private boolean sweepBackAndForth = true; - private boolean lockOnTarget = false; - - // Runtime state (transient) - private transient float currentSweepAngle = 0; - private transient boolean sweepingRight = true; - private transient int ticksSinceDamage = 0; - private transient Set hitThisTick = new HashSet<>(); - - public AbilityBeam() { - this.typeId = "ability.cnpc.beam"; - this.name = "Beam"; - this.targetingMode = TargetingMode.AGGRO_TARGET; - this.maxRange = 15.0f; - this.minRange = 0.0f; - this.lockMovement = true; - this.cooldownTicks = 200; - this.windUpTicks = 60; - this.activeTicks = 80; - this.recoveryTicks = 20; - // No telegraph for beam - it's a continuous line attack - this.telegraphType = TelegraphType.NONE; - this.showTelegraph = false; - } - - @Override - public boolean hasTypeSettings() { - return true; - } - - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityBeam(this, callback); - } - - @Override - public boolean isTargetingModeLocked() { - return true; - } - - @Override - public TargetingMode[] getAllowedTargetingModes() { - return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; - } - - @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - // Initialize beam - currentSweepAngle = -sweepAngle / 2; - sweepingRight = true; - ticksSinceDamage = damageInterval; // Deal damage immediately - hitThisTick.clear(); - } - - @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - if (world.isRemote) return; - - hitThisTick.clear(); - ticksSinceDamage++; - - // Update sweep angle - if (sweepAngle > 0) { - if (sweepBackAndForth) { - if (sweepingRight) { - currentSweepAngle += sweepSpeed; - if (currentSweepAngle >= sweepAngle / 2) { - sweepingRight = false; - } - } else { - currentSweepAngle -= sweepSpeed; - if (currentSweepAngle <= -sweepAngle / 2) { - sweepingRight = true; - } - } - } else { - currentSweepAngle += sweepSpeed; - if (currentSweepAngle >= sweepAngle) { - currentSweepAngle = 0; - } - } - } - - // Calculate beam direction - float baseYaw = npc.rotationYaw; - - // If lock on target, point at target - if (lockOnTarget && target != null) { - double dx = target.posX - npc.posX; - double dz = target.posZ - npc.posZ; - baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); - } - - float beamYaw = baseYaw + currentSweepAngle; - float yawRad = (float) Math.toRadians(beamYaw); - - // Calculate beam start position - double startX = npc.posX; - double startY = npc.posY + npc.getEyeHeight() * 0.8; - double startZ = npc.posZ; - - double dirX = -Math.sin(yawRad); - double dirZ = Math.cos(yawRad); - - // Check for entities along the beam - if (ticksSinceDamage >= damageInterval) { - ticksSinceDamage = 0; - - // Sample points along the beam - int samples = (int) (beamLength / 0.5); - for (int i = 1; i <= samples; i++) { - double progress = (double) i / samples; - double checkX = startX + dirX * beamLength * progress; - double checkY = startY; - double checkZ = startZ + dirZ * beamLength * progress; - - // Find entities at this point - AxisAlignedBB checkBox = AxisAlignedBB.getBoundingBox( - checkX - beamWidth, checkY - 1, checkZ - beamWidth, - checkX + beamWidth, checkY + 2, checkZ + beamWidth - ); - - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, checkBox); - - for (Entity entity : entities) { - if (!(entity instanceof EntityLivingBase)) continue; - if (entity == npc) continue; - if (hitThisTick.contains(entity.getEntityId())) continue; - - EntityLivingBase livingEntity = (EntityLivingBase) entity; - - // Check if actually in beam path - if (isInBeamPath(livingEntity, startX, startY, startZ, dirX, dirZ)) { - hitThisTick.add(entity.getEntityId()); - // Apply damage with scripted event support (no knockback for beam) - applyAbilityDamage(npc, livingEntity, damage, 0); - - if (!piercing) { - return; // Stop at first hit - } - } - } - } - } - - // Spawn beam particles - spawnBeamParticles(world, startX, startY, startZ, dirX, dirZ); - } - - private boolean isInBeamPath(EntityLivingBase entity, double startX, double startY, double startZ, - double dirX, double dirZ) { - // Project entity position onto beam line - double entityX = entity.posX - startX; - double entityZ = entity.posZ - startZ; - - // Dot product to find projection distance - double projectionDist = entityX * dirX + entityZ * dirZ; - - // Check if within beam length - if (projectionDist < 0 || projectionDist > beamLength) { - return false; - } - - // Calculate perpendicular distance - double projX = dirX * projectionDist; - double projZ = dirZ * projectionDist; - double perpX = entityX - projX; - double perpZ = entityZ - projZ; - double perpDist = Math.sqrt(perpX * perpX + perpZ * perpZ); - - // Check if within beam width - return perpDist <= beamWidth + entity.width / 2; - } - - private void spawnBeamParticles(World world, double startX, double startY, double startZ, - double dirX, double dirZ) { - // Spawn particles along beam line - for (int i = 0; i < beamLength; i++) { - double px = startX + dirX * i; - double pz = startZ + dirZ * i; - world.spawnParticle("flame", px, startY, pz, 0, 0, 0); - } - } - - @Override - public void reset() { - super.reset(); - currentSweepAngle = 0; - sweepingRight = true; - ticksSinceDamage = 0; - hitThisTick.clear(); - } - - @Override - public float getTelegraphRadius() { - return beamLength; - } - - @Override - public void writeTypeNBT(NBTTagCompound nbt) { - nbt.setFloat("beamLength", beamLength); - nbt.setFloat("beamWidth", beamWidth); - nbt.setFloat("damage", damage); - nbt.setInteger("damageInterval", damageInterval); - nbt.setBoolean("piercing", piercing); - nbt.setFloat("sweepAngle", sweepAngle); - nbt.setFloat("sweepSpeed", sweepSpeed); - nbt.setBoolean("sweepBackAndForth", sweepBackAndForth); - nbt.setBoolean("lockOnTarget", lockOnTarget); - } - - @Override - public void readTypeNBT(NBTTagCompound nbt) { - this.beamLength = nbt.hasKey("beamLength") ? nbt.getFloat("beamLength") : 10.0f; - this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 1.0f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 5.0f; - this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 5; - this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing"); - this.sweepAngle = nbt.hasKey("sweepAngle") ? nbt.getFloat("sweepAngle") : 0.0f; - this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 2.0f; - this.sweepBackAndForth = !nbt.hasKey("sweepBackAndForth") || nbt.getBoolean("sweepBackAndForth"); - this.lockOnTarget = nbt.hasKey("lockOnTarget") && nbt.getBoolean("lockOnTarget"); - } - - // Getters & Setters - public float getBeamLength() { - return beamLength; - } - - public void setBeamLength(float beamLength) { - this.beamLength = beamLength; - } - - public float getBeamWidth() { - return beamWidth; - } - - public void setBeamWidth(float beamWidth) { - this.beamWidth = beamWidth; - } - - public float getDamage() { - return damage; - } - - public void setDamage(float damage) { - this.damage = damage; - } - - public int getDamageInterval() { - return damageInterval; - } - - public void setDamageInterval(int damageInterval) { - this.damageInterval = damageInterval; - } - - public boolean isPiercing() { - return piercing; - } - - public void setPiercing(boolean piercing) { - this.piercing = piercing; - } - - public float getSweepAngle() { - return sweepAngle; - } - - public void setSweepAngle(float sweepAngle) { - this.sweepAngle = sweepAngle; - } - - public float getSweepSpeed() { - return sweepSpeed; - } - - public void setSweepSpeed(float sweepSpeed) { - this.sweepSpeed = sweepSpeed; - } - - public boolean isSweepBackAndForth() { - return sweepBackAndForth; - } - - public void setSweepBackAndForth(boolean sweepBackAndForth) { - this.sweepBackAndForth = sweepBackAndForth; - } - - public boolean isLockOnTarget() { - return lockOnTarget; - } - - public void setLockOnTarget(boolean lockOnTarget) { - this.lockOnTarget = lockOnTarget; - } -} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 6c43ea1cb..696367ce8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -49,7 +49,7 @@ public class AbilityCharge extends Ability { this.cooldownTicks = 80; this.windUpTicks = 20; this.activeTicks = 40; - this.recoveryTicks = 20; + this.recoveryTicks = 10; // LINE telegraph showing charge path this.telegraphType = TelegraphType.LINE; this.showTelegraph = true; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java new file mode 100644 index 000000000..97d69e19c --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -0,0 +1,237 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityDisc; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.DamageSource; +import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDisc; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Disc ability: Spawns a flat spinning disc projectile. + * Has optional boomerang behavior to return to owner. + */ +public class AbilityDisc extends Ability { + + // Movement properties + private float speed = 0.6f; + private float discRadius = 1.0f; + private float discThickness = 0.2f; + private float maxDistance = 35.0f; + private int maxLifetime = 250; + + // Combat properties + private float damage = 8.0f; + private float knockback = 1.2f; + private float knockbackUp = 0.15f; + + // Homing properties + private boolean homing = true; + private float homingStrength = 0.12f; + private float homingRange = 18.0f; + + // Boomerang properties + private boolean boomerang = false; + private int boomerangDelay = 40; + + // Explosion properties + private boolean explosive = false; + private float explosionRadius = 3.0f; + private float explosionDamageFalloff = 0.5f; + + // Effect properties + private int stunDuration = 0; + private int slowDuration = 0; + private int slowLevel = 0; + + // Visual properties + private int innerColor = 0xFFFFFF; + private int outerColor = 0xFF8800; + private float outerColorWidth = 1.8f; + private boolean outerColorEnabled = true; + private float rotationSpeed = 5.0f; + + public AbilityDisc() { + this.typeId = "ability.cnpc.disc"; + this.name = "Disc"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 30.0f; + this.minRange = 5.0f; + this.cooldownTicks = 80; + this.windUpTicks = 20; + this.activeTicks = 1; + this.recoveryTicks = 10; + this.telegraphType = TelegraphType.POINT; + this.showTelegraph = false; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + @SideOnly(Side.CLIENT) + public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { + return new SubGuiAbilityDisc(this, callback); + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + if (world.isRemote) return; + + double spawnX = npc.posX; + double spawnY = npc.posY + npc.getEyeHeight() * 0.8; + double spawnZ = npc.posZ; + + EntityAbilityDisc disc = new EntityAbilityDisc( + world, npc, target, + spawnX, spawnY, spawnZ, + discRadius, discThickness, innerColor, outerColor, rotationSpeed, + damage, knockback, knockbackUp, + speed, homing, homingStrength, homingRange, + boomerang, boomerangDelay, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime + ); + + world.spawnEntityInWorld(disc); + } + + @Override + public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + } + + @Override + public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + } + + @Override + public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("speed", speed); + nbt.setFloat("discRadius", discRadius); + nbt.setFloat("discThickness", discThickness); + nbt.setFloat("maxDistance", maxDistance); + nbt.setInteger("maxLifetime", maxLifetime); + nbt.setFloat("damage", damage); + nbt.setFloat("knockback", knockback); + nbt.setFloat("knockbackUp", knockbackUp); + nbt.setBoolean("homing", homing); + nbt.setFloat("homingStrength", homingStrength); + nbt.setFloat("homingRange", homingRange); + nbt.setBoolean("boomerang", boomerang); + nbt.setInteger("boomerangDelay", boomerangDelay); + nbt.setBoolean("explosive", explosive); + nbt.setFloat("explosionRadius", explosionRadius); + nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff); + nbt.setInteger("stunDuration", stunDuration); + nbt.setInteger("slowDuration", slowDuration); + nbt.setInteger("slowLevel", slowLevel); + nbt.setInteger("innerColor", innerColor); + nbt.setInteger("outerColor", outerColor); + nbt.setFloat("outerColorWidth", outerColorWidth); + nbt.setBoolean("outerColorEnabled", outerColorEnabled); + nbt.setFloat("rotationSpeed", rotationSpeed); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + this.speed = nbt.hasKey("speed") ? nbt.getFloat("speed") : 0.6f; + this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f; + this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; + this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 35.0f; + this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 250; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; + this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.2f; + this.knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.15f; + this.homing = !nbt.hasKey("homing") || nbt.getBoolean("homing"); + this.homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.12f; + this.homingRange = nbt.hasKey("homingRange") ? nbt.getFloat("homingRange") : 18.0f; + this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang"); + this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40; + this.explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); + this.explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 3.0f; + this.explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f; + this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; + this.slowDuration = nbt.hasKey("slowDuration") ? nbt.getInteger("slowDuration") : 0; + this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; + this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; + this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF8800; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); + this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 8.0f; + } + + // Getters & Setters + public float getSpeed() { return speed; } + public void setSpeed(float speed) { this.speed = speed; } + public float getDiscRadius() { return discRadius; } + public void setDiscRadius(float discRadius) { this.discRadius = discRadius; } + public float getDiscThickness() { return discThickness; } + public void setDiscThickness(float discThickness) { this.discThickness = discThickness; } + public float getMaxDistance() { return maxDistance; } + public void setMaxDistance(float maxDistance) { this.maxDistance = maxDistance; } + public int getMaxLifetime() { return maxLifetime; } + public void setMaxLifetime(int maxLifetime) { this.maxLifetime = maxLifetime; } + public float getDamage() { return damage; } + public void setDamage(float damage) { this.damage = damage; } + public float getKnockback() { return knockback; } + public void setKnockback(float knockback) { this.knockback = knockback; } + public float getKnockbackUp() { return knockbackUp; } + public void setKnockbackUp(float knockbackUp) { this.knockbackUp = knockbackUp; } + public boolean isHoming() { return homing; } + public void setHoming(boolean homing) { this.homing = homing; } + public float getHomingStrength() { return homingStrength; } + public void setHomingStrength(float homingStrength) { this.homingStrength = homingStrength; } + public float getHomingRange() { return homingRange; } + public void setHomingRange(float homingRange) { this.homingRange = homingRange; } + public boolean isBoomerang() { return boomerang; } + public void setBoomerang(boolean boomerang) { this.boomerang = boomerang; } + public int getBoomerangDelay() { return boomerangDelay; } + public void setBoomerangDelay(int boomerangDelay) { this.boomerangDelay = boomerangDelay; } + public boolean isExplosive() { return explosive; } + public void setExplosive(boolean explosive) { this.explosive = explosive; } + public float getExplosionRadius() { return explosionRadius; } + public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.explosionDamageFalloff = explosionDamageFalloff; } + public int getStunDuration() { return stunDuration; } + public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public int getSlowDuration() { return slowDuration; } + public void setSlowDuration(int slowDuration) { this.slowDuration = slowDuration; } + public int getSlowLevel() { return slowLevel; } + public void setSlowLevel(int slowLevel) { this.slowLevel = slowLevel; } + public int getInnerColor() { return innerColor; } + public void setInnerColor(int innerColor) { this.innerColor = innerColor; } + public int getOuterColor() { return outerColor; } + public void setOuterColor(int outerColor) { this.outerColor = outerColor; } + public float getOuterColorWidth() { return outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; } + public boolean isOuterColorEnabled() { return outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } + public float getRotationSpeed() { return rotationSpeed; } + public void setRotationSpeed(float rotationSpeed) { this.rotationSpeed = rotationSpeed; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java new file mode 100644 index 000000000..2790eafd8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -0,0 +1,227 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityBeam; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.DamageSource; +import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityEnergyBeam; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Energy Beam ability: A homing head with a trailing path. + * The beam stays attached to the origin and curves as it homes in on target. + * Cannot cross over itself (self-intersection prevention). + */ +public class AbilityEnergyBeam extends Ability { + + // Movement properties + private float speed = 0.4f; + private float maxDistance = 25.0f; + private int maxLifetime = 200; + + // Shape properties + private float beamWidth = 0.4f; + private float headSize = 0.6f; + + // Combat properties + private float damage = 10.0f; + private float knockback = 1.5f; + private float knockbackUp = 0.2f; + + // Homing properties + private boolean homing = true; + private float homingStrength = 0.1f; + private float homingRange = 15.0f; + + // Explosion properties + private boolean explosive = false; + private float explosionRadius = 4.0f; + private float explosionDamageFalloff = 0.5f; + + // Effect properties + private int stunDuration = 0; + private int slowDuration = 0; + private int slowLevel = 0; + + // Visual properties + private int innerColor = 0xFFFFFF; + private int outerColor = 0x00AAFF; + private float outerColorWidth = 1.8f; + private boolean outerColorEnabled = true; + private float rotationSpeed = 6.0f; + + public AbilityEnergyBeam() { + this.typeId = "ability.cnpc.beam"; + this.name = "Beam"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 20.0f; + this.minRange = 5.0f; + this.cooldownTicks = 150; + this.windUpTicks = 40; + this.activeTicks = 200; // Match maxLifetime to keep NPC locked while beam is active + this.recoveryTicks = 5; + this.telegraphType = TelegraphType.POINT; + this.showTelegraph = false; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + @SideOnly(Side.CLIENT) + public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { + return new SubGuiAbilityEnergyBeam(this, callback); + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + if (world.isRemote) return; + + double spawnX = npc.posX; + double spawnY = npc.posY + npc.getEyeHeight() * 0.7; + double spawnZ = npc.posZ; + + EntityAbilityBeam beam = new EntityAbilityBeam( + world, npc, target, + spawnX, spawnY, spawnZ, + beamWidth, headSize, innerColor, outerColor, rotationSpeed, + damage, knockback, knockbackUp, + speed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime + ); + + world.spawnEntityInWorld(beam); + } + + @Override + public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + } + + @Override + public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + } + + @Override + public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("speed", speed); + nbt.setFloat("maxDistance", maxDistance); + nbt.setInteger("maxLifetime", maxLifetime); + nbt.setFloat("beamWidth", beamWidth); + nbt.setFloat("headSize", headSize); + nbt.setFloat("damage", damage); + nbt.setFloat("knockback", knockback); + nbt.setFloat("knockbackUp", knockbackUp); + nbt.setBoolean("homing", homing); + nbt.setFloat("homingStrength", homingStrength); + nbt.setFloat("homingRange", homingRange); + nbt.setBoolean("explosive", explosive); + nbt.setFloat("explosionRadius", explosionRadius); + nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff); + nbt.setInteger("stunDuration", stunDuration); + nbt.setInteger("slowDuration", slowDuration); + nbt.setInteger("slowLevel", slowLevel); + nbt.setInteger("innerColor", innerColor); + nbt.setInteger("outerColor", outerColor); + nbt.setFloat("outerColorWidth", outerColorWidth); + nbt.setBoolean("outerColorEnabled", outerColorEnabled); + nbt.setFloat("rotationSpeed", rotationSpeed); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + this.speed = nbt.hasKey("speed") ? nbt.getFloat("speed") : 0.4f; + this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 25.0f; + this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200; + this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f; + this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 10.0f; + this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.5f; + this.knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.2f; + this.homing = !nbt.hasKey("homing") || nbt.getBoolean("homing"); + this.homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.1f; + this.homingRange = nbt.hasKey("homingRange") ? nbt.getFloat("homingRange") : 15.0f; + this.explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); + this.explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 4.0f; + this.explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f; + this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; + this.slowDuration = nbt.hasKey("slowDuration") ? nbt.getInteger("slowDuration") : 0; + this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; + this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; + this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0x00AAFF; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); + this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 6.0f; + } + + // Getters & Setters + public float getSpeed() { return speed; } + public void setSpeed(float speed) { this.speed = speed; } + public float getMaxDistance() { return maxDistance; } + public void setMaxDistance(float maxDistance) { this.maxDistance = maxDistance; } + public int getMaxLifetime() { return maxLifetime; } + public void setMaxLifetime(int maxLifetime) { this.maxLifetime = maxLifetime; } + public float getBeamWidth() { return beamWidth; } + public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; } + public float getHeadSize() { return headSize; } + public void setHeadSize(float headSize) { this.headSize = headSize; } + public float getDamage() { return damage; } + public void setDamage(float damage) { this.damage = damage; } + public float getKnockback() { return knockback; } + public void setKnockback(float knockback) { this.knockback = knockback; } + public float getKnockbackUp() { return knockbackUp; } + public void setKnockbackUp(float knockbackUp) { this.knockbackUp = knockbackUp; } + public boolean isHoming() { return homing; } + public void setHoming(boolean homing) { this.homing = homing; } + public float getHomingStrength() { return homingStrength; } + public void setHomingStrength(float homingStrength) { this.homingStrength = homingStrength; } + public float getHomingRange() { return homingRange; } + public void setHomingRange(float homingRange) { this.homingRange = homingRange; } + public boolean isExplosive() { return explosive; } + public void setExplosive(boolean explosive) { this.explosive = explosive; } + public float getExplosionRadius() { return explosionRadius; } + public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.explosionDamageFalloff = explosionDamageFalloff; } + public int getStunDuration() { return stunDuration; } + public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public int getSlowDuration() { return slowDuration; } + public void setSlowDuration(int slowDuration) { this.slowDuration = slowDuration; } + public int getSlowLevel() { return slowLevel; } + public void setSlowLevel(int slowLevel) { this.slowLevel = slowLevel; } + public int getInnerColor() { return innerColor; } + public void setInnerColor(int innerColor) { this.innerColor = innerColor; } + public int getOuterColor() { return outerColor; } + public void setOuterColor(int outerColor) { this.outerColor = outerColor; } + public float getOuterColorWidth() { return outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; } + public boolean isOuterColorEnabled() { return outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } + public float getRotationSpeed() { return rotationSpeed; } + public void setRotationSpeed(float rotationSpeed) { this.rotationSpeed = rotationSpeed; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index fa46c2341..6a98b643a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -51,7 +51,7 @@ public class AbilityGuard extends Ability { this.cooldownTicks = 120; this.windUpTicks = 10; this.activeTicks = 60; - this.recoveryTicks = 20; + this.recoveryTicks = 10; this.interruptible = false; // Hard to interrupt while guarding // No telegraph for guard - it's a defensive stance this.telegraphType = TelegraphType.NONE; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 028259826..3161c5dbe 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -146,34 +146,6 @@ public class AbilityHazard extends Ability { return instance; } - /** - * Calculates offset position near the given coordinates. - * Used to place hazard near target rather than exactly on them. - */ - private double[] calculateOffsetPosition(double baseX, double baseY, double baseZ) { - if (maxOffset <= 0) { - return new double[]{baseX, baseY, baseZ}; - } - - double offsetDist; - double offsetAngle; - - if (randomOffset) { - // Random offset within min-max range - offsetDist = minOffset + RANDOM.nextDouble() * (maxOffset - minOffset); - offsetAngle = RANDOM.nextDouble() * Math.PI * 2; - } else { - // Fixed offset (use max, random angle) - offsetDist = maxOffset; - offsetAngle = RANDOM.nextDouble() * Math.PI * 2; - } - - double offsetX = Math.cos(offsetAngle) * offsetDist; - double offsetZ = Math.sin(offsetAngle) * offsetDist; - - return new double[]{baseX + offsetX, baseY, baseZ + offsetZ}; - } - @Override public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { // AT_CASTER locks immediately, AT_TARGET follows during windup and locks on execute @@ -214,12 +186,18 @@ public class AbilityHazard extends Ability { case AT_TARGET: // Use telegraph position if available, apply offset if (telegraph != null) { - double[] pos = calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ()); + double[] pos = Ability.calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ(), + minOffset, maxOffset, randomOffset, RANDOM); zoneX = pos[0]; zoneY = pos[1]; zoneZ = pos[2]; + // Update telegraph to show actual hazard position + telegraph.setX(zoneX); + telegraph.setY(zoneY); + telegraph.setZ(zoneZ); } else if (target != null) { - double[] pos = calculateOffsetPosition(target.posX, target.posY, target.posZ); + double[] pos = Ability.calculateOffsetPosition(target.posX, target.posY, target.posZ, + minOffset, maxOffset, randomOffset, RANDOM); zoneX = pos[0]; zoneY = pos[1]; zoneZ = pos[2]; @@ -474,18 +452,6 @@ public class AbilityHazard extends Ability { this.damagePerSecond = damagePerSecond; } - /** @deprecated Use {@link #getDamagePerSecond()} instead */ - @Deprecated - public float getDamagePerTick() { - return damagePerSecond; - } - - /** @deprecated Use {@link #setDamagePerSecond(float)} instead */ - @Deprecated - public void setDamagePerTick(float damage) { - this.damagePerSecond = damage; - } - public int getDamageInterval() { return damageInterval; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index 834b9c055..2e23a1e56 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -76,16 +76,24 @@ public class AbilityHeal extends Ability { healedAllies.clear(); + // Always find allies if we're healing them (needed for both instant and HoT) + if (healAllies && healRadius > 0) { + findAlliesInRadius(npc, world); + } + if (instantHeal) { + // Instant heal - apply all healing now if (healSelf) { healEntity(npc); spawnHealParticles(world, npc); } - if (healAllies && healRadius > 0) { - healAlliesInRadius(npc, world); + for (EntityLivingBase ally : healedAllies) { + healEntity(ally); + spawnHealParticles(world, ally); } } + // If not instant, HoT healing is applied in onActiveTick } @Override @@ -104,6 +112,9 @@ public class AbilityHeal extends Ability { selfTickHeal += (npc.getMaxHealth() * healPercent / (float) activeTicks) * 10; } npc.heal(selfTickHeal); + if (tick % 20 == 0) { + spawnHealParticles(world, npc); + } } if (healAllies && healRadius > 0) { @@ -115,21 +126,19 @@ public class AbilityHeal extends Ability { allyTickHeal += (ally.getMaxHealth() * healPercent / (float) activeTicks) * 10; } ally.heal(allyTickHeal); + if (tick % 20 == 0) { + spawnHealParticles(world, ally); + } } } } } } - private void healEntity(EntityLivingBase entity) { - float totalHeal = healAmount; - if (healPercent > 0) { - totalHeal += entity.getMaxHealth() * healPercent; - } - entity.heal(totalHeal); - } - - private void healAlliesInRadius(EntityNPCInterface npc, World world) { + /** + * Find all allies in radius and add them to healedAllies list. + */ + private void findAlliesInRadius(EntityNPCInterface npc, World world) { AxisAlignedBB aabb = AxisAlignedBB.getBoundingBox( npc.posX - healRadius, npc.posY - 2, npc.posZ - healRadius, npc.posX + healRadius, npc.posY + 3, npc.posZ + healRadius @@ -148,14 +157,20 @@ public class AbilityHeal extends Ability { if (living instanceof EntityNPCInterface) { float dist = npc.getDistanceToEntity(living); if (dist <= healRadius) { - healEntity(living); healedAllies.add(living); - spawnHealParticles(world, living); } } } } + private void healEntity(EntityLivingBase entity) { + float totalHeal = healAmount; + if (healPercent > 0) { + totalHeal += entity.getMaxHealth() * healPercent; + } + entity.heal(totalHeal); + } + private void spawnHealParticles(World world, EntityLivingBase entity) { for (int i = 0; i < 10; i++) { double offsetX = (world.rand.nextDouble() - 0.5) * entity.width; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index 408120694..abdcc6f86 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -37,7 +37,7 @@ public class AbilityHeavyHit extends Ability { this.cooldownTicks = 80; this.windUpTicks = 30; this.activeTicks = 5; - this.recoveryTicks = 20; + this.recoveryTicks = 10; this.telegraphType = TelegraphType.POINT; this.showTelegraph = false; this.windUpSound = "random.anvil_use"; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java new file mode 100644 index 000000000..2b224e400 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -0,0 +1,202 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityLaser; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.DamageSource; +import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityLaserShot; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Laser Shot ability: Fast expanding thin line that pierces through targets. + * Travels in a straight line from origin, damaging all entities it passes through. + */ +public class AbilityLaserShot extends Ability { + + // Laser properties + private float laserWidth = 0.3f; + private float expansionSpeed = 3.0f; + private int lingerTicks = 8; + private float maxDistance = 40.0f; + private int maxLifetime = 100; + + // Combat properties + private float damage = 6.0f; + private float knockback = 0.5f; + private float knockbackUp = 0.05f; + + // Explosion properties + private boolean explosive = false; + private float explosionRadius = 2.0f; + private float explosionDamageFalloff = 0.5f; + + // Effect properties + private int stunDuration = 0; + private int slowDuration = 0; + private int slowLevel = 0; + + // Visual properties + private int innerColor = 0xFFFFFF; + private int outerColor = 0xFF0000; + + public AbilityLaserShot() { + this.typeId = "ability.cnpc.laser_shot"; + this.name = "Laser Shot"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 35.0f; + this.minRange = 3.0f; + this.cooldownTicks = 60; + this.windUpTicks = 15; + this.activeTicks = 1; + this.recoveryTicks = 5; + this.telegraphType = TelegraphType.LINE; + this.showTelegraph = true; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + @SideOnly(Side.CLIENT) + public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { + return new SubGuiAbilityLaserShot(this, callback); + } + + @Override + public boolean isTargetingModeLocked() { + return false; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public float getTelegraphLength() { + return maxDistance; + } + + @Override + public float getTelegraphWidth() { + return laserWidth * 2.0f; // Make telegraph slightly wider for visibility + } + + @Override + public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + if (world.isRemote) return; + + double spawnX = npc.posX; + double spawnY = npc.posY + npc.getEyeHeight() * 0.8; + double spawnZ = npc.posZ; + + EntityAbilityLaser laser = new EntityAbilityLaser( + world, npc, target, + spawnX, spawnY, spawnZ, + laserWidth, innerColor, outerColor, + damage, knockback, knockbackUp, + expansionSpeed, lingerTicks, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime + ); + + world.spawnEntityInWorld(laser); + } + + @Override + public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + } + + @Override + public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + } + + @Override + public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("laserWidth", laserWidth); + nbt.setFloat("expansionSpeed", expansionSpeed); + nbt.setInteger("lingerTicks", lingerTicks); + nbt.setFloat("maxDistance", maxDistance); + nbt.setInteger("maxLifetime", maxLifetime); + nbt.setFloat("damage", damage); + nbt.setFloat("knockback", knockback); + nbt.setFloat("knockbackUp", knockbackUp); + nbt.setBoolean("explosive", explosive); + nbt.setFloat("explosionRadius", explosionRadius); + nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff); + nbt.setInteger("stunDuration", stunDuration); + nbt.setInteger("slowDuration", slowDuration); + nbt.setInteger("slowLevel", slowLevel); + nbt.setInteger("innerColor", innerColor); + nbt.setInteger("outerColor", outerColor); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + this.laserWidth = nbt.hasKey("laserWidth") ? nbt.getFloat("laserWidth") : 0.3f; + this.expansionSpeed = nbt.hasKey("expansionSpeed") ? nbt.getFloat("expansionSpeed") : 3.0f; + this.lingerTicks = nbt.hasKey("lingerTicks") ? nbt.getInteger("lingerTicks") : 8; + this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 40.0f; + this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 100; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 6.0f; + this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f; + this.knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.05f; + this.explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); + this.explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 2.0f; + this.explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f; + this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; + this.slowDuration = nbt.hasKey("slowDuration") ? nbt.getInteger("slowDuration") : 0; + this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; + this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; + this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF0000; + } + + // Getters & Setters + public float getLaserWidth() { return laserWidth; } + public void setLaserWidth(float laserWidth) { this.laserWidth = laserWidth; } + public float getExpansionSpeed() { return expansionSpeed; } + public void setExpansionSpeed(float expansionSpeed) { this.expansionSpeed = expansionSpeed; } + public int getLingerTicks() { return lingerTicks; } + public void setLingerTicks(int lingerTicks) { this.lingerTicks = lingerTicks; } + public float getMaxDistance() { return maxDistance; } + public void setMaxDistance(float maxDistance) { this.maxDistance = maxDistance; } + public int getMaxLifetime() { return maxLifetime; } + public void setMaxLifetime(int maxLifetime) { this.maxLifetime = maxLifetime; } + public float getDamage() { return damage; } + public void setDamage(float damage) { this.damage = damage; } + public float getKnockback() { return knockback; } + public void setKnockback(float knockback) { this.knockback = knockback; } + public float getKnockbackUp() { return knockbackUp; } + public void setKnockbackUp(float knockbackUp) { this.knockbackUp = knockbackUp; } + public boolean isExplosive() { return explosive; } + public void setExplosive(boolean explosive) { this.explosive = explosive; } + public float getExplosionRadius() { return explosionRadius; } + public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.explosionDamageFalloff = explosionDamageFalloff; } + public int getStunDuration() { return stunDuration; } + public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } + public int getSlowDuration() { return slowDuration; } + public void setSlowDuration(int slowDuration) { this.slowDuration = slowDuration; } + public int getSlowLevel() { return slowLevel; } + public void setSlowLevel(int slowLevel) { this.slowLevel = slowLevel; } + public int getInnerColor() { return innerColor; } + public void setInnerColor(int innerColor) { this.innerColor = innerColor; } + public int getOuterColor() { return outerColor; } + public void setOuterColor(int outerColor) { this.outerColor = outerColor; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 99583a5d0..ff03ac39f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -5,11 +5,9 @@ import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityOrb; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; -import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -17,39 +15,45 @@ import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityOrb; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; -import java.util.List; - /** - * Orb ability: Homing projectile sphere that tracks target. - * Can be explosive on impact and/or apply debuffs. + * Orb ability: Spawns a homing projectile sphere that tracks target. + * The EntityAbilityOrb handles all movement, collision, and damage logic. + * This ability just configures and spawns the orb entity. */ public class AbilityOrb extends Ability { + // Movement properties private float orbSpeed = 0.5f; private float orbSize = 1.0f; + private float maxDistance = 30.0f; // Max distance before orb dies + private int maxLifetime = 200; // Ticks the orb lives + + // Combat properties private float damage = 7.0f; private float knockback = 1.0f; - private float maxDistance = 30.0f; - private int maxLifetime = 200; + private float knockbackUp = 0.1f; + // Homing properties private boolean homing = true; private float homingStrength = 0.15f; private float homingRange = 20.0f; + // Explosion properties private boolean explosive = false; private float explosionRadius = 3.0f; private float explosionDamageFalloff = 0.5f; + // Effect properties private int stunDuration = 0; private int slowDuration = 0; private int slowLevel = 0; - // Runtime state - private transient double orbX, orbY, orbZ; - private transient double velocityX, velocityY, velocityZ; - private transient double startX, startY, startZ; - private transient boolean hasHit = false; - private transient int ticksAlive = 0; + // Visual properties + private int innerColor = 0xFFFFFF; // White core + private int outerColor = 0x8888FF; // Light blue glow + private float outerColorWidth = 1.8f; // Multiplier for outer glow size + private boolean outerColorEnabled = true; // Whether to render outer glow + private float rotationSpeed = 4.0f; // Degrees per tick public AbilityOrb() { this.typeId = "ability.cnpc.orb"; @@ -59,7 +63,7 @@ public class AbilityOrb extends Ability { this.minRange = 5.0f; this.cooldownTicks = 100; this.windUpTicks = 30; - this.activeTicks = 200; + this.activeTicks = 1; // Minimal - just spawn and done this.recoveryTicks = 10; this.telegraphType = TelegraphType.POINT; this.showTelegraph = false; @@ -72,8 +76,7 @@ public class AbilityOrb extends Ability { @Override @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { + public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { return new SubGuiAbilityOrb(this, callback); } @@ -89,203 +92,52 @@ public class AbilityOrb extends Ability { @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - startX = orbX = npc.posX; - startY = orbY = npc.posY + npc.getEyeHeight() * 0.8; - startZ = orbZ = npc.posZ; - hasHit = false; - ticksAlive = 0; + if (world.isRemote) return; - if (target != null) { - double dx = target.posX - orbX; - double dy = (target.posY + target.height * 0.5) - orbY; - double dz = target.posZ - orbZ; - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - if (len > 0) { - velocityX = (dx / len) * orbSpeed; - velocityY = (dy / len) * orbSpeed; - velocityZ = (dz / len) * orbSpeed; - } - } else { - float yaw = (float) Math.toRadians(npc.rotationYaw); - float pitch = (float) Math.toRadians(npc.rotationPitch); - velocityX = -Math.sin(yaw) * Math.cos(pitch) * orbSpeed; - velocityY = -Math.sin(pitch) * orbSpeed; - velocityZ = Math.cos(yaw) * Math.cos(pitch) * orbSpeed; - } + // Spawn position at NPC's eye level + double spawnX = npc.posX; + double spawnY = npc.posY + npc.getEyeHeight() * 0.8; + double spawnZ = npc.posZ; + + // Create and spawn the self-managing orb entity + EntityAbilityOrb orb = new EntityAbilityOrb( + world, npc, target, + spawnX, spawnY, spawnZ, + orbSize, innerColor, outerColor, rotationSpeed, + damage, knockback, knockbackUp, + orbSpeed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime + ); + + world.spawnEntityInWorld(orb); } @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - if (hasHit) return; - - ticksAlive++; - - if (ticksAlive >= maxLifetime) { - hasHit = true; - return; - } - - double distTraveled = Math.sqrt( - Math.pow(orbX - startX, 2) + - Math.pow(orbY - startY, 2) + - Math.pow(orbZ - startZ, 2) - ); - if (distTraveled >= maxDistance) { - hasHit = true; - return; - } - - if (homing && target != null && target.isEntityAlive()) { - double targetX = target.posX; - double targetY = target.posY + target.height * 0.5; - double targetZ = target.posZ; - - double distToTarget = Math.sqrt( - Math.pow(targetX - orbX, 2) + - Math.pow(targetY - orbY, 2) + - Math.pow(targetZ - orbZ, 2) - ); - - if (distToTarget <= homingRange) { - double dx = targetX - orbX; - double dy = targetY - orbY; - double dz = targetZ - orbZ; - double len = Math.sqrt(dx * dx + dy * dy + dz * dz); - if (len > 0) { - double desiredVX = (dx / len) * orbSpeed; - double desiredVY = (dy / len) * orbSpeed; - double desiredVZ = (dz / len) * orbSpeed; - - velocityX += (desiredVX - velocityX) * homingStrength; - velocityY += (desiredVY - velocityY) * homingStrength; - velocityZ += (desiredVZ - velocityZ) * homingStrength; - - double vLen = Math.sqrt(velocityX * velocityX + velocityY * velocityY + velocityZ * velocityZ); - if (vLen > 0) { - velocityX = (velocityX / vLen) * orbSpeed; - velocityY = (velocityY / vLen) * orbSpeed; - velocityZ = (velocityZ / vLen) * orbSpeed; - } - } - } - } - - orbX += velocityX; - orbY += velocityY; - orbZ += velocityZ; - - if (!world.isRemote) { - checkCollision(npc, world); - } - } - - private void checkCollision(EntityNPCInterface npc, World world) { - AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( - orbX - orbSize, orbY - orbSize, orbZ - orbSize, - orbX + orbSize, orbY + orbSize, orbZ + orbSize - ); - - @SuppressWarnings("unchecked") - List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); - - for (EntityLivingBase entity : entities) { - if (entity == npc) continue; - - hasHit = true; - - if (explosive) { - doExplosion(npc, world); - } else { - // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, entity, damage, knockback); - if (wasHit) { - applyEffects(entity); - } - } - break; - } - - if (!hasHit && world.getBlock((int) orbX, (int) orbY, (int) orbZ).getMaterial().isSolid()) { - hasHit = true; - if (explosive) { - doExplosion(npc, world); - } - } - } - - private void doExplosion(EntityNPCInterface npc, World world) { - AxisAlignedBB explosionBox = AxisAlignedBB.getBoundingBox( - orbX - explosionRadius, orbY - explosionRadius, orbZ - explosionRadius, - orbX + explosionRadius, orbY + explosionRadius, orbZ + explosionRadius - ); - - @SuppressWarnings("unchecked") - List blastTargets = world.getEntitiesWithinAABB(EntityLivingBase.class, explosionBox); - - for (EntityLivingBase blastTarget : blastTargets) { - if (blastTarget == npc) continue; - - double dist = Math.sqrt( - Math.pow(blastTarget.posX - orbX, 2) + - Math.pow(blastTarget.posY - orbY, 2) + - Math.pow(blastTarget.posZ - orbZ, 2) - ); - - if (dist <= explosionRadius) { - float falloff = 1.0f - (float) (dist / explosionRadius) * explosionDamageFalloff; - // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, blastTarget, damage * falloff, knockback * falloff); - if (wasHit) { - applyEffects(blastTarget); - } - } - } - - world.playSoundEffect(orbX, orbY, orbZ, "random.explode", 1.0f, 1.0f); - } - - private void applyKnockback(EntityLivingBase target, float strength, float upward) { - double dx = target.posX - orbX; - double dz = target.posZ - orbZ; - double dist = Math.sqrt(dx * dx + dz * dz); - if (dist > 0) { - target.motionX += (dx / dist) * strength; - target.motionZ += (dz / dist) * strength; - } - target.motionY += upward; - target.velocityChanged = true; - } - - private void applyEffects(EntityLivingBase target) { - if (stunDuration > 0) { - target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); - target.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); - } - if (slowDuration > 0 && slowLevel > 0) { - target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, slowDuration, slowLevel)); - } + // Nothing to do - entity manages itself } @Override public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - hasHit = false; - ticksAlive = 0; + // Nothing to clean up - entity manages itself } @Override public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - hasHit = false; - ticksAlive = 0; + // Nothing to clean up - entity manages itself } @Override public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("orbSpeed", orbSpeed); nbt.setFloat("orbSize", orbSize); - nbt.setFloat("damage", damage); - nbt.setFloat("knockback", knockback); nbt.setFloat("maxDistance", maxDistance); nbt.setInteger("maxLifetime", maxLifetime); + nbt.setFloat("damage", damage); + nbt.setFloat("knockback", knockback); + nbt.setFloat("knockbackUp", knockbackUp); nbt.setBoolean("homing", homing); nbt.setFloat("homingStrength", homingStrength); nbt.setFloat("homingRange", homingRange); @@ -295,16 +147,22 @@ public class AbilityOrb extends Ability { nbt.setInteger("stunDuration", stunDuration); nbt.setInteger("slowDuration", slowDuration); nbt.setInteger("slowLevel", slowLevel); + nbt.setInteger("innerColor", innerColor); + nbt.setInteger("outerColor", outerColor); + nbt.setFloat("outerColorWidth", outerColorWidth); + nbt.setBoolean("outerColorEnabled", outerColorEnabled); + nbt.setFloat("rotationSpeed", rotationSpeed); } @Override public void readTypeNBT(NBTTagCompound nbt) { this.orbSpeed = nbt.hasKey("orbSpeed") ? nbt.getFloat("orbSpeed") : 0.5f; this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 7.0f; - this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.0f; this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 30.0f; this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 7.0f; + this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.0f; + this.knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.1f; this.homing = !nbt.hasKey("homing") || nbt.getBoolean("homing"); this.homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.15f; this.homingRange = nbt.hasKey("homingRange") ? nbt.getFloat("homingRange") : 20.0f; @@ -314,6 +172,11 @@ public class AbilityOrb extends Ability { this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; this.slowDuration = nbt.hasKey("slowDuration") ? nbt.getInteger("slowDuration") : 0; this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; + this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; + this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0x8888FF; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); + this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 4.0f; } // Getters & Setters @@ -333,6 +196,22 @@ public class AbilityOrb extends Ability { this.orbSize = orbSize; } + public float getMaxDistance() { + return maxDistance; + } + + public void setMaxDistance(float maxDistance) { + this.maxDistance = maxDistance; + } + + public int getMaxLifetime() { + return maxLifetime; + } + + public void setMaxLifetime(int maxLifetime) { + this.maxLifetime = maxLifetime; + } + public float getDamage() { return damage; } @@ -349,20 +228,12 @@ public class AbilityOrb extends Ability { this.knockback = knockback; } - public float getMaxDistance() { - return maxDistance; + public float getKnockbackUp() { + return knockbackUp; } - public void setMaxDistance(float maxDistance) { - this.maxDistance = maxDistance; - } - - public int getMaxLifetime() { - return maxLifetime; - } - - public void setMaxLifetime(int maxLifetime) { - this.maxLifetime = maxLifetime; + public void setKnockbackUp(float knockbackUp) { + this.knockbackUp = knockbackUp; } public boolean isHoming() { @@ -437,20 +308,43 @@ public class AbilityOrb extends Ability { this.slowLevel = slowLevel; } - // Runtime getters for rendering - public double getOrbX() { - return orbX; + public int getInnerColor() { + return innerColor; } - public double getOrbY() { - return orbY; + public void setInnerColor(int innerColor) { + this.innerColor = innerColor; } - public double getOrbZ() { - return orbZ; + public int getOuterColor() { + return outerColor; } - public boolean hasHit() { - return hasHit; + public void setOuterColor(int outerColor) { + this.outerColor = outerColor; + } + + public float getOuterColorWidth() { + return outerColorWidth; + } + + public void setOuterColorWidth(float outerColorWidth) { + this.outerColorWidth = outerColorWidth; + } + + public boolean isOuterColorEnabled() { + return outerColorEnabled; + } + + public void setOuterColorEnabled(boolean outerColorEnabled) { + this.outerColorEnabled = outerColorEnabled; + } + + public float getRotationSpeed() { + return rotationSpeed; + } + + public void setRotationSpeed(float rotationSpeed) { + this.rotationSpeed = rotationSpeed; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index af3489214..1708b49bd 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -43,7 +43,7 @@ public class AbilityShockwave extends Ability { this.cooldownTicks = 100; this.windUpTicks = 25; this.activeTicks = 5; - this.recoveryTicks = 20; + this.recoveryTicks = 10; this.telegraphType = TelegraphType.CIRCLE; this.windUpSound = "random.explode"; this.activeSound = "random.explode"; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 00da63e7f..6068b269f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -50,7 +50,7 @@ public class AbilitySlam extends Ability { this.targetingMode = TargetingMode.AOE_SELF; // Can also be AOE_TARGET to leap to target this.windUpTicks = 30; this.activeTicks = 60; // Max time in air + landing - this.recoveryTicks = 20; + this.recoveryTicks = 10; this.cooldownTicks = 100; this.lockMovement = true; this.minRange = 2.0f; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java new file mode 100644 index 000000000..d1a67eb3a --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -0,0 +1,217 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilitySweeper; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.world.World; +import noppes.npcs.LogWriter; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilitySweeper; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Sweeper ability: A low sweeping beam that rotates around the NPC. + * Players can jump over it. The beam completes a set number of rotations before ending. + * Telegraph shows a large circle on the ground indicating range. + * + * All damage logic is handled by the EntityAbilitySweeper entity to ensure + * visual and damage hitbox alignment. + */ +public class AbilitySweeper extends Ability { + + // Type-specific parameters + private float beamLength = 10.0f; + private float beamWidth = 0.3f; // Thin like beam trail + private float beamHeight = 0.8f; // Height above ground (low enough to jump over) + private float damage = 5.0f; + private int damageInterval = 5; + private boolean piercing = true; + private float sweepSpeed = 3.0f; + private int numberOfRotations = 2; // How many full rotations before ability ends + private boolean lockOnTarget = false; + + // Visual properties + private int innerColor = 0xFF6600; + private int outerColor = 0xFF0000; + private float outerColorWidth = 1.8f; + private boolean outerColorEnabled = true; + + // Runtime state (transient) + private transient EntityAbilitySweeper activeEntity = null; + + public AbilitySweeper() { + this.typeId = "ability.cnpc.sweeper"; + this.name = "Sweeper"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 15.0f; + this.minRange = 0.0f; + this.lockMovement = true; + this.cooldownTicks = 200; + this.windUpTicks = 60; + this.activeTicks = 400; // Long duration - entity controls actual end + this.recoveryTicks = 5; // Short recovery + // Circle telegraph to show range + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + @SideOnly(Side.CLIENT) + public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { + return new SubGuiAbilitySweeper(this, callback); + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + if (!world.isRemote) { + LogWriter.info("[Sweeper] onExecute called at NPC pos " + npc.posX + ", " + npc.posY + ", " + npc.posZ); + + // Spawn the entity that handles BOTH visuals AND damage + activeEntity = new EntityAbilitySweeper(world, npc, target, + beamLength, beamWidth, beamHeight, + innerColor, outerColor, + sweepSpeed, numberOfRotations, + damage, damageInterval, piercing, + lockOnTarget); + world.spawnEntityInWorld(activeEntity); + + LogWriter.info("[Sweeper] Spawned sweeper entity"); + } + } + + @Override + public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + // Entity handles everything - nothing to do here + // Check if entity died early (entity manages its own death based on rotations) + if (activeEntity != null && activeEntity.isDead) { + // Entity finished, ability can end early + // The ability system will handle the recovery phase + } + } + + @Override + public void reset() { + super.reset(); + + // Clean up entity if still alive + if (activeEntity != null && !activeEntity.isDead) { + activeEntity.setDead(); + } + activeEntity = null; + } + + @Override + public float getTelegraphRadius() { + return beamLength; + } + + /** + * Override to position telegraph at NPC (not target) since the sweep is centered on NPC. + */ + @Override + public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE) { + return null; + } + + // Create circle telegraph centered on NPC + Telegraph telegraph = Telegraph.circle(beamLength); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at NPC, not target + TelegraphInstance instance = new TelegraphInstance(telegraph, npc.posX, npc.posY, npc.posZ, npc.rotationYaw); + instance.setCasterEntityId(npc.getEntityId()); + instance.setEntityIdToFollow(npc.getEntityId()); // Follow NPC + + return instance; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("beamLength", beamLength); + nbt.setFloat("beamWidth", beamWidth); + nbt.setFloat("beamHeight", beamHeight); + nbt.setFloat("damage", damage); + nbt.setInteger("damageInterval", damageInterval); + nbt.setBoolean("piercing", piercing); + nbt.setFloat("sweepSpeed", sweepSpeed); + nbt.setInteger("numberOfRotations", numberOfRotations); + nbt.setBoolean("lockOnTarget", lockOnTarget); + nbt.setInteger("innerColor", innerColor); + nbt.setInteger("outerColor", outerColor); + nbt.setFloat("outerColorWidth", outerColorWidth); + nbt.setBoolean("outerColorEnabled", outerColorEnabled); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + this.beamLength = nbt.hasKey("beamLength") ? nbt.getFloat("beamLength") : 10.0f; + this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.3f; + this.beamHeight = nbt.hasKey("beamHeight") ? nbt.getFloat("beamHeight") : 0.8f; + this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 5.0f; + this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 5; + this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing"); + this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 3.0f; + this.numberOfRotations = nbt.hasKey("numberOfRotations") ? nbt.getInteger("numberOfRotations") : 2; + this.lockOnTarget = nbt.hasKey("lockOnTarget") && nbt.getBoolean("lockOnTarget"); + this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFF6600; + this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF0000; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); + } + + // Getters & Setters + public float getBeamLength() { return beamLength; } + public void setBeamLength(float beamLength) { this.beamLength = beamLength; } + public float getBeamWidth() { return beamWidth; } + public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; } + public float getBeamHeight() { return beamHeight; } + public void setBeamHeight(float beamHeight) { this.beamHeight = beamHeight; } + public float getDamage() { return damage; } + public void setDamage(float damage) { this.damage = damage; } + public int getDamageInterval() { return damageInterval; } + public void setDamageInterval(int damageInterval) { this.damageInterval = damageInterval; } + public boolean isPiercing() { return piercing; } + public void setPiercing(boolean piercing) { this.piercing = piercing; } + public float getSweepSpeed() { return sweepSpeed; } + public void setSweepSpeed(float sweepSpeed) { this.sweepSpeed = sweepSpeed; } + public int getNumberOfRotations() { return numberOfRotations; } + public void setNumberOfRotations(int numberOfRotations) { this.numberOfRotations = numberOfRotations; } + public boolean isLockOnTarget() { return lockOnTarget; } + public void setLockOnTarget(boolean lockOnTarget) { this.lockOnTarget = lockOnTarget; } + public int getInnerColor() { return innerColor; } + public void setInnerColor(int innerColor) { this.innerColor = innerColor; } + public int getOuterColor() { return outerColor; } + public void setOuterColor(int outerColor) { this.outerColor = outerColor; } + public float getOuterColorWidth() { return outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; } + public boolean isOuterColorEnabled() { return outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 05eb43798..fa2fefb4c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -137,31 +137,6 @@ public class AbilityTrap extends Ability { return instance; } - /** - * Calculates offset position near the given coordinates. - */ - private double[] calculateOffsetPosition(double baseX, double baseY, double baseZ) { - if (maxOffset <= 0) { - return new double[]{baseX, baseY, baseZ}; - } - - double offsetDist; - double offsetAngle; - - if (randomOffset) { - offsetDist = minOffset + RANDOM.nextDouble() * (maxOffset - minOffset); - offsetAngle = RANDOM.nextDouble() * Math.PI * 2; - } else { - offsetDist = maxOffset; - offsetAngle = RANDOM.nextDouble() * Math.PI * 2; - } - - double offsetX = Math.cos(offsetAngle) * offsetDist; - double offsetZ = Math.sin(offsetAngle) * offsetDist; - - return new double[]{baseX + offsetX, baseY, baseZ + offsetZ}; - } - @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { armed = false; @@ -181,12 +156,18 @@ public class AbilityTrap extends Ability { case AT_TARGET: // Use telegraph position with offset if (telegraph != null) { - double[] pos = calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ()); + double[] pos = Ability.calculateOffsetPosition(telegraph.getX(), telegraph.getY(), telegraph.getZ(), + minOffset, maxOffset, randomOffset, RANDOM); trapX = pos[0]; trapY = pos[1]; trapZ = pos[2]; + // Update telegraph to show actual trap position + telegraph.setX(trapX); + telegraph.setY(trapY); + telegraph.setZ(trapZ); } else if (target != null) { - double[] pos = calculateOffsetPosition(target.posX, target.posY, target.posZ); + double[] pos = Ability.calculateOffsetPosition(target.posX, target.posY, target.posZ, + minOffset, maxOffset, randomOffset, RANDOM); trapX = pos[0]; trapY = pos[1]; trapZ = pos[2]; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index dee010da0..f0e08dd56 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -53,7 +53,7 @@ public class AbilityVortex extends Ability { this.cooldownTicks = 120; this.windUpTicks = 30; this.activeTicks = 40; - this.recoveryTicks = 20; + this.recoveryTicks = 5; this.telegraphType = TelegraphType.CIRCLE; this.windUpSound = "mob.ghast.charge"; this.activeSound = "mob.ghast.fireball"; diff --git a/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java index 9f90bc29b..47fdf8cbb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java @@ -1,6 +1,6 @@ package kamkeel.npcs.controllers.data.telegraph; -import net.minecraft.block.Block; +import kamkeel.npcs.controllers.data.ability.Ability; import net.minecraft.entity.Entity; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; @@ -77,7 +77,7 @@ public class TelegraphInstance { this.x = entity.posX; this.z = entity.posZ; // Find ground level below the entity for proper telegraph placement - this.y = findGroundLevel(world, entity.posX, entity.posY, entity.posZ); + this.y = Ability.findGroundLevel(world, entity.posX, entity.posY, entity.posZ); } } @@ -91,34 +91,6 @@ public class TelegraphInstance { return true; } - /** - * Find the ground level at a given position. - * Searches downward from the given Y to find a solid block. - * - * @param world The world - * @param x X coordinate - * @param startY Starting Y coordinate (entity feet position) - * @param z Z coordinate - * @return The Y coordinate of the ground surface - */ - private double findGroundLevel(World world, double x, double startY, double z) { - int blockX = (int) Math.floor(x); - int blockZ = (int) Math.floor(z); - int startBlockY = (int) Math.floor(startY); - - // Search downward for solid ground (max 10 blocks down) - for (int checkY = startBlockY; checkY >= startBlockY - 10 && checkY >= 0; checkY--) { - Block block = world.getBlock(blockX, checkY, blockZ); - if (block != null && block.getMaterial().isSolid()) { - // Found solid block, telegraph goes on top of it - return checkY + 1; - } - } - - // No ground found, use original position - return startY; - } - /** * Get progress from 0.0 (just started) to 1.0 (finished). */ diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java new file mode 100644 index 000000000..586e57ae9 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -0,0 +1,428 @@ +package kamkeel.npcs.entity; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.LogWriter; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.ArrayList; +import java.util.List; + +/** + * Beam projectile - head with trailing path that curves with homing. + * Stays attached to origin point, trail shows path of head. + * + * IMPORTANT: Trail points are stored RELATIVE to startX/Y/Z (origin). + * This ensures rendering is stable regardless of entity position interpolation. + * + * Design inspired by LouisXIV's energy attack system. + */ +public class EntityAbilityBeam extends EntityAbilityProjectile { + + // Beam-specific movement properties + private float speed = 0.5f; + private boolean homing = true; + private float homingStrength = 0.35f; // Increased from 0.15 for better tracking + private float homingRange = 20.0f; + + // Beam shape properties + private float beamWidth = 0.3f; + private float headSize = 0.5f; + + // Trail - list of points showing beam path (RELATIVE to origin!) + private List trailPoints = new ArrayList<>(); + private static final int MAX_TRAIL_POINTS = 200; + private static final double MIN_POINT_DISTANCE = 0.2; + + // Head position relative to origin + private double headOffsetX, headOffsetY, headOffsetZ; + private double prevHeadOffsetX, prevHeadOffsetY, prevHeadOffsetZ; + + // Origin stays fixed (or follows owner) + private boolean attachedToOwner = true; + + // Debug logging + private static final boolean DEBUG_LOGGING = true; + + public EntityAbilityBeam(World world) { + super(world); + } + + /** + * Full constructor with all parameters. + */ + public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float beamWidth, float headSize, int innerColor, int outerColor, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime) { + super(world); + + // Initialize base properties + initProjectile(owner, target, x, y, z, + headSize, innerColor, outerColor, rotationSpeed, + damage, knockback, knockbackUp, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime); + + // Beam-specific properties + this.speed = speed; + this.homing = homing; + this.homingStrength = homingStrength; + this.homingRange = homingRange; + this.beamWidth = beamWidth; + this.headSize = headSize; + + // Initialize head offset at origin (0,0,0 relative) + this.headOffsetX = 0; + this.headOffsetY = 0; + this.headOffsetZ = 0; + this.prevHeadOffsetX = 0; + this.prevHeadOffsetY = 0; + this.prevHeadOffsetZ = 0; + + // Add initial trail point at origin (relative 0,0,0) + trailPoints.add(Vec3.createVectorHelper(0, 0, 0)); + + if (DEBUG_LOGGING && !world.isRemote) { + LogWriter.info("[Beam] Created at origin " + x + ", " + y + ", " + z + " maxDist=" + maxDistance); + } + + // Calculate initial velocity toward target + if (target != null) { + double dx = target.posX - x; + double dy = (target.posY + target.height * 0.5) - y; + double dz = target.posZ - z; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + this.motionX = (dx / len) * speed; + this.motionY = (dy / len) * speed; + this.motionZ = (dz / len) * speed; + } + } else { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + this.motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + this.motionY = -Math.sin(pitch) * speed; + this.motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + } + + @Override + protected boolean checkMaxDistance() { + // Check distance using head offset (distance from origin) + double distFromOrigin = Math.sqrt( + headOffsetX * headOffsetX + + headOffsetY * headOffsetY + + headOffsetZ * headOffsetZ + ); + boolean exceeded = distFromOrigin >= maxDistance; + if (exceeded && !worldObj.isRemote && DEBUG_LOGGING) { + LogWriter.info("[Beam] DEAD: Max distance exceeded. dist=" + distFromOrigin + " max=" + maxDistance); + } + return exceeded; + } + + @Override + protected void updateProjectile() { + // Store previous head offset for interpolation + prevHeadOffsetX = headOffsetX; + prevHeadOffsetY = headOffsetY; + prevHeadOffsetZ = headOffsetZ; + + if (worldObj.isRemote) { + // Client: interpolate entity position, derive head from offset + handleClientInterpolation(); + + // Client also updates head offset to match motion for smooth movement + headOffsetX += motionX; + headOffsetY += motionY; + headOffsetZ += motionZ; + + // Add trail point on client + addTrailPoint(); + } else { + // Server-side logic + updateHoming(); + + // Move head offset + headOffsetX += motionX; + headOffsetY += motionY; + headOffsetZ += motionZ; + + // Update origin if attached to owner + if (attachedToOwner) { + Entity owner = getOwner(); + if (owner != null) { + startX = owner.posX; + startY = owner.posY + owner.height * 0.7; + startZ = owner.posZ; + } + } + + // Calculate world position of head for collision detection + double headWorldX = startX + headOffsetX; + double headWorldY = startY + headOffsetY; + double headWorldZ = startZ + headOffsetZ; + + // Update entity position to head (for network sync) + this.setPosition(headWorldX, headWorldY, headWorldZ); + + // Add trail point + addTrailPoint(); + + // Debug log periodically + if (DEBUG_LOGGING && ticksExisted % 20 == 0) { + LogWriter.info("[Beam] Server tick=" + ticksExisted + " headOffset=(" + + String.format("%.2f", headOffsetX) + "," + + String.format("%.2f", headOffsetY) + "," + + String.format("%.2f", headOffsetZ) + ") origin=(" + + String.format("%.2f", startX) + "," + + String.format("%.2f", startY) + "," + + String.format("%.2f", startZ) + ") trail=" + trailPoints.size()); + } + + // Skip collision checks on first few ticks + if (ticksExisted > 2) { + checkBlockCollision(headWorldX, headWorldY, headWorldZ); + checkEntityCollision(headWorldX, headWorldY, headWorldZ); + } + } + } + + private void addTrailPoint() { + // Trail points are RELATIVE to origin + if (trailPoints.isEmpty()) { + trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ)); + return; + } + + Vec3 lastPoint = trailPoints.get(trailPoints.size() - 1); + double dist = Math.sqrt( + (headOffsetX - lastPoint.xCoord) * (headOffsetX - lastPoint.xCoord) + + (headOffsetY - lastPoint.yCoord) * (headOffsetY - lastPoint.yCoord) + + (headOffsetZ - lastPoint.zCoord) * (headOffsetZ - lastPoint.zCoord) + ); + + if (dist >= MIN_POINT_DISTANCE) { + trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ)); + + // Limit trail length + while (trailPoints.size() > MAX_TRAIL_POINTS) { + trailPoints.remove(0); + } + } + } + + private void updateHoming() { + if (!homing) return; + + Entity target = getTarget(); + if (target == null || !target.isEntityAlive()) return; + + // Calculate world position of head + double headWorldX = startX + headOffsetX; + double headWorldY = startY + headOffsetY; + double headWorldZ = startZ + headOffsetZ; + + double dx = target.posX - headWorldX; + double dy = (target.posY + target.height * 0.5) - headWorldY; + double dz = target.posZ - headWorldZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist <= homingRange && dist > 0) { + // Calculate effective homing strength - increases when closer to commit to target + float effectiveStrength = homingStrength; + if (dist < homingRange * 0.3) { + effectiveStrength = Math.min(1.0f, homingStrength * 2.5f); + } else if (dist < homingRange * 0.6) { + effectiveStrength = Math.min(0.8f, homingStrength * 1.5f); + } + + double desiredVX = (dx / dist) * speed; + double desiredVY = (dy / dist) * speed; + double desiredVZ = (dz / dist) * speed; + + motionX += (desiredVX - motionX) * effectiveStrength; + motionY += (desiredVY - motionY) * effectiveStrength; + motionZ += (desiredVZ - motionZ) * effectiveStrength; + + double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); + if (vLen > 0) { + motionX = (motionX / vLen) * speed; + motionY = (motionY / vLen) * speed; + motionZ = (motionZ / vLen) * speed; + } + } + } + + private void checkBlockCollision(double headX, double headY, double headZ) { + // Previous head world position + double prevHeadWorldX = startX + prevHeadOffsetX; + double prevHeadWorldY = startY + prevHeadOffsetY; + double prevHeadWorldZ = startZ + prevHeadOffsetZ; + + Vec3 currentPos = Vec3.createVectorHelper(prevHeadWorldX, prevHeadWorldY, prevHeadWorldZ); + Vec3 nextPos = Vec3.createVectorHelper(headX, headY, headZ); + MovingObjectPosition blockHit = worldObj.rayTraceBlocks(currentPos, nextPos); + + if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + if (DEBUG_LOGGING) { + LogWriter.info("[Beam] DEAD: Block collision at tick " + ticksExisted); + } + hasHit = true; + if (explosive) { + posX = blockHit.hitVec.xCoord; + posY = blockHit.hitVec.yCoord; + posZ = blockHit.hitVec.zCoord; + doExplosion(); + } + this.setDead(); + } + } + + private void checkEntityCollision(double headX, double headY, double headZ) { + double hitSize = headSize * 0.5; + AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( + headX - hitSize, headY - hitSize, headZ - hitSize, + headX + hitSize, headY + hitSize, headZ + hitSize + ); + + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); + + for (EntityLivingBase entity : entities) { + if (shouldIgnoreEntity(entity)) continue; + + if (DEBUG_LOGGING) { + LogWriter.info("[Beam] DEAD: Entity collision with " + entity.getClass().getSimpleName() + " at tick " + ticksExisted); + } + hasHit = true; + + if (explosive) { + posX = headX; + posY = headY; + posZ = headZ; + doExplosion(); + } else { + applyDamage(entity); + } + + this.setDead(); + return; + } + } + + // ==================== GETTERS FOR RENDERER ==================== + + public float getBeamWidth() { + return beamWidth; + } + + public float getHeadSize() { + return headSize; + } + + /** + * Get interpolated head offset for smooth rendering. + * Returns offset RELATIVE to origin. + */ + public double getInterpolatedHeadOffsetX(float partialTicks) { + return prevHeadOffsetX + (headOffsetX - prevHeadOffsetX) * partialTicks; + } + + public double getInterpolatedHeadOffsetY(float partialTicks) { + return prevHeadOffsetY + (headOffsetY - prevHeadOffsetY) * partialTicks; + } + + public double getInterpolatedHeadOffsetZ(float partialTicks) { + return prevHeadOffsetZ + (headOffsetZ - prevHeadOffsetZ) * partialTicks; + } + + /** + * Get trail points. These are RELATIVE to origin (startX/Y/Z). + */ + public List getTrailPoints() { + return trailPoints; + } + + public double getOriginX() { + return startX; + } + + public double getOriginY() { + return startY; + } + + public double getOriginZ() { + return startZ; + } + + // ==================== NBT ==================== + + @Override + protected void readProjectileNBT(NBTTagCompound nbt) { + this.speed = nbt.hasKey("Speed") ? nbt.getFloat("Speed") : 0.5f; + this.homing = !nbt.hasKey("Homing") || nbt.getBoolean("Homing"); + this.homingStrength = nbt.hasKey("HomingStrength") ? nbt.getFloat("HomingStrength") : 0.15f; + this.homingRange = nbt.hasKey("HomingRange") ? nbt.getFloat("HomingRange") : 20.0f; + this.beamWidth = nbt.hasKey("BeamWidth") ? nbt.getFloat("BeamWidth") : 0.3f; + this.headSize = nbt.hasKey("HeadSize") ? nbt.getFloat("HeadSize") : 0.5f; + this.headOffsetX = nbt.hasKey("HeadOffsetX") ? nbt.getDouble("HeadOffsetX") : 0; + this.headOffsetY = nbt.hasKey("HeadOffsetY") ? nbt.getDouble("HeadOffsetY") : 0; + this.headOffsetZ = nbt.hasKey("HeadOffsetZ") ? nbt.getDouble("HeadOffsetZ") : 0; + this.prevHeadOffsetX = this.headOffsetX; + this.prevHeadOffsetY = this.headOffsetY; + this.prevHeadOffsetZ = this.headOffsetZ; + this.attachedToOwner = !nbt.hasKey("AttachedToOwner") || nbt.getBoolean("AttachedToOwner"); + + // Read trail points (relative to origin) + trailPoints.clear(); + if (nbt.hasKey("Trail")) { + NBTTagList trailList = nbt.getTagList("Trail", 10); + for (int i = 0; i < trailList.tagCount(); i++) { + NBTTagCompound point = trailList.getCompoundTagAt(i); + trailPoints.add(Vec3.createVectorHelper( + point.getDouble("X"), + point.getDouble("Y"), + point.getDouble("Z") + )); + } + } + } + + @Override + protected void writeProjectileNBT(NBTTagCompound nbt) { + nbt.setFloat("Speed", speed); + nbt.setBoolean("Homing", homing); + nbt.setFloat("HomingStrength", homingStrength); + nbt.setFloat("HomingRange", homingRange); + nbt.setFloat("BeamWidth", beamWidth); + nbt.setFloat("HeadSize", headSize); + nbt.setDouble("HeadOffsetX", headOffsetX); + nbt.setDouble("HeadOffsetY", headOffsetY); + nbt.setDouble("HeadOffsetZ", headOffsetZ); + nbt.setBoolean("AttachedToOwner", attachedToOwner); + + // Write trail points + NBTTagList trailList = new NBTTagList(); + for (Vec3 point : trailPoints) { + NBTTagCompound pointNbt = new NBTTagCompound(); + pointNbt.setDouble("X", point.xCoord); + pointNbt.setDouble("Y", point.yCoord); + pointNbt.setDouble("Z", point.zCoord); + trailList.appendTag(pointNbt); + } + nbt.setTag("Trail", trailList); + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java new file mode 100644 index 000000000..e46542d8c --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -0,0 +1,329 @@ +package kamkeel.npcs.entity; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.List; + +/** + * Disc projectile - flat spinning disc with optional boomerang behavior. + * Has a wider, thinner hitbox compared to Orb. + * + * Design inspired by LouisXIV's energy attack system. + */ +public class EntityAbilityDisc extends EntityAbilityProjectile { + + // Disc-specific movement properties + private float speed = 0.5f; + private boolean homing = true; + private float homingStrength = 0.35f; // Increased from 0.15 for better tracking + private float homingRange = 20.0f; + + // Boomerang properties + private boolean boomerang = false; + private int boomerangDelay = 40; // Ticks before returning + private boolean returning = false; + private int ticksSinceMiss = 0; + + // Disc shape properties + private float discRadius = 1.0f; // Width of disc + private float discThickness = 0.2f; // Height of disc + + public EntityAbilityDisc(World world) { + super(world); + } + + /** + * Full constructor with all parameters. + */ + public EntityAbilityDisc(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float discRadius, float discThickness, int innerColor, int outerColor, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean boomerang, int boomerangDelay, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime) { + super(world); + + // Initialize base properties (use discRadius as size for base) + initProjectile(owner, target, x, y, z, + discRadius, innerColor, outerColor, rotationSpeed, + damage, knockback, knockbackUp, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime); + + // Disc-specific properties + this.speed = speed; + this.homing = homing; + this.homingStrength = homingStrength; + this.homingRange = homingRange; + + this.boomerang = boomerang; + this.boomerangDelay = boomerangDelay; + + this.discRadius = discRadius; + this.discThickness = discThickness; + + // Calculate initial velocity toward target + if (target != null) { + double dx = target.posX - x; + double dy = (target.posY + target.height * 0.5) - y; + double dz = target.posZ - z; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + this.motionX = (dx / len) * speed; + this.motionY = (dy / len) * speed; + this.motionZ = (dz / len) * speed; + } + } else { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + this.motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + this.motionY = -Math.sin(pitch) * speed; + this.motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + } + + @Override + protected void updateRotation() { + // Disc spins ONLY on Y axis (flat spin like a saw blade) + this.rotationValY += rotationSpeed; + // No wobble - keep X and Z at 0 for stable flight + if (this.rotationValY > 360.0f) this.rotationValY -= 360.0f; + } + + @Override + protected void updateProjectile() { + if (worldObj.isRemote) { + handleClientInterpolation(); + } else { + // Server-side logic + if (returning) { + updateReturnToOwner(); + } else { + updateHoming(); + } + + checkBlockCollision(); + checkEntityCollision(); + this.moveEntity(motionX, motionY, motionZ); + + // Boomerang: if we've traveled far without hitting, start returning + if (boomerang && !returning && !hasHit) { + double distTraveled = Math.sqrt( + (posX - startX) * (posX - startX) + + (posY - startY) * (posY - startY) + + (posZ - startZ) * (posZ - startZ) + ); + // Start returning at 80% of max distance or after delay + if (distTraveled >= maxDistance * 0.8 || ticksSinceMiss >= boomerangDelay) { + returning = true; + } + ticksSinceMiss++; + } + } + + // Check wall collision + if (this.isCollidedHorizontally || this.isCollidedVertically) { + if (!worldObj.isRemote) { + hasHit = true; + if (explosive) { + doExplosion(); + } + } + this.setDead(); + } + } + + @Override + protected boolean checkMaxDistance() { + // If boomerang and returning, don't die from max distance + if (boomerang && returning) { + // Die if we get close to owner + Entity owner = getOwner(); + if (owner != null) { + double distToOwner = Math.sqrt( + (posX - owner.posX) * (posX - owner.posX) + + (posY - owner.posY) * (posY - owner.posY) + + (posZ - owner.posZ) * (posZ - owner.posZ) + ); + return distToOwner < 1.0; + } + } + return super.checkMaxDistance(); + } + + private void updateHoming() { + if (!homing) return; + + Entity target = getTarget(); + if (target == null || !target.isEntityAlive()) return; + + double dx = target.posX - posX; + double dy = (target.posY + target.height * 0.5) - posY; + double dz = target.posZ - posZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist <= homingRange && dist > 0) { + // Calculate effective homing strength - increases when closer to commit to target + float effectiveStrength = homingStrength; + if (dist < homingRange * 0.3) { + effectiveStrength = Math.min(1.0f, homingStrength * 2.5f); + } else if (dist < homingRange * 0.6) { + effectiveStrength = Math.min(0.8f, homingStrength * 1.5f); + } + + double desiredVX = (dx / dist) * speed; + double desiredVY = (dy / dist) * speed; + double desiredVZ = (dz / dist) * speed; + + motionX += (desiredVX - motionX) * effectiveStrength; + motionY += (desiredVY - motionY) * effectiveStrength; + motionZ += (desiredVZ - motionZ) * effectiveStrength; + + double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); + if (vLen > 0) { + motionX = (motionX / vLen) * speed; + motionY = (motionY / vLen) * speed; + motionZ = (motionZ / vLen) * speed; + } + } + } + + private void updateReturnToOwner() { + Entity owner = getOwner(); + if (owner == null) { + this.setDead(); + return; + } + + double dx = owner.posX - posX; + double dy = (owner.posY + owner.height * 0.5) - posY; + double dz = owner.posZ - posZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist > 0) { + // Stronger homing when returning + double returnStrength = homingStrength * 2.0; + double desiredVX = (dx / dist) * speed; + double desiredVY = (dy / dist) * speed; + double desiredVZ = (dz / dist) * speed; + + motionX += (desiredVX - motionX) * returnStrength; + motionY += (desiredVY - motionY) * returnStrength; + motionZ += (desiredVZ - motionZ) * returnStrength; + + double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); + if (vLen > 0) { + motionX = (motionX / vLen) * speed; + motionY = (motionY / vLen) * speed; + motionZ = (motionZ / vLen) * speed; + } + } + } + + private void checkBlockCollision() { + Vec3 currentPos = Vec3.createVectorHelper(posX, posY, posZ); + Vec3 nextPos = Vec3.createVectorHelper(posX + motionX, posY + motionY, posZ + motionZ); + MovingObjectPosition blockHit = worldObj.rayTraceBlocks(currentPos, nextPos); + + if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + hasHit = true; + if (explosive) { + doExplosion(); + } + this.setDead(); + } + } + + private void checkEntityCollision() { + // Disc hitbox: wider but thinner + double halfWidth = discRadius * 0.5; + double halfHeight = discThickness * 0.5; + AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( + posX - halfWidth, posY - halfHeight, posZ - halfWidth, + posX + halfWidth, posY + halfHeight, posZ + halfWidth + ); + + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); + + for (EntityLivingBase entity : entities) { + if (shouldIgnoreEntity(entity)) continue; + + hasHit = true; + + if (explosive) { + doExplosion(); + } else { + applyDamage(entity); + } + + this.setDead(); + return; + } + } + + // ==================== GETTERS ==================== + + public float getSpeed() { + return speed; + } + + public boolean isHoming() { + return homing; + } + + public float getDiscRadius() { + return discRadius; + } + + public float getDiscThickness() { + return discThickness; + } + + public boolean isBoomerang() { + return boomerang; + } + + public boolean isReturning() { + return returning; + } + + // ==================== NBT ==================== + + @Override + protected void readProjectileNBT(NBTTagCompound nbt) { + this.speed = nbt.hasKey("Speed") ? nbt.getFloat("Speed") : 0.5f; + this.homing = !nbt.hasKey("Homing") || nbt.getBoolean("Homing"); + this.homingStrength = nbt.hasKey("HomingStrength") ? nbt.getFloat("HomingStrength") : 0.15f; + this.homingRange = nbt.hasKey("HomingRange") ? nbt.getFloat("HomingRange") : 20.0f; + this.boomerang = nbt.hasKey("Boomerang") && nbt.getBoolean("Boomerang"); + this.boomerangDelay = nbt.hasKey("BoomerangDelay") ? nbt.getInteger("BoomerangDelay") : 40; + this.discRadius = nbt.hasKey("DiscRadius") ? nbt.getFloat("DiscRadius") : 1.0f; + this.discThickness = nbt.hasKey("DiscThickness") ? nbt.getFloat("DiscThickness") : 0.2f; + this.returning = nbt.hasKey("Returning") && nbt.getBoolean("Returning"); + } + + @Override + protected void writeProjectileNBT(NBTTagCompound nbt) { + nbt.setFloat("Speed", speed); + nbt.setBoolean("Homing", homing); + nbt.setFloat("HomingStrength", homingStrength); + nbt.setFloat("HomingRange", homingRange); + nbt.setBoolean("Boomerang", boomerang); + nbt.setInteger("BoomerangDelay", boomerangDelay); + nbt.setFloat("DiscRadius", discRadius); + nbt.setFloat("DiscThickness", discThickness); + nbt.setBoolean("Returning", returning); + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java new file mode 100644 index 000000000..02b797524 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -0,0 +1,326 @@ +package kamkeel.npcs.entity; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Laser projectile - fast expanding thin line that pierces through multiple targets. + * No homing, travels in a straight line from origin to max distance. + * + * Design inspired by LouisXIV's energy attack system. + */ +public class EntityAbilityLaser extends EntityAbilityProjectile { + + // Laser-specific properties + private float laserWidth = 0.2f; + private float expansionSpeed = 2.0f; // Blocks per tick + private int lingerTicks = 10; // How long laser stays visible after reaching max + + // State + private float currentLength = 0.0f; + private boolean fullyExtended = false; + private int ticksSinceFullExtension = 0; + + // Direction (normalized) + private double dirX, dirY, dirZ; + + // Track hit entities to avoid double-damage + private Set hitEntities = new HashSet<>(); + + // End point for rendering + private double endX, endY, endZ; + + public EntityAbilityLaser(World world) { + super(world); + } + + /** + * Full constructor with all parameters. + */ + public EntityAbilityLaser(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float laserWidth, int innerColor, int outerColor, + float damage, float knockback, float knockbackUp, + float expansionSpeed, int lingerTicks, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime) { + super(world); + + // Initialize base properties + initProjectile(owner, target, x, y, z, + laserWidth, innerColor, outerColor, 0.0f, // No rotation for laser + damage, knockback, knockbackUp, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime); + + // Laser-specific properties + this.laserWidth = laserWidth; + this.expansionSpeed = expansionSpeed; + this.lingerTicks = lingerTicks; + + // Calculate direction toward target or forward + if (target != null) { + double dx = target.posX - x; + double dy = (target.posY + target.height * 0.5) - y; + double dz = target.posZ - z; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + this.dirX = dx / len; + this.dirY = dy / len; + this.dirZ = dz / len; + } + } else { + // Fire in NPC's facing direction + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + this.dirX = -Math.sin(yaw) * Math.cos(pitch); + this.dirY = -Math.sin(pitch); + this.dirZ = Math.cos(yaw) * Math.cos(pitch); + } + + // Initialize end point + this.endX = x; + this.endY = y; + this.endZ = z; + } + + @Override + protected void updateRotation() { + // Laser doesn't rotate + } + + @Override + protected boolean checkMaxDistance() { + // Laser handles distance differently - it expands to max then lingers + return false; + } + + @Override + protected void updateProjectile() { + if (!fullyExtended) { + // Expand the laser + currentLength += expansionSpeed; + + if (currentLength >= maxDistance) { + currentLength = maxDistance; + fullyExtended = true; + } + + // Update end point + endX = startX + dirX * currentLength; + endY = startY + dirY * currentLength; + endZ = startZ + dirZ * currentLength; + + // Check for block collision along the new segment + if (!worldObj.isRemote) { + checkBlockCollision(); + checkEntityCollisionAlongLine(); + } + } else { + // Laser is fully extended, count down linger time + ticksSinceFullExtension++; + if (ticksSinceFullExtension >= lingerTicks) { + this.setDead(); + } + } + } + + private void checkBlockCollision() { + Vec3 start = Vec3.createVectorHelper(startX, startY, startZ); + Vec3 end = Vec3.createVectorHelper(endX, endY, endZ); + MovingObjectPosition blockHit = worldObj.rayTraceBlocks(start, end); + + if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + // Laser stops at block + currentLength = (float) Math.sqrt( + (blockHit.hitVec.xCoord - startX) * (blockHit.hitVec.xCoord - startX) + + (blockHit.hitVec.yCoord - startY) * (blockHit.hitVec.yCoord - startY) + + (blockHit.hitVec.zCoord - startZ) * (blockHit.hitVec.zCoord - startZ) + ); + endX = blockHit.hitVec.xCoord; + endY = blockHit.hitVec.yCoord; + endZ = blockHit.hitVec.zCoord; + fullyExtended = true; + + if (explosive) { + // Explode at hit point + double oldPosX = posX; + double oldPosY = posY; + double oldPosZ = posZ; + posX = endX; + posY = endY; + posZ = endZ; + doExplosion(); + posX = oldPosX; + posY = oldPosY; + posZ = oldPosZ; + } + } + } + + private void checkEntityCollisionAlongLine() { + // Check collision along the entire laser line + // Use a slightly expanded AABB for the full line + double minX = Math.min(startX, endX) - laserWidth; + double minY = Math.min(startY, endY) - laserWidth; + double minZ = Math.min(startZ, endZ) - laserWidth; + double maxX = Math.max(startX, endX) + laserWidth; + double maxY = Math.max(startY, endY) + laserWidth; + double maxZ = Math.max(startZ, endZ) + laserWidth; + + AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox(minX, minY, minZ, maxX, maxY, maxZ); + + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); + + for (EntityLivingBase entity : entities) { + if (shouldIgnoreEntity(entity)) continue; + if (hitEntities.contains(entity.getEntityId())) continue; + + // Check if entity is close to the laser line + if (isEntityOnLine(entity)) { + hitEntities.add(entity.getEntityId()); + applyDamage(entity); + + // Piercing - don't stop, continue to next entity + } + } + } + + /** + * Check if an entity intersects with the laser line. + */ + private boolean isEntityOnLine(EntityLivingBase entity) { + // Get entity center + double ex = entity.posX; + double ey = entity.posY + entity.height * 0.5; + double ez = entity.posZ; + + // Vector from start to entity + double vx = ex - startX; + double vy = ey - startY; + double vz = ez - startZ; + + // Project onto line direction + double dot = vx * dirX + vy * dirY + vz * dirZ; + + // Clamp to line segment + dot = Math.max(0, Math.min(dot, currentLength)); + + // Closest point on line + double closestX = startX + dirX * dot; + double closestY = startY + dirY * dot; + double closestZ = startZ + dirZ * dot; + + // Distance from entity to closest point + double distSq = (ex - closestX) * (ex - closestX) + + (ey - closestY) * (ey - closestY) + + (ez - closestZ) * (ez - closestZ); + + // Hit if within laser width + entity radius + double hitRadius = laserWidth * 0.5 + entity.width * 0.5; + return distSq <= hitRadius * hitRadius; + } + + // ==================== GETTERS FOR RENDERER ==================== + + public float getLaserWidth() { + return laserWidth; + } + + public float getCurrentLength() { + return currentLength; + } + + public boolean isFullyExtended() { + return fullyExtended; + } + + public double getStartX() { + return startX; + } + + public double getStartY() { + return startY; + } + + public double getStartZ() { + return startZ; + } + + public double getEndX() { + return endX; + } + + public double getEndY() { + return endY; + } + + public double getEndZ() { + return endZ; + } + + public double getDirX() { + return dirX; + } + + public double getDirY() { + return dirY; + } + + public double getDirZ() { + return dirZ; + } + + /** + * Get the alpha for fade-out effect during linger. + */ + public float getLingerAlpha() { + if (!fullyExtended) return 1.0f; + return 1.0f - ((float) ticksSinceFullExtension / lingerTicks); + } + + // ==================== NBT ==================== + + @Override + protected void readProjectileNBT(NBTTagCompound nbt) { + this.laserWidth = nbt.hasKey("LaserWidth") ? nbt.getFloat("LaserWidth") : 0.2f; + this.expansionSpeed = nbt.hasKey("ExpansionSpeed") ? nbt.getFloat("ExpansionSpeed") : 2.0f; + this.lingerTicks = nbt.hasKey("LingerTicks") ? nbt.getInteger("LingerTicks") : 10; + this.dirX = nbt.getDouble("DirX"); + this.dirY = nbt.getDouble("DirY"); + this.dirZ = nbt.getDouble("DirZ"); + this.currentLength = nbt.getFloat("CurrentLength"); + this.fullyExtended = nbt.getBoolean("FullyExtended"); + this.endX = nbt.getDouble("EndX"); + this.endY = nbt.getDouble("EndY"); + this.endZ = nbt.getDouble("EndZ"); + } + + @Override + protected void writeProjectileNBT(NBTTagCompound nbt) { + nbt.setFloat("LaserWidth", laserWidth); + nbt.setFloat("ExpansionSpeed", expansionSpeed); + nbt.setInteger("LingerTicks", lingerTicks); + nbt.setDouble("DirX", dirX); + nbt.setDouble("DirY", dirY); + nbt.setDouble("DirZ", dirZ); + nbt.setFloat("CurrentLength", currentLength); + nbt.setBoolean("FullyExtended", fullyExtended); + nbt.setDouble("EndX", endX); + nbt.setDouble("EndY", endY); + nbt.setDouble("EndZ", endZ); + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java new file mode 100644 index 000000000..c218007a0 --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -0,0 +1,225 @@ +package kamkeel.npcs.entity; + +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.MovingObjectPosition; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.List; + +/** + * Orb projectile - spherical homing energy ball. + * Extends EntityAbilityProjectile for shared functionality. + * + * Design inspired by LouisXIV's energy attack system. + */ +public class EntityAbilityOrb extends EntityAbilityProjectile { + + // Orb-specific movement properties + private float speed = 0.5f; + private boolean homing = true; + private float homingStrength = 0.35f; // Increased from 0.15 for better tracking + private float homingRange = 20.0f; + + public EntityAbilityOrb(World world) { + super(world); + } + + /** + * Full constructor with all parameters. + */ + public EntityAbilityOrb(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float orbSize, int innerColor, int outerColor, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime) { + super(world); + + // Initialize base properties + initProjectile(owner, target, x, y, z, + orbSize, innerColor, outerColor, rotationSpeed, + damage, knockback, knockbackUp, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime); + + // Orb-specific properties + this.speed = speed; + this.homing = homing; + this.homingStrength = homingStrength; + this.homingRange = homingRange; + + // Calculate initial velocity toward target + if (target != null) { + double dx = target.posX - x; + double dy = (target.posY + target.height * 0.5) - y; + double dz = target.posZ - z; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + this.motionX = (dx / len) * speed; + this.motionY = (dy / len) * speed; + this.motionZ = (dz / len) * speed; + } + } else { + // Fire in NPC's facing direction + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + this.motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + this.motionY = -Math.sin(pitch) * speed; + this.motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + } + + @Override + protected void updateProjectile() { + if (worldObj.isRemote) { + handleClientInterpolation(); + } else { + // Server-side logic + updateHoming(); + checkBlockCollision(); + checkEntityCollision(); + this.moveEntity(motionX, motionY, motionZ); + } + + // Check wall collision + if (this.isCollidedHorizontally || this.isCollidedVertically) { + if (!worldObj.isRemote) { + hasHit = true; + if (explosive) { + doExplosion(); + } + } + this.setDead(); + } + } + + private void updateHoming() { + if (!homing) return; + + Entity target = getTarget(); + if (target == null || !target.isEntityAlive()) return; + + double dx = target.posX - posX; + double dy = (target.posY + target.height * 0.5) - posY; + double dz = target.posZ - posZ; + double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); + + if (dist <= homingRange && dist > 0) { + // Calculate effective homing strength - increases when closer to commit to target + // This prevents orbiting behavior at close range + float effectiveStrength = homingStrength; + if (dist < homingRange * 0.3) { + // Within 30% of homing range, dramatically increase strength to commit + effectiveStrength = Math.min(1.0f, homingStrength * 2.5f); + } else if (dist < homingRange * 0.6) { + // Within 60% of homing range, moderately increase strength + effectiveStrength = Math.min(0.8f, homingStrength * 1.5f); + } + + double desiredVX = (dx / dist) * speed; + double desiredVY = (dy / dist) * speed; + double desiredVZ = (dz / dist) * speed; + + motionX += (desiredVX - motionX) * effectiveStrength; + motionY += (desiredVY - motionY) * effectiveStrength; + motionZ += (desiredVZ - motionZ) * effectiveStrength; + + // Normalize to maintain speed + double vLen = Math.sqrt(motionX * motionX + motionY * motionY + motionZ * motionZ); + if (vLen > 0) { + motionX = (motionX / vLen) * speed; + motionY = (motionY / vLen) * speed; + motionZ = (motionZ / vLen) * speed; + } + } + } + + private void checkBlockCollision() { + Vec3 currentPos = Vec3.createVectorHelper(posX, posY, posZ); + Vec3 nextPos = Vec3.createVectorHelper(posX + motionX, posY + motionY, posZ + motionZ); + MovingObjectPosition blockHit = worldObj.rayTraceBlocks(currentPos, nextPos); + + if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { + hasHit = true; + if (explosive) { + doExplosion(); + } + this.setDead(); + } + } + + private void checkEntityCollision() { + double hitSize = size * 0.5; + AxisAlignedBB hitBox = AxisAlignedBB.getBoundingBox( + posX - hitSize, posY - hitSize, posZ - hitSize, + posX + hitSize, posY + hitSize, posZ + hitSize + ); + + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); + + for (EntityLivingBase entity : entities) { + if (shouldIgnoreEntity(entity)) continue; + + hasHit = true; + + if (explosive) { + doExplosion(); + } else { + applyDamage(entity); + } + + this.setDead(); + return; + } + } + + // ==================== GETTERS ==================== + + public float getSpeed() { + return speed; + } + + public boolean isHoming() { + return homing; + } + + public float getHomingStrength() { + return homingStrength; + } + + public float getHomingRange() { + return homingRange; + } + + // Legacy getter for renderer compatibility + public float getOrbSize() { + return size; + } + + // ==================== NBT ==================== + + @Override + protected void readProjectileNBT(NBTTagCompound nbt) { + this.speed = nbt.hasKey("OrbSpeed") ? nbt.getFloat("OrbSpeed") : 0.5f; + this.homing = !nbt.hasKey("Homing") || nbt.getBoolean("Homing"); + this.homingStrength = nbt.hasKey("HomingStrength") ? nbt.getFloat("HomingStrength") : 0.15f; + this.homingRange = nbt.hasKey("HomingRange") ? nbt.getFloat("HomingRange") : 20.0f; + } + + @Override + protected void writeProjectileNBT(NBTTagCompound nbt) { + nbt.setFloat("OrbSpeed", speed); + nbt.setBoolean("Homing", homing); + nbt.setFloat("HomingStrength", homingStrength); + nbt.setFloat("HomingRange", homingRange); + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java new file mode 100644 index 000000000..fa84d933f --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -0,0 +1,570 @@ +package kamkeel.npcs.entity; + +import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.potion.Potion; +import net.minecraft.potion.PotionEffect; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.NpcDamageSource; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.List; + +/** + * Base class for all ability projectiles (Orb, Disc, Beam, Laser). + * Provides common functionality for visuals, combat, effects, and interpolation. + * + * Design inspired by LouisXIV's energy attack system. + */ +public abstract class EntityAbilityProjectile extends Entity implements IEntityAdditionalSpawnData { + + // ==================== VISUAL PROPERTIES ==================== + protected float size = 1.0f; + protected int innerColor = 0xFFFFFF; + protected int outerColor = 0x8888FF; + protected float rotationSpeed = 4.0f; + + // ==================== COMBAT PROPERTIES ==================== + protected float damage = 7.0f; + protected float knockback = 1.0f; + protected float knockbackUp = 0.1f; + + // ==================== EFFECT PROPERTIES ==================== + protected int stunDuration = 0; + protected int slowDuration = 0; + protected int slowLevel = 0; + + // ==================== EXPLOSION PROPERTIES ==================== + protected boolean explosive = false; + protected float explosionRadius = 3.0f; + protected float explosionDamageFalloff = 0.5f; + + // ==================== LIFESPAN ==================== + protected float maxDistance = 30.0f; + protected int maxLifetime = 200; + protected long deathWorldTime = -1; // World time when entity should die (-1 = not set) + + // ==================== TRACKING ==================== + protected double startX, startY, startZ; + protected int ownerEntityId = -1; + protected int targetEntityId = -1; + + // ==================== STATE ==================== + protected boolean hasHit = false; + + // ==================== ROTATION INTERPOLATION ==================== + public float prevRotationValX, prevRotationValY, prevRotationValZ; + public float rotationValX, rotationValY, rotationValZ; + + // ==================== SIZE INTERPOLATION ==================== + protected float renderCurrentSize; + protected float prevRenderSize; + + // ==================== POSITION INTERPOLATION ==================== + protected double interpTargetX, interpTargetY, interpTargetZ; + protected double interpTargetMotionX, interpTargetMotionY, interpTargetMotionZ; + protected int interpSteps; + + public EntityAbilityProjectile(World world) { + super(world); + this.setSize(0.5f, 0.5f); + this.noClip = false; + this.isImmuneToFire = true; + this.ignoreFrustumCheck = true; + } + + /** + * Initialize common properties. Call from subclass constructors. + */ + protected void initProjectile(EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float size, int innerColor, int outerColor, float rotationSpeed, + float damage, float knockback, float knockbackUp, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime) { + this.setPosition(x, y, z); + this.startX = x; + this.startY = y; + this.startZ = z; + + this.ownerEntityId = owner.getEntityId(); + this.targetEntityId = target != null ? target.getEntityId() : -1; + + // Visual + this.size = size; + this.innerColor = innerColor; + this.outerColor = outerColor; + this.rotationSpeed = rotationSpeed; + + // Combat + this.damage = damage; + this.knockback = knockback; + this.knockbackUp = knockbackUp; + + // Explosion + this.explosive = explosive; + this.explosionRadius = explosionRadius; + this.explosionDamageFalloff = explosionDamageFalloff; + + // Effects + this.stunDuration = stunDuration; + this.slowDuration = slowDuration; + this.slowLevel = slowLevel; + + // Lifespan + this.maxDistance = maxDistance; + this.maxLifetime = maxLifetime; + // deathWorldTime will be set on first tick when world is available + + // Initialize render size + this.renderCurrentSize = size; + this.prevRenderSize = size; + } + + @Override + protected void entityInit() { + // No DataWatcher needed - using IEntityAdditionalSpawnData with NBT + } + + @Override + @SideOnly(Side.CLIENT) + public boolean isInRangeToRenderDist(double distance) { + double d1 = this.boundingBox.getAverageEdgeLength() * 4.0D; + d1 *= 128.0D; + return distance < d1 * d1; + } + + @Override + public boolean shouldRenderInPass(int pass) { + return pass == 1; // Translucent pass + } + + @Override + public void onUpdate() { + // Store previous values for interpolation + this.prevPosX = this.posX; + this.prevPosY = this.posY; + this.prevPosZ = this.posZ; + this.prevRotationValX = this.rotationValX; + this.prevRotationValY = this.rotationValY; + this.prevRotationValZ = this.rotationValZ; + this.prevRenderSize = this.renderCurrentSize; + + super.onUpdate(); + + // Update rotation + updateRotation(); + + // Lerp render size toward actual size + this.renderCurrentSize = this.renderCurrentSize + (this.size - this.renderCurrentSize) * 0.15f; + + // Set death time on first tick if not already set (handles chunk load/unload) + if (deathWorldTime < 0 && worldObj != null) { + deathWorldTime = worldObj.getTotalWorldTime() + maxLifetime; + } + + // Check lifespan using world time (survives chunk unload/reload) + if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) { + if (!worldObj.isRemote) { + noppes.npcs.LogWriter.info("[Projectile:" + this.getClass().getSimpleName() + "] DEAD: Lifetime exceeded. worldTime=" + worldObj.getTotalWorldTime() + " deathTime=" + deathWorldTime); + } + this.setDead(); + return; + } + + // Check max distance (subclass can override if needed) + if (checkMaxDistance()) { + this.setDead(); + return; + } + + if (hasHit) { + if (!worldObj.isRemote) { + noppes.npcs.LogWriter.info("[Projectile:" + this.getClass().getSimpleName() + "] DEAD: hasHit=true at tick " + ticksExisted); + } + this.setDead(); + return; + } + + // Subclass-specific update + updateProjectile(); + } + + /** + * Update rotation values. Override for custom rotation behavior. + */ + protected void updateRotation() { + this.rotationValX += rotationSpeed * 0.7f; + this.rotationValY += rotationSpeed; + this.rotationValZ += rotationSpeed * 0.5f; + + if (this.rotationValX > 360.0f) this.rotationValX -= 360.0f; + if (this.rotationValY > 360.0f) this.rotationValY -= 360.0f; + if (this.rotationValZ > 360.0f) this.rotationValZ -= 360.0f; + } + + /** + * Check if projectile has exceeded max distance. + * @return true if should die + */ + protected boolean checkMaxDistance() { + double distTraveled = Math.sqrt( + (posX - startX) * (posX - startX) + + (posY - startY) * (posY - startY) + + (posZ - startZ) * (posZ - startZ) + ); + return distTraveled >= maxDistance; + } + + /** + * Subclass-specific update logic. Called every tick. + */ + protected abstract void updateProjectile(); + + // ==================== POSITION INTERPOLATION ==================== + + @Override + public void setPositionAndRotation2(double x, double y, double z, float yaw, float pitch, int posRotationIncrements) { + this.interpTargetX = x; + this.interpTargetY = y; + this.interpTargetZ = z; + this.interpSteps = posRotationIncrements; + } + + @Override + public void setVelocity(double motionX, double motionY, double motionZ) { + this.interpTargetMotionX = motionX; + this.interpTargetMotionY = motionY; + this.interpTargetMotionZ = motionZ; + this.motionX = motionX; + this.motionY = motionY; + this.motionZ = motionZ; + } + + /** + * Handle client-side position interpolation. + */ + protected void handleClientInterpolation() { + if (this.interpSteps > 0) { + double newX = this.posX + (this.interpTargetX - this.posX) / this.interpSteps; + double newY = this.posY + (this.interpTargetY - this.posY) / this.interpSteps; + double newZ = this.posZ + (this.interpTargetZ - this.posZ) / this.interpSteps; + + this.motionX = this.motionX + (this.interpTargetMotionX - this.motionX) / this.interpSteps; + this.motionY = this.motionY + (this.interpTargetMotionY - this.motionY) / this.interpSteps; + this.motionZ = this.motionZ + (this.interpTargetMotionZ - this.motionZ) / this.interpSteps; + + this.setPosition(newX, newY, newZ); + this.interpSteps--; + } else { + this.posX += this.motionX; + this.posY += this.motionY; + this.posZ += this.motionZ; + this.setPosition(this.posX, this.posY, this.posZ); + } + } + + // ==================== DAMAGE & EFFECTS ==================== + + protected void applyDamage(EntityLivingBase target) { + applyDamage(target, this.damage, this.knockback); + } + + protected void applyDamage(EntityLivingBase target, float dmg, float kb) { + Entity owner = getOwner(); + EntityNPCInterface npc = (owner instanceof EntityNPCInterface) ? (EntityNPCInterface) owner : null; + + if (npc != null) { + target.attackEntityFrom(new NpcDamageSource("npc_ability", npc), dmg); + } else { + target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmg); + } + + if (kb > 0) { + double dx = target.posX - posX; + double dz = target.posZ - posZ; + double len = Math.sqrt(dx * dx + dz * dz); + if (len > 0) { + double kbX = (dx / len) * kb * 0.5; + double kbZ = (dz / len) * kb * 0.5; + double kbY = knockbackUp > 0 ? knockbackUp : 0.1; + target.addVelocity(kbX, kbY, kbZ); + target.velocityChanged = true; + } + } + + applyEffects(target); + } + + protected void applyEffects(EntityLivingBase target) { + if (stunDuration > 0) { + target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); + target.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); + } + if (slowDuration > 0 && slowLevel > 0) { + target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, slowDuration, slowLevel)); + } + } + + protected void doExplosion() { + worldObj.playSoundEffect(posX, posY, posZ, "random.explode", 1.0f, 1.0f); + + Entity owner = getOwner(); + + AxisAlignedBB explosionBox = AxisAlignedBB.getBoundingBox( + posX - explosionRadius, posY - explosionRadius, posZ - explosionRadius, + posX + explosionRadius, posY + explosionRadius, posZ + explosionRadius + ); + + @SuppressWarnings("unchecked") + List targets = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, explosionBox); + + for (EntityLivingBase target : targets) { + if (target == owner) continue; + + double dist = Math.sqrt( + Math.pow(target.posX - posX, 2) + + Math.pow(target.posY - posY, 2) + + Math.pow(target.posZ - posZ, 2) + ); + + if (dist <= explosionRadius) { + float falloff = 1.0f - (float) (dist / explosionRadius) * explosionDamageFalloff; + applyDamage(target, damage * falloff, knockback * falloff); + } + } + } + + // ==================== ENTITY HELPERS ==================== + + protected Entity getOwner() { + if (ownerEntityId == -1) return null; + return worldObj.getEntityByID(ownerEntityId); + } + + protected Entity getTarget() { + if (targetEntityId == -1) return null; + return worldObj.getEntityByID(targetEntityId); + } + + /** + * Check if an entity should be ignored for collision. + */ + protected boolean shouldIgnoreEntity(Entity entity) { + Entity owner = getOwner(); + if (entity == owner) return true; + + if (owner instanceof EntityNPCInterface && entity instanceof EntityNPCInterface) { + EntityNPCInterface ownerNpc = (EntityNPCInterface) owner; + EntityNPCInterface targetNpc = (EntityNPCInterface) entity; + if (ownerNpc.faction.id == targetNpc.faction.id) return true; + } + return false; + } + + // ==================== VISUAL GETTERS ==================== + + public float getSize() { + return size; + } + + public int getInnerColor() { + return innerColor; + } + + public int getOuterColor() { + return outerColor; + } + + public float getRotationSpeed() { + return rotationSpeed; + } + + public float getInterpolatedRotationX(float partialTicks) { + return this.prevRotationValX + (this.rotationValX - this.prevRotationValX) * partialTicks; + } + + public float getInterpolatedRotationY(float partialTicks) { + return this.prevRotationValY + (this.rotationValY - this.prevRotationValY) * partialTicks; + } + + public float getInterpolatedRotationZ(float partialTicks) { + return this.prevRotationValZ + (this.rotationValZ - this.prevRotationValZ) * partialTicks; + } + + public float getInterpolatedSize(float partialTicks) { + return this.prevRenderSize + (this.renderCurrentSize - this.prevRenderSize) * partialTicks; + } + + // ==================== BRIGHTNESS ==================== + + @Override + public float getBrightness(float partialTicks) { + return 1.0f; + } + + @Override + @SideOnly(Side.CLIENT) + public int getBrightnessForRender(float partialTicks) { + return 0xF000F0; + } + + // ==================== NBT ==================== + + @Override + protected void readEntityFromNBT(NBTTagCompound nbt) { + readBaseNBT(nbt); + readProjectileNBT(nbt); + } + + @Override + protected void writeEntityToNBT(NBTTagCompound nbt) { + writeBaseNBT(nbt); + writeProjectileNBT(nbt); + } + + /** + * Read base projectile properties from NBT. + */ + protected void readBaseNBT(NBTTagCompound nbt) { + this.size = nbt.getFloat("Size"); + this.innerColor = nbt.getInteger("InnerColor"); + this.outerColor = nbt.getInteger("OuterColor"); + this.rotationSpeed = nbt.hasKey("RotationSpeed") ? nbt.getFloat("RotationSpeed") : 4.0f; + + this.damage = nbt.getFloat("Damage"); + this.knockback = nbt.getFloat("Knockback"); + this.knockbackUp = nbt.getFloat("KnockbackUp"); + + this.explosive = nbt.getBoolean("Explosive"); + this.explosionRadius = nbt.getFloat("ExplosionRadius"); + this.explosionDamageFalloff = nbt.getFloat("ExplosionDamageFalloff"); + + this.stunDuration = nbt.getInteger("StunDuration"); + this.slowDuration = nbt.getInteger("SlowDuration"); + this.slowLevel = nbt.getInteger("SlowLevel"); + + this.maxDistance = nbt.getFloat("MaxDistance"); + this.maxLifetime = nbt.getInteger("MaxLifetime"); + this.deathWorldTime = nbt.hasKey("DeathWorldTime") ? nbt.getLong("DeathWorldTime") : -1; + + this.startX = nbt.getDouble("StartX"); + this.startY = nbt.getDouble("StartY"); + this.startZ = nbt.getDouble("StartZ"); + + this.ownerEntityId = nbt.getInteger("OwnerId"); + this.targetEntityId = nbt.getInteger("TargetId"); + + this.motionX = nbt.getDouble("MotionX"); + this.motionY = nbt.getDouble("MotionY"); + this.motionZ = nbt.getDouble("MotionZ"); + + if (this.worldObj != null && this.worldObj.isRemote) { + this.renderCurrentSize = this.size; + this.prevRenderSize = this.size; + } + } + + /** + * Write base projectile properties to NBT. + */ + protected void writeBaseNBT(NBTTagCompound nbt) { + nbt.setFloat("Size", size); + nbt.setInteger("InnerColor", innerColor); + nbt.setInteger("OuterColor", outerColor); + nbt.setFloat("RotationSpeed", rotationSpeed); + + nbt.setFloat("Damage", damage); + nbt.setFloat("Knockback", knockback); + nbt.setFloat("KnockbackUp", knockbackUp); + + nbt.setBoolean("Explosive", explosive); + nbt.setFloat("ExplosionRadius", explosionRadius); + nbt.setFloat("ExplosionDamageFalloff", explosionDamageFalloff); + + nbt.setInteger("StunDuration", stunDuration); + nbt.setInteger("SlowDuration", slowDuration); + nbt.setInteger("SlowLevel", slowLevel); + + nbt.setFloat("MaxDistance", maxDistance); + nbt.setLong("DeathWorldTime", deathWorldTime); + nbt.setInteger("MaxLifetime", maxLifetime); + + nbt.setDouble("StartX", startX); + nbt.setDouble("StartY", startY); + nbt.setDouble("StartZ", startZ); + + nbt.setInteger("OwnerId", ownerEntityId); + nbt.setInteger("TargetId", targetEntityId); + + nbt.setDouble("MotionX", motionX); + nbt.setDouble("MotionY", motionY); + nbt.setDouble("MotionZ", motionZ); + } + + /** + * Subclass-specific NBT reading. + */ + protected abstract void readProjectileNBT(NBTTagCompound nbt); + + /** + * Subclass-specific NBT writing. + */ + protected abstract void writeProjectileNBT(NBTTagCompound nbt); + + // ==================== SPAWN DATA ==================== + + @Override + public void writeSpawnData(ByteBuf buffer) { + try { + NBTTagCompound compound = new NBTTagCompound(); + this.writeEntityToNBT(compound); + cpw.mods.fml.common.network.ByteBufUtils.writeTag(buffer, compound); + } catch (Exception ignored) {} + } + + @Override + public void readSpawnData(ByteBuf buffer) { + try { + NBTTagCompound compound = cpw.mods.fml.common.network.ByteBufUtils.readTag(buffer); + if (compound != null) { + this.readEntityFromNBT(compound); + } + } catch (Exception ignored) {} + } + + // ==================== COLLISION SETTINGS ==================== + + @Override + public boolean canBeCollidedWith() { + return false; + } + + @Override + public boolean canBePushed() { + return false; + } + + @Override + protected boolean canTriggerWalking() { + return false; + } + + @Override + public boolean isBurning() { + return false; + } + + @SideOnly(Side.CLIENT) + public float getShadowSize() { + return 0.0f; + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java new file mode 100644 index 000000000..a5758a2ed --- /dev/null +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java @@ -0,0 +1,437 @@ +package kamkeel.npcs.entity; + +import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.world.World; +import noppes.npcs.LogWriter; +import noppes.npcs.NpcDamageSource; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * Sweeper ability entity - a rotating beam attached to the NPC. + * Unlike other projectiles, this handles its own damage logic since it's + * attached to and rotates around the caster. + * + * Design: Similar to Beam trail visuals but rotating around origin. + */ +public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpawnData { + + // Beam properties + private float beamLength = 10.0f; + private float beamWidth = 0.3f; // Thin like beam trail + private float beamHeight = 0.8f; + private int innerColor = 0xFF6600; + private int outerColor = 0xFF0000; + + // Combat properties + private float damage = 5.0f; + private int damageInterval = 5; + private boolean piercing = true; + + // Rotation + private float currentAngle = 0; + private float prevAngle = 0; + private float sweepSpeed = 3.0f; + private int numberOfRotations = 2; + private int completedRotations = 0; + private float baseYaw = 0; // Starting yaw direction + private boolean lockOnTarget = false; + + // Owner tracking + private int ownerEntityId = -1; + private int targetEntityId = -1; + + // Lifetime + private int maxTicks = 400; + private long deathWorldTime = -1; + + // Damage state + private transient int ticksSinceDamage = 0; + private transient Set hitThisTick = new HashSet<>(); + + public EntityAbilitySweeper(World world) { + super(world); + this.setSize(0.1f, 0.1f); + this.noClip = true; + this.isImmuneToFire = true; + this.ignoreFrustumCheck = true; + } + + public EntityAbilitySweeper(World world, EntityNPCInterface owner, EntityLivingBase target, + float beamLength, float beamWidth, float beamHeight, + int innerColor, int outerColor, + float sweepSpeed, int numberOfRotations, + float damage, int damageInterval, boolean piercing, + boolean lockOnTarget) { + this(world); + + this.ownerEntityId = owner.getEntityId(); + this.targetEntityId = target != null ? target.getEntityId() : -1; + this.beamLength = beamLength; + this.beamWidth = beamWidth; + this.beamHeight = beamHeight; + this.innerColor = innerColor; + this.outerColor = outerColor; + this.sweepSpeed = sweepSpeed; + this.numberOfRotations = numberOfRotations; + this.damage = damage; + this.damageInterval = damageInterval; + this.piercing = piercing; + this.lockOnTarget = lockOnTarget; + + // Calculate max ticks based on rotations + this.maxTicks = (int) ((360.0f * numberOfRotations) / sweepSpeed) + 10; + + // Calculate base yaw + if (lockOnTarget && target != null) { + double dx = target.posX - owner.posX; + double dz = target.posZ - owner.posZ; + this.baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); + } else { + this.baseYaw = owner.rotationYaw; + } + + // Position at owner + this.setPosition(owner.posX, owner.posY + beamHeight, owner.posZ); + + // Ready to deal damage immediately + this.ticksSinceDamage = damageInterval; + } + + @Override + protected void entityInit() { + // No DataWatcher needed + } + + @Override + public void onUpdate() { + this.prevPosX = this.posX; + this.prevPosY = this.posY; + this.prevPosZ = this.posZ; + this.prevAngle = this.currentAngle; + + super.onUpdate(); + + // Set death time on first tick + if (deathWorldTime < 0 && worldObj != null) { + deathWorldTime = worldObj.getTotalWorldTime() + maxTicks; + } + + // Check lifetime + if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) { + this.setDead(); + return; + } + + // Follow owner + Entity owner = getOwner(); + if (owner == null) { + this.setDead(); + return; + } + + // Update position to follow owner + this.setPosition(owner.posX, owner.posY + beamHeight, owner.posZ); + + // Check if sweep is done + if (completedRotations >= numberOfRotations) { + this.setDead(); + return; + } + + // Update rotation + currentAngle += sweepSpeed; + if (currentAngle >= 360) { + currentAngle -= 360; + completedRotations++; + if (!worldObj.isRemote) { + LogWriter.info("[Sweeper] Completed rotation " + completedRotations + "/" + numberOfRotations); + } + } + + // Server handles damage + if (!worldObj.isRemote) { + handleDamage(owner); + } + } + + /** + * Handle damage detection and application. + * This runs on server only. + */ + private void handleDamage(Entity owner) { + hitThisTick.clear(); + ticksSinceDamage++; + + if (ticksSinceDamage < damageInterval) { + return; + } + ticksSinceDamage = 0; + + // Calculate beam direction using the ACTUAL render angle + float beamYaw = baseYaw + currentAngle; + float yawRad = (float) Math.toRadians(beamYaw); + + double startX = posX; + double startY = posY; + double startZ = posZ; + + double dirX = -Math.sin(yawRad); + double dirZ = Math.cos(yawRad); + + // Sample points along the beam for collision + int samples = (int) (beamLength / 0.5); + for (int i = 1; i <= samples; i++) { + double progress = (double) i / samples; + double checkX = startX + dirX * beamLength * progress; + double checkY = startY; + double checkZ = startZ + dirZ * beamLength * progress; + + // Narrow hitbox for thin beam + AxisAlignedBB checkBox = AxisAlignedBB.getBoundingBox( + checkX - beamWidth * 2, checkY - 0.5, checkZ - beamWidth * 2, + checkX + beamWidth * 2, checkY + 1.0, checkZ + beamWidth * 2 + ); + + @SuppressWarnings("unchecked") + List entities = worldObj.getEntitiesWithinAABB(EntityLivingBase.class, checkBox); + + for (Entity entity : entities) { + if (!(entity instanceof EntityLivingBase)) continue; + if (entity == owner) continue; + if (hitThisTick.contains(entity.getEntityId())) continue; + + EntityLivingBase livingEntity = (EntityLivingBase) entity; + + // Check if actually in beam path + if (isInBeamPath(livingEntity, startX, startY, startZ, dirX, dirZ)) { + hitThisTick.add(entity.getEntityId()); + applyDamage(livingEntity, owner); + + if (!piercing) { + return; + } + } + } + } + } + + private boolean isInBeamPath(EntityLivingBase entity, double startX, double startY, double startZ, + double dirX, double dirZ) { + double entityX = entity.posX - startX; + double entityZ = entity.posZ - startZ; + + // Dot product to find projection distance + double projectionDist = entityX * dirX + entityZ * dirZ; + + // Check if within beam length + if (projectionDist < 0 || projectionDist > beamLength) { + return false; + } + + // Calculate perpendicular distance + double projX = dirX * projectionDist; + double projZ = dirZ * projectionDist; + double perpX = entityX - projX; + double perpZ = entityZ - projZ; + double perpDist = Math.sqrt(perpX * perpX + perpZ * perpZ); + + // Check if within beam width (use wider hitbox than visual) + if (perpDist > beamWidth * 3 + entity.width / 2) { + return false; + } + + // Height check - entity's feet must be below beam top (allows jumping over) + double entityFeetY = entity.posY; + double beamTopY = startY + 0.5; + + return entityFeetY < beamTopY; + } + + private void applyDamage(EntityLivingBase target, Entity owner) { + EntityNPCInterface npc = (owner instanceof EntityNPCInterface) ? (EntityNPCInterface) owner : null; + + if (npc != null) { + target.attackEntityFrom(new NpcDamageSource("npc_ability", npc), damage); + } else { + target.attackEntityFrom(new NpcDamageSource("npc_ability", null), damage); + } + + // Small upward knockback to help escape + target.addVelocity(0, 0.2, 0); + target.velocityChanged = true; + } + + private Entity getOwner() { + if (ownerEntityId == -1) return null; + return worldObj.getEntityByID(ownerEntityId); + } + + // ==================== RENDERING ==================== + + @Override + @SideOnly(Side.CLIENT) + public boolean isInRangeToRenderDist(double distance) { + return distance < 16384.0D; // 128 blocks squared + } + + @Override + public boolean shouldRenderInPass(int pass) { + return pass == 1; // Translucent pass + } + + @Override + public float getBrightness(float partialTicks) { + return 1.0f; + } + + @Override + @SideOnly(Side.CLIENT) + public int getBrightnessForRender(float partialTicks) { + return 0xF000F0; + } + + // ==================== GETTERS FOR RENDERER ==================== + + public float getBeamLength() { + return beamLength; + } + + public float getBeamWidth() { + return beamWidth; + } + + public float getBeamHeight() { + return beamHeight; + } + + public int getInnerColor() { + return innerColor; + } + + public int getOuterColor() { + return outerColor; + } + + /** + * Get the full interpolated angle INCLUDING base yaw for rendering. + */ + public float getInterpolatedAngle(float partialTicks) { + // Handle wrap-around for the rotating part + float diff = currentAngle - prevAngle; + if (diff > 180) diff -= 360; + if (diff < -180) diff += 360; + float interpAngle = prevAngle + diff * partialTicks; + // Add base yaw for correct world orientation + return baseYaw + interpAngle; + } + + public float getCurrentAngle() { + return currentAngle; + } + + public float getBaseYaw() { + return baseYaw; + } + + // ==================== NBT ==================== + + @Override + protected void readEntityFromNBT(NBTTagCompound nbt) { + this.beamLength = nbt.getFloat("BeamLength"); + this.beamWidth = nbt.getFloat("BeamWidth"); + this.beamHeight = nbt.getFloat("BeamHeight"); + this.innerColor = nbt.getInteger("InnerColor"); + this.outerColor = nbt.getInteger("OuterColor"); + this.sweepSpeed = nbt.getFloat("SweepSpeed"); + this.numberOfRotations = nbt.getInteger("NumRotations"); + this.completedRotations = nbt.getInteger("CompletedRotations"); + this.maxTicks = nbt.getInteger("MaxTicks"); + this.ownerEntityId = nbt.getInteger("OwnerId"); + this.targetEntityId = nbt.getInteger("TargetId"); + this.currentAngle = nbt.getFloat("CurrentAngle"); + this.baseYaw = nbt.getFloat("BaseYaw"); + this.deathWorldTime = nbt.getLong("DeathWorldTime"); + this.damage = nbt.getFloat("Damage"); + this.damageInterval = nbt.getInteger("DamageInterval"); + this.piercing = nbt.getBoolean("Piercing"); + this.lockOnTarget = nbt.getBoolean("LockOnTarget"); + } + + @Override + protected void writeEntityToNBT(NBTTagCompound nbt) { + nbt.setFloat("BeamLength", beamLength); + nbt.setFloat("BeamWidth", beamWidth); + nbt.setFloat("BeamHeight", beamHeight); + nbt.setInteger("InnerColor", innerColor); + nbt.setInteger("OuterColor", outerColor); + nbt.setFloat("SweepSpeed", sweepSpeed); + nbt.setInteger("NumRotations", numberOfRotations); + nbt.setInteger("CompletedRotations", completedRotations); + nbt.setInteger("MaxTicks", maxTicks); + nbt.setInteger("OwnerId", ownerEntityId); + nbt.setInteger("TargetId", targetEntityId); + nbt.setFloat("CurrentAngle", currentAngle); + nbt.setFloat("BaseYaw", baseYaw); + nbt.setLong("DeathWorldTime", deathWorldTime); + nbt.setFloat("Damage", damage); + nbt.setInteger("DamageInterval", damageInterval); + nbt.setBoolean("Piercing", piercing); + nbt.setBoolean("LockOnTarget", lockOnTarget); + } + + // ==================== SPAWN DATA ==================== + + @Override + public void writeSpawnData(ByteBuf buffer) { + buffer.writeFloat(beamLength); + buffer.writeFloat(beamWidth); + buffer.writeFloat(beamHeight); + buffer.writeInt(innerColor); + buffer.writeInt(outerColor); + buffer.writeFloat(sweepSpeed); + buffer.writeInt(numberOfRotations); + buffer.writeInt(completedRotations); + buffer.writeInt(maxTicks); + buffer.writeInt(ownerEntityId); + buffer.writeInt(targetEntityId); + buffer.writeFloat(currentAngle); + buffer.writeFloat(baseYaw); + buffer.writeFloat(damage); + buffer.writeInt(damageInterval); + buffer.writeBoolean(piercing); + buffer.writeBoolean(lockOnTarget); + } + + @Override + public void readSpawnData(ByteBuf buffer) { + this.beamLength = buffer.readFloat(); + this.beamWidth = buffer.readFloat(); + this.beamHeight = buffer.readFloat(); + this.innerColor = buffer.readInt(); + this.outerColor = buffer.readInt(); + this.sweepSpeed = buffer.readFloat(); + this.numberOfRotations = buffer.readInt(); + this.completedRotations = buffer.readInt(); + this.maxTicks = buffer.readInt(); + this.ownerEntityId = buffer.readInt(); + this.targetEntityId = buffer.readInt(); + this.currentAngle = buffer.readFloat(); + this.baseYaw = buffer.readFloat(); + this.damage = buffer.readFloat(); + this.damageInterval = buffer.readInt(); + this.piercing = buffer.readBoolean(); + this.lockOnTarget = buffer.readBoolean(); + this.prevAngle = this.currentAngle; + } +} diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index ed084c578..0005341f8 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -86,6 +86,11 @@ import noppes.npcs.entity.EntityNpcDragon; import noppes.npcs.entity.EntityNpcPony; import noppes.npcs.entity.EntityNpcSlime; import noppes.npcs.entity.EntityProjectile; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityAbilitySweeper; import noppes.npcs.entity.old.EntityNPCDwarfFemale; import noppes.npcs.entity.old.EntityNPCDwarfMale; import noppes.npcs.entity.old.EntityNPCElfFemale; @@ -276,6 +281,11 @@ public class CustomNpcs { registerNewEntity(EntityChairMount.class, "CustomNpcChairMount", 64, 10, false); registerNewEntity(EntityProjectile.class, "throwableitem", 64, 3, true); registerNewEntity(EntityMagicProjectile.class, "magicprojectile", 64, 3, true); + registerNewEntity(EntityAbilityOrb.class, "abilityorb", 64, 3, true); + registerNewEntity(EntityAbilityDisc.class, "abilitydisc", 64, 3, true); + registerNewEntity(EntityAbilityLaser.class, "abilitylaser", 64, 3, true); + registerNewEntity(EntityAbilityBeam.class, "abilitybeam", 64, 3, true); + registerNewEntity(EntityAbilitySweeper.class, "abilitysweeper", 64, 3, true); new RecipeController(); diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 8d390f5cc..51d2831ac 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -3,6 +3,7 @@ package noppes.npcs; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; @@ -444,7 +445,7 @@ public class DataAbilities { // ═══════════════════════════════════════════════════════════════════ /** - * Handle damage taken - may interrupt current ability. + * Handle damage taken - may interrupt current ability or trigger guard counter. * * @param source The damage source * @param amount The damage amount @@ -454,6 +455,14 @@ public class DataAbilities { // Track hit for hit count condition recordHit(); + // Check if currently executing a Guard ability and notify it of damage taken + if (currentAbility instanceof AbilityGuard) { + AbilityGuard guard = (AbilityGuard) currentAbility; + if (guard.isGuarding() && source.getEntity() instanceof EntityLivingBase) { + guard.onDamageTaken(npc, (EntityLivingBase) source.getEntity(), source, amount); + } + } + if (currentAbility == null || !currentAbility.isExecuting()) { return false; } @@ -466,6 +475,18 @@ public class DataAbilities { return false; } + /** + * Get the damage reduction factor if a Guard ability is currently active. + * + * @return The damage reduction factor (0.0 = no reduction, 1.0 = full immunity), or 0 if not guarding + */ + public float getGuardDamageReduction() { + if (currentAbility instanceof AbilityGuard) { + return ((AbilityGuard) currentAbility).getDamageReductionFactor(); + } + return 0.0f; + } + /** * Record a hit for the hit count condition. */ @@ -553,7 +574,7 @@ public class DataAbilities { * @param hitEntity The entity being hit * @param damage The damage amount * @param knockback The horizontal knockback - * @param knockbackUp The vertical knockback (deprecated, ignored) + * @param knockbackUp The vertical knockback * @return The event (with possibly modified values), or null if cancelled */ public AbilityEvent.HitEvent fireHitEvent(Ability ability, EntityLivingBase target, @@ -686,16 +707,6 @@ public class DataAbilities { } } - /** - * Check if NPC movement should be blocked due to ability execution. - * - * @deprecated Use isAbilityControllingMovement() instead - */ - @Deprecated - public boolean isMovementBlocked() { - return isAbilityControllingMovement(); - } - /** * Check if NPC should skip normal attacks during ability execution. */ diff --git a/src/main/java/noppes/npcs/ai/CombatHandler.java b/src/main/java/noppes/npcs/ai/CombatHandler.java index e3eb85fbe..750725551 100644 --- a/src/main/java/noppes/npcs/ai/CombatHandler.java +++ b/src/main/java/noppes/npcs/ai/CombatHandler.java @@ -179,10 +179,10 @@ public class CombatHandler { } /** - * Check if NPC movement should be blocked due to ability execution. + * Check if ability is controlling NPC movement (AI pathfinding should be blocked). */ - public boolean isMovementBlocked() { - return npc.abilities.isMovementBlocked(); + public boolean isAbilityControllingMovement() { + return npc.abilities.isAbilityControllingMovement(); } /** diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 95dceaec2..534963e38 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -144,6 +144,16 @@ import noppes.npcs.client.renderer.RenderNpcCrystal; import noppes.npcs.client.renderer.RenderNpcDragon; import noppes.npcs.client.renderer.RenderNpcSlime; import noppes.npcs.client.renderer.RenderProjectile; +import kamkeel.npcs.client.renderer.RenderAbilityOrbCube; +import kamkeel.npcs.client.renderer.RenderAbilityDisc; +import kamkeel.npcs.client.renderer.RenderAbilityLaser; +import kamkeel.npcs.client.renderer.RenderAbilityBeam; +import kamkeel.npcs.client.renderer.RenderAbilitySweeper; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityAbilitySweeper; import noppes.npcs.client.renderer.blocks.BlockBannerRenderer; import noppes.npcs.client.renderer.blocks.BlockBarrelRenderer; import noppes.npcs.client.renderer.blocks.BlockBeamRenderer; @@ -251,6 +261,12 @@ public class ClientProxy extends CommonProxy { RenderingRegistry.registerEntityRenderingHandler(EntityNpcDragon.class, new RenderNpcDragon(new ModelNpcDragon(0.0F), 0.5F)); RenderingRegistry.registerEntityRenderingHandler(EntityNpcSlime.class, new RenderNpcSlime(new ModelNpcSlime(16), new ModelNpcSlime(0), 0.25F)); RenderingRegistry.registerEntityRenderingHandler(EntityProjectile.class, new RenderProjectile()); + // RenderingRegistry.registerEntityRenderingHandler(EntityAbilityOrb.class, new RenderAbilityOrb()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilityOrb.class, new RenderAbilityOrbCube()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilityDisc.class, new RenderAbilityDisc()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilityLaser.class, new RenderAbilityLaser()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilityBeam.class, new RenderAbilityBeam()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilitySweeper.class, new RenderAbilitySweeper()); RenderingRegistry.registerEntityRenderingHandler(EntityCustomNpc.class, new RenderCustomNpc()); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index 58e133bc8..5f03cbd9f 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -42,6 +42,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi protected static final int TAB_TYPE = 1; protected static final int TAB_TARGET = 2; protected static final int TAB_EFFECTS = 3; + protected static final int TAB_VISUAL = 4; // Core references protected final Ability ability; @@ -98,6 +99,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi protected final boolean supportsTelegraph; protected final boolean targetingModeLocked; protected final boolean hasTypeSettings; + protected final boolean hasVisualSettings; public SubGuiAbilityConfig(Ability ability, IAbilityConfigCallback callback) { this.ability = ability; @@ -138,6 +140,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi this.supportsTelegraph = defaultType != null && defaultType != TelegraphType.NONE; this.targetingModeLocked = ability.isTargetingModeLocked(); this.hasTypeSettings = ability.hasTypeSettings(); + this.hasVisualSettings = hasVisualSettings(); setBackground("menubg.png"); xSize = 356; @@ -172,6 +175,13 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi GuiMenuTopButton effectsTab = new GuiMenuTopButton(93, lastTab, "ability.tab.effects"); effectsTab.active = (activeTab == TAB_EFFECTS); addTopButton(effectsTab); + lastTab = effectsTab; + + if (hasVisualSettings) { + GuiMenuTopButton visualTab = new GuiMenuTopButton(94, lastTab, "ability.tab.visual"); + visualTab.active = (activeTab == TAB_VISUAL); + addTopButton(visualTab); + } // Close button (X) in top menu bar - use -1000 to avoid any ID conflicts GuiMenuTopButton closeBtn = new GuiMenuTopButton(-1000, guiLeft + xSize - 22, guiTop - 17, "X"); @@ -195,15 +205,20 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi case TAB_EFFECTS: initEffectsTab(contentY); break; + case TAB_VISUAL: + initVisualTab(contentY); + break; } // ═══════════════════════════════════════════════════════════════════════ - // BOTTOM ROW - Enabled toggle + // BOTTOM ROW - Enabled toggle (only on General tab) // ═══════════════════════════════════════════════════════════════════════ - addLabel(new GuiNpcLabel(99, "gui.enabled", guiLeft + 8, guiTop + ySize - 18)); - GuiNpcButton enabledBtn = new GuiNpcButton(2, guiLeft + 55, guiTop + ySize - 24, 40, 20, new String[]{"gui.no", "gui.yes"}, enabled ? 1 : 0); - enabledBtn.setTextColor(enabled ? 0x00FF00 : 0xFF0000); - addButton(enabledBtn); + if (activeTab == TAB_GENERAL) { + addLabel(new GuiNpcLabel(99, "gui.enabled", guiLeft + 8, guiTop + ySize - 18)); + GuiNpcButton enabledBtn = new GuiNpcButton(2, guiLeft + 55, guiTop + ySize - 24, 40, 20, new String[]{"gui.no", "gui.yes"}, enabled ? 1 : 0); + enabledBtn.setTextColor(enabled ? 0x00FF00 : 0xFF0000); + addButton(enabledBtn); + } } // ═══════════════════════════════════════════════════════════════════════════ @@ -310,6 +325,45 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Default: no-op } + // ═══════════════════════════════════════════════════════════════════════════ + // VISUAL TAB - Override in subclasses for visual settings (colors, etc.) + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Override to return true if this ability has visual settings (colors, glow, etc.). + * When true, a Visual tab will be shown in the GUI. + */ + protected boolean hasVisualSettings() { + return false; + } + + /** + * Override this method to render visual settings in the Visual tab. + * Use button/text field IDs starting at 200 for visual settings. + * + * @param startY The Y position to start rendering from + */ + protected void initVisualTab(int startY) { + // Default: show message that no visual settings are available + addLabel(new GuiNpcLabel(200, "ability.noVisualSettings", guiLeft + 8, startY + 5)); + } + + /** + * Override this method to handle button clicks for visual-specific buttons. + * Button IDs 200+ are reserved for visual-specific use. + */ + protected void handleVisualButton(int id, GuiNpcButton button) { + // Default: no-op + } + + /** + * Override this method to handle text field changes for visual-specific fields. + * Field IDs 200+ are reserved for visual-specific use. + */ + protected void handleVisualTextField(int id, GuiNpcTextField field) { + // Default: no-op + } + // ═══════════════════════════════════════════════════════════════════════════ // TARGET TAB // ═══════════════════════════════════════════════════════════════════════════ @@ -563,6 +617,11 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi initGui(); return; } + if (id == 94 && hasVisualSettings) { + activeTab = TAB_VISUAL; + initGui(); + return; + } // Enabled button (bottom left) if (id == 2) { @@ -668,7 +727,11 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi initGui(); } - // Type-specific buttons (100+) - delegate to subclass + // Visual-specific buttons (200+) - delegate to subclass + else if (id >= 200) { + handleVisualButton(id, (GuiNpcButton) guibutton); + } + // Type-specific buttons (100-199) - delegate to subclass else if (id >= 100) { handleTypeButton(id, (GuiNpcButton) guibutton); } @@ -707,7 +770,11 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi recoveryTicks = textField.getInteger(); } - // Type-specific fields (100+) - delegate to subclass + // Visual-specific fields (200+) - delegate to subclass + else if (id >= 200) { + handleVisualTextField(id, textField); + } + // Type-specific fields (100-199) - delegate to subclass else if (id >= 100) { handleTypeTextField(id, textField); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java deleted file mode 100644 index ce56f809e..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeam.java +++ /dev/null @@ -1,110 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityBeam; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; - -/** - * GUI for configuring Beam ability type-specific settings. - */ -public class SubGuiAbilityBeam extends SubGuiAbilityConfig { - - private final AbilityBeam beam; - - public SubGuiAbilityBeam(AbilityBeam ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.beam = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Damage Interval - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, beam.getDamage())); - - addLabel(new GuiNpcLabel(101, "ability.dmgInterval", col2LabelX, y + 5)); - addTextField(createIntField(101, col2FieldX, y, 50, beam.getDamageInterval())); - - y += 24; - - // Row 2: Beam Length + Beam Width - addLabel(new GuiNpcLabel(102, "ability.beamLength", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, beam.getBeamLength())); - - addLabel(new GuiNpcLabel(103, "ability.beamWidth", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, beam.getBeamWidth())); - - y += 24; - - // Row 3: Sweep Angle + Sweep Speed - addLabel(new GuiNpcLabel(104, "ability.sweepAngle", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, beam.getSweepAngle())); - - addLabel(new GuiNpcLabel(105, "ability.sweepSpeed", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, beam.getSweepSpeed())); - - y += 24; - - // Row 4: Piercing + Lock On Target - addLabel(new GuiNpcLabel(106, "ability.piercing", labelX, y + 5)); - addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isPiercing() ? 1 : 0)); - - addLabel(new GuiNpcLabel(107, "ability.lockTarget", col2LabelX, y + 5)); - addButton(new GuiNpcButton(107, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isLockOnTarget() ? 1 : 0)); - - y += 24; - - // Row 5: Sweep Back & Forth - addLabel(new GuiNpcLabel(108, "ability.sweepBackForth", labelX, y + 5)); - addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isSweepBackAndForth() ? 1 : 0)); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 106: - beam.setPiercing(value == 1); - break; - case 107: - beam.setLockOnTarget(value == 1); - break; - case 108: - beam.setSweepBackAndForth(value == 1); - break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - beam.setDamage(parseFloat(field, beam.getDamage())); - break; - case 101: - beam.setDamageInterval(field.getInteger()); - break; - case 102: - beam.setBeamLength(parseFloat(field, beam.getBeamLength())); - break; - case 103: - beam.setBeamWidth(parseFloat(field, beam.getBeamWidth())); - break; - case 104: - beam.setSweepAngle(parseFloat(field, beam.getSweepAngle())); - break; - case 105: - beam.setSweepSpeed(parseFloat(field, beam.getSweepSpeed())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java new file mode 100644 index 000000000..ec80ed5d6 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java @@ -0,0 +1,227 @@ +package noppes.npcs.client.gui.advanced.ability; + +import kamkeel.npcs.controllers.data.ability.type.AbilityDisc; +import noppes.npcs.client.gui.SubGuiColorSelector; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * GUI for configuring Disc ability type-specific settings. + */ +public class SubGuiAbilityDisc extends SubGuiAbilityConfig { + + private final AbilityDisc disc; + private int editingVisualColorId = 0; + + public SubGuiAbilityDisc(AbilityDisc ability, IAbilityConfigCallback callback) { + super(ability, callback); + this.disc = ability; + } + + @Override + protected boolean hasVisualSettings() { + return true; + } + + @Override + protected void initTypeTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 145; + int col2FieldX = guiLeft + 205; + + // Row 1: Damage + Speed + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); + addTextField(createFloatField(100, fieldX, y, 50, disc.getDamage())); + + addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); + addTextField(createFloatField(101, col2FieldX, y, 50, disc.getSpeed())); + + y += 24; + + // Row 2: Disc Radius + Disc Thickness + addLabel(new GuiNpcLabel(102, "ability.discRadius", labelX, y + 5)); + addTextField(createFloatField(102, fieldX, y, 50, disc.getDiscRadius())); + + addLabel(new GuiNpcLabel(103, "ability.discThickness", col2LabelX, y + 5)); + addTextField(createFloatField(103, col2FieldX, y, 50, disc.getDiscThickness())); + + y += 24; + + // Row 3: Homing + Homing Strength + addLabel(new GuiNpcLabel(104, "ability.homing", labelX, y + 5)); + addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isHoming() ? 1 : 0)); + + addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); + addTextField(createFloatField(105, col2FieldX, y, 50, disc.getHomingStrength())); + + y += 24; + + // Row 4: Boomerang + Boomerang Delay + addLabel(new GuiNpcLabel(106, "ability.boomerang", labelX, y + 5)); + addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isBoomerang() ? 1 : 0)); + + addLabel(new GuiNpcLabel(107, "ability.boomerangDelay", col2LabelX, y + 5)); + addTextField(createIntField(107, col2FieldX, y, 50, disc.getBoomerangDelay())); + + y += 24; + + // Row 5: Explosive + Explosion Radius + addLabel(new GuiNpcLabel(108, "ability.explosive", labelX, y + 5)); + addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isExplosive() ? 1 : 0)); + + addLabel(new GuiNpcLabel(109, "ability.explosionRad", col2LabelX, y + 5)); + addTextField(createFloatField(109, col2FieldX, y, 50, disc.getExplosionRadius())); + + y += 24; + + // Row 6: Knockback + Max Distance + addLabel(new GuiNpcLabel(110, "ability.knockback", labelX, y + 5)); + addTextField(createFloatField(110, fieldX, y, 50, disc.getKnockback())); + + addLabel(new GuiNpcLabel(111, "ability.maxDist", col2LabelX, y + 5)); + addTextField(createFloatField(111, col2FieldX, y, 50, disc.getMaxDistance())); + } + + @Override + protected void initVisualTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 180; + int col2FieldX = guiLeft + 260; + + // Row 1: Inner Color + Outer Color + addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); + String innerHex = String.format("%06X", disc.getInnerColor() & 0xFFFFFF); + GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); + innerColorBtn.setTextColor(disc.getInnerColor() & 0xFFFFFF); + addButton(innerColorBtn); + + addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); + String outerHex = String.format("%06X", disc.getOuterColor() & 0xFFFFFF); + GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); + outerColorBtn.setTextColor(disc.getOuterColor() & 0xFFFFFF); + outerColorBtn.setEnabled(disc.isOuterColorEnabled()); + addButton(outerColorBtn); + + y += 24; + + // Row 2: Outer Color Enabled + Outer Color Width + addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); + addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isOuterColorEnabled() ? 1 : 0)); + + addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); + GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, disc.getOuterColorWidth()); + widthField.setEnabled(disc.isOuterColorEnabled()); + addTextField(widthField); + + y += 24; + + // Row 3: Rotation Speed + addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", labelX, y + 5)); + addTextField(createFloatField(204, fieldX, y, 50, disc.getRotationSpeed())); + } + + @Override + protected void handleTypeButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 104: + disc.setHoming(value == 1); + break; + case 106: + disc.setBoomerang(value == 1); + break; + case 108: + disc.setExplosive(value == 1); + break; + } + } + + @Override + protected void handleVisualButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 200: + editingVisualColorId = 200; + setSubGui(new SubGuiColorSelector(disc.getInnerColor())); + break; + case 201: + editingVisualColorId = 201; + setSubGui(new SubGuiColorSelector(disc.getOuterColor())); + break; + case 202: + disc.setOuterColorEnabled(value == 1); + initGui(); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { + SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; + int rgb = colorSelector.color & 0x00FFFFFF; + if (editingVisualColorId == 200) { + disc.setInnerColor(rgb); + } else if (editingVisualColorId == 201) { + disc.setOuterColor(rgb); + } + editingVisualColorId = 0; + initGui(); + } else { + super.subGuiClosed(subgui); + } + } + + @Override + protected void handleTypeTextField(int id, GuiNpcTextField field) { + switch (id) { + case 100: + disc.setDamage(parseFloat(field, disc.getDamage())); + break; + case 101: + disc.setSpeed(parseFloat(field, disc.getSpeed())); + break; + case 102: + disc.setDiscRadius(parseFloat(field, disc.getDiscRadius())); + break; + case 103: + disc.setDiscThickness(parseFloat(field, disc.getDiscThickness())); + break; + case 105: + disc.setHomingStrength(parseFloat(field, disc.getHomingStrength())); + break; + case 107: + disc.setBoomerangDelay(field.getInteger()); + break; + case 109: + disc.setExplosionRadius(parseFloat(field, disc.getExplosionRadius())); + break; + case 110: + disc.setKnockback(parseFloat(field, disc.getKnockback())); + break; + case 111: + disc.setMaxDistance(parseFloat(field, disc.getMaxDistance())); + break; + } + } + + @Override + protected void handleVisualTextField(int id, GuiNpcTextField field) { + switch (id) { + case 203: + disc.setOuterColorWidth(parseFloat(field, disc.getOuterColorWidth())); + break; + case 204: + disc.setRotationSpeed(parseFloat(field, disc.getRotationSpeed())); + break; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java new file mode 100644 index 000000000..afb89ffed --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java @@ -0,0 +1,228 @@ +package noppes.npcs.client.gui.advanced.ability; + +import kamkeel.npcs.controllers.data.ability.type.AbilityEnergyBeam; +import noppes.npcs.client.gui.SubGuiColorSelector; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * GUI for configuring Energy Beam ability type-specific settings. + * Energy Beam is a homing head with trailing path (DBZ-style). + */ +public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { + + private final AbilityEnergyBeam beam; + private int editingVisualColorId = 0; + + public SubGuiAbilityEnergyBeam(AbilityEnergyBeam ability, IAbilityConfigCallback callback) { + super(ability, callback); + this.beam = ability; + } + + @Override + protected boolean hasVisualSettings() { + return true; + } + + @Override + protected void initTypeTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 145; + int col2FieldX = guiLeft + 205; + + // Row 1: Damage + Speed + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); + addTextField(createFloatField(100, fieldX, y, 50, beam.getDamage())); + + addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); + addTextField(createFloatField(101, col2FieldX, y, 50, beam.getSpeed())); + + y += 24; + + // Row 2: Beam Width + Head Size + addLabel(new GuiNpcLabel(102, "ability.beamWidth", labelX, y + 5)); + addTextField(createFloatField(102, fieldX, y, 50, beam.getBeamWidth())); + + addLabel(new GuiNpcLabel(103, "ability.headSize", col2LabelX, y + 5)); + addTextField(createFloatField(103, col2FieldX, y, 50, beam.getHeadSize())); + + y += 24; + + // Row 3: Homing + Homing Strength + addLabel(new GuiNpcLabel(104, "ability.homing", labelX, y + 5)); + addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isHoming() ? 1 : 0)); + + addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); + addTextField(createFloatField(105, col2FieldX, y, 50, beam.getHomingStrength())); + + y += 24; + + // Row 4: Homing Range + Max Distance + addLabel(new GuiNpcLabel(106, "ability.homingRange", labelX, y + 5)); + addTextField(createFloatField(106, fieldX, y, 50, beam.getHomingRange())); + + addLabel(new GuiNpcLabel(107, "ability.maxDist", col2LabelX, y + 5)); + addTextField(createFloatField(107, col2FieldX, y, 50, beam.getMaxDistance())); + + y += 24; + + // Row 5: Explosive + Explosion Radius + addLabel(new GuiNpcLabel(108, "ability.explosive", labelX, y + 5)); + addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isExplosive() ? 1 : 0)); + + addLabel(new GuiNpcLabel(109, "ability.explosionRad", col2LabelX, y + 5)); + addTextField(createFloatField(109, col2FieldX, y, 50, beam.getExplosionRadius())); + + y += 24; + + // Row 6: Knockback + Max Lifetime + addLabel(new GuiNpcLabel(110, "ability.knockback", labelX, y + 5)); + addTextField(createFloatField(110, fieldX, y, 50, beam.getKnockback())); + + addLabel(new GuiNpcLabel(111, "ability.lifetime", col2LabelX, y + 5)); + addTextField(createIntField(111, col2FieldX, y, 50, beam.getMaxLifetime())); + } + + @Override + protected void initVisualTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 180; + int col2FieldX = guiLeft + 260; + + // Row 1: Inner Color + Outer Color + addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); + String innerHex = String.format("%06X", beam.getInnerColor() & 0xFFFFFF); + GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); + innerColorBtn.setTextColor(beam.getInnerColor() & 0xFFFFFF); + addButton(innerColorBtn); + + addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); + String outerHex = String.format("%06X", beam.getOuterColor() & 0xFFFFFF); + GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); + outerColorBtn.setTextColor(beam.getOuterColor() & 0xFFFFFF); + outerColorBtn.setEnabled(beam.isOuterColorEnabled()); + addButton(outerColorBtn); + + y += 24; + + // Row 2: Outer Color Enabled + Outer Color Width + addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); + addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isOuterColorEnabled() ? 1 : 0)); + + addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); + GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, beam.getOuterColorWidth()); + widthField.setEnabled(beam.isOuterColorEnabled()); + addTextField(widthField); + + y += 24; + + // Row 3: Rotation Speed + addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", labelX, y + 5)); + addTextField(createFloatField(204, fieldX, y, 50, beam.getRotationSpeed())); + } + + @Override + protected void handleTypeButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 104: + beam.setHoming(value == 1); + break; + case 108: + beam.setExplosive(value == 1); + break; + } + } + + @Override + protected void handleVisualButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 200: + editingVisualColorId = 200; + setSubGui(new SubGuiColorSelector(beam.getInnerColor())); + break; + case 201: + editingVisualColorId = 201; + setSubGui(new SubGuiColorSelector(beam.getOuterColor())); + break; + case 202: + beam.setOuterColorEnabled(value == 1); + initGui(); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { + SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; + int rgb = colorSelector.color & 0x00FFFFFF; + if (editingVisualColorId == 200) { + beam.setInnerColor(rgb); + } else if (editingVisualColorId == 201) { + beam.setOuterColor(rgb); + } + editingVisualColorId = 0; + initGui(); + } else { + super.subGuiClosed(subgui); + } + } + + @Override + protected void handleTypeTextField(int id, GuiNpcTextField field) { + switch (id) { + case 100: + beam.setDamage(parseFloat(field, beam.getDamage())); + break; + case 101: + beam.setSpeed(parseFloat(field, beam.getSpeed())); + break; + case 102: + beam.setBeamWidth(parseFloat(field, beam.getBeamWidth())); + break; + case 103: + beam.setHeadSize(parseFloat(field, beam.getHeadSize())); + break; + case 105: + beam.setHomingStrength(parseFloat(field, beam.getHomingStrength())); + break; + case 106: + beam.setHomingRange(parseFloat(field, beam.getHomingRange())); + break; + case 107: + beam.setMaxDistance(parseFloat(field, beam.getMaxDistance())); + break; + case 109: + beam.setExplosionRadius(parseFloat(field, beam.getExplosionRadius())); + break; + case 110: + beam.setKnockback(parseFloat(field, beam.getKnockback())); + break; + case 111: + beam.setMaxLifetime(field.getInteger()); + break; + } + } + + @Override + protected void handleVisualTextField(int id, GuiNpcTextField field) { + switch (id) { + case 203: + beam.setOuterColorWidth(parseFloat(field, beam.getOuterColorWidth())); + break; + case 204: + beam.setRotationSpeed(parseFloat(field, beam.getRotationSpeed())); + break; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java new file mode 100644 index 000000000..6f5c8ea57 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java @@ -0,0 +1,159 @@ +package noppes.npcs.client.gui.advanced.ability; + +import kamkeel.npcs.controllers.data.ability.type.AbilityLaserShot; +import noppes.npcs.client.gui.SubGuiColorSelector; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * GUI for configuring Laser Shot ability type-specific settings. + */ +public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { + + private final AbilityLaserShot laser; + private int editingTypeColorId = 0; + + public SubGuiAbilityLaserShot(AbilityLaserShot ability, IAbilityConfigCallback callback) { + super(ability, callback); + this.laser = ability; + } + + @Override + protected void initTypeTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 145; + int col2FieldX = guiLeft + 205; + + // Row 1: Damage + Laser Width + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); + addTextField(createFloatField(100, fieldX, y, 50, laser.getDamage())); + + addLabel(new GuiNpcLabel(101, "ability.laserWidth", col2LabelX, y + 5)); + addTextField(createFloatField(101, col2FieldX, y, 50, laser.getLaserWidth())); + + y += 24; + + // Row 2: Expansion Speed + Linger Ticks + addLabel(new GuiNpcLabel(102, "ability.expansionSpeed", labelX, y + 5)); + addTextField(createFloatField(102, fieldX, y, 50, laser.getExpansionSpeed())); + + addLabel(new GuiNpcLabel(103, "ability.lingerTicks", col2LabelX, y + 5)); + addTextField(createIntField(103, col2FieldX, y, 50, laser.getLingerTicks())); + + y += 24; + + // Row 3: Max Distance + Max Lifetime + addLabel(new GuiNpcLabel(104, "ability.maxDist", labelX, y + 5)); + addTextField(createFloatField(104, fieldX, y, 50, laser.getMaxDistance())); + + addLabel(new GuiNpcLabel(105, "ability.lifetime", col2LabelX, y + 5)); + addTextField(createIntField(105, col2FieldX, y, 50, laser.getMaxLifetime())); + + y += 24; + + // Row 4: Explosive + Explosion Radius + addLabel(new GuiNpcLabel(106, "ability.explosive", labelX, y + 5)); + addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, laser.isExplosive() ? 1 : 0)); + + addLabel(new GuiNpcLabel(107, "ability.explosionRad", col2LabelX, y + 5)); + addTextField(createFloatField(107, col2FieldX, y, 50, laser.getExplosionRadius())); + + y += 24; + + // Row 5: Knockback + Knockback Up + addLabel(new GuiNpcLabel(108, "ability.knockback", labelX, y + 5)); + addTextField(createFloatField(108, fieldX, y, 50, laser.getKnockback())); + + addLabel(new GuiNpcLabel(109, "ability.knockbackUp", col2LabelX, y + 5)); + addTextField(createFloatField(109, col2FieldX, y, 50, laser.getKnockbackUp())); + + y += 24; + + // Row 6: Inner Color + Outer Color + addLabel(new GuiNpcLabel(120, "ability.innerColor", labelX, y + 5)); + String innerHex = String.format("%06X", laser.getInnerColor() & 0xFFFFFF); + GuiNpcButton innerColorBtn = new GuiNpcButton(120, fieldX, y, 50, 20, innerHex); + innerColorBtn.setTextColor(laser.getInnerColor() & 0xFFFFFF); + addButton(innerColorBtn); + + addLabel(new GuiNpcLabel(121, "ability.outerColor", col2LabelX, y + 5)); + String outerHex = String.format("%06X", laser.getOuterColor() & 0xFFFFFF); + GuiNpcButton outerColorBtn = new GuiNpcButton(121, col2FieldX, y, 50, 20, outerHex); + outerColorBtn.setTextColor(laser.getOuterColor() & 0xFFFFFF); + addButton(outerColorBtn); + } + + @Override + protected void handleTypeButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 106: + laser.setExplosive(value == 1); + break; + case 120: + editingTypeColorId = 120; + setSubGui(new SubGuiColorSelector(laser.getInnerColor())); + break; + case 121: + editingTypeColorId = 121; + setSubGui(new SubGuiColorSelector(laser.getOuterColor())); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiColorSelector && editingTypeColorId != 0) { + SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; + int rgb = colorSelector.color & 0x00FFFFFF; + if (editingTypeColorId == 120) { + laser.setInnerColor(rgb); + } else if (editingTypeColorId == 121) { + laser.setOuterColor(rgb); + } + editingTypeColorId = 0; + initGui(); + } else { + super.subGuiClosed(subgui); + } + } + + @Override + protected void handleTypeTextField(int id, GuiNpcTextField field) { + switch (id) { + case 100: + laser.setDamage(parseFloat(field, laser.getDamage())); + break; + case 101: + laser.setLaserWidth(parseFloat(field, laser.getLaserWidth())); + break; + case 102: + laser.setExpansionSpeed(parseFloat(field, laser.getExpansionSpeed())); + break; + case 103: + laser.setLingerTicks(field.getInteger()); + break; + case 104: + laser.setMaxDistance(parseFloat(field, laser.getMaxDistance())); + break; + case 105: + laser.setMaxLifetime(field.getInteger()); + break; + case 107: + laser.setExplosionRadius(parseFloat(field, laser.getExplosionRadius())); + break; + case 108: + laser.setKnockback(parseFloat(field, laser.getKnockback())); + break; + case 109: + laser.setKnockbackUp(parseFloat(field, laser.getKnockbackUp())); + break; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java index f94e8d0cf..9f5ac4caf 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -1,11 +1,13 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; +import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; /** * GUI for configuring Orb ability type-specific settings. @@ -13,12 +15,18 @@ import noppes.npcs.client.gui.util.IAbilityConfigCallback; public class SubGuiAbilityOrb extends SubGuiAbilityConfig { private final AbilityOrb orb; + private int editingVisualColorId = 0; public SubGuiAbilityOrb(AbilityOrb ability, IAbilityConfigCallback callback) { super(ability, callback); this.orb = ability; } + @Override + protected boolean hasVisualSettings() { + return true; + } + @Override protected void initTypeTab(int startY) { int y = startY; @@ -71,6 +79,46 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { addTextField(createIntField(109, col2FieldX, y, 50, orb.getMaxLifetime())); } + @Override + protected void initVisualTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 180; + int col2FieldX = guiLeft + 260; + + // Row 1: Inner Color + Outer Color + addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); + String innerHex = String.format("%06X", orb.getInnerColor() & 0xFFFFFF); + GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); + innerColorBtn.setTextColor(orb.getInnerColor() & 0xFFFFFF); + addButton(innerColorBtn); + + addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); + String outerHex = String.format("%06X", orb.getOuterColor() & 0xFFFFFF); + GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); + outerColorBtn.setTextColor(orb.getOuterColor() & 0xFFFFFF); + outerColorBtn.setEnabled(orb.isOuterColorEnabled()); + addButton(outerColorBtn); + + y += 24; + + // Row 2: Outer Color Enabled + Outer Color Width + addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); + addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isOuterColorEnabled() ? 1 : 0)); + + addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); + GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, orb.getOuterColorWidth()); + widthField.setEnabled(orb.isOuterColorEnabled()); + addTextField(widthField); + + y += 24; + + // Row 3: Rotation Speed + addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", labelX, y + 5)); + addTextField(createFloatField(204, fieldX, y, 50, orb.getRotationSpeed())); + } + @Override protected void handleTypeButton(int id, GuiNpcButton button) { int value = button.getValue(); @@ -84,6 +132,42 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { } } + @Override + protected void handleVisualButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 200: + editingVisualColorId = 200; + setSubGui(new SubGuiColorSelector(orb.getInnerColor())); + break; + case 201: + editingVisualColorId = 201; + setSubGui(new SubGuiColorSelector(orb.getOuterColor())); + break; + case 202: + orb.setOuterColorEnabled(value == 1); + initGui(); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { + SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; + int rgb = colorSelector.color & 0x00FFFFFF; + if (editingVisualColorId == 200) { + orb.setInnerColor(rgb); + } else if (editingVisualColorId == 201) { + orb.setOuterColor(rgb); + } + editingVisualColorId = 0; + initGui(); + } else { + super.subGuiClosed(subgui); + } + } + @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { @@ -113,4 +197,16 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { break; } } + + @Override + protected void handleVisualTextField(int id, GuiNpcTextField field) { + switch (id) { + case 203: + orb.setOuterColorWidth(parseFloat(field, orb.getOuterColorWidth())); + break; + case 204: + orb.setRotationSpeed(parseFloat(field, orb.getRotationSpeed())); + break; + } + } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java new file mode 100644 index 000000000..95f54619b --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java @@ -0,0 +1,198 @@ +package noppes.npcs.client.gui.advanced.ability; + +import kamkeel.npcs.controllers.data.ability.type.AbilitySweeper; +import noppes.npcs.client.gui.SubGuiColorSelector; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * GUI for configuring Sweeper ability type-specific settings. + * Sweeper is a low rotating beam that players can jump over. + */ +public class SubGuiAbilitySweeper extends SubGuiAbilityConfig { + + private final AbilitySweeper sweeper; + private int editingVisualColorId = 0; + + public SubGuiAbilitySweeper(AbilitySweeper ability, IAbilityConfigCallback callback) { + super(ability, callback); + this.sweeper = ability; + } + + @Override + protected boolean hasVisualSettings() { + return true; + } + + @Override + protected void initTypeTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 145; + int col2FieldX = guiLeft + 205; + + // Row 1: Damage + Damage Interval + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); + addTextField(createFloatField(100, fieldX, y, 50, sweeper.getDamage())); + + addLabel(new GuiNpcLabel(101, "ability.dmgInterval", col2LabelX, y + 5)); + addTextField(createIntField(101, col2FieldX, y, 50, sweeper.getDamageInterval())); + + y += 24; + + // Row 2: Beam Length + Beam Width + addLabel(new GuiNpcLabel(102, "ability.beamLength", labelX, y + 5)); + addTextField(createFloatField(102, fieldX, y, 50, sweeper.getBeamLength())); + + addLabel(new GuiNpcLabel(103, "ability.beamWidth", col2LabelX, y + 5)); + addTextField(createFloatField(103, col2FieldX, y, 50, sweeper.getBeamWidth())); + + y += 24; + + // Row 3: Beam Height + Sweep Speed + addLabel(new GuiNpcLabel(104, "ability.beamHeight", labelX, y + 5)); + addTextField(createFloatField(104, fieldX, y, 50, sweeper.getBeamHeight())); + + addLabel(new GuiNpcLabel(105, "ability.sweepSpeed", col2LabelX, y + 5)); + addTextField(createFloatField(105, col2FieldX, y, 50, sweeper.getSweepSpeed())); + + y += 24; + + // Row 4: Number of Rotations + Piercing + addLabel(new GuiNpcLabel(106, "ability.rotations", labelX, y + 5)); + addTextField(createIntField(106, fieldX, y, 50, sweeper.getNumberOfRotations())); + + addLabel(new GuiNpcLabel(107, "ability.piercing", col2LabelX, y + 5)); + addButton(new GuiNpcButton(107, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, sweeper.isPiercing() ? 1 : 0)); + + y += 24; + + // Row 5: Lock On Target + addLabel(new GuiNpcLabel(108, "ability.lockTarget", labelX, y + 5)); + addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, sweeper.isLockOnTarget() ? 1 : 0)); + } + + @Override + protected void initVisualTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 180; + int col2FieldX = guiLeft + 260; + + // Row 1: Inner Color + Outer Color + addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); + String innerHex = String.format("%06X", sweeper.getInnerColor() & 0xFFFFFF); + GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); + innerColorBtn.setTextColor(sweeper.getInnerColor() & 0xFFFFFF); + addButton(innerColorBtn); + + addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); + String outerHex = String.format("%06X", sweeper.getOuterColor() & 0xFFFFFF); + GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); + outerColorBtn.setTextColor(sweeper.getOuterColor() & 0xFFFFFF); + outerColorBtn.setEnabled(sweeper.isOuterColorEnabled()); + addButton(outerColorBtn); + + y += 24; + + // Row 2: Outer Color Enabled + Outer Color Width + addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); + addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, sweeper.isOuterColorEnabled() ? 1 : 0)); + + addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); + GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, sweeper.getOuterColorWidth()); + widthField.setEnabled(sweeper.isOuterColorEnabled()); + addTextField(widthField); + } + + @Override + protected void handleTypeButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 107: + sweeper.setPiercing(value == 1); + break; + case 108: + sweeper.setLockOnTarget(value == 1); + break; + } + } + + @Override + protected void handleVisualButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 200: + editingVisualColorId = 200; + setSubGui(new SubGuiColorSelector(sweeper.getInnerColor())); + break; + case 201: + editingVisualColorId = 201; + setSubGui(new SubGuiColorSelector(sweeper.getOuterColor())); + break; + case 202: + sweeper.setOuterColorEnabled(value == 1); + initGui(); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { + SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; + int rgb = colorSelector.color & 0x00FFFFFF; + if (editingVisualColorId == 200) { + sweeper.setInnerColor(rgb); + } else if (editingVisualColorId == 201) { + sweeper.setOuterColor(rgb); + } + editingVisualColorId = 0; + initGui(); + } else { + super.subGuiClosed(subgui); + } + } + + @Override + protected void handleTypeTextField(int id, GuiNpcTextField field) { + switch (id) { + case 100: + sweeper.setDamage(parseFloat(field, sweeper.getDamage())); + break; + case 101: + sweeper.setDamageInterval(field.getInteger()); + break; + case 102: + sweeper.setBeamLength(parseFloat(field, sweeper.getBeamLength())); + break; + case 103: + sweeper.setBeamWidth(parseFloat(field, sweeper.getBeamWidth())); + break; + case 104: + sweeper.setBeamHeight(parseFloat(field, sweeper.getBeamHeight())); + break; + case 105: + sweeper.setSweepSpeed(parseFloat(field, sweeper.getSweepSpeed())); + break; + case 106: + sweeper.setNumberOfRotations(field.getInteger()); + break; + } + } + + @Override + protected void handleVisualTextField(int id, GuiNpcTextField field) { + switch (id) { + case 203: + sweeper.setOuterColorWidth(parseFloat(field, sweeper.getOuterColorWidth())); + break; + } + } +} diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 7f0618786..f3bba0c1d 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1534,6 +1534,7 @@ telegraph.point=Point ability.tab.timing=Timing ability.tab.effects=Effects ability.tab.telegraph=Telegraph +ability.tab.visual=Visual ability.showTelegraph=Show Telegraph ability.telegraphColor=Color ability.telegraphSizeNote=Size is based on ability parameters @@ -1559,6 +1560,9 @@ ability.cnpc.guard=Guard ability.cnpc.heal=Heal ability.cnpc.hazard=Hazard ability.cnpc.trap=Trap +ability.cnpc.disc=Disc +ability.cnpc.laser_shot=Laser Shot +ability.cnpc.sweeper=Sweeper # Ability GUI - Type Tab @@ -1627,10 +1631,12 @@ ability.explRadius=Expl Rad # Beam Settings ability.beamLength=Length ability.beamWidth=Width +ability.beamHeight=Height ability.dmgInterval=Interval ability.sweepAngle=Sweep Ang ability.sweepSpeed=Sweep Spd ability.lockTarget=Lock Tgt +ability.rotations=Rotations # Guard Settings ability.dmgReduce=Dmg Reduce @@ -1667,6 +1673,30 @@ ability.triggerRadius=Trig Rad ability.armTime=Arm Time ability.maxTriggers=Max Trig +# Disc Settings +ability.discRadius=Disc Rad +ability.discThickness=Thickness +ability.boomerang=Boomerang +ability.boomerangDelay=Return Delay +ability.homingStr=Homing Str +ability.homingRange=Homing Rng +ability.explosionRad=Expl Rad +ability.rotationSpeed=Rot Speed +ability.innerColor=Inner Color +ability.outerColor=Outer Color +ability.outerEnabled=Outer Glow +ability.outerWidth=Outer Width +ability.noVisualSettings=No visual settings for this ability type + +# Laser Shot Settings +ability.laserWidth=Laser Width +ability.expansionSpeed=Expansion Spd +ability.lingerTicks=Linger Ticks + +# Energy Beam Settings +ability.headSize=Head Size +ability.attachedToOwner=Attached + # Ability Save/Load ability.save.confirm=Save ability preset ability.save.overwrite=Overwrite existing preset diff --git a/src/main/resources/assets/customnpcs/textures/entity/white.png b/src/main/resources/assets/customnpcs/textures/entity/white.png new file mode 100644 index 0000000000000000000000000000000000000000..32a20fb7c718f900475b55d010cbb94045caba98 GIT binary patch literal 176 zcmeAS@N?(olHy`uVBq!ia0y~yU<5K58CaNs)Vi3hp&(vykh>GZx^prwfgF}}M_)$< zhK>E)e-c@NeC7b35ZC|z|1UoxeFDhkEbxddW? Date: Mon, 26 Jan 2026 04:42:23 -0500 Subject: [PATCH 284/358] More Abilities --- .../client/renderer/RenderAbilityBeam.java | 45 +++--- .../client/renderer/RenderAbilityDisc.java | 11 +- .../client/renderer/RenderAbilityLaser.java | 27 ++-- .../client/renderer/RenderAbilityOrb.java | 16 +- .../client/renderer/RenderAbilitySweeper.java | 21 ++- .../controllers/data/ability/Ability.java | 147 +++++++++++------ .../data/ability/type/AbilityDisc.java | 37 ++++- .../data/ability/type/AbilityEnergyBeam.java | 35 ++++- .../data/ability/type/AbilityGuard.java | 13 +- .../data/ability/type/AbilityHeal.java | 5 + .../data/ability/type/AbilityLaserShot.java | 12 +- .../data/ability/type/AbilityOrb.java | 35 ++++- .../data/ability/type/AbilitySlam.java | 8 +- .../data/ability/type/AbilitySweeper.java | 2 +- .../data/telegraph/TelegraphInstance.java | 33 +++- .../npcs/entity/EntityAbilityBeam.java | 5 +- .../npcs/entity/EntityAbilityDisc.java | 5 +- .../npcs/entity/EntityAbilityLaser.java | 3 +- .../kamkeel/npcs/entity/EntityAbilityOrb.java | 5 +- .../npcs/entity/EntityAbilityProjectile.java | 19 ++- .../npcs/entity/EntityAbilitySweeper.java | 21 +++ src/main/java/noppes/npcs/DataAbilities.java | 148 +++++++++++------- .../client/gui/advanced/GuiNPCAbilities.java | 53 +++++-- .../ability/SubGuiAbilityLaserShot.java | 74 +++++++-- 24 files changed, 560 insertions(+), 220 deletions(-) diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java index 49cb4fb6c..204be9d7e 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java @@ -61,10 +61,10 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { GL11.glTranslated(renderOriginX, renderOriginY, renderOriginZ); // Render tail orb at origin (0,0,0) - static, no rotation - renderTailOrb(headSize * 0.8f, innerColor, outerColor); + renderTailOrb(headSize * 0.8f, innerColor, outerColor, beam.isOuterColorEnabled()); // Render trail segments with smooth connections - renderSmoothTrail(trail, beamWidth, innerColor, outerColor); + renderSmoothTrail(trail, beamWidth, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth()); // Render head at interpolated offset position renderHead(beam, headOffsetX, headOffsetY, headOffsetZ, headSize, innerColor, outerColor, partialTicks); @@ -78,17 +78,19 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { * Render the tail orb at the origin (where the beam emanates from). * This orb does not rotate - it's a stable anchor point. */ - private void renderTailOrb(float size, int innerColor, int outerColor) { + private void renderTailOrb(float size, int innerColor, int outerColor, boolean outerColorEnabled) { GL11.glPushMatrix(); // Already at origin (0,0,0) - // Render outer glow - GL11.glDepthMask(false); - GL11.glPushMatrix(); - GL11.glScalef(size, size, size); - renderCube(outerColor, 0.4f, 0.5f); - GL11.glPopMatrix(); - GL11.glDepthMask(true); + // Render outer glow only if enabled + if (outerColorEnabled) { + GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(size, size, size); + renderCube(outerColor, 0.4f, 0.5f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + } // Render inner core (smaller, solid) GL11.glPushMatrix(); @@ -103,7 +105,8 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { * Render the beam trail with smooth connections between segments. * Uses averaged perpendicular vectors at connection points to prevent gaps. */ - private void renderSmoothTrail(List trail, float width, int innerColor, int outerColor) { + private void renderSmoothTrail(List trail, float width, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth) { if (trail.size() < 2) return; int trailSize = trail.size(); @@ -112,10 +115,12 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { // At connection points, average the perpendiculars for smooth transitions List perpFrames = computePerpendiculars(trail); - // Render outer glow trail (wider, translucent) - GL11.glDepthMask(false); - renderTrailTube(trail, perpFrames, width * 1.8f, outerColor, 0.3f, true); - GL11.glDepthMask(true); + // Render outer glow trail (wider, translucent) - only if enabled + if (outerColorEnabled) { + GL11.glDepthMask(false); + renderTrailTube(trail, perpFrames, width * outerColorWidth, outerColor, 0.3f, true); + GL11.glDepthMask(true); + } // Render inner core trail renderTrailTube(trail, perpFrames, width * 0.6f, innerColor, 0.9f, false); @@ -331,10 +336,12 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { GL11.glPushMatrix(); GL11.glScalef(scale, scale, scale); - // Render outer glow - GL11.glDepthMask(false); - renderCube(outerColor, 0.5f, 0.5f); - GL11.glDepthMask(true); + // Render outer glow only if enabled + if (beam.isOuterColorEnabled()) { + GL11.glDepthMask(false); + renderCube(outerColor, 0.5f, 0.5f); + GL11.glDepthMask(true); + } // Render inner core float innerScale = 0.6f; diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java index 93049fad5..13de38b50 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java @@ -39,10 +39,13 @@ public class RenderAbilityDisc extends RenderAbilityProjectile { // Apply ONLY Y-axis rotation for flat spin GL11.glRotatef(disc.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); - // Render outer square (translucent) - GL11.glDepthMask(false); - renderFlatSquare(disc.getOuterColor(), 0.5f, radius, thickness); - GL11.glDepthMask(true); + // Render outer square (translucent) - only if enabled + if (disc.isOuterColorEnabled()) { + float outerScale = disc.getOuterColorWidth() / 1.8f; // Normalize to default width + GL11.glDepthMask(false); + renderFlatSquare(disc.getOuterColor(), 0.5f, radius * outerScale, thickness); + GL11.glDepthMask(true); + } // Render inner square (solid, smaller) float innerScale = 0.6f; diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java index 0c1d4a1e8..fce85d2db 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java @@ -84,19 +84,22 @@ public class RenderAbilityLaser extends RenderAbilityProjectile { double vertY = dz * horzX - dx * horzZ; double vertZ = dx * horzY - dy * horzX; - // Render outer rectangle (larger, outer color, translucent) - GL11.glDepthMask(false); - renderBeamRectangle(renderStartX, renderStartY, renderStartZ, - renderEndX, renderEndY, renderEndZ, - horzX, horzY, horzZ, vertX, vertY, vertZ, - width * 1.8f, laser.getOuterColor(), alpha * 0.4f); + // Render outer rectangle (larger, outer color, translucent) - only if enabled + if (laser.isOuterColorEnabled()) { + float outerScale = laser.getOuterColorWidth(); + GL11.glDepthMask(false); + renderBeamRectangle(renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, + horzX, horzY, horzZ, vertX, vertY, vertZ, + width * outerScale, laser.getOuterColor(), alpha * 0.4f); - // Render middle layer - renderBeamRectangle(renderStartX, renderStartY, renderStartZ, - renderEndX, renderEndY, renderEndZ, - horzX, horzY, horzZ, vertX, vertY, vertZ, - width * 1.3f, laser.getOuterColor(), alpha * 0.7f); - GL11.glDepthMask(true); + // Render middle layer + renderBeamRectangle(renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, + horzX, horzY, horzZ, vertX, vertY, vertZ, + width * 1.3f, laser.getOuterColor(), alpha * 0.7f); + GL11.glDepthMask(true); + } // Render inner core (smaller, inner color, solid) renderBeamRectangle(renderStartX, renderStartY, renderStartZ, diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java index 3a09e7b6a..39048648d 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java @@ -69,11 +69,17 @@ public class RenderAbilityOrb extends Render { GL11.glRotatef(orb.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); GL11.glRotatef(orb.getInterpolatedRotationZ(partialTicks), 0.0f, 0.0f, 1.0f); - // Render outer sphere (translucent) - GL11.glDepthMask(false); - this.bindTexture(WHITE_TEXTURE); - renderSphere(orb.getOuterColor(), 0.6f); - GL11.glDepthMask(true); + // Render outer sphere (translucent) - only if enabled + if (orb.isOuterColorEnabled()) { + GL11.glDepthMask(false); + this.bindTexture(WHITE_TEXTURE); + float outerScale = orb.getOuterColorWidth() / 1.8f; // Normalize to default width + GL11.glPushMatrix(); + GL11.glScalef(outerScale, outerScale, outerScale); + renderSphere(orb.getOuterColor(), 0.6f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + } // Render inner sphere (more opaque) float innerScale = 0.75f; diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java index 735c1e183..0a24f1773 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java @@ -26,6 +26,8 @@ public class RenderAbilitySweeper extends Render { float beamWidth = sweeper.getBeamWidth(); int innerColor = sweeper.getInnerColor(); int outerColor = sweeper.getOuterColor(); + boolean outerColorEnabled = sweeper.isOuterColorEnabled(); + float outerColorWidth = sweeper.getOuterColorWidth(); float angle = sweeper.getInterpolatedAngle(partialTicks); GL11.glPushMatrix(); @@ -35,7 +37,7 @@ public class RenderAbilitySweeper extends Render { GL11.glRotatef(-angle, 0, 1, 0); // Negative for correct direction // Render the beam extending in the +Z direction (like beam trail segments) - renderBeam(beamLength, beamWidth, innerColor, outerColor); + renderBeam(beamLength, beamWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth); GL11.glPopMatrix(); @@ -65,7 +67,8 @@ public class RenderAbilitySweeper extends Render { * Render beam using the same style as the Beam entity trail. * Three layers: outer glow, middle, inner core. */ - private void renderBeam(float length, float width, int innerColor, int outerColor) { + private void renderBeam(float length, float width, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth) { // Extract colors float outerR = ((outerColor >> 16) & 0xFF) / 255.0f; float outerG = ((outerColor >> 8) & 0xFF) / 255.0f; @@ -78,13 +81,15 @@ public class RenderAbilitySweeper extends Render { // Render from origin (0,0,0) to (0,0,length) in local coords // After rotation this extends in the correct world direction - // Outer glow (wider, translucent) - like beam trail - GL11.glDepthMask(false); - renderBeamSegment(0, 0, 0, 0, 0, length, width * 1.8f, outerR, outerG, outerB, 0.3f); - GL11.glDepthMask(true); + // Outer glow (wider, translucent) - like beam trail - only if enabled + if (outerColorEnabled) { + GL11.glDepthMask(false); + renderBeamSegment(0, 0, 0, 0, 0, length, width * outerColorWidth, outerR, outerG, outerB, 0.3f); + GL11.glDepthMask(true); - // Middle layer - renderBeamSegment(0, 0, 0, 0, 0, length, width * 1.3f, outerR, outerG, outerB, 0.6f); + // Middle layer + renderBeamSegment(0, 0, 0, 0, 0, length, width * 1.3f, outerR, outerG, outerB, 0.6f); + } // Inner core (solid) - like beam trail inner renderBeamSegment(0, 0, 0, 0, 0, length, width * 0.6f, innerR, innerG, innerB, 1.0f); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 18406c313..3f2165718 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -53,6 +53,10 @@ public abstract class Ability implements IAbility { // Interruption protected boolean interruptible = true; + // Wait for completion - if true, ability controls when active phase ends (ignores activeTicks) + // Used for abilities like Slam that need to wait for landing, or Beam that runs until entity dies + protected boolean waitForCompletion = false; + // Feedback protected boolean lockMovement = true; protected int windUpColor = 0x80FF4400; // Telegraph color during wind up @@ -133,7 +137,9 @@ public abstract class Ability implements IAbility { */ protected boolean applyAbilityDamage(EntityNPCInterface npc, EntityLivingBase hitEntity, float damage, float knockback) { - return applyAbilityDamage(npc, hitEntity, damage, knockback, 0.0f); + double dx = hitEntity.posX - npc.posX; + double dz = hitEntity.posZ - npc.posZ; + return applyAbilityDamageInternal(npc, hitEntity, damage, knockback, 0.0f, dx, dz); } /** @@ -149,6 +155,36 @@ public abstract class Ability implements IAbility { */ protected boolean applyAbilityDamage(EntityNPCInterface npc, EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { + double dx = hitEntity.posX - npc.posX; + double dz = hitEntity.posZ - npc.posZ; + return applyAbilityDamageInternal(npc, hitEntity, damage, knockback, knockbackUp, dx, dz); + } + + /** + * Apply damage to an entity with ability hit event support and custom knockback direction. + * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. + * + * @param npc The NPC executing the ability + * @param hitEntity The entity being hit + * @param damage The damage amount + * @param knockback The horizontal knockback + * @param knockbackDirX The X component of knockback direction + * @param knockbackDirZ The Z component of knockback direction + * @return true if damage was applied (not cancelled), false if cancelled + */ + protected boolean applyAbilityDamageWithDirection(EntityNPCInterface npc, EntityLivingBase hitEntity, + float damage, float knockback, + double knockbackDirX, double knockbackDirZ) { + return applyAbilityDamageInternal(npc, hitEntity, damage, knockback, 0.0f, knockbackDirX, knockbackDirZ); + } + + /** + * Internal unified damage application method. + * All public damage methods delegate to this. + */ + private boolean applyAbilityDamageInternal(EntityNPCInterface npc, EntityLivingBase hitEntity, + float damage, float knockback, float knockbackUp, + double knockbackDirX, double knockbackDirZ) { DataAbilities dataAbilities = npc.abilities; AbilityEvent.HitEvent event = dataAbilities.fireHitEvent( this, currentTarget, hitEntity, damage, knockback, knockbackUp); @@ -168,49 +204,6 @@ public abstract class Ability implements IAbility { } // Apply knockback if any - if (finalKnockback > 0 || finalKnockbackUp > 0) { - double dx = hitEntity.posX - npc.posX; - double dz = hitEntity.posZ - npc.posZ; - applyNpcKnockback(hitEntity, dx, dz, finalKnockback, finalKnockbackUp); - } - - return true; - } - - /** - * Apply damage to an entity with ability hit event support and custom knockback direction. - * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. - * - * @param npc The NPC executing the ability - * @param hitEntity The entity being hit - * @param damage The damage amount - * @param knockback The horizontal knockback - * @param knockbackDirX The X component of knockback direction (normalized) - * @param knockbackDirZ The Z component of knockback direction (normalized) - * @return true if damage was applied (not cancelled), false if cancelled - */ - protected boolean applyAbilityDamageWithDirection(EntityNPCInterface npc, EntityLivingBase hitEntity, - float damage, float knockback, - double knockbackDirX, double knockbackDirZ) { - DataAbilities dataAbilities = npc.abilities; - AbilityEvent.HitEvent event = dataAbilities.fireHitEvent( - this, currentTarget, hitEntity, damage, knockback, 0.0f); - - if (event == null) { - return false; // Cancelled - } - - // Apply damage with potentially modified values - float finalDamage = event.getDamage(); - float finalKnockback = event.getKnockback(); - float finalKnockbackUp = event.getKnockbackUp(); - - // Apply damage - use NpcDamageSource for consistency - if (finalDamage > 0) { - hitEntity.attackEntityFrom(new NpcDamageSource("mob", npc), finalDamage); - } - - // Apply knockback in specified direction if (finalKnockback > 0 || finalKnockbackUp > 0) { applyNpcKnockback(hitEntity, knockbackDirX, knockbackDirZ, finalKnockback, finalKnockbackUp); } @@ -383,6 +376,11 @@ public abstract class Ability implements IAbility { if (positionAtNpc) { // AOE_SELF abilities: telegraph follows NPC during windup instance.setEntityIdToFollow(npc.getEntityId()); + + // For LINE/CONE telegraphs: track target direction during windup + if ((telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE) && target != null) { + instance.setTargetEntityId(target.getEntityId()); + } } else if (target != null) { // AOE_TARGET abilities: telegraph follows target during windup instance.setEntityIdToFollow(target.getEntityId()); @@ -487,8 +485,15 @@ public abstract class Ability implements IAbility { } /** - * Tick the ability. Returns true if phase changed. - * Note: Cooldown is managed by DataAbilities when the ability completes. + * Tick the ability forward. Called every game tick while executing. + *

+ * Normal flow: WINDUP -> ACTIVE -> IDLE (no recovery in normal completion) + * Interrupted flow: WINDUP -> RECOVERY -> IDLE (recovery only on interrupt) + *

+ * If waitForCompletion is true, the ability controls when active ends + * by calling signalCompletion() instead of using activeTicks. + * + * @return true if phase changed this tick */ public boolean tick() { if (phase == AbilityPhase.IDLE) return false; @@ -504,16 +509,20 @@ public abstract class Ability implements IAbility { } break; case ACTIVE: - if (currentTick >= activeTicks) { - phase = AbilityPhase.RECOVERY; + // If waitForCompletion is true, ability must call signalCompletion() + // Otherwise, auto-complete when activeTicks is reached + if (!waitForCompletion && currentTick >= activeTicks) { + phase = AbilityPhase.IDLE; currentTick = 0; return true; } break; case RECOVERY: + // Recovery phase is only entered on interrupt + // Count down recoveryTicks then go to IDLE if (currentTick >= recoveryTicks) { phase = AbilityPhase.IDLE; - // Cooldown is managed by DataAbilities.onAbilityComplete() + currentTick = 0; return true; } break; @@ -522,11 +531,35 @@ public abstract class Ability implements IAbility { } /** - * Interrupt this ability + * Signal that the ability has completed its active phase. + * Called by abilities with waitForCompletion=true when they are done. + * For example, AbilitySlam calls this when the NPC lands. + * + * @return true if phase changed to IDLE + */ + public boolean signalCompletion() { + if (phase == AbilityPhase.ACTIVE) { + phase = AbilityPhase.IDLE; + currentTick = 0; + return true; + } + return false; + } + + /** + * Interrupt this ability. Transitions to RECOVERY phase if interruptible. + * Called when NPC takes direct damage during WINDUP. */ public void interrupt() { - phase = AbilityPhase.IDLE; - currentTick = 0; + if (interruptible && phase == AbilityPhase.WINDUP) { + // Interrupted during windup - go to recovery (dazed state) + phase = AbilityPhase.RECOVERY; + currentTick = 0; + } else { + // Force stop - go directly to IDLE + phase = AbilityPhase.IDLE; + currentTick = 0; + } currentTarget = null; telegraphInstance = null; } @@ -611,6 +644,7 @@ public abstract class Ability implements IAbility { nbt.setInteger("active", activeTicks); nbt.setInteger("recovery", recoveryTicks); nbt.setBoolean("interruptible", interruptible); + nbt.setBoolean("waitForCompletion", waitForCompletion); nbt.setBoolean("lockMovement", lockMovement); nbt.setInteger("windUpColor", windUpColor); nbt.setInteger("activeColor", activeColor); @@ -650,6 +684,7 @@ public abstract class Ability implements IAbility { activeTicks = nbt.getInteger("active"); recoveryTicks = nbt.getInteger("recovery"); interruptible = nbt.getBoolean("interruptible"); + waitForCompletion = nbt.getBoolean("waitForCompletion"); lockMovement = nbt.getBoolean("lockMovement"); // Support old telegraphColor key for backwards compatibility if (nbt.hasKey("windUpColor")) { @@ -807,6 +842,14 @@ public abstract class Ability implements IAbility { this.interruptible = interruptible; } + public boolean isWaitForCompletion() { + return waitForCompletion; + } + + public void setWaitForCompletion(boolean waitForCompletion) { + this.waitForCompletion = waitForCompletion; + } + public boolean isLockMovement() { return lockMovement; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 97d69e19c..0db44ea8a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -4,6 +4,8 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityDisc; import net.minecraft.entity.EntityLivingBase; @@ -69,8 +71,8 @@ public class AbilityDisc extends Ability { this.windUpTicks = 20; this.activeTicks = 1; this.recoveryTicks = 10; - this.telegraphType = TelegraphType.POINT; - this.showTelegraph = false; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; } @Override @@ -105,7 +107,7 @@ public class AbilityDisc extends Ability { EntityAbilityDisc disc = new EntityAbilityDisc( world, npc, target, spawnX, spawnY, spawnZ, - discRadius, discThickness, innerColor, outerColor, rotationSpeed, + discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, boomerang, boomerangDelay, @@ -129,6 +131,33 @@ public class AbilityDisc extends Ability { public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { } + @Override + public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create circle telegraph at target position + Telegraph telegraph = Telegraph.circle(discRadius * 1.5f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, npc.rotationYaw); + instance.setCasterEntityId(npc.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return discRadius * 1.5f; + } + @Override public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("speed", speed); @@ -182,7 +211,7 @@ public class AbilityDisc extends Ability { this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF8800; this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); - this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 8.0f; + this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 5.0f; } // Getters & Setters diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 2790eafd8..58a224c64 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -4,6 +4,8 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityBeam; import net.minecraft.entity.EntityLivingBase; @@ -68,8 +70,8 @@ public class AbilityEnergyBeam extends Ability { this.windUpTicks = 40; this.activeTicks = 200; // Match maxLifetime to keep NPC locked while beam is active this.recoveryTicks = 5; - this.telegraphType = TelegraphType.POINT; - this.showTelegraph = false; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; } @Override @@ -104,7 +106,7 @@ public class AbilityEnergyBeam extends Ability { EntityAbilityBeam beam = new EntityAbilityBeam( world, npc, target, spawnX, spawnY, spawnZ, - beamWidth, headSize, innerColor, outerColor, rotationSpeed, + beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, explosive, explosionRadius, explosionDamageFalloff, @@ -127,6 +129,33 @@ public class AbilityEnergyBeam extends Ability { public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { } + @Override + public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create circle telegraph at target position + Telegraph telegraph = Telegraph.circle(headSize * 2.0f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, npc.rotationYaw); + instance.setCasterEntityId(npc.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return headSize * 2.0f; + } + @Override public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("speed", speed); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index 6a98b643a..404589c6e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -128,17 +128,8 @@ public class AbilityGuard extends Ability { if (wasHit && counterSound != null && !counterSound.isEmpty()) { world.playSoundAtEntity(npc, counterSound, 1.0f, 1.2f); } - if (counterAnimationId >= 0) { - if (noppes.npcs.controllers.AnimationController.Instance != null) { - noppes.npcs.controllers.data.Animation animation = - noppes.npcs.controllers.AnimationController.Instance.animations.get(counterAnimationId); - if (animation != null) { - npc.display.animationData.setEnabled(true); - npc.display.animationData.setAnimation(animation); - npc.display.animationData.updateClient(); - } - } - } + // Use shared animation utility + npc.abilities.playAbilityAnimation(counterAnimationId); counterTriggered = false; lastAttacker = null; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index 2e23a1e56..3cf04db2c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -184,6 +184,11 @@ public class AbilityHeal extends Ability { } } + @Override + public void onInterrupt(EntityNPCInterface npc, net.minecraft.util.DamageSource source, float damage) { + healedAllies.clear(); + } + @Override public void reset() { super.reset(); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index 2b224e400..64a3da8ad 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -46,6 +46,8 @@ public class AbilityLaserShot extends Ability { // Visual properties private int innerColor = 0xFFFFFF; private int outerColor = 0xFF0000; + private float outerColorWidth = 1.8f; + private boolean outerColorEnabled = true; public AbilityLaserShot() { this.typeId = "ability.cnpc.laser_shot"; @@ -103,7 +105,7 @@ public class AbilityLaserShot extends Ability { EntityAbilityLaser laser = new EntityAbilityLaser( world, npc, target, spawnX, spawnY, spawnZ, - laserWidth, innerColor, outerColor, + laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, damage, knockback, knockbackUp, expansionSpeed, lingerTicks, explosive, explosionRadius, explosionDamageFalloff, @@ -144,6 +146,8 @@ public class AbilityLaserShot extends Ability { nbt.setInteger("slowLevel", slowLevel); nbt.setInteger("innerColor", innerColor); nbt.setInteger("outerColor", outerColor); + nbt.setFloat("outerColorWidth", outerColorWidth); + nbt.setBoolean("outerColorEnabled", outerColorEnabled); } @Override @@ -164,6 +168,8 @@ public class AbilityLaserShot extends Ability { this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF0000; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); } // Getters & Setters @@ -199,4 +205,8 @@ public class AbilityLaserShot extends Ability { public void setInnerColor(int innerColor) { this.innerColor = innerColor; } public int getOuterColor() { return outerColor; } public void setOuterColor(int outerColor) { this.outerColor = outerColor; } + public float getOuterColorWidth() { return outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; } + public boolean isOuterColorEnabled() { return outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index ff03ac39f..7b6f34b68 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -4,6 +4,8 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityOrb; import net.minecraft.entity.EntityLivingBase; @@ -65,8 +67,8 @@ public class AbilityOrb extends Ability { this.windUpTicks = 30; this.activeTicks = 1; // Minimal - just spawn and done this.recoveryTicks = 10; - this.telegraphType = TelegraphType.POINT; - this.showTelegraph = false; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; } @Override @@ -103,7 +105,7 @@ public class AbilityOrb extends Ability { EntityAbilityOrb orb = new EntityAbilityOrb( world, npc, target, spawnX, spawnY, spawnZ, - orbSize, innerColor, outerColor, rotationSpeed, + orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, orbSpeed, homing, homingStrength, homingRange, explosive, explosionRadius, explosionDamageFalloff, @@ -129,6 +131,33 @@ public class AbilityOrb extends Ability { // Nothing to clean up - entity manages itself } + @Override + public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create small circle telegraph at target position + Telegraph telegraph = Telegraph.circle(orbSize * 1.5f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, npc.rotationYaw); + instance.setCasterEntityId(npc.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return orbSize * 1.5f; + } + @Override public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("orbSpeed", orbSpeed); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 6068b269f..d2c17a3e8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -49,10 +49,11 @@ public class AbilitySlam extends Ability { this.name = "Slam"; this.targetingMode = TargetingMode.AOE_SELF; // Can also be AOE_TARGET to leap to target this.windUpTicks = 30; - this.activeTicks = 60; // Max time in air + landing + this.activeTicks = 60; // Max time in air (used as timeout if waitForCompletion fails) this.recoveryTicks = 10; this.cooldownTicks = 100; this.lockMovement = true; + this.waitForCompletion = true; // Ability completes when NPC lands, not by activeTicks this.minRange = 2.0f; this.maxRange = 15.0f; this.telegraphType = TelegraphType.CIRCLE; @@ -258,12 +259,15 @@ public class AbilitySlam extends Ability { } /** - * Called when NPC lands - deal AOE damage. + * Called when NPC lands - deal AOE damage and signal completion. */ private void onLanding(EntityNPCInterface npc, World world) { hasLanded = true; npc.setNpcJumpingState(false); + // Signal that the ability has completed its active phase + signalCompletion(); + // Reset fall distance to prevent fall damage on landing npc.fallDistance = 0; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index d1a67eb3a..b7ca2aa22 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -92,7 +92,7 @@ public class AbilitySweeper extends Ability { // Spawn the entity that handles BOTH visuals AND damage activeEntity = new EntityAbilitySweeper(world, npc, target, beamLength, beamWidth, beamHeight, - innerColor, outerColor, + innerColor, outerColor, outerColorEnabled, outerColorWidth, sweepSpeed, numberOfRotations, damage, damageInterval, piercing, lockOnTarget); diff --git a/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java index 47fdf8cbb..71d63f9c6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java @@ -26,6 +26,7 @@ public class TelegraphInstance { // Entity tracking private int entityIdToFollow = -1; private int casterEntityId = -1; + private int targetEntityId = -1; // For LINE telegraphs: entity to face (yaw tracking) // Timing private int remainingTicks; @@ -81,6 +82,16 @@ public class TelegraphInstance { } } + // Update yaw to face target entity (for LINE telegraphs) + if (targetEntityId >= 0 && world != null) { + Entity target = world.getEntityByID(targetEntityId); + if (target != null) { + double dx = target.posX - this.x; + double dz = target.posZ - this.z; + this.yaw = (float) (Math.atan2(-dx, dz) * 180.0 / Math.PI); + } + } + // Check for warning phase transition if (!isWarning && telegraph != null) { if (remainingTicks <= telegraph.getWarningStartTick()) { @@ -108,7 +119,7 @@ public class TelegraphInstance { } /** - * Get alpha-modulated color for pulsing animation. + * Get alpha-modulated color for breathing animation. */ public int getAnimatedColor(float partialTicks) { int baseColor = getCurrentColor(); @@ -116,9 +127,12 @@ public class TelegraphInstance { return baseColor; } - // Pulsing animation: modulate alpha based on time + // Breathing animation: slow, smooth modulation of alpha float time = (totalTicks - remainingTicks) + partialTicks; - float pulse = (float) (Math.sin(time * 0.3) * 0.3 + 0.7); // 0.4 to 1.0 range + // Slower breathing: 0.08 for ~4 second cycle, smoother sine curve + float breathPhase = (float) Math.sin(time * 0.08); + // Smooth easing: square the sine for more organic feel, range 0.5 to 1.0 + float pulse = 0.5f + (breathPhase * breathPhase * 0.5f * Math.signum(breathPhase)); int alpha = (baseColor >> 24) & 0xFF; int r = (baseColor >> 16) & 0xFF; @@ -149,6 +163,7 @@ public class TelegraphInstance { nbt.setInteger("entityIdToFollow", entityIdToFollow); nbt.setInteger("casterEntityId", casterEntityId); + nbt.setInteger("targetEntityId", targetEntityId); nbt.setInteger("remainingTicks", remainingTicks); nbt.setInteger("totalTicks", totalTicks); @@ -173,6 +188,7 @@ public class TelegraphInstance { this.entityIdToFollow = nbt.getInteger("entityIdToFollow"); this.casterEntityId = nbt.getInteger("casterEntityId"); + this.targetEntityId = nbt.getInteger("targetEntityId"); this.remainingTicks = nbt.getInteger("remainingTicks"); this.totalTicks = nbt.getInteger("totalTicks"); @@ -241,11 +257,12 @@ public class TelegraphInstance { } /** - * Locks the telegraph at its current position. + * Locks the telegraph at its current position and direction. * Stops following any entity - used when ability commits to action. */ public void lockPosition() { this.entityIdToFollow = -1; + this.targetEntityId = -1; } public int getCasterEntityId() { @@ -256,6 +273,14 @@ public class TelegraphInstance { this.casterEntityId = casterEntityId; } + public int getTargetEntityId() { + return targetEntityId; + } + + public void setTargetEntityId(int targetEntityId) { + this.targetEntityId = targetEntityId; + } + public int getRemainingTicks() { return remainingTicks; } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 586e57ae9..b0c3579e7 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -59,7 +59,8 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { */ public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, - float beamWidth, float headSize, int innerColor, int outerColor, float rotationSpeed, + float beamWidth, float headSize, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean explosive, float explosionRadius, float explosionDamageFalloff, @@ -69,7 +70,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { // Initialize base properties initProjectile(owner, target, x, y, z, - headSize, innerColor, outerColor, rotationSpeed, + headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index e46542d8c..ef513f80d 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -44,7 +44,8 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { */ public EntityAbilityDisc(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, - float discRadius, float discThickness, int innerColor, int outerColor, float rotationSpeed, + float discRadius, float discThickness, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean boomerang, int boomerangDelay, @@ -55,7 +56,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { // Initialize base properties (use discRadius as size for base) initProjectile(owner, target, x, y, z, - discRadius, innerColor, outerColor, rotationSpeed, + discRadius, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 02b797524..78668377a 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -50,6 +50,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { public EntityAbilityLaser(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, float laserWidth, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float damage, float knockback, float knockbackUp, float expansionSpeed, int lingerTicks, boolean explosive, float explosionRadius, float explosionDamageFalloff, @@ -59,7 +60,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { // Initialize base properties initProjectile(owner, target, x, y, z, - laserWidth, innerColor, outerColor, 0.0f, // No rotation for laser + laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, 0.0f, // No rotation for laser damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index c218007a0..de447333b 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -34,7 +34,8 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { */ public EntityAbilityOrb(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, - float orbSize, int innerColor, int outerColor, float rotationSpeed, + float orbSize, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean explosive, float explosionRadius, float explosionDamageFalloff, @@ -44,7 +45,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { // Initialize base properties initProjectile(owner, target, x, y, z, - orbSize, innerColor, outerColor, rotationSpeed, + orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index fa84d933f..eed12d10b 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -29,6 +29,8 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA protected float size = 1.0f; protected int innerColor = 0xFFFFFF; protected int outerColor = 0x8888FF; + protected boolean outerColorEnabled = true; + protected float outerColorWidth = 1.8f; protected float rotationSpeed = 4.0f; // ==================== COMBAT PROPERTIES ==================== @@ -85,7 +87,8 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA */ protected void initProjectile(EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, - float size, int innerColor, int outerColor, float rotationSpeed, + float size, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, float damage, float knockback, float knockbackUp, boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, @@ -102,6 +105,8 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA this.size = size; this.innerColor = innerColor; this.outerColor = outerColor; + this.outerColorEnabled = outerColorEnabled; + this.outerColorWidth = outerColorWidth; this.rotationSpeed = rotationSpeed; // Combat @@ -384,6 +389,14 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA return outerColor; } + public boolean isOuterColorEnabled() { + return outerColorEnabled; + } + + public float getOuterColorWidth() { + return outerColorWidth; + } + public float getRotationSpeed() { return rotationSpeed; } @@ -438,6 +451,8 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA this.size = nbt.getFloat("Size"); this.innerColor = nbt.getInteger("InnerColor"); this.outerColor = nbt.getInteger("OuterColor"); + this.outerColorEnabled = !nbt.hasKey("OuterColorEnabled") || nbt.getBoolean("OuterColorEnabled"); + this.outerColorWidth = nbt.hasKey("OuterColorWidth") ? nbt.getFloat("OuterColorWidth") : 1.8f; this.rotationSpeed = nbt.hasKey("RotationSpeed") ? nbt.getFloat("RotationSpeed") : 4.0f; this.damage = nbt.getFloat("Damage"); @@ -480,6 +495,8 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA nbt.setFloat("Size", size); nbt.setInteger("InnerColor", innerColor); nbt.setInteger("OuterColor", outerColor); + nbt.setBoolean("OuterColorEnabled", outerColorEnabled); + nbt.setFloat("OuterColorWidth", outerColorWidth); nbt.setFloat("RotationSpeed", rotationSpeed); nbt.setFloat("Damage", damage); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java index a5758a2ed..6ec4f8b38 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java @@ -32,6 +32,8 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa private float beamHeight = 0.8f; private int innerColor = 0xFF6600; private int outerColor = 0xFF0000; + private boolean outerColorEnabled = true; + private float outerColorWidth = 1.8f; // Combat properties private float damage = 5.0f; @@ -70,6 +72,7 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa public EntityAbilitySweeper(World world, EntityNPCInterface owner, EntityLivingBase target, float beamLength, float beamWidth, float beamHeight, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float sweepSpeed, int numberOfRotations, float damage, int damageInterval, boolean piercing, boolean lockOnTarget) { @@ -82,6 +85,8 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa this.beamHeight = beamHeight; this.innerColor = innerColor; this.outerColor = outerColor; + this.outerColorEnabled = outerColorEnabled; + this.outerColorWidth = outerColorWidth; this.sweepSpeed = sweepSpeed; this.numberOfRotations = numberOfRotations; this.damage = damage; @@ -323,6 +328,14 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa return outerColor; } + public boolean isOuterColorEnabled() { + return outerColorEnabled; + } + + public float getOuterColorWidth() { + return outerColorWidth; + } + /** * Get the full interpolated angle INCLUDING base yaw for rendering. */ @@ -353,6 +366,8 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa this.beamHeight = nbt.getFloat("BeamHeight"); this.innerColor = nbt.getInteger("InnerColor"); this.outerColor = nbt.getInteger("OuterColor"); + this.outerColorEnabled = !nbt.hasKey("OuterColorEnabled") || nbt.getBoolean("OuterColorEnabled"); + this.outerColorWidth = nbt.hasKey("OuterColorWidth") ? nbt.getFloat("OuterColorWidth") : 1.8f; this.sweepSpeed = nbt.getFloat("SweepSpeed"); this.numberOfRotations = nbt.getInteger("NumRotations"); this.completedRotations = nbt.getInteger("CompletedRotations"); @@ -375,6 +390,8 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa nbt.setFloat("BeamHeight", beamHeight); nbt.setInteger("InnerColor", innerColor); nbt.setInteger("OuterColor", outerColor); + nbt.setBoolean("OuterColorEnabled", outerColorEnabled); + nbt.setFloat("OuterColorWidth", outerColorWidth); nbt.setFloat("SweepSpeed", sweepSpeed); nbt.setInteger("NumRotations", numberOfRotations); nbt.setInteger("CompletedRotations", completedRotations); @@ -399,6 +416,8 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa buffer.writeFloat(beamHeight); buffer.writeInt(innerColor); buffer.writeInt(outerColor); + buffer.writeBoolean(outerColorEnabled); + buffer.writeFloat(outerColorWidth); buffer.writeFloat(sweepSpeed); buffer.writeInt(numberOfRotations); buffer.writeInt(completedRotations); @@ -420,6 +439,8 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa this.beamHeight = buffer.readFloat(); this.innerColor = buffer.readInt(); this.outerColor = buffer.readInt(); + this.outerColorEnabled = buffer.readBoolean(); + this.outerColorWidth = buffer.readFloat(); this.sweepSpeed = buffer.readFloat(); this.numberOfRotations = buffer.readInt(); this.completedRotations = buffer.readInt(); diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 51d2831ac..8875508b1 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -18,9 +18,7 @@ import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Random; /** @@ -47,9 +45,14 @@ public class DataAbilities { public boolean enabled = false; /** - * Minimum ticks between ability selections (global cooldown) + * Minimum cooldown ticks between abilities (global minimum) */ - public int globalCooldown = 20; + public int minCooldown = 80; + + /** + * Maximum cooldown ticks between abilities (global maximum) + */ + public int maxCooldown = 200; // ═══════════════════════════════════════════════════════════════════ // RUNTIME STATE (not saved) @@ -61,14 +64,9 @@ public class DataAbilities { private transient Ability currentAbility; /** - * Per-ability cooldown timers (ability ID -> world time when cooldown ends) + * World time when NPC cooldown ends (can select next ability) */ - private transient Map cooldowns = new HashMap<>(); - - /** - * Ticks until next ability can be selected (global cooldown timer) - */ - private transient int globalCooldownTimer = 0; + private transient long cooldownEndTime = 0; /** * Last target used for ability execution @@ -101,11 +99,6 @@ public class DataAbilities { return; } - // Tick global cooldown - if (globalCooldownTimer > 0) { - globalCooldownTimer--; - } - // Tick current ability if executing if (currentAbility != null && currentAbility.isExecuting()) { tickCurrentAbility(); @@ -192,24 +185,33 @@ public class DataAbilities { npc.motionZ = 0; } } + + // Lock look direction at target during ACTIVE phase only + if (shouldLockLookDirection() && target != null && target.isEntityAlive()) { + npc.getLookHelper().setLookPositionWithEntity(target, 30.0F, 30.0F); + } } } /** * Called when an ability completes. + * Calculates cooldown as: random(minCooldown, maxCooldown) + ability's cooldown offset */ private void onAbilityComplete() { if (currentAbility != null) { // Stop any ability animation stopAbilityAnimation(); - // Start per-ability cooldown - startCooldown(currentAbility); + // Calculate cooldown: random(min, max) + ability offset + int baseCooldown = minCooldown; + if (maxCooldown > minCooldown) { + baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1); + } + int totalCooldown = baseCooldown + currentAbility.getCooldownTicks(); + cooldownEndTime = npc.worldObj.getTotalWorldTime() + totalCooldown; + currentAbility = null; lastTarget = null; - - // Start global cooldown - globalCooldownTimer = globalCooldown; } } @@ -247,7 +249,8 @@ public class DataAbilities { if (currentAbility != null && currentAbility.isExecuting()) { return false; } - if (globalCooldownTimer > 0) { + // Check if still on cooldown + if (npc.worldObj.getTotalWorldTime() < cooldownEndTime) { return false; } return true; @@ -293,11 +296,6 @@ public class DataAbilities { return false; } - // Check cooldown - if (isOnCooldown(ability)) { - return false; - } - // Check if already executing if (ability.isExecuting()) { return false; @@ -358,8 +356,11 @@ public class DataAbilities { /** * Play an animation on the NPC by ID. + * Public so abilities can trigger animations directly if needed. + * + * @param animationId The global animation ID to play, or -1 for none */ - private void playAbilityAnimation(int animationId) { + public void playAbilityAnimation(int animationId) { if (animationId < 0) return; if (AnimationController.Instance == null) return; @@ -407,37 +408,25 @@ public class DataAbilities { // ═══════════════════════════════════════════════════════════════════ /** - * Check if an ability is on cooldown. + * Check if NPC is on cooldown (cannot use any ability). */ - public boolean isOnCooldown(Ability ability) { - Long endTime = cooldowns.get(ability.getId()); - if (endTime == null) { - return false; - } - return npc.worldObj.getTotalWorldTime() < endTime; + public boolean isOnCooldown() { + return npc.worldObj.getTotalWorldTime() < cooldownEndTime; } /** - * Start cooldown for an ability. + * Get remaining cooldown ticks. */ - private void startCooldown(Ability ability) { - long endTime = npc.worldObj.getTotalWorldTime() + ability.getCooldownTicks(); - cooldowns.put(ability.getId(), endTime); + public long getRemainingCooldown() { + long remaining = cooldownEndTime - npc.worldObj.getTotalWorldTime(); + return remaining > 0 ? remaining : 0; } /** - * Reset cooldown for a specific ability. + * Reset cooldown (allow immediate ability use). */ - public void resetCooldown(String abilityId) { - cooldowns.remove(abilityId); - } - - /** - * Reset all cooldowns. - */ - public void resetAllCooldowns() { - cooldowns.clear(); - globalCooldownTimer = 0; + public void resetCooldown() { + cooldownEndTime = 0; } // ═══════════════════════════════════════════════════════════════════ @@ -489,9 +478,18 @@ public class DataAbilities { /** * Record a hit for the hit count condition. + * Includes periodic cleanup to prevent memory leak. */ private void recordHit() { - recentHitTimes.add(npc.worldObj.getTotalWorldTime()); + long currentTime = npc.worldObj.getTotalWorldTime(); + recentHitTimes.add(currentTime); + + // Cleanup old entries periodically to prevent unbounded growth + // Keep only hits from last 5 minutes (6000 ticks) max + if (recentHitTimes.size() > 50) { + long cutoff = currentTime - 6000; + recentHitTimes.removeIf(time -> time < cutoff); + } } /** @@ -517,6 +515,8 @@ public class DataAbilities { /** * Interrupt the currently executing ability. + * The ability will transition to RECOVERY phase (dazed state) and continue ticking. + * When RECOVERY completes, the ability will move to IDLE and onAbilityComplete() is called. */ public void interruptCurrentAbility(DamageSource source, float damage) { if (currentAbility != null) { @@ -532,9 +532,10 @@ public class DataAbilities { NpcAPI.EVENT_BUS.post(interruptEvent); currentAbility.onInterrupt(npc, source, damage); - currentAbility.interrupt(); - currentAbility = null; - lastTarget = null; + currentAbility.interrupt(); // This now transitions to RECOVERY, not IDLE + + // Don't clear currentAbility - let it tick through RECOVERY phase + // When RECOVERY ends and phase becomes IDLE, onAbilityComplete() will be called } } @@ -605,7 +606,7 @@ public class DataAbilities { */ public void reset() { stopCurrentAbility(); - resetAllCooldowns(); + resetCooldown(); // Reset execution state on all abilities for (Ability ability : abilities) { @@ -707,6 +708,24 @@ public class DataAbilities { } } + /** + * Check if NPC's look direction should be locked to target. + * Only locks during ACTIVE phase when lockMovement is true. + * This allows the NPC to freely track target during WINDUP and RECOVERY. + */ + public boolean shouldLockLookDirection() { + if (currentAbility == null || !currentAbility.isExecuting()) { + return false; + } + + // Only lock look direction during ACTIVE phase + if (currentAbility.getPhase() != AbilityPhase.ACTIVE) { + return false; + } + + return currentAbility.isLockMovement(); + } + /** * Check if NPC should skip normal attacks during ability execution. */ @@ -724,7 +743,8 @@ public class DataAbilities { public NBTTagCompound writeToNBT(NBTTagCompound compound) { compound.setBoolean("AbilitiesEnabled", enabled); - compound.setInteger("AbilityGlobalCooldown", globalCooldown); + compound.setInteger("AbilityMinCooldown", minCooldown); + compound.setInteger("AbilityMaxCooldown", maxCooldown); NBTTagList abilityList = new NBTTagList(); for (Ability ability : abilities) { @@ -737,8 +757,20 @@ public class DataAbilities { public void readFromNBT(NBTTagCompound compound) { enabled = compound.getBoolean("AbilitiesEnabled"); - globalCooldown = compound.hasKey("AbilityGlobalCooldown") ? - compound.getInteger("AbilityGlobalCooldown") : 20; + + // Support old globalCooldown for backwards compatibility + if (compound.hasKey("AbilityMinCooldown")) { + minCooldown = compound.getInteger("AbilityMinCooldown"); + maxCooldown = compound.getInteger("AbilityMaxCooldown"); + } else if (compound.hasKey("AbilityGlobalCooldown")) { + // Migrate old single cooldown to min/max range + int oldCooldown = compound.getInteger("AbilityGlobalCooldown"); + minCooldown = oldCooldown; + maxCooldown = oldCooldown * 2; // Give some range + } else { + minCooldown = 20; + maxCooldown = 60; + } abilities.clear(); NBTTagList abilityList = compound.getTagList("Abilities", 10); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java index 0f41347d4..c0fb15596 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -51,7 +51,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Settings private boolean abilitiesEnabled = false; - private int globalCooldown = 20; + private int minCooldown = 20; + private int maxCooldown = 60; private String search = ""; private int selectedAbilityIndex = -1; @@ -72,12 +73,18 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC addLabel(new GuiNpcLabel(100, "gui.enabled", guiLeft + 5, y + 5)); addButton(new GuiNpcButton(100, guiLeft + 55, y, 50, 20, new String[]{"gui.no", "gui.yes"}, abilitiesEnabled ? 1 : 0)); - // Global cooldown (spaced out since combat only is removed) - addLabel(new GuiNpcLabel(101, "ability.globalCooldown", guiLeft + 140, y + 5)); - GuiNpcTextField cooldownField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 230, y, 50, 20, String.valueOf(globalCooldown)); - cooldownField.setIntegersOnly(); - cooldownField.setMinMaxDefault(0, 10000, 20); - addTextField(cooldownField); + // Cooldown range: Min Cooldown and Max Cooldown + addLabel(new GuiNpcLabel(101, "ability.minCooldown", guiLeft + 115, y + 5)); + GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 185, y, 40, 20, String.valueOf(minCooldown)); + minField.setIntegersOnly(); + minField.setMinMaxDefault(0, 10000, 20); + addTextField(minField); + + addLabel(new GuiNpcLabel(102, "ability.maxCooldown", guiLeft + 235, y + 5)); + GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 305, y, 40, 20, String.valueOf(maxCooldown)); + maxField.setIntegersOnly(); + maxField.setMinMaxDefault(0, 10000, 60); + addTextField(maxField); y += 28; @@ -254,7 +261,19 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @Override public void setGuiData(NBTTagCompound compound) { abilitiesEnabled = compound.getBoolean("AbilitiesEnabled"); - globalCooldown = compound.hasKey("AbilityGlobalCooldown") ? compound.getInteger("AbilityGlobalCooldown") : 20; + + // Support old globalCooldown for backwards compatibility + if (compound.hasKey("AbilityMinCooldown")) { + minCooldown = compound.getInteger("AbilityMinCooldown"); + maxCooldown = compound.getInteger("AbilityMaxCooldown"); + } else if (compound.hasKey("AbilityGlobalCooldown")) { + int oldCooldown = compound.getInteger("AbilityGlobalCooldown"); + minCooldown = oldCooldown; + maxCooldown = oldCooldown * 2; + } else { + minCooldown = 20; + maxCooldown = 60; + } npcAbilities.clear(); NBTTagList abilityList = compound.getTagList("Abilities", 10); @@ -352,7 +371,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC public void save() { NBTTagCompound compound = new NBTTagCompound(); compound.setBoolean("AbilitiesEnabled", abilitiesEnabled); - compound.setInteger("AbilityGlobalCooldown", globalCooldown); + compound.setInteger("AbilityMinCooldown", minCooldown); + compound.setInteger("AbilityMaxCooldown", maxCooldown); NBTTagList abilityList = new NBTTagList(); for (Ability ability : npcAbilities) { @@ -421,7 +441,20 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @Override public void unFocused(GuiNpcTextField textField) { if (textField.id == 101) { - globalCooldown = textField.getInteger(); + minCooldown = textField.getInteger(); + // Ensure min <= max + if (minCooldown > maxCooldown) { + maxCooldown = minCooldown; + initGui(); + } + save(); + } else if (textField.id == 102) { + maxCooldown = textField.getInteger(); + // Ensure min <= max + if (maxCooldown < minCooldown) { + minCooldown = maxCooldown; + initGui(); + } save(); } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java index 6f5c8ea57..47385f7cb 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java @@ -15,13 +15,18 @@ import noppes.npcs.client.gui.util.SubGuiInterface; public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { private final AbilityLaserShot laser; - private int editingTypeColorId = 0; + private int editingVisualColorId = 0; public SubGuiAbilityLaserShot(AbilityLaserShot ability, IAbilityConfigCallback callback) { super(ability, callback); this.laser = ability; } + @Override + protected boolean hasVisualSettings() { + return true; + } + @Override protected void initTypeTab(int startY) { int y = startY; @@ -72,21 +77,40 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { addLabel(new GuiNpcLabel(109, "ability.knockbackUp", col2LabelX, y + 5)); addTextField(createFloatField(109, col2FieldX, y, 50, laser.getKnockbackUp())); + } - y += 24; + @Override + protected void initVisualTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 180; + int col2FieldX = guiLeft + 260; - // Row 6: Inner Color + Outer Color - addLabel(new GuiNpcLabel(120, "ability.innerColor", labelX, y + 5)); + // Row 1: Inner Color + Outer Color + addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); String innerHex = String.format("%06X", laser.getInnerColor() & 0xFFFFFF); - GuiNpcButton innerColorBtn = new GuiNpcButton(120, fieldX, y, 50, 20, innerHex); + GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); innerColorBtn.setTextColor(laser.getInnerColor() & 0xFFFFFF); addButton(innerColorBtn); - addLabel(new GuiNpcLabel(121, "ability.outerColor", col2LabelX, y + 5)); + addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); String outerHex = String.format("%06X", laser.getOuterColor() & 0xFFFFFF); - GuiNpcButton outerColorBtn = new GuiNpcButton(121, col2FieldX, y, 50, 20, outerHex); + GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); outerColorBtn.setTextColor(laser.getOuterColor() & 0xFFFFFF); + outerColorBtn.setEnabled(laser.isOuterColorEnabled()); addButton(outerColorBtn); + + y += 24; + + // Row 2: Outer Color Enabled + Outer Color Width + addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); + addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, laser.isOuterColorEnabled() ? 1 : 0)); + + addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); + GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, laser.getOuterColorWidth()); + widthField.setEnabled(laser.isOuterColorEnabled()); + addTextField(widthField); } @Override @@ -96,34 +120,54 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { case 106: laser.setExplosive(value == 1); break; - case 120: - editingTypeColorId = 120; + } + } + + @Override + protected void handleVisualButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 200: + editingVisualColorId = 200; setSubGui(new SubGuiColorSelector(laser.getInnerColor())); break; - case 121: - editingTypeColorId = 121; + case 201: + editingVisualColorId = 201; setSubGui(new SubGuiColorSelector(laser.getOuterColor())); break; + case 202: + laser.setOuterColorEnabled(value == 1); + initGui(); + break; } } @Override public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiColorSelector && editingTypeColorId != 0) { + if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; int rgb = colorSelector.color & 0x00FFFFFF; - if (editingTypeColorId == 120) { + if (editingVisualColorId == 200) { laser.setInnerColor(rgb); - } else if (editingTypeColorId == 121) { + } else if (editingVisualColorId == 201) { laser.setOuterColor(rgb); } - editingTypeColorId = 0; + editingVisualColorId = 0; initGui(); } else { super.subGuiClosed(subgui); } } + @Override + protected void handleVisualTextField(int id, GuiNpcTextField field) { + switch (id) { + case 203: + laser.setOuterColorWidth(parseFloat(field, laser.getOuterColorWidth())); + break; + } + } + @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { From 6c7c72b0f987059c6eeeeb661134864201b2e100 Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 26 Jan 2026 18:38:43 -0500 Subject: [PATCH 285/358] Lightning, Orb and Beam Improvements. Outer Glow Configurations for Abilities --- .gitignore | 1 + .../client/renderer/RenderAbilityBeam.java | 271 ++++++++++++- .../client/renderer/RenderAbilityDisc.java | 45 ++- .../client/renderer/RenderAbilityLaser.java | 84 +++- .../client/renderer/RenderAbilityOrb.java | 113 +++--- .../client/renderer/RenderAbilityOrbCube.java | 56 --- .../client/renderer/TelegraphRenderer.java | 12 +- .../lightning/AttachedLightningRenderer.java | 281 +++++++++++++ .../renderer/lightning/LightningBolt.java | 295 ++++++++++++++ .../renderer/lightning/LightningHandler.java | 202 ++++++++++ .../client/renderer/lightning/Quaternion.java | 85 ++++ .../npcs/client/renderer/lightning/Vec3d.java | 184 +++++++++ .../data/ability/type/AbilityDisc.java | 24 +- .../data/ability/type/AbilityEnergyBeam.java | 82 +++- .../data/ability/type/AbilityLaserShot.java | 24 +- .../data/ability/type/AbilityOrb.java | 53 ++- .../data/telegraph/TelegraphInstance.java | 56 ++- .../npcs/entity/EntityAbilityBeam.java | 381 +++++++++++++++++- .../npcs/entity/EntityAbilityDisc.java | 29 +- .../npcs/entity/EntityAbilityLaser.java | 28 +- .../kamkeel/npcs/entity/EntityAbilityOrb.java | 28 +- .../npcs/entity/EntityAbilityProjectile.java | 71 +++- .../java/noppes/npcs/client/ClientProxy.java | 9 +- .../noppes/npcs/client/ClientTickHandler.java | 4 + .../advanced/ability/SubGuiAbilityDisc.java | 32 ++ .../ability/SubGuiAbilityEnergyBeam.java | 32 ++ .../ability/SubGuiAbilityLaserShot.java | 32 ++ .../advanced/ability/SubGuiAbilityOrb.java | 62 ++- .../assets/customnpcs/lang/en_US.lang | 3 + 29 files changed, 2372 insertions(+), 207 deletions(-) delete mode 100644 src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrbCube.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/lightning/Quaternion.java create mode 100644 src/main/java/kamkeel/npcs/client/renderer/lightning/Vec3d.java diff --git a/.gitignore b/.gitignore index d24cc7cf7..908eae39d 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,4 @@ out run src/main/resources/assets/customnpcs/api df/ +tmp/ diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java index 204be9d7e..554b07e31 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java @@ -2,6 +2,7 @@ package kamkeel.npcs.client.renderer; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; import kamkeel.npcs.entity.EntityAbilityBeam; import net.minecraft.client.renderer.Tessellator; import net.minecraft.entity.Entity; @@ -31,6 +32,13 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { setupRenderState(); + // Handle charging state (windup phase) - render only growing orb + if (beam.isCharging()) { + renderChargingOrb(beam, x, y, z, partialTicks); + restoreRenderState(); + return; + } + // Get trail points (these are RELATIVE to origin) List trail = beam.getTrailPoints(); if (trail.isEmpty()) { @@ -60,11 +68,17 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { GL11.glPushMatrix(); GL11.glTranslated(renderOriginX, renderOriginY, renderOriginZ); - // Render tail orb at origin (0,0,0) - static, no rotation - renderTailOrb(headSize * 0.8f, innerColor, outerColor, beam.isOuterColorEnabled()); + // Render tail orb at origin (0,0,0) - only in anchored mode + if (beam.shouldRenderTailOrb()) { + renderTailOrb(headSize * 0.8f, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth()); + } - // Render trail segments with smooth connections - renderSmoothTrail(trail, beamWidth, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth()); + // Render trail segments with smooth connections (with fading for non-anchored) + if (beam.hasFadingTrail()) { + renderFadingTrail(beam, trail, beamWidth, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth()); + } else { + renderSmoothTrail(trail, beamWidth, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth()); + } // Render head at interpolated offset position renderHead(beam, headOffsetX, headOffsetY, headOffsetZ, headSize, innerColor, outerColor, partialTicks); @@ -74,27 +88,84 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { restoreRenderState(); } + /** + * Render the charging orb during windup phase. + * Grows from 0 to headSize based on charge progress. + */ + private void renderChargingOrb(EntityAbilityBeam beam, double x, double y, double z, float partialTicks) { + float headSize = beam.getHeadSize(); + float chargeProgress = beam.getInterpolatedChargeProgress(partialTicks); + float size = headSize * chargeProgress; + + if (size <= 0.01f) return; + + int innerColor = beam.getInnerColor(); + int outerColor = beam.getOuterColor(); + + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Pulsing effect + float pulseTime = beam.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.2f) * 0.08f; + float scale = size * (1.0f + scaleModifier); + + // Render lightning during charging if enabled + if (beam.hasLightningEffect()) { + renderAttachedLightning(beam, scale); + } + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer glow only if enabled + if (beam.isOuterColorEnabled()) { + float outerScale = innerScale + beam.getOuterColorWidth(); + GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(outerScale, outerScale, outerScale); + renderCube(outerColor, 0.5f, 0.5f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + } + + // Render inner core + GL11.glScalef(innerScale, innerScale, innerScale); + renderCube(innerColor, 1.0f, 0.5f); + + GL11.glPopMatrix(); + GL11.glPopMatrix(); + } + /** * Render the tail orb at the origin (where the beam emanates from). * This orb does not rotate - it's a stable anchor point. */ - private void renderTailOrb(float size, int innerColor, int outerColor, boolean outerColorEnabled) { + private void renderTailOrb(float size, int innerColor, int outerColor, boolean outerColorEnabled, float outerColorWidth) { GL11.glPushMatrix(); // Already at origin (0,0,0) + // Inner scale defines the core size + float innerScale = 0.6f; + // Render outer glow only if enabled + // outerColorWidth is an additive offset from inner size if (outerColorEnabled) { + float outerScale = innerScale + outerColorWidth; GL11.glDepthMask(false); GL11.glPushMatrix(); - GL11.glScalef(size, size, size); + GL11.glScalef(size * outerScale, size * outerScale, size * outerScale); renderCube(outerColor, 0.4f, 0.5f); GL11.glPopMatrix(); GL11.glDepthMask(true); } - // Render inner core (smaller, solid) + // Render inner core (solid) GL11.glPushMatrix(); - GL11.glScalef(size * 0.6f, size * 0.6f, size * 0.6f); + GL11.glScalef(size * innerScale, size * innerScale, size * innerScale); renderCube(innerColor, 1.0f, 0.5f); GL11.glPopMatrix(); @@ -115,15 +186,153 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { // At connection points, average the perpendiculars for smooth transitions List perpFrames = computePerpendiculars(trail); + // Inner scale defines the core width + float innerScale = 0.6f; + float innerWidth = width * innerScale; + // Render outer glow trail (wider, translucent) - only if enabled + // outerColorWidth is an additive offset from inner width if (outerColorEnabled) { + float outerWidth = innerWidth + outerColorWidth * width; GL11.glDepthMask(false); - renderTrailTube(trail, perpFrames, width * outerColorWidth, outerColor, 0.3f, true); + renderTrailTube(trail, perpFrames, outerWidth, outerColor, 0.3f, true); GL11.glDepthMask(true); } // Render inner core trail - renderTrailTube(trail, perpFrames, width * 0.6f, innerColor, 0.9f, false); + renderTrailTube(trail, perpFrames, innerWidth, innerColor, 0.9f, false); + } + + /** + * Render trail with fading effect (comet style) for non-anchored beams. + * Older trail points fade out based on their age. + */ + private void renderFadingTrail(EntityAbilityBeam beam, List trail, float width, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth) { + if (trail.size() < 2) return; + + List ages = beam.getTrailPointAges(); + int fadeTime = beam.getTrailFadeTime(); + + // Pre-compute perpendicular frames + List perpFrames = computePerpendiculars(trail); + + // Inner scale defines the core width + float innerScale = 0.6f; + float innerWidth = width * innerScale; + + // Render outer glow trail with fading + if (outerColorEnabled) { + float outerWidth = innerWidth + outerColorWidth * width; + GL11.glDepthMask(false); + renderFadingTrailTube(trail, perpFrames, ages, fadeTime, outerWidth, outerColor, 0.3f); + GL11.glDepthMask(true); + } + + // Render inner core trail with fading + renderFadingTrailTube(trail, perpFrames, ages, fadeTime, innerWidth, innerColor, 0.9f); + } + + /** + * Render trail tube with age-based fading. + */ + private void renderFadingTrailTube(List trail, List perpFrames, List ages, + int fadeTime, float width, int color, float baseAlpha) { + float[] rgb = extractRGB(color); + float r = rgb[0], g = rgb[1], b = rgb[2]; + float halfWidth = width * 0.5f; + int size = trail.size(); + + Tessellator tess = Tessellator.instance; + tess.startDrawingQuads(); + + for (int i = 0; i < size - 1; i++) { + Vec3 p1 = trail.get(i); + Vec3 p2 = trail.get(i + 1); + double[] frame1 = perpFrames.get(i); + double[] frame2 = perpFrames.get(i + 1); + + // Calculate alpha based on age (older = more faded) + float age1 = i < ages.size() ? ages.get(i) : 0; + float age2 = (i + 1) < ages.size() ? ages.get(i + 1) : 0; + float fade1 = 1.0f - Math.min(1.0f, age1 / fadeTime); + float fade2 = 1.0f - Math.min(1.0f, age2 / fadeTime); + float alpha1 = baseAlpha * fade1; + float alpha2 = baseAlpha * fade2; + + // Skip fully faded segments + if (alpha1 <= 0.01f && alpha2 <= 0.01f) continue; + + // Calculate 4 corners at start + double s1x = p1.xCoord - frame1[0] * halfWidth - frame1[3] * halfWidth; + double s1y = p1.yCoord - frame1[1] * halfWidth - frame1[4] * halfWidth; + double s1z = p1.zCoord - frame1[2] * halfWidth - frame1[5] * halfWidth; + + double s2x = p1.xCoord + frame1[0] * halfWidth - frame1[3] * halfWidth; + double s2y = p1.yCoord + frame1[1] * halfWidth - frame1[4] * halfWidth; + double s2z = p1.zCoord + frame1[2] * halfWidth - frame1[5] * halfWidth; + + double s3x = p1.xCoord + frame1[0] * halfWidth + frame1[3] * halfWidth; + double s3y = p1.yCoord + frame1[1] * halfWidth + frame1[4] * halfWidth; + double s3z = p1.zCoord + frame1[2] * halfWidth + frame1[5] * halfWidth; + + double s4x = p1.xCoord - frame1[0] * halfWidth + frame1[3] * halfWidth; + double s4y = p1.yCoord - frame1[1] * halfWidth + frame1[4] * halfWidth; + double s4z = p1.zCoord - frame1[2] * halfWidth + frame1[5] * halfWidth; + + // Calculate 4 corners at end + double e1x = p2.xCoord - frame2[0] * halfWidth - frame2[3] * halfWidth; + double e1y = p2.yCoord - frame2[1] * halfWidth - frame2[4] * halfWidth; + double e1z = p2.zCoord - frame2[2] * halfWidth - frame2[5] * halfWidth; + + double e2x = p2.xCoord + frame2[0] * halfWidth - frame2[3] * halfWidth; + double e2y = p2.yCoord + frame2[1] * halfWidth - frame2[4] * halfWidth; + double e2z = p2.zCoord + frame2[2] * halfWidth - frame2[5] * halfWidth; + + double e3x = p2.xCoord + frame2[0] * halfWidth + frame2[3] * halfWidth; + double e3y = p2.yCoord + frame2[1] * halfWidth + frame2[4] * halfWidth; + double e3z = p2.zCoord + frame2[2] * halfWidth + frame2[5] * halfWidth; + + double e4x = p2.xCoord - frame2[0] * halfWidth + frame2[3] * halfWidth; + double e4y = p2.yCoord - frame2[1] * halfWidth + frame2[4] * halfWidth; + double e4z = p2.zCoord - frame2[2] * halfWidth + frame2[5] * halfWidth; + + // Bottom face + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s1x, s1y, s1z); + tess.addVertex(s2x, s2y, s2z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e2x, e2y, e2z); + tess.addVertex(e1x, e1y, e1z); + + // Top face + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s4x, s4y, s4z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e4x, e4y, e4z); + tess.addVertex(e3x, e3y, e3z); + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s3x, s3y, s3z); + + // Left face + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s1x, s1y, s1z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e1x, e1y, e1z); + tess.addVertex(e4x, e4y, e4z); + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s4x, s4y, s4z); + + // Right face + tess.setColorRGBA_F(r, g, b, alpha1); + tess.addVertex(s2x, s2y, s2z); + tess.addVertex(s3x, s3y, s3z); + tess.setColorRGBA_F(r, g, b, alpha2); + tess.addVertex(e3x, e3y, e3z); + tess.addVertex(e2x, e2y, e2z); + } + + tess.draw(); } /** @@ -328,6 +537,11 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { float scaleModifier = (float) Math.sin(pulseTime * 0.2f) * 0.1f; float scale = size * (1.0f + scaleModifier); + // Render lightning BEFORE rotation so it crackles in all directions (only on head) + if (beam.hasLightningEffect()) { + renderAttachedLightning(beam, scale); + } + // Rotation GL11.glRotatef(beam.getInterpolatedRotationX(partialTicks), 1.0f, 0.0f, 0.0f); GL11.glRotatef(beam.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); @@ -336,19 +550,54 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { GL11.glPushMatrix(); GL11.glScalef(scale, scale, scale); + // Inner scale defines the core size + float innerScale = 0.6f; + // Render outer glow only if enabled + // outerColorWidth is an additive offset from inner size if (beam.isOuterColorEnabled()) { + float outerScale = innerScale + beam.getOuterColorWidth(); GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(outerScale, outerScale, outerScale); renderCube(outerColor, 0.5f, 0.5f); + GL11.glPopMatrix(); GL11.glDepthMask(true); } // Render inner core - float innerScale = 0.6f; GL11.glScalef(innerScale, innerScale, innerScale); renderCube(innerColor, 1.0f, 0.5f); GL11.glPopMatrix(); GL11.glPopMatrix(); } + + /** + * Render fading lightning arcs attached to the beam head (in local space). + */ + private void renderAttachedLightning(EntityAbilityBeam beam, float headScale) { + AttachedLightningRenderer.LightningState state = getLightningState(beam); + + float density = beam.getLightningDensity(); + // Lightning radius extends outward from inner surface (innerScale = 0.6) + float innerRadius = 0.6f * headScale * 0.5f; + float radius = innerRadius + beam.getLightningRadius() * headScale; + int outerColor = beam.getOuterColor(); + int innerColor = beam.getInnerColor(); + int fadeTime = beam.getLightningFadeTime(); + + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + } + + /** + * Get or create the lightning state for an entity. + */ + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityBeam beam) { + if (beam.lightningState == null) { + beam.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) beam.lightningState; + } } diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java index 13de38b50..fdd80689a 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java @@ -2,6 +2,7 @@ package kamkeel.npcs.client.renderer; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; import kamkeel.npcs.entity.EntityAbilityDisc; import net.minecraft.entity.Entity; import org.lwjgl.opengl.GL11; @@ -21,6 +22,7 @@ public class RenderAbilityDisc extends RenderAbilityProjectile { setupRenderState(); + GL11.glPushMatrix(); GL11.glTranslated(x, y, z); // Get interpolated values @@ -33,29 +35,66 @@ public class RenderAbilityDisc extends RenderAbilityProjectile { float scaleModifier = (float) Math.sin(pulseTime * 0.1f) * 0.03f; float scale = 1.0f + scaleModifier; + // Render lightning BEFORE rotation so it crackles in all directions + if (disc.hasLightningEffect()) { + renderAttachedLightning(disc, radius * scale); + } + GL11.glPushMatrix(); GL11.glScalef(scale, scale, scale); // Apply ONLY Y-axis rotation for flat spin GL11.glRotatef(disc.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); + // Inner scale defines the core size + float innerScale = 0.6f; + // Render outer square (translucent) - only if enabled + // outerColorWidth is an additive offset from inner size if (disc.isOuterColorEnabled()) { - float outerScale = disc.getOuterColorWidth() / 1.8f; // Normalize to default width + float outerScale = innerScale + disc.getOuterColorWidth(); GL11.glDepthMask(false); renderFlatSquare(disc.getOuterColor(), 0.5f, radius * outerScale, thickness); GL11.glDepthMask(true); } - // Render inner square (solid, smaller) - float innerScale = 0.6f; + // Render inner square (solid) renderFlatSquare(disc.getInnerColor(), 1.0f, radius * innerScale, thickness * 1.2f); + GL11.glPopMatrix(); GL11.glPopMatrix(); restoreRenderState(); } + /** + * Render fading lightning arcs attached to the disc (in local space). + */ + private void renderAttachedLightning(EntityAbilityDisc disc, float discSize) { + AttachedLightningRenderer.LightningState state = getLightningState(disc); + + float density = disc.getLightningDensity(); + // Lightning radius extends outward from inner surface (innerScale = 0.6) + float innerRadius = 0.6f * discSize * 0.5f; + float radius = innerRadius + disc.getLightningRadius() * discSize; + int outerColor = disc.getOuterColor(); + int innerColor = disc.getInnerColor(); + int fadeTime = disc.getLightningFadeTime(); + + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + } + + /** + * Get or create the lightning state for an entity. + */ + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityDisc disc) { + if (disc.lightningState == null) { + disc.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) disc.lightningState; + } + /** * Render a flat square (thin cube) centered at origin. * The square lies flat on the XZ plane with thin Y height. diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java index fce85d2db..bf4df79eb 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java @@ -2,6 +2,7 @@ package kamkeel.npcs.client.renderer; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; import kamkeel.npcs.entity.EntityAbilityLaser; import net.minecraft.client.renderer.Tessellator; import net.minecraft.entity.Entity; @@ -84,32 +85,103 @@ public class RenderAbilityLaser extends RenderAbilityProjectile { double vertY = dz * horzX - dx * horzZ; double vertZ = dx * horzY - dy * horzX; + // Inner scale defines the core width + float innerScale = 0.6f; + float innerWidth = width * innerScale; + // Render outer rectangle (larger, outer color, translucent) - only if enabled + // outerColorWidth is an additive offset from inner width if (laser.isOuterColorEnabled()) { - float outerScale = laser.getOuterColorWidth(); + float outerWidth = innerWidth + laser.getOuterColorWidth() * width; GL11.glDepthMask(false); renderBeamRectangle(renderStartX, renderStartY, renderStartZ, renderEndX, renderEndY, renderEndZ, horzX, horzY, horzZ, vertX, vertY, vertZ, - width * outerScale, laser.getOuterColor(), alpha * 0.4f); + outerWidth, laser.getOuterColor(), alpha * 0.4f); - // Render middle layer + // Render middle layer (halfway between inner and outer) + float midWidth = innerWidth + (outerWidth - innerWidth) * 0.5f; renderBeamRectangle(renderStartX, renderStartY, renderStartZ, renderEndX, renderEndY, renderEndZ, horzX, horzY, horzZ, vertX, vertY, vertZ, - width * 1.3f, laser.getOuterColor(), alpha * 0.7f); + midWidth, laser.getOuterColor(), alpha * 0.7f); GL11.glDepthMask(true); } - // Render inner core (smaller, inner color, solid) + // Render inner core (solid) renderBeamRectangle(renderStartX, renderStartY, renderStartZ, renderEndX, renderEndY, renderEndZ, horzX, horzY, horzZ, vertX, vertY, vertZ, - width * 0.6f, laser.getInnerColor(), alpha); + innerWidth, laser.getInnerColor(), alpha); + + // Render lightning along the laser beam + if (laser.hasLightningEffect() && laser.getCurrentLength() > 0.1f) { + renderLaserLightning(laser, renderStartX, renderStartY, renderStartZ, + renderEndX, renderEndY, renderEndZ, width); + } restoreRenderState(); } + /** + * Render lightning effects along the laser beam at multiple points. + */ + private void renderLaserLightning(EntityAbilityLaser laser, + double startX, double startY, double startZ, + double endX, double endY, double endZ, float width) { + AttachedLightningRenderer.LightningState state = getLightningState(laser); + + float density = laser.getLightningDensity(); + // Lightning radius extends outward from inner width + float innerRadius = width * 0.6f * 0.5f; + float radius = innerRadius + laser.getLightningRadius() * width; + int outerColor = laser.getOuterColor(); + int innerColor = laser.getInnerColor(); + int fadeTime = laser.getLightningFadeTime(); + + // Render lightning at multiple points along the laser + // Start, middle, and end points + double midX = (startX + endX) * 0.5; + double midY = (startY + endY) * 0.5; + double midZ = (startZ + endZ) * 0.5; + + GL11.glPushMatrix(); + GL11.glTranslated(midX, midY, midZ); + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + GL11.glPopMatrix(); + + // Also render at the tip + GL11.glPushMatrix(); + GL11.glTranslated(endX, endY, endZ); + // Use a secondary state for the tip + AttachedLightningRenderer.LightningState tipState = getTipLightningState(laser); + tipState.update(density * 0.7f, radius * 0.8f, outerColor, innerColor, fadeTime); + tipState.render(); + GL11.glPopMatrix(); + } + + /** + * Get or create the main lightning state for the laser. + */ + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityLaser laser) { + if (laser.lightningState == null) { + laser.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) laser.lightningState; + } + + /** + * Get or create the tip lightning state for the laser. + * Stored as a secondary object in lightningState2. + */ + private AttachedLightningRenderer.LightningState getTipLightningState(EntityAbilityLaser laser) { + // Use a simple approach - store in a transient field + // Since lightningState is already used, we'll create a new state each time + // This works because we're updating and rendering immediately + return new AttachedLightningRenderer.LightningState(); + } + /** * Render a beam as a 3D rectangular prism (box along a line). */ diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java index 39048648d..e2e6f8234 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java @@ -2,57 +2,29 @@ package kamkeel.npcs.client.renderer; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; -import kamkeel.npcs.client.renderer.quadric.SphereRenderer; +import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; import kamkeel.npcs.entity.EntityAbilityOrb; -import net.minecraft.client.renderer.entity.Render; import net.minecraft.entity.Entity; -import net.minecraft.util.ResourceLocation; import org.lwjgl.opengl.GL11; -import org.lwjgl.opengl.GL12; /** - * Renders the AbilityOrb entity as a glowing sphere with inner and outer layers. - * - * Design inspired by LouisXIV's energy rendering system. + * Renders the AbilityOrb entity as a cube with inner and outer layers. + * Minecraft-style blocky appearance using Tessellator. */ @SideOnly(Side.CLIENT) -public class RenderAbilityOrb extends Render { +public class RenderAbilityOrb extends RenderAbilityProjectile { - // White texture for solid color rendering - private static final ResourceLocation WHITE_TEXTURE = new ResourceLocation("customnpcs", "textures/entity/white.png"); - - // Static sphere renderer - shared across all orb renders - private static final SphereRenderer sphere = new SphereRenderer(0.5f, 16); - - public RenderAbilityOrb() { - this.shadowSize = 0.0f; - } + private static final int LIGHTNING_FADE_TICKS = 6; @Override public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { EntityAbilityOrb orb = (EntityAbilityOrb) entity; + setupRenderState(); + GL11.glPushMatrix(); - - // Disable culling, enable rescale normal - GL11.glDisable(GL11.GL_CULL_FACE); - GL11.glEnable(GL12.GL_RESCALE_NORMAL); - GL11.glEnable(GL11.GL_ALPHA_TEST); - - // Enable blending for transparency - GL11.glEnable(GL11.GL_BLEND); - GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); - GL11.glAlphaFunc(GL11.GL_GREATER, 0.003921569F); - - // Disable lighting for self-illumination - GL11.glDisable(GL11.GL_LIGHTING); - - // Translate to render position GL11.glTranslated(x, y, z); - // Bind texture - this.bindTexture(WHITE_TEXTURE); - // Get interpolated size for smooth rendering float size = orb.getInterpolatedSize(partialTicks); @@ -61,51 +33,74 @@ public class RenderAbilityOrb extends Render { float scaleModifier = (float) Math.sin(pulseTime * 0.15f) * 0.05f; float scale = size * (1.0f + scaleModifier); - GL11.glPushMatrix(); - GL11.glScalef(scale, scale, scale); + // Render lightning BEFORE rotation so it crackles in all directions + if (orb.hasLightningEffect()) { + renderAttachedLightning(orb, scale); + } // Use entity's interpolated rotation for smooth spinning GL11.glRotatef(orb.getInterpolatedRotationX(partialTicks), 1.0f, 0.0f, 0.0f); GL11.glRotatef(orb.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); GL11.glRotatef(orb.getInterpolatedRotationZ(partialTicks), 0.0f, 0.0f, 1.0f); - // Render outer sphere (translucent) - only if enabled + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer cube (translucent) - only if enabled + // outerColorWidth is an additive offset from inner size if (orb.isOuterColorEnabled()) { + float outerScale = innerScale + orb.getOuterColorWidth(); GL11.glDepthMask(false); - this.bindTexture(WHITE_TEXTURE); - float outerScale = orb.getOuterColorWidth() / 1.8f; // Normalize to default width GL11.glPushMatrix(); GL11.glScalef(outerScale, outerScale, outerScale); - renderSphere(orb.getOuterColor(), 0.6f); + renderCube(orb.getOuterColor(), 0.5f, 0.5f); GL11.glPopMatrix(); GL11.glDepthMask(true); } - // Render inner sphere (more opaque) - float innerScale = 0.75f; - GL11.glPushMatrix(); + // Render inner cube (solid) GL11.glScalef(innerScale, innerScale, innerScale); - this.bindTexture(WHITE_TEXTURE); - renderSphere(orb.getInnerColor(), 1.0f); - GL11.glPopMatrix(); + renderCube(orb.getInnerColor(), 1.0f, 0.5f); GL11.glPopMatrix(); - - // Restore GL state - GL11.glEnable(GL11.GL_LIGHTING); GL11.glPopMatrix(); + + restoreRenderState(); } - private void renderSphere(int color, float alpha) { - // Set sphere properties and render (rotation already applied to matrix) - sphere.setScale(1.0f); - sphere.setAlpha(alpha); - sphere.setColor(color); - sphere.render(0); + /** + * Render fading lightning arcs attached to the orb (in local space). + * Arcs persist for a few ticks and fade out, while staying attached to the orb. + */ + private void renderAttachedLightning(EntityAbilityOrb orb, float orbScale) { + // Get or create the lightning state for this entity + AttachedLightningRenderer.LightningState state = getLightningState(orb); + + float density = orb.getLightningDensity(); + // Lightning radius extends outward from inner surface (innerScale = 0.6) + float innerRadius = 0.6f * orbScale * 0.5f; // Half size of inner cube + float radius = innerRadius + orb.getLightningRadius() * orbScale; + int outerColor = orb.getOuterColor(); + int innerColor = orb.getInnerColor(); + int fadeTime = orb.getLightningFadeTime(); + + // Update: age existing arcs, spawn new ones based on density + state.update(density, radius, outerColor, innerColor, fadeTime); + + // Render all active arcs in local space (they move with the orb) + state.render(); } - @Override - protected ResourceLocation getEntityTexture(Entity entity) { - return WHITE_TEXTURE; + /** + * Get or create the lightning state for an entity. + */ + private AttachedLightningRenderer.LightningState getLightningState(EntityAbilityOrb orb) { + if (orb.lightningState == null) { + orb.lightningState = new AttachedLightningRenderer.LightningState(); + } + return (AttachedLightningRenderer.LightningState) orb.lightningState; } } diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrbCube.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrbCube.java deleted file mode 100644 index 29656e597..000000000 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrbCube.java +++ /dev/null @@ -1,56 +0,0 @@ -package kamkeel.npcs.client.renderer; - -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; -import kamkeel.npcs.entity.EntityAbilityOrb; -import net.minecraft.entity.Entity; -import org.lwjgl.opengl.GL11; - -/** - * Renders the AbilityOrb entity as a cube with inner and outer layers. - * Minecraft-style blocky appearance using Tessellator. - * - * Design inspired by LouisXIV's energy rendering system. - */ -@SideOnly(Side.CLIENT) -public class RenderAbilityOrbCube extends RenderAbilityProjectile { - - @Override - public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { - EntityAbilityOrb orb = (EntityAbilityOrb) entity; - - setupRenderState(); - - GL11.glTranslated(x, y, z); - - // Get interpolated size for smooth rendering - float size = orb.getInterpolatedSize(partialTicks); - - // Pulsing scale effect - float pulseTime = entity.ticksExisted + partialTicks; - float scaleModifier = (float) Math.sin(pulseTime * 0.15f) * 0.05f; - float scale = size * (1.0f + scaleModifier); - - // Use entity's interpolated rotation for smooth spinning - GL11.glRotatef(orb.getInterpolatedRotationX(partialTicks), 1.0f, 0.0f, 0.0f); - GL11.glRotatef(orb.getInterpolatedRotationY(partialTicks), 0.0f, 1.0f, 0.0f); - GL11.glRotatef(orb.getInterpolatedRotationZ(partialTicks), 0.0f, 0.0f, 1.0f); - - GL11.glPushMatrix(); - GL11.glScalef(scale, scale, scale); - - // Render outer cube (translucent) - GL11.glDepthMask(false); - renderCube(orb.getOuterColor(), 0.5f, 0.5f); - GL11.glDepthMask(true); - - // Render inner cube (solid, smaller) - float innerScale = 0.6f; - GL11.glScalef(innerScale, innerScale, innerScale); - renderCube(orb.getInnerColor(), 1.0f, 0.5f); - - GL11.glPopMatrix(); - - restoreRenderState(); - } -} diff --git a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java index 0cac8805e..5f4e815b7 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java +++ b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java @@ -76,10 +76,10 @@ public class TelegraphRenderer { return; } - // Calculate render position relative to camera - double renderX = instance.getX() - playerX; - double renderY = instance.getY() - playerY + telegraph.getHeightOffset(); - double renderZ = instance.getZ() - playerZ; + // Calculate render position relative to camera using interpolated positions for smooth movement + double renderX = instance.getInterpolatedX(partialTicks) - playerX; + double renderY = instance.getInterpolatedY(partialTicks) - playerY + telegraph.getHeightOffset(); + double renderZ = instance.getInterpolatedZ(partialTicks) - playerZ; // Get animated color int color = instance.getAnimatedColor(partialTicks); @@ -91,8 +91,8 @@ public class TelegraphRenderer { GL11.glPushMatrix(); GL11.glTranslated(renderX, renderY, renderZ); - // Rotate for directional shapes - GL11.glRotatef(-instance.getYaw(), 0, 1, 0); + // Rotate for directional shapes using interpolated yaw for smooth rotation + GL11.glRotatef(-instance.getInterpolatedYaw(partialTicks), 0, 1, 0); switch (telegraph.getType()) { case CIRCLE: diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java new file mode 100644 index 000000000..4fa28cda8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/AttachedLightningRenderer.java @@ -0,0 +1,281 @@ +package kamkeel.npcs.client.renderer.lightning; + +import net.minecraft.client.renderer.Tessellator; +import org.lwjgl.opengl.GL11; + +import java.awt.Color; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Random; + +/** + * Renders lightning arcs attached to an entity (in local space). + * Supports both instant arcs and fading persistent bolts that move with the entity. + */ +public class AttachedLightningRenderer { + + private static final Random rand = new Random(); + + /** + * Data for a single lightning bolt with fading support. + */ + public static class LightningArc { + public List points; + public int age; + public int maxAge; + public int outerColor; + public int innerColor; + + public LightningArc(List points, int maxAge, int outerColor, int innerColor) { + this.points = points; + this.age = 0; + this.maxAge = maxAge; + this.outerColor = outerColor; + this.innerColor = innerColor; + } + + public float getAlphaMultiplier() { + // Fade out over lifetime + return 1.0f - ((float) age / (float) maxAge); + } + + public boolean isDead() { + return age >= maxAge; + } + + public void tick() { + age++; + } + } + + /** + * Storage for persistent lightning arcs per entity. + * Call updateAndRender() each frame from the entity's renderer. + */ + public static class LightningState { + private final List arcs = new ArrayList<>(); + private float spawnAccumulator = 0; + + /** + * Update arc ages, remove dead arcs, spawn new ones based on density. + * + * @param density Arcs per tick (can be fractional) + * @param radius Max arc distance from center + * @param outerColor Outer glow color + * @param innerColor Inner core color + * @param maxAge How many ticks arcs live (for fading) + */ + public void update(float density, float radius, int outerColor, int innerColor, int maxAge) { + // Age existing arcs and remove dead ones + Iterator iter = arcs.iterator(); + while (iter.hasNext()) { + LightningArc arc = iter.next(); + arc.tick(); + if (arc.isDead()) { + iter.remove(); + } + } + + // Spawn new arcs based on density + spawnAccumulator += density; + while (spawnAccumulator >= 1.0f) { + spawnAccumulator -= 1.0f; + arcs.add(createArc(radius, outerColor, innerColor, maxAge)); + } + } + + /** + * Render all active arcs in local space. + * Call this after glTranslated to entity position. + */ + public void render() { + if (arcs.isEmpty()) return; + + // Setup GL state + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDepthMask(false); + + Tessellator tess = Tessellator.instance; + + for (LightningArc arc : arcs) { + float alphaMult = arc.getAlphaMultiplier(); + + // Render outer glow + renderBoltPath(tess, arc.points, arc.outerColor, 0.025f, 0.35f * alphaMult); + + // Render inner core + renderBoltPath(tess, arc.points, arc.innerColor, 0.012f, 0.7f * alphaMult); + } + + GL11.glDepthMask(true); + GL11.glPopAttrib(); + } + + public int getArcCount() { + return arcs.size(); + } + } + + /** + * Create a new lightning arc from center to a random point. + */ + private static LightningArc createArc(float radius, int outerColor, int innerColor, int maxAge) { + // Random end point on sphere + double theta = rand.nextDouble() * Math.PI * 2; + double phi = Math.acos(2 * rand.nextDouble() - 1); + double r = radius * (0.5 + rand.nextDouble() * 0.5); + + double endX = r * Math.sin(phi) * Math.cos(theta); + double endY = r * Math.sin(phi) * Math.sin(theta); + double endZ = r * Math.cos(phi); + + int segments = 4 + rand.nextInt(3); + float displacement = radius * 0.25f; + + List points = generateLightningPath(0, 0, 0, endX, endY, endZ, segments, displacement); + + return new LightningArc(points, maxAge, outerColor, innerColor); + } + + /** + * Generate a jagged lightning path between two points. + */ + private static List generateLightningPath(double x1, double y1, double z1, + double x2, double y2, double z2, + int segments, float displacement) { + List points = new ArrayList<>(); + points.add(new double[]{x1, y1, z1}); + + double dx = x2 - x1; + double dy = y2 - y1; + double dz = z2 - z1; + + // Create perpendicular vectors for displacement + Vec3d dir = new Vec3d(dx, dy, dz); + Vec3d perp1 = dir.copy().perpendicular().normalize(); + Vec3d perp2 = dir.copy().crossProduct(perp1).normalize(); + + for (int i = 1; i < segments; i++) { + double t = (double) i / segments; + + double px = x1 + dx * t; + double py = y1 + dy * t; + double pz = z1 + dz * t; + + // Displacement strongest in middle + float strength = (float) (1.0 - Math.abs(t - 0.5) * 2.0) * displacement; + double offset1 = (rand.nextDouble() - 0.5) * 2 * strength; + double offset2 = (rand.nextDouble() - 0.5) * 2 * strength; + + px += perp1.x * offset1 + perp2.x * offset2; + py += perp1.y * offset1 + perp2.y * offset2; + pz += perp1.z * offset1 + perp2.z * offset2; + + points.add(new double[]{px, py, pz}); + } + + points.add(new double[]{x2, y2, z2}); + return points; + } + + /** + * Render the lightning path as quads. + */ + private static void renderBoltPath(Tessellator tess, List points, int color, float width, float alpha) { + if (alpha <= 0.01f) return; + + Color c = new Color(color); + int r = c.getRed(); + int g = c.getGreen(); + int b = c.getBlue(); + int a = Math.min(255, Math.max(0, (int) (alpha * 255))); + + for (int i = 0; i < points.size() - 1; i++) { + double[] p1 = points.get(i); + double[] p2 = points.get(i + 1); + + double segDx = p2[0] - p1[0]; + double segDy = p2[1] - p1[1]; + double segDz = p2[2] - p1[2]; + + Vec3d segDir = new Vec3d(segDx, segDy, segDz); + Vec3d perp = segDir.copy().perpendicular().normalize().multiply(width); + + // First quad + tess.startDrawingQuads(); + tess.setBrightness(0xF000F0); + tess.setColorRGBA(r, g, b, a); + + tess.addVertex(p1[0] - perp.x, p1[1] - perp.y, p1[2] - perp.z); + tess.addVertex(p1[0] + perp.x, p1[1] + perp.y, p1[2] + perp.z); + tess.addVertex(p2[0] + perp.x, p2[1] + perp.y, p2[2] + perp.z); + tess.addVertex(p2[0] - perp.x, p2[1] - perp.y, p2[2] - perp.z); + + tess.draw(); + + // Second quad (perpendicular for 3D look) + Vec3d perp2 = segDir.copy().crossProduct(perp).normalize().multiply(width); + + tess.startDrawingQuads(); + tess.setBrightness(0xF000F0); + tess.setColorRGBA(r, g, b, a); + + tess.addVertex(p1[0] - perp2.x, p1[1] - perp2.y, p1[2] - perp2.z); + tess.addVertex(p1[0] + perp2.x, p1[1] + perp2.y, p1[2] + perp2.z); + tess.addVertex(p2[0] + perp2.x, p2[1] + perp2.y, p2[2] + perp2.z); + tess.addVertex(p2[0] - perp2.x, p2[1] - perp2.y, p2[2] - perp2.z); + + tess.draw(); + } + } + + // ==================== INSTANT RENDERING (no persistence) ==================== + + /** + * Render a single instant lightning arc (no fading, drawn this frame only). + */ + public static void renderInstantArc(float radius, int outerColor, int innerColor) { + double theta = rand.nextDouble() * Math.PI * 2; + double phi = Math.acos(2 * rand.nextDouble() - 1); + double r = radius * (0.5 + rand.nextDouble() * 0.5); + + double endX = r * Math.sin(phi) * Math.cos(theta); + double endY = r * Math.sin(phi) * Math.sin(theta); + double endZ = r * Math.cos(phi); + + int segments = 4 + rand.nextInt(3); + float displacement = radius * 0.25f; + + List points = generateLightningPath(0, 0, 0, endX, endY, endZ, segments, displacement); + + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDepthMask(false); + + Tessellator tess = Tessellator.instance; + renderBoltPath(tess, points, outerColor, 0.025f, 0.35f); + renderBoltPath(tess, points, innerColor, 0.012f, 0.7f); + + GL11.glDepthMask(true); + GL11.glPopAttrib(); + } + + /** + * Render multiple instant arcs. + */ + public static void renderCracklingLightning(int count, float radius, int outerColor, int innerColor) { + for (int i = 0; i < count; i++) { + renderInstantArc(radius, outerColor, innerColor); + } + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java new file mode 100644 index 000000000..6cf6e7d3a --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningBolt.java @@ -0,0 +1,295 @@ +package kamkeel.npcs.client.renderer.lightning; + +import net.minecraft.world.World; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Random; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Lightning bolt data model with fractal segment generation. + * Adapted from Botania's LightningBolt by Vazkii/ChickenBones. + */ +public class LightningBolt { + + public static ConcurrentLinkedQueue boltList = new ConcurrentLinkedQueue<>(); + + public ArrayList segments = new ArrayList<>(); + public Vec3d start; + public Vec3d end; + + public double length; + public int numSegments0; + private int numSplits; + private boolean finalized; + private Random rand; + public long seed; + + public int particleAge; + public int particleMaxAge; + public boolean isDead; + + private World world; + private HashMap splitParents = new HashMap<>(); + + public float speed = 1.5F; + public static final int FADE_TIME = 4; // Short lifespan for projectile-attached lightning + + public int colorOuter; + public int colorInner; + + public LightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, long seed, int colorOuter, int colorInner) { + this.world = world; + this.seed = seed; + this.rand = new Random(seed); + + this.start = start; + this.end = end; + this.speed = ticksPerMeter; + + this.colorOuter = colorOuter; + this.colorInner = colorInner; + + this.numSegments0 = 1; + this.length = end.copy().subtract(start).mag(); + this.particleMaxAge = FADE_TIME + rand.nextInt(FADE_TIME) - FADE_TIME / 2; + this.particleAge = -(int) (length * speed); + + segments.add(new Segment(start, end)); + } + + /** + * Apply default fractal splitting for natural-looking lightning. + */ + public void defaultFractal() { + fractal(2, length / 1.5, 0.7F, 0.7F, 45); + fractal(2, length / 4, 0.5F, 0.8F, 50); + fractal(2, length / 15, 0.5F, 0.9F, 55); + fractal(2, length / 30, 0.5F, 1.0F, 60); + fractal(2, length / 60, 0, 0, 0); + fractal(2, length / 100, 0, 0, 0); + fractal(2, length / 400, 0, 0, 0); + } + + /** + * Simplified fractal for short-lived projectile lightning. + * Less detail but faster and more appropriate for fast-moving objects. + */ + public void simpleFractal() { + fractal(2, length / 2.0, 0.4F, 0.6F, 35); + fractal(2, length / 6, 0.3F, 0.7F, 40); + fractal(2, length / 20, 0, 0, 0); + } + + /** + * Apply fractal splitting to create branching lightning. + * + * @param splits Number of splits per segment + * @param amount Displacement amount + * @param splitChance Probability of branching (0-1) + * @param splitLength Length multiplier for branches + * @param splitAngle Angle for branch splits in degrees + */ + public void fractal(int splits, double amount, double splitChance, double splitLength, double splitAngle) { + if (finalized) return; + + ArrayList oldSegments = segments; + segments = new ArrayList<>(); + + Segment prev = null; + + for (Segment segment : oldSegments) { + prev = segment.prev; + + Vec3d subSegment = segment.diff.copy().multiply(1.0 / splits); + + BoltPoint[] newPoints = new BoltPoint[splits + 1]; + + Vec3d startPoint = segment.startPoint.point; + newPoints[0] = segment.startPoint; + newPoints[splits] = segment.endPoint; + + for (int i = 1; i < splits; i++) { + Vec3d randOff = segment.diff.copy().perpendicular().normalize() + .rotate(rand.nextFloat() * 360, segment.diff); + randOff.multiply((rand.nextFloat() - 0.5F) * amount * 2); + + Vec3d basePoint = startPoint.copy().add(subSegment.copy().multiply(i)); + + newPoints[i] = new BoltPoint(basePoint, randOff); + } + + for (int i = 0; i < splits; i++) { + Segment next = new Segment(newPoints[i], newPoints[i + 1], segment.light, + segment.segmentNo * splits + i, segment.splitNo); + next.prev = prev; + if (prev != null) { + prev.next = next; + } + + if (i != 0 && rand.nextFloat() < splitChance) { + Vec3d splitRot = next.diff.copy().xCrossProduct() + .rotate(rand.nextFloat() * 360, next.diff); + Vec3d diff = next.diff.copy() + .rotate((rand.nextFloat() * 0.66F + 0.33F) * splitAngle, splitRot) + .multiply(splitLength); + + numSplits++; + splitParents.put(numSplits, next.splitNo); + + Segment split = new Segment(newPoints[i], + new BoltPoint(newPoints[i + 1].basePoint, newPoints[i + 1].offsetVec.copy().add(diff)), + segment.light / 2F, next.segmentNo, numSplits); + split.prev = prev; + + segments.add(split); + } + + prev = next; + segments.add(next); + } + + if (segment.next != null) { + segment.next.prev = prev; + } + } + + numSegments0 *= splits; + } + + /** + * Finalize the bolt after all fractal passes. + */ + public void finalizeBolt() { + if (finalized) return; + finalized = true; + + calculateEndDiffs(); + Collections.sort(segments, new SegmentLightSorter()); + } + + private void calculateEndDiffs() { + Collections.sort(segments, new SegmentSorter()); + + for (Segment segment : segments) { + segment.calcEndDiffs(); + } + } + + public void onUpdate() { + particleAge++; + if (particleAge >= particleMaxAge) { + isDead = true; + } + } + + /** + * Update all active lightning bolts. Called from client tick handler. + */ + public static void updateAll() { + for (Iterator iterator = boltList.iterator(); iterator.hasNext(); ) { + LightningBolt bolt = iterator.next(); + bolt.onUpdate(); + if (bolt.isDead) { + iterator.remove(); + } + } + } + + // ==================== INNER CLASSES ==================== + + public class BoltPoint { + public Vec3d point; + public Vec3d basePoint; + public Vec3d offsetVec; + + public BoltPoint(Vec3d basePoint, Vec3d offsetVec) { + this.point = basePoint.copy().add(offsetVec); + this.basePoint = basePoint; + this.offsetVec = offsetVec; + } + } + + public class Segment { + public BoltPoint startPoint; + public BoltPoint endPoint; + + public Vec3d diff; + public Segment prev; + public Segment next; + + public Vec3d nextDiff; + public Vec3d prevDiff; + + public float sinPrev; + public float sinNext; + public float light; + + public int segmentNo; + public int splitNo; + + public Segment(BoltPoint start, BoltPoint end, float light, int segmentNo, int splitNo) { + this.startPoint = start; + this.endPoint = end; + this.light = light; + this.segmentNo = segmentNo; + this.splitNo = splitNo; + calcDiff(); + } + + public Segment(Vec3d start, Vec3d end) { + this(new BoltPoint(start, new Vec3d(0, 0, 0)), + new BoltPoint(end, new Vec3d(0, 0, 0)), 1, 0, 0); + } + + public void calcDiff() { + diff = endPoint.point.copy().subtract(startPoint.point); + } + + public void calcEndDiffs() { + if (prev != null) { + Vec3d prevDiffNorm = prev.diff.copy().normalize(); + Vec3d thisDiffNorm = diff.copy().normalize(); + + prevDiff = thisDiffNorm.copy().add(prevDiffNorm).normalize(); + sinPrev = (float) Math.sin(thisDiffNorm.angle(prevDiffNorm.multiply(-1)) / 2); + } else { + prevDiff = diff.copy().normalize(); + sinPrev = 1; + } + + if (next != null) { + Vec3d nextDiffNorm = next.diff.copy().normalize(); + Vec3d thisDiffNorm = diff.copy().normalize(); + + nextDiff = thisDiffNorm.add(nextDiffNorm).normalize(); + sinNext = (float) Math.sin(thisDiffNorm.angle(nextDiffNorm.multiply(-1)) / 2); + } else { + nextDiff = diff.copy().normalize(); + sinNext = 1; + } + } + } + + public class SegmentSorter implements Comparator { + @Override + public int compare(Segment o1, Segment o2) { + int comp = Integer.compare(o1.splitNo, o2.splitNo); + if (comp == 0) { + return Integer.compare(o1.segmentNo, o2.segmentNo); + } + return comp; + } + } + + public class SegmentLightSorter implements Comparator { + @Override + public int compare(Segment o1, Segment o2) { + return Float.compare(o2.light, o1.light); + } + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java new file mode 100644 index 000000000..1951666e4 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/LightningHandler.java @@ -0,0 +1,202 @@ +package kamkeel.npcs.client.renderer.lightning; + +import cpw.mods.fml.common.eventhandler.SubscribeEvent; +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.client.renderer.lightning.LightningBolt.Segment; +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.ActiveRenderInfo; +import net.minecraft.client.renderer.Tessellator; +import net.minecraft.client.renderer.texture.TextureManager; +import net.minecraft.entity.Entity; +import net.minecraft.util.ResourceLocation; +import net.minecraft.world.World; +import net.minecraftforge.client.event.RenderWorldLastEvent; +import org.lwjgl.opengl.GL11; + +import java.awt.Color; + +/** + * Handles rendering of lightning bolts in the world. + * Adapted from Botania's LightningHandler by Vazkii/ChickenBones. + * + * Register this handler with: MinecraftForge.EVENT_BUS.register(new LightningHandler()); + */ +@SideOnly(Side.CLIENT) +public class LightningHandler { + + private static final ResourceLocation TEXTURE_OUTER = new ResourceLocation("customnpcs", "textures/effects/lightning_outer.png"); + private static final ResourceLocation TEXTURE_INNER = new ResourceLocation("customnpcs", "textures/effects/lightning_inner.png"); + + public static int lightningCount = 0; + + private static Vec3d getRelativeViewVector(Vec3d pos) { + Entity renderEntity = Minecraft.getMinecraft().renderViewEntity; + return new Vec3d( + (float) renderEntity.posX - pos.x, + (float) renderEntity.posY + renderEntity.getEyeHeight() - pos.y, + (float) renderEntity.posZ - pos.z + ); + } + + @SubscribeEvent + public void onRenderWorldLast(RenderWorldLastEvent event) { + float partialTicks = event.partialTicks; + Entity entity = Minecraft.getMinecraft().thePlayer; + TextureManager textureManager = Minecraft.getMinecraft().renderEngine; + + double interpPosX = entity.lastTickPosX + (entity.posX - entity.lastTickPosX) * partialTicks; + double interpPosY = entity.lastTickPosY + (entity.posY - entity.lastTickPosY) * partialTicks; + double interpPosZ = entity.lastTickPosZ + (entity.posZ - entity.lastTickPosZ) * partialTicks; + + if (LightningBolt.boltList.isEmpty()) { + return; + } + + GL11.glPushMatrix(); + GL11.glTranslated(-interpPosX, -interpPosY, -interpPosZ); + + Tessellator tessellator = Tessellator.instance; + + GL11.glDepthMask(false); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_CULL_FACE); + + lightningCount = 0; + + // Render outer layer (translucent glow) + for (LightningBolt bolt : LightningBolt.boltList) { + renderBolt(bolt, tessellator, partialTicks, false); + } + + // Render inner layer (bright core) + for (LightningBolt bolt : LightningBolt.boltList) { + renderBolt(bolt, tessellator, partialTicks, true); + } + + GL11.glEnable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_BLEND); + GL11.glDepthMask(true); + + GL11.glPopMatrix(); + } + + private void renderBolt(LightningBolt bolt, Tessellator tessellator, float partialTicks, boolean inner) { + lightningCount++; + + float boltAge = bolt.particleAge < 0 ? 0 : (float) bolt.particleAge / (float) bolt.particleMaxAge; + float mainAlpha; + if (!inner) { + mainAlpha = (1 - boltAge) * 0.4F; + } else { + mainAlpha = 1 - boltAge * 0.5F; + } + + int expandTime = (int) (bolt.length * bolt.speed); + + int renderStart = (int) ((expandTime / 2 - bolt.particleMaxAge + bolt.particleAge) / (float) (expandTime / 2) * bolt.numSegments0); + int renderEnd = (int) ((bolt.particleAge + expandTime) / (float) expandTime * bolt.numSegments0); + + for (Segment segment : bolt.segments) { + if (segment.segmentNo < renderStart || segment.segmentNo > renderEnd) { + continue; + } + + Vec3d playerVec = getRelativeViewVector(segment.startPoint.point).multiply(-1); + + double width = 0.008F * (playerVec.mag() / 8 + 1) * (1 + segment.light) * 0.4F; + + Vec3d diff1 = playerVec.copy().crossProduct(segment.prevDiff).normalize().multiply(width / segment.sinPrev); + Vec3d diff2 = playerVec.copy().crossProduct(segment.nextDiff).normalize().multiply(width / segment.sinNext); + + Vec3d startVec = segment.startPoint.point; + Vec3d endVec = segment.endPoint.point; + + int color = inner ? bolt.colorInner : bolt.colorOuter; + Color c = new Color(color); + int alpha = (int) (mainAlpha * segment.light * c.getAlpha()); + + tessellator.startDrawingQuads(); + tessellator.setBrightness(0xF000F0); + tessellator.setColorRGBA(c.getRed(), c.getGreen(), c.getBlue(), alpha); + + tessellator.addVertex(endVec.x - diff2.x, endVec.y - diff2.y, endVec.z - diff2.z); + tessellator.addVertex(startVec.x - diff1.x, startVec.y - diff1.y, startVec.z - diff1.z); + tessellator.addVertex(startVec.x + diff1.x, startVec.y + diff1.y, startVec.z + diff1.z); + tessellator.addVertex(endVec.x + diff2.x, endVec.y + diff2.y, endVec.z + diff2.z); + + tessellator.draw(); + + // Rounded end caps + if (segment.next == null) { + Vec3d roundEnd = segment.endPoint.point.copy().add(segment.diff.copy().normalize().multiply(width)); + + tessellator.startDrawingQuads(); + tessellator.setBrightness(0xF000F0); + tessellator.setColorRGBA(c.getRed(), c.getGreen(), c.getBlue(), alpha); + + tessellator.addVertex(roundEnd.x - diff2.x, roundEnd.y - diff2.y, roundEnd.z - diff2.z); + tessellator.addVertex(endVec.x - diff2.x, endVec.y - diff2.y, endVec.z - diff2.z); + tessellator.addVertex(endVec.x + diff2.x, endVec.y + diff2.y, endVec.z + diff2.z); + tessellator.addVertex(roundEnd.x + diff2.x, roundEnd.y + diff2.y, roundEnd.z + diff2.z); + + tessellator.draw(); + } + + if (segment.prev == null) { + Vec3d roundEnd = segment.startPoint.point.copy().subtract(segment.diff.copy().normalize().multiply(width)); + + tessellator.startDrawingQuads(); + tessellator.setBrightness(0xF000F0); + tessellator.setColorRGBA(c.getRed(), c.getGreen(), c.getBlue(), alpha); + + tessellator.addVertex(startVec.x - diff1.x, startVec.y - diff1.y, startVec.z - diff1.z); + tessellator.addVertex(roundEnd.x - diff1.x, roundEnd.y - diff1.y, roundEnd.z - diff1.z); + tessellator.addVertex(roundEnd.x + diff1.x, roundEnd.y + diff1.y, roundEnd.z + diff1.z); + tessellator.addVertex(startVec.x + diff1.x, startVec.y + diff1.y, startVec.z + diff1.z); + + tessellator.draw(); + } + } + } + + /** + * Spawn a lightning bolt between two points. + * + * @param world The world + * @param start Start position + * @param end End position + * @param ticksPerMeter Speed of bolt expansion + * @param seed Random seed for consistent appearance + * @param colorOuter Outer glow color (ARGB) + * @param colorInner Inner core color (ARGB) + */ + public static void spawnLightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, long seed, int colorOuter, int colorInner) { + LightningBolt bolt = new LightningBolt(world, start, end, ticksPerMeter, seed, colorOuter, colorInner); + bolt.defaultFractal(); + bolt.finalizeBolt(); + LightningBolt.boltList.add(bolt); + } + + /** + * Spawn a lightning bolt with automatic seed. + */ + public static void spawnLightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, int colorOuter, int colorInner) { + spawnLightningBolt(world, start, end, ticksPerMeter, System.nanoTime(), colorOuter, colorInner); + } + + /** + * Spawn a simple lightning bolt for projectiles - less detail, faster. + */ + public static void spawnSimpleLightningBolt(World world, Vec3d start, Vec3d end, float ticksPerMeter, int colorOuter, int colorInner) { + LightningBolt bolt = new LightningBolt(world, start, end, ticksPerMeter, System.nanoTime(), colorOuter, colorInner); + bolt.simpleFractal(); + bolt.finalizeBolt(); + LightningBolt.boltList.add(bolt); + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/Quaternion.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/Quaternion.java new file mode 100644 index 000000000..cda0307b7 --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/Quaternion.java @@ -0,0 +1,85 @@ +package kamkeel.npcs.client.renderer.lightning; + +/** + * Quaternion class for axis-angle rotations in lightning calculations. + * Adapted from Botania's Quat by ChickenBones. + */ +public class Quaternion { + public double x; + public double y; + public double z; + public double s; + + public Quaternion() { + s = 1.0; + x = 0.0; + y = 0.0; + z = 0.0; + } + + public Quaternion(Quaternion quat) { + x = quat.x; + y = quat.y; + z = quat.z; + s = quat.s; + } + + public Quaternion(double s, double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + this.s = s; + } + + public static Quaternion aroundAxis(double ax, double ay, double az, double angle) { + angle *= 0.5; + double sinAngle = Math.sin(angle); + return new Quaternion(Math.cos(angle), ax * sinAngle, ay * sinAngle, az * sinAngle); + } + + public static Quaternion aroundAxis(Vec3d axis, double angle) { + return aroundAxis(axis.x, axis.y, axis.z, angle); + } + + public void multiply(Quaternion quat) { + double d = s * quat.s - x * quat.x - y * quat.y - z * quat.z; + double d1 = s * quat.x + x * quat.s - y * quat.z + z * quat.y; + double d2 = s * quat.y + x * quat.z + y * quat.s - z * quat.x; + double d3 = s * quat.z - x * quat.y + y * quat.x + z * quat.s; + s = d; + x = d1; + y = d2; + z = d3; + } + + public double mag() { + return Math.sqrt(x * x + y * y + z * z + s * s); + } + + public void normalize() { + double d = mag(); + if (d == 0.0) { + return; + } + d = 1.0 / d; + x *= d; + y *= d; + z *= d; + s *= d; + } + + public void rotate(Vec3d vec) { + double d = -x * vec.x - y * vec.y - z * vec.z; + double d1 = s * vec.x + y * vec.z - z * vec.y; + double d2 = s * vec.y - x * vec.z + z * vec.x; + double d3 = s * vec.z + x * vec.y - y * vec.x; + vec.x = d1 * s - d * x - d2 * z + d3 * y; + vec.y = d2 * s - d * y + d1 * z - d3 * x; + vec.z = d3 * s - d * z - d1 * y + d2 * x; + } + + @Override + public String toString() { + return "Quaternion(" + s + ", " + x + ", " + y + ", " + z + ")"; + } +} diff --git a/src/main/java/kamkeel/npcs/client/renderer/lightning/Vec3d.java b/src/main/java/kamkeel/npcs/client/renderer/lightning/Vec3d.java new file mode 100644 index 000000000..018e55bdb --- /dev/null +++ b/src/main/java/kamkeel/npcs/client/renderer/lightning/Vec3d.java @@ -0,0 +1,184 @@ +package kamkeel.npcs.client.renderer.lightning; + +import net.minecraft.entity.Entity; +import net.minecraft.util.Vec3; +import org.lwjgl.opengl.GL11; + +/** + * 3D Vector helper class for lightning calculations. + * Adapted from Botania's Vector3 by ChickenBones. + */ +public class Vec3d { + public double x; + public double y; + public double z; + + public Vec3d() { + } + + public Vec3d(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + } + + public Vec3d(Vec3d vec) { + this.x = vec.x; + this.y = vec.y; + this.z = vec.z; + } + + public Vec3d(Vec3 vec) { + this.x = vec.xCoord; + this.y = vec.yCoord; + this.z = vec.zCoord; + } + + public Vec3d copy() { + return new Vec3d(this); + } + + public static Vec3d fromEntity(Entity e) { + return new Vec3d(e.posX, e.posY, e.posZ); + } + + public static Vec3d fromEntityCenter(Entity e) { + return new Vec3d(e.posX, e.posY - e.yOffset + e.height / 2, e.posZ); + } + + public Vec3d set(double x, double y, double z) { + this.x = x; + this.y = y; + this.z = z; + return this; + } + + public Vec3d set(Vec3d vec) { + this.x = vec.x; + this.y = vec.y; + this.z = vec.z; + return this; + } + + public double dotProduct(Vec3d vec) { + double d = vec.x * x + vec.y * y + vec.z * z; + if (d > 1 && d < 1.00001) d = 1; + else if (d < -1 && d > -1.00001) d = -1; + return d; + } + + public Vec3d crossProduct(Vec3d vec) { + double d = y * vec.z - z * vec.y; + double d1 = z * vec.x - x * vec.z; + double d2 = x * vec.y - y * vec.x; + x = d; + y = d1; + z = d2; + return this; + } + + public Vec3d add(double x, double y, double z) { + this.x += x; + this.y += y; + this.z += z; + return this; + } + + public Vec3d add(Vec3d vec) { + this.x += vec.x; + this.y += vec.y; + this.z += vec.z; + return this; + } + + public Vec3d subtract(Vec3d vec) { + this.x -= vec.x; + this.y -= vec.y; + this.z -= vec.z; + return this; + } + + public Vec3d multiply(double d) { + this.x *= d; + this.y *= d; + this.z *= d; + return this; + } + + public double mag() { + return Math.sqrt(x * x + y * y + z * z); + } + + public double magSquared() { + return x * x + y * y + z * z; + } + + public Vec3d normalize() { + double d = mag(); + if (d != 0) { + multiply(1 / d); + } + return this; + } + + public Vec3d perpendicular() { + if (z == 0) { + return zCrossProduct(); + } + return xCrossProduct(); + } + + public Vec3d xCrossProduct() { + double d = z; + double d1 = -y; + x = 0; + y = d; + z = d1; + return this; + } + + public Vec3d zCrossProduct() { + double d = y; + double d1 = -x; + x = d; + y = d1; + z = 0; + return this; + } + + public Vec3 toVec3() { + return Vec3.createVectorHelper(x, y, z); + } + + public double angle(Vec3d vec) { + return Math.acos(copy().normalize().dotProduct(vec.copy().normalize())); + } + + public Vec3d negate() { + x = -x; + y = -y; + z = -z; + return this; + } + + public Vec3d rotate(double angle, Vec3d axis) { + Quaternion.aroundAxis(axis.copy().normalize(), angle).rotate(this); + return this; + } + + public void glVertex() { + GL11.glVertex3d(x, y, z); + } + + @Override + public String toString() { + return "Vec3d(" + x + ", " + y + ", " + z + ")"; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof Vec3d)) return false; + Vec3d v = (Vec3d) o; + return x == v.x && y == v.y && z == v.z; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 0db44ea8a..5aa34a969 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -57,10 +57,15 @@ public class AbilityDisc extends Ability { // Visual properties private int innerColor = 0xFFFFFF; private int outerColor = 0xFF8800; - private float outerColorWidth = 1.8f; + private float outerColorWidth = 0.4f; // Additive offset from inner size private boolean outerColorEnabled = true; private float rotationSpeed = 5.0f; + // Lightning effect properties + private boolean lightningEffect = false; + private float lightningDensity = 0.15f; + private float lightningRadius = 0.5f; + public AbilityDisc() { this.typeId = "ability.cnpc.disc"; this.name = "Disc"; @@ -113,7 +118,8 @@ public class AbilityDisc extends Ability { boomerang, boomerangDelay, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius ); world.spawnEntityInWorld(disc); @@ -184,6 +190,9 @@ public class AbilityDisc extends Ability { nbt.setFloat("outerColorWidth", outerColorWidth); nbt.setBoolean("outerColorEnabled", outerColorEnabled); nbt.setFloat("rotationSpeed", rotationSpeed); + nbt.setBoolean("lightningEffect", lightningEffect); + nbt.setFloat("lightningDensity", lightningDensity); + nbt.setFloat("lightningRadius", lightningRadius); } @Override @@ -209,9 +218,12 @@ public class AbilityDisc extends Ability { this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF8800; - this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 5.0f; + this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); + this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; + this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; } // Getters & Setters @@ -263,4 +275,10 @@ public class AbilityDisc extends Ability { public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } public float getRotationSpeed() { return rotationSpeed; } public void setRotationSpeed(float rotationSpeed) { this.rotationSpeed = rotationSpeed; } + public boolean hasLightningEffect() { return lightningEffect; } + public void setLightningEffect(boolean lightningEffect) { this.lightningEffect = lightningEffect; } + public float getLightningDensity() { return lightningDensity; } + public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } + public float getLightningRadius() { return lightningRadius; } + public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 58a224c64..9a5345435 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -8,6 +8,7 @@ import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityBeam; +import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; @@ -56,10 +57,18 @@ public class AbilityEnergyBeam extends Ability { // Visual properties private int innerColor = 0xFFFFFF; private int outerColor = 0x00AAFF; - private float outerColorWidth = 1.8f; + private float outerColorWidth = 0.4f; // Additive offset from inner size private boolean outerColorEnabled = true; private float rotationSpeed = 6.0f; + // Lightning effect properties (only on head) + private boolean lightningEffect = false; + private float lightningDensity = 0.15f; + private float lightningRadius = 0.5f; + + // Transient state for beam entity (used during windup charging) + private transient int beamEntityId = -1; + public AbilityEnergyBeam() { this.typeId = "ability.cnpc.beam"; this.name = "Beam"; @@ -99,22 +108,43 @@ public class AbilityEnergyBeam extends Ability { public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { if (world.isRemote) return; - double spawnX = npc.posX; - double spawnY = npc.posY + npc.getEyeHeight() * 0.7; - double spawnZ = npc.posZ; + // Find and start firing the beam that was spawned during windup + if (beamEntityId != -1) { + Entity entity = world.getEntityByID(beamEntityId); + if (entity instanceof EntityAbilityBeam) { + EntityAbilityBeam beam = (EntityAbilityBeam) entity; + beam.startFiring(target); + } + beamEntityId = -1; + } + } - EntityAbilityBeam beam = new EntityAbilityBeam( - world, npc, target, - spawnX, spawnY, spawnZ, - beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, - damage, knockback, knockbackUp, - speed, homing, homingStrength, homingRange, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime - ); + @Override + public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; - world.spawnEntityInWorld(beam); + // Spawn beam in charging mode on first tick of windup + if (tick == 1) { + float offsetDist = 1.0f; + + // Create beam in charging mode - follows NPC's look direction during windup + EntityAbilityBeam beam = EntityAbilityBeam.createCharging( + world, npc, target, + beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, + speed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, + lockMovement, // Anchored mode + windUpTicks, // Charge duration = windup duration + offsetDist // Offset distance in front of NPC + ); + + world.spawnEntityInWorld(beam); + beamEntityId = beam.getEntityId(); + } } @Override @@ -127,6 +157,14 @@ public class AbilityEnergyBeam extends Ability { @Override public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + // Despawn charging beam if interrupted during windup + if (beamEntityId != -1) { + Entity entity = npc.worldObj.getEntityByID(beamEntityId); + if (entity != null) { + entity.setDead(); + } + beamEntityId = -1; + } } @Override @@ -180,6 +218,9 @@ public class AbilityEnergyBeam extends Ability { nbt.setFloat("outerColorWidth", outerColorWidth); nbt.setBoolean("outerColorEnabled", outerColorEnabled); nbt.setFloat("rotationSpeed", rotationSpeed); + nbt.setBoolean("lightningEffect", lightningEffect); + nbt.setFloat("lightningDensity", lightningDensity); + nbt.setFloat("lightningRadius", lightningRadius); } @Override @@ -203,9 +244,12 @@ public class AbilityEnergyBeam extends Ability { this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0x00AAFF; - this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 6.0f; + this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); + this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; + this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; } // Getters & Setters @@ -253,4 +297,10 @@ public class AbilityEnergyBeam extends Ability { public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } public float getRotationSpeed() { return rotationSpeed; } public void setRotationSpeed(float rotationSpeed) { this.rotationSpeed = rotationSpeed; } + public boolean hasLightningEffect() { return lightningEffect; } + public void setLightningEffect(boolean lightningEffect) { this.lightningEffect = lightningEffect; } + public float getLightningDensity() { return lightningDensity; } + public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } + public float getLightningRadius() { return lightningRadius; } + public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index 64a3da8ad..77df6ff35 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -46,9 +46,14 @@ public class AbilityLaserShot extends Ability { // Visual properties private int innerColor = 0xFFFFFF; private int outerColor = 0xFF0000; - private float outerColorWidth = 1.8f; + private float outerColorWidth = 0.4f; // Additive offset from inner width private boolean outerColorEnabled = true; + // Lightning effect properties + private boolean lightningEffect = false; + private float lightningDensity = 0.15f; + private float lightningRadius = 0.5f; + public AbilityLaserShot() { this.typeId = "ability.cnpc.laser_shot"; this.name = "Laser Shot"; @@ -110,7 +115,8 @@ public class AbilityLaserShot extends Ability { expansionSpeed, lingerTicks, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius ); world.spawnEntityInWorld(laser); @@ -148,6 +154,9 @@ public class AbilityLaserShot extends Ability { nbt.setInteger("outerColor", outerColor); nbt.setFloat("outerColorWidth", outerColorWidth); nbt.setBoolean("outerColorEnabled", outerColorEnabled); + nbt.setBoolean("lightningEffect", lightningEffect); + nbt.setFloat("lightningDensity", lightningDensity); + nbt.setFloat("lightningRadius", lightningRadius); } @Override @@ -168,8 +177,11 @@ public class AbilityLaserShot extends Ability { this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF0000; - this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); + this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); + this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; + this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; } // Getters & Setters @@ -209,4 +221,10 @@ public class AbilityLaserShot extends Ability { public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; } public boolean isOuterColorEnabled() { return outerColorEnabled; } public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } + public boolean hasLightningEffect() { return lightningEffect; } + public void setLightningEffect(boolean lightningEffect) { this.lightningEffect = lightningEffect; } + public float getLightningDensity() { return lightningDensity; } + public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } + public float getLightningRadius() { return lightningRadius; } + public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 7b6f34b68..358b82896 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -53,10 +53,16 @@ public class AbilityOrb extends Ability { // Visual properties private int innerColor = 0xFFFFFF; // White core private int outerColor = 0x8888FF; // Light blue glow - private float outerColorWidth = 1.8f; // Multiplier for outer glow size + private float outerColorWidth = 0.4f; // Additive offset from inner size private boolean outerColorEnabled = true; // Whether to render outer glow private float rotationSpeed = 4.0f; // Degrees per tick + // Lightning effect properties + private boolean lightningEffect = false; + private float lightningDensity = 0.15f; // Bolts spawned per tick (0.15 = ~15% chance per frame) + private float lightningRadius = 0.5f; // Max distance lightning arcs - scales with orbSize + private int lightningFadeTime = 6; // Ticks before lightning fades out + public AbilityOrb() { this.typeId = "ability.cnpc.orb"; this.name = "Orb"; @@ -110,7 +116,8 @@ public class AbilityOrb extends Ability { orbSpeed, homing, homingStrength, homingRange, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, lightningFadeTime ); world.spawnEntityInWorld(orb); @@ -181,6 +188,10 @@ public class AbilityOrb extends Ability { nbt.setFloat("outerColorWidth", outerColorWidth); nbt.setBoolean("outerColorEnabled", outerColorEnabled); nbt.setFloat("rotationSpeed", rotationSpeed); + nbt.setBoolean("lightningEffect", lightningEffect); + nbt.setFloat("lightningDensity", lightningDensity); + nbt.setFloat("lightningRadius", lightningRadius); + nbt.setInteger("lightningFadeTime", lightningFadeTime); } @Override @@ -203,9 +214,13 @@ public class AbilityOrb extends Ability { this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0x8888FF; - this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; + this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 4.0f; + this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); + this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; + this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; + this.lightningFadeTime = nbt.hasKey("lightningFadeTime") ? nbt.getInteger("lightningFadeTime") : 6; } // Getters & Setters @@ -376,4 +391,36 @@ public class AbilityOrb extends Ability { public void setRotationSpeed(float rotationSpeed) { this.rotationSpeed = rotationSpeed; } + + public boolean hasLightningEffect() { + return lightningEffect; + } + + public void setLightningEffect(boolean lightningEffect) { + this.lightningEffect = lightningEffect; + } + + public float getLightningDensity() { + return lightningDensity; + } + + public void setLightningDensity(float lightningDensity) { + this.lightningDensity = lightningDensity; + } + + public float getLightningRadius() { + return lightningRadius; + } + + public void setLightningRadius(float lightningRadius) { + this.lightningRadius = lightningRadius; + } + + public int getLightningFadeTime() { + return lightningFadeTime; + } + + public void setLightningFadeTime(int lightningFadeTime) { + this.lightningFadeTime = lightningFadeTime; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java index 71d63f9c6..a5d2f0d95 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java +++ b/src/main/java/kamkeel/npcs/controllers/data/telegraph/TelegraphInstance.java @@ -17,12 +17,18 @@ public class TelegraphInstance { private String instanceId; private Telegraph telegraph; - // Position + // Position (current) private double x; private double y; private double z; private float yaw; + // Position (previous tick - for interpolation) + private double prevX; + private double prevY; + private double prevZ; + private float prevYaw; + // Entity tracking private int entityIdToFollow = -1; private int casterEntityId = -1; @@ -50,6 +56,11 @@ public class TelegraphInstance { this.y = y; this.z = z; this.yaw = yaw; + // Initialize prev positions to current for first frame + this.prevX = x; + this.prevY = y; + this.prevZ = z; + this.prevYaw = yaw; this.remainingTicks = telegraph.getDurationTicks(); this.totalTicks = telegraph.getDurationTicks(); } @@ -69,6 +80,12 @@ public class TelegraphInstance { return false; } + // Save previous position for interpolation + this.prevX = this.x; + this.prevY = this.y; + this.prevZ = this.z; + this.prevYaw = this.yaw; + remainingTicks--; // Update position if following an entity @@ -144,6 +161,43 @@ public class TelegraphInstance { return (alpha << 24) | (r << 16) | (g << 8) | b; } + // ═══════════════════════════════════════════════════════════════════ + // INTERPOLATED POSITION (for smooth rendering) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Get interpolated X position for smooth rendering. + */ + public double getInterpolatedX(float partialTicks) { + return prevX + (x - prevX) * partialTicks; + } + + /** + * Get interpolated Y position for smooth rendering. + */ + public double getInterpolatedY(float partialTicks) { + return prevY + (y - prevY) * partialTicks; + } + + /** + * Get interpolated Z position for smooth rendering. + */ + public double getInterpolatedZ(float partialTicks) { + return prevZ + (z - prevZ) * partialTicks; + } + + /** + * Get interpolated yaw for smooth rendering. + * Handles angle wrapping for smooth rotation. + */ + public float getInterpolatedYaw(float partialTicks) { + // Handle angle wrapping to prevent spinning the wrong way + float diff = yaw - prevYaw; + while (diff > 180.0f) diff -= 360.0f; + while (diff < -180.0f) diff += 360.0f; + return prevYaw + diff * partialTicks; + } + // ═══════════════════════════════════════════════════════════════════ // NBT SERIALIZATION // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index b0c3579e7..ab62c4ca9 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -47,6 +47,23 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { // Origin stays fixed (or follows owner) private boolean attachedToOwner = true; + // Whether to render tail orb (only when anchored) + private boolean renderTailOrb = true; + + // Charging state (during windup) + private boolean charging = false; + private int chargeDuration = 40; + private int chargeTick = 0; + private float chargeOffsetDistance = 1.0f; + + // Trail fading for non-anchored beams (comet effect) + private boolean fadeTrail = false; + private int trailFadeTime = 20; // Ticks for trail to fully fade + private List trailPointAges = new ArrayList<>(); + + // Data watcher index for charging state (synced to clients) + private static final int DW_CHARGING = 20; + // Debug logging private static final boolean DEBUG_LOGGING = true; @@ -54,8 +71,32 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { super(world); } + @Override + protected void entityInit() { + super.entityInit(); + // Register data watcher for charging state + this.dataWatcher.addObject(DW_CHARGING, (byte) 0); + } + /** - * Full constructor with all parameters. + * Check if beam is in charging state (synced via data watcher). + */ + public boolean isCharging() { + return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; + } + + /** + * Set charging state (server only, synced to clients via data watcher). + */ + private void setCharging(boolean value) { + this.charging = value; + if (!worldObj.isRemote) { + this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); + } + } + + /** + * Full constructor with all parameters (no lightning). */ public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, @@ -66,15 +107,62 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime) { + this(world, owner, target, x, y, z, + beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, + false, 0.15f, 0.5f); + } + + /** + * Full constructor with all parameters including lightning. + */ + public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float beamWidth, float headSize, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius) { + this(world, owner, target, x, y, z, + beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, + true); // Default to anchored mode + } + + /** + * Full constructor with all parameters including lightning and anchored mode. + * @param anchoredMode If true, origin follows owner and tail orb is rendered. + * If false, beam is free-moving with trailing length (no tail orb). + */ + public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float beamWidth, float headSize, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius, + boolean anchoredMode) { super(world); - // Initialize base properties + // Initialize base properties with lightning initProjectile(owner, target, x, y, z, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime); + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, 6); // Beam-specific properties this.speed = speed; @@ -84,6 +172,10 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { this.beamWidth = beamWidth; this.headSize = headSize; + // Anchored mode controls whether origin follows owner and tail orb is rendered + this.attachedToOwner = anchoredMode; + this.renderTailOrb = anchoredMode; + // Initialize head offset at origin (0,0,0 relative) this.headOffsetX = 0; this.headOffsetY = 0; @@ -119,6 +211,118 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { } } + /** + * Create a beam in charging mode (for windup phase). + * The beam will grow from 0 to headSize over chargeDuration ticks. + * Position follows the owner's look direction. + */ + public static EntityAbilityBeam createCharging(World world, EntityNPCInterface owner, EntityLivingBase target, + float beamWidth, float headSize, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius, + boolean anchoredMode, int chargeDuration, float chargeOffsetDistance) { + // Calculate initial position in front of owner's look direction + float yaw = (float) Math.toRadians(owner.rotationYawHead); + double spawnX = owner.posX - Math.sin(yaw) * chargeOffsetDistance; + double spawnY = owner.posY + owner.getEyeHeight() * 0.7; + double spawnZ = owner.posZ + Math.cos(yaw) * chargeOffsetDistance; + + EntityAbilityBeam beam = new EntityAbilityBeam( + world, owner, target, + spawnX, spawnY, spawnZ, + beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, + anchoredMode); + + // Set charging state (uses data watcher for client sync) + beam.setCharging(true); + beam.chargeDuration = chargeDuration; + beam.chargeTick = 0; + beam.chargeOffsetDistance = chargeOffsetDistance; + + // Non-anchored beams have fading trail (comet effect) + beam.fadeTrail = !anchoredMode; + + // Clear motion while charging + beam.motionX = 0; + beam.motionY = 0; + beam.motionZ = 0; + + return beam; + } + + /** + * Start the beam firing (exit charging mode). + * Called by ability when windup ends. + * + * For anchored beams: origin follows owner, head starts at charged position + * For non-anchored beams: origin fixed at charged position, head starts there + */ + public void startFiring(EntityLivingBase target) { + if (!isCharging()) return; + + setCharging(false); + + // Origin (tail) stays at the charged position - where the orb was + // This is the same for both anchored and non-anchored modes + startX = posX; + startY = posY; + startZ = posZ; + + // Head starts at the origin (tail position) + headOffsetX = 0; + headOffsetY = 0; + headOffsetZ = 0; + prevHeadOffsetX = 0; + prevHeadOffsetY = 0; + prevHeadOffsetZ = 0; + + // Origin no longer follows owner after firing starts + // (for anchored mode, tail is fixed in space; for non-anchored, there's no tail) + attachedToOwner = false; + + // Initialize trail with just the origin point + trailPoints.clear(); + trailPointAges.clear(); + trailPoints.add(Vec3.createVectorHelper(0, 0, 0)); + if (fadeTrail) trailPointAges.add(0); + + // Calculate velocity toward target (head starts at origin = startX/Y/Z) + Entity owner = getOwner(); + + if (target != null) { + double dx = target.posX - startX; + double dy = (target.posY + target.height * 0.5) - startY; + double dz = target.posZ - startZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + motionX = (dx / len) * speed; + motionY = (dy / len) * speed; + motionZ = (dz / len) * speed; + } + } else if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + + if (DEBUG_LOGGING && !worldObj.isRemote) { + LogWriter.info("[Beam] startFiring: origin=(" + startX + "," + startY + "," + startZ + + ") headOffset=(" + headOffsetX + "," + headOffsetY + "," + headOffsetZ + + ") motion=(" + motionX + "," + motionY + "," + motionZ + ")"); + } + } + @Override protected boolean checkMaxDistance() { // Check distance using head offset (distance from origin) @@ -136,19 +340,41 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { @Override protected void updateProjectile() { + // Handle charging state (windup phase) - use isCharging() for synced value + if (isCharging()) { + updateCharging(); + return; + } + // Store previous head offset for interpolation prevHeadOffsetX = headOffsetX; prevHeadOffsetY = headOffsetY; prevHeadOffsetZ = headOffsetZ; + // Age trail points for fading effect + if (fadeTrail) { + ageTrailPoints(); + } + if (worldObj.isRemote) { - // Client: interpolate entity position, derive head from offset + // Client: interpolate entity position handleClientInterpolation(); - // Client also updates head offset to match motion for smooth movement - headOffsetX += motionX; - headOffsetY += motionY; - headOffsetZ += motionZ; + // Client: update origin if attached to owner (mirrors server logic) + if (attachedToOwner) { + Entity owner = getOwner(); + if (owner != null) { + startX = owner.posX; + startY = owner.posY + owner.getEyeHeight() * 0.7; + startZ = owner.posZ; + } + } + + // Client: derive head offset from entity position and origin + // Entity position = head world position, so headOffset = pos - origin + headOffsetX = posX - startX; + headOffsetY = posY - startY; + headOffsetZ = posZ - startZ; // Add trail point on client addTrailPoint(); @@ -166,7 +392,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { Entity owner = getOwner(); if (owner != null) { startX = owner.posX; - startY = owner.posY + owner.height * 0.7; + startY = owner.posY + owner.getEyeHeight() * 0.7; startZ = owner.posZ; } } @@ -201,10 +427,66 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { } } + /** + * Update during charging state - follow owner's look direction. + */ + private void updateCharging() { + chargeTick++; + + Entity owner = getOwner(); + if (owner == null || owner.isDead) { + setDead(); + return; + } + + // Follow owner's head/look direction (rotationYawHead for NPCs/players) + float yaw; + if (owner instanceof EntityLivingBase) { + yaw = (float) Math.toRadians(((EntityLivingBase) owner).rotationYawHead); + } else { + yaw = (float) Math.toRadians(owner.rotationYaw); + } + + double offsetX = -Math.sin(yaw) * chargeOffsetDistance; + double offsetZ = Math.cos(yaw) * chargeOffsetDistance; + + double newX = owner.posX + offsetX; + double newY = owner.posY + owner.getEyeHeight() * 0.7; + double newZ = owner.posZ + offsetZ; + + setPosition(newX, newY, newZ); + + // Also update origin for when firing starts + startX = newX; + startY = newY; + startZ = newZ; + } + + /** + * Age trail points and remove old ones (for comet effect). + */ + private void ageTrailPoints() { + // Age all trail points + for (int i = 0; i < trailPointAges.size(); i++) { + trailPointAges.set(i, trailPointAges.get(i) + 1); + } + + // Remove trail points that have fully faded + while (!trailPointAges.isEmpty() && trailPointAges.get(0) >= trailFadeTime) { + trailPointAges.remove(0); + if (!trailPoints.isEmpty()) { + trailPoints.remove(0); + } + } + } + private void addTrailPoint() { // Trail points are RELATIVE to origin if (trailPoints.isEmpty()) { trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ)); + if (fadeTrail) { + trailPointAges.add(0); + } return; } @@ -217,10 +499,15 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { if (dist >= MIN_POINT_DISTANCE) { trailPoints.add(Vec3.createVectorHelper(headOffsetX, headOffsetY, headOffsetZ)); + if (fadeTrail) { + trailPointAges.add(0); + } - // Limit trail length - while (trailPoints.size() > MAX_TRAIL_POINTS) { - trailPoints.remove(0); + // Limit trail length (only if not using fading - fading handles its own cleanup) + if (!fadeTrail) { + while (trailPoints.size() > MAX_TRAIL_POINTS) { + trailPoints.remove(0); + } } } } @@ -369,6 +656,59 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { return startZ; } + /** + * Whether the tail orb should be rendered (only true in anchored mode). + */ + public boolean shouldRenderTailOrb() { + return renderTailOrb; + } + + /** + * Whether the beam is attached to its owner (anchored mode). + */ + public boolean isAttachedToOwner() { + return attachedToOwner; + } + + /** + * Get the charge progress (0-1). + */ + public float getChargeProgress() { + if (chargeDuration <= 0) return 1.0f; + return Math.min(1.0f, (float) chargeTick / chargeDuration); + } + + /** + * Get interpolated charge progress for smooth rendering. + */ + public float getInterpolatedChargeProgress(float partialTicks) { + if (chargeDuration <= 0) return 1.0f; + float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration); + float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration); + return prevProgress + (currProgress - prevProgress) * partialTicks; + } + + /** + * Whether trail should fade (comet effect for non-anchored beams). + */ + public boolean hasFadingTrail() { + return fadeTrail; + } + + /** + * Get trail point ages for fading calculation. + */ + public List getTrailPointAges() { + return trailPointAges; + } + + /** + * Get trail fade time in ticks. + */ + public int getTrailFadeTime() { + return trailFadeTime; + } + // ==================== NBT ==================== @Override @@ -386,6 +726,16 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { this.prevHeadOffsetY = this.headOffsetY; this.prevHeadOffsetZ = this.headOffsetZ; this.attachedToOwner = !nbt.hasKey("AttachedToOwner") || nbt.getBoolean("AttachedToOwner"); + this.renderTailOrb = !nbt.hasKey("RenderTailOrb") || nbt.getBoolean("RenderTailOrb"); + // Read charging state and sync to data watcher + boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); + this.charging = isCharging; + this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); + this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; + this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; + this.chargeOffsetDistance = nbt.hasKey("ChargeOffsetDistance") ? nbt.getFloat("ChargeOffsetDistance") : 1.0f; + this.fadeTrail = nbt.hasKey("FadeTrail") && nbt.getBoolean("FadeTrail"); + this.trailFadeTime = nbt.hasKey("TrailFadeTime") ? nbt.getInteger("TrailFadeTime") : 20; // Read trail points (relative to origin) trailPoints.clear(); @@ -414,6 +764,13 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { nbt.setDouble("HeadOffsetY", headOffsetY); nbt.setDouble("HeadOffsetZ", headOffsetZ); nbt.setBoolean("AttachedToOwner", attachedToOwner); + nbt.setBoolean("RenderTailOrb", renderTailOrb); + nbt.setBoolean("Charging", isCharging()); + nbt.setInteger("ChargeDuration", chargeDuration); + nbt.setInteger("ChargeTick", chargeTick); + nbt.setFloat("ChargeOffsetDistance", chargeOffsetDistance); + nbt.setBoolean("FadeTrail", fadeTrail); + nbt.setInteger("TrailFadeTime", trailFadeTime); // Write trail points NBTTagList trailList = new NBTTagList(); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index ef513f80d..c0127286f 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -40,7 +40,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { } /** - * Full constructor with all parameters. + * Full constructor with all parameters (no lightning). */ public EntityAbilityDisc(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, @@ -52,15 +52,38 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime) { + this(world, owner, target, x, y, z, + discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, + boomerang, boomerangDelay, explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, + false, 0.15f, 0.5f); + } + + /** + * Full constructor with all parameters including lightning. + */ + public EntityAbilityDisc(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float discRadius, float discThickness, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean boomerang, int boomerangDelay, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius) { super(world); - // Initialize base properties (use discRadius as size for base) + // Initialize base properties with lightning (use discRadius as size for base) initProjectile(owner, target, x, y, z, discRadius, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime); + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, 6); // Disc-specific properties this.speed = speed; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 78668377a..7ad1f0d45 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -45,7 +45,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { } /** - * Full constructor with all parameters. + * Full constructor with all parameters (no lightning). */ public EntityAbilityLaser(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, @@ -56,15 +56,37 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime) { + this(world, owner, target, x, y, z, + laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, + damage, knockback, knockbackUp, expansionSpeed, lingerTicks, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, + false, 0.15f, 0.5f); + } + + /** + * Full constructor with all parameters including lightning. + */ + public EntityAbilityLaser(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float laserWidth, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, + float damage, float knockback, float knockbackUp, + float expansionSpeed, int lingerTicks, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius) { super(world); - // Initialize base properties + // Initialize base properties with lightning initProjectile(owner, target, x, y, z, laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, 0.0f, // No rotation for laser damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime); + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, 6); // Laser-specific properties this.laserWidth = laserWidth; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index de447333b..38e3741f9 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -41,15 +41,39 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime) { + this(world, owner, target, x, y, z, + orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, + speed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime, + false, 0.15f, 0.5f, 6); // Default: no lightning, low density, small radius, 6 tick fade + } + + /** + * Full constructor with lightning effect support. + */ + public EntityAbilityOrb(World world, EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float orbSize, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius, int lightningFadeTime) { super(world); - // Initialize base properties + // Initialize base properties with lightning initProjectile(owner, target, x, y, z, orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime); + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, lightningFadeTime); // Orb-specific properties this.speed = speed; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index eed12d10b..32c5796bd 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -30,7 +30,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA protected int innerColor = 0xFFFFFF; protected int outerColor = 0x8888FF; protected boolean outerColorEnabled = true; - protected float outerColorWidth = 1.8f; + protected float outerColorWidth = 0.4f; // Additive offset from inner size protected float rotationSpeed = 4.0f; // ==================== COMBAT PROPERTIES ==================== @@ -48,6 +48,16 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA protected float explosionRadius = 3.0f; protected float explosionDamageFalloff = 0.5f; + // ==================== LIGHTNING EFFECT PROPERTIES ==================== + protected boolean lightningEffect = false; + protected float lightningDensity = 0.15f; // Bolts spawned per tick (0.15 = ~15% chance per frame) + protected float lightningRadius = 0.5f; // Max distance lightning can arc from center + protected int lightningFadeTime = 6; // Ticks before lightning fades out + + // Client-side lightning state (not saved to NBT) + @cpw.mods.fml.relauncher.SideOnly(cpw.mods.fml.relauncher.Side.CLIENT) + public transient Object lightningState; // Actually AttachedLightningRenderer.LightningState + // ==================== LIFESPAN ==================== protected float maxDistance = 30.0f; protected int maxLifetime = 200; @@ -93,6 +103,27 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime) { + initProjectile(owner, target, x, y, z, + size, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime, + false, 0.15f, 0.5f, 6); // Default: no lightning, low density, small radius, 6 tick fade + } + + /** + * Initialize common properties with lightning effect support. + */ + protected void initProjectile(EntityNPCInterface owner, EntityLivingBase target, + double x, double y, double z, + float size, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + float damage, float knockback, float knockbackUp, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius, int lightningFadeTime) { this.setPosition(x, y, z); this.startX = x; this.startY = y; @@ -129,6 +160,12 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA this.maxLifetime = maxLifetime; // deathWorldTime will be set on first tick when world is available + // Lightning + this.lightningEffect = lightningEffect; + this.lightningDensity = lightningDensity; + this.lightningRadius = lightningRadius; + this.lightningFadeTime = lightningFadeTime; + // Initialize render size this.renderCurrentSize = size; this.prevRenderSize = size; @@ -417,6 +454,24 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA return this.prevRenderSize + (this.renderCurrentSize - this.prevRenderSize) * partialTicks; } + // ==================== LIGHTNING GETTERS ==================== + + public boolean hasLightningEffect() { + return lightningEffect; + } + + public float getLightningDensity() { + return lightningDensity; + } + + public float getLightningRadius() { + return lightningRadius; + } + + public int getLightningFadeTime() { + return lightningFadeTime; + } + // ==================== BRIGHTNESS ==================== @Override @@ -452,7 +507,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA this.innerColor = nbt.getInteger("InnerColor"); this.outerColor = nbt.getInteger("OuterColor"); this.outerColorEnabled = !nbt.hasKey("OuterColorEnabled") || nbt.getBoolean("OuterColorEnabled"); - this.outerColorWidth = nbt.hasKey("OuterColorWidth") ? nbt.getFloat("OuterColorWidth") : 1.8f; + this.outerColorWidth = nbt.hasKey("OuterColorWidth") ? nbt.getFloat("OuterColorWidth") : 0.4f; this.rotationSpeed = nbt.hasKey("RotationSpeed") ? nbt.getFloat("RotationSpeed") : 4.0f; this.damage = nbt.getFloat("Damage"); @@ -482,6 +537,12 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA this.motionY = nbt.getDouble("MotionY"); this.motionZ = nbt.getDouble("MotionZ"); + // Lightning + this.lightningEffect = nbt.getBoolean("LightningEffect"); + this.lightningDensity = nbt.hasKey("LightningDensity") ? nbt.getFloat("LightningDensity") : 0.15f; + this.lightningRadius = nbt.hasKey("LightningRadius") ? nbt.getFloat("LightningRadius") : 0.5f; + this.lightningFadeTime = nbt.hasKey("LightningFadeTime") ? nbt.getInteger("LightningFadeTime") : 6; + if (this.worldObj != null && this.worldObj.isRemote) { this.renderCurrentSize = this.size; this.prevRenderSize = this.size; @@ -525,6 +586,12 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA nbt.setDouble("MotionX", motionX); nbt.setDouble("MotionY", motionY); nbt.setDouble("MotionZ", motionZ); + + // Lightning + nbt.setBoolean("LightningEffect", lightningEffect); + nbt.setFloat("LightningDensity", lightningDensity); + nbt.setFloat("LightningRadius", lightningRadius); + nbt.setInteger("LightningFadeTime", lightningFadeTime); } /** diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 534963e38..9a0044191 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -144,11 +144,12 @@ import noppes.npcs.client.renderer.RenderNpcCrystal; import noppes.npcs.client.renderer.RenderNpcDragon; import noppes.npcs.client.renderer.RenderNpcSlime; import noppes.npcs.client.renderer.RenderProjectile; -import kamkeel.npcs.client.renderer.RenderAbilityOrbCube; +import kamkeel.npcs.client.renderer.RenderAbilityOrb; import kamkeel.npcs.client.renderer.RenderAbilityDisc; import kamkeel.npcs.client.renderer.RenderAbilityLaser; import kamkeel.npcs.client.renderer.RenderAbilityBeam; import kamkeel.npcs.client.renderer.RenderAbilitySweeper; +import kamkeel.npcs.client.renderer.lightning.LightningHandler; import kamkeel.npcs.entity.EntityAbilityOrb; import kamkeel.npcs.entity.EntityAbilityDisc; import kamkeel.npcs.entity.EntityAbilityLaser; @@ -261,8 +262,7 @@ public class ClientProxy extends CommonProxy { RenderingRegistry.registerEntityRenderingHandler(EntityNpcDragon.class, new RenderNpcDragon(new ModelNpcDragon(0.0F), 0.5F)); RenderingRegistry.registerEntityRenderingHandler(EntityNpcSlime.class, new RenderNpcSlime(new ModelNpcSlime(16), new ModelNpcSlime(0), 0.25F)); RenderingRegistry.registerEntityRenderingHandler(EntityProjectile.class, new RenderProjectile()); - // RenderingRegistry.registerEntityRenderingHandler(EntityAbilityOrb.class, new RenderAbilityOrb()); - RenderingRegistry.registerEntityRenderingHandler(EntityAbilityOrb.class, new RenderAbilityOrbCube()); + RenderingRegistry.registerEntityRenderingHandler(EntityAbilityOrb.class, new RenderAbilityOrb()); RenderingRegistry.registerEntityRenderingHandler(EntityAbilityDisc.class, new RenderAbilityDisc()); RenderingRegistry.registerEntityRenderingHandler(EntityAbilityLaser.class, new RenderAbilityLaser()); RenderingRegistry.registerEntityRenderingHandler(EntityAbilityBeam.class, new RenderAbilityBeam()); @@ -340,6 +340,9 @@ public class ClientProxy extends CommonProxy { TelegraphManager.initClient(); MinecraftForge.EVENT_BUS.register(new TelegraphRenderer()); + // Lightning effect rendering system + MinecraftForge.EVENT_BUS.register(new LightningHandler()); + if (ConfigClient.InventoryGuiEnabled) { MinecraftForge.EVENT_BUS.register(new TabRegistry()); if (TabRegistry.getTabList().isEmpty()) { diff --git a/src/main/java/noppes/npcs/client/ClientTickHandler.java b/src/main/java/noppes/npcs/client/ClientTickHandler.java index 6142de16c..203cef3e2 100644 --- a/src/main/java/noppes/npcs/client/ClientTickHandler.java +++ b/src/main/java/noppes/npcs/client/ClientTickHandler.java @@ -5,6 +5,7 @@ import cpw.mods.fml.common.eventhandler.SubscribeEvent; import cpw.mods.fml.common.gameevent.InputEvent; import cpw.mods.fml.common.gameevent.TickEvent; import cpw.mods.fml.common.gameevent.TickEvent.Phase; +import kamkeel.npcs.client.renderer.lightning.LightningBolt; import kamkeel.npcs.controllers.data.telegraph.TelegraphManager; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.player.CheckPlayerValue; @@ -115,6 +116,9 @@ public class ClientTickHandler { if (TelegraphManager.ClientInstance != null) { TelegraphManager.ClientInstance.tick(mc.theWorld); } + + // Update lightning bolts for ability effects + LightningBolt.updateAll(); if (Minecraft.getMinecraft().thePlayer != null && (prevWidth != mc.displayWidth || prevHeight != mc.displayHeight)) { prevWidth = mc.displayWidth; prevHeight = mc.displayHeight; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java index ec80ed5d6..373752fa6 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java @@ -126,6 +126,28 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { // Row 3: Rotation Speed addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", labelX, y + 5)); addTextField(createFloatField(204, fieldX, y, 50, disc.getRotationSpeed())); + + y += 24; + + // Row 4: Lightning Effect Enabled + addLabel(new GuiNpcLabel(205, "ability.lightning", labelX, y + 5)); + addButton(new GuiNpcButton(205, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.hasLightningEffect() ? 1 : 0)); + + // Only show Lightning settings if Lightning is enabled + if (disc.hasLightningEffect()) { + y += 24; + + // Row 5: Density + Radius + addLabel(new GuiNpcLabel(206, "ability.lightningDensity", labelX, y + 5)); + GuiNpcTextField densityField = new GuiNpcTextField(206, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(disc.getLightningDensity())); + densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); + addTextField(densityField); + + addLabel(new GuiNpcLabel(207, "ability.lightningRadius", col2LabelX, y + 5)); + GuiNpcTextField radiusField = new GuiNpcTextField(207, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(disc.getLightningRadius())); + radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); + addTextField(radiusField); + } } @Override @@ -160,6 +182,10 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { disc.setOuterColorEnabled(value == 1); initGui(); break; + case 205: + disc.setLightningEffect(value == 1); + initGui(); + break; } } @@ -222,6 +248,12 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { case 204: disc.setRotationSpeed(parseFloat(field, disc.getRotationSpeed())); break; + case 206: + disc.setLightningDensity(parseFloat(field, disc.getLightningDensity())); + break; + case 207: + disc.setLightningRadius(parseFloat(field, disc.getLightningRadius())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java index afb89ffed..ad15bb576 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java @@ -127,6 +127,28 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { // Row 3: Rotation Speed addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", labelX, y + 5)); addTextField(createFloatField(204, fieldX, y, 50, beam.getRotationSpeed())); + + y += 24; + + // Row 4: Lightning Effect Enabled (only affects head) + addLabel(new GuiNpcLabel(205, "ability.lightning", labelX, y + 5)); + addButton(new GuiNpcButton(205, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.hasLightningEffect() ? 1 : 0)); + + // Only show Lightning settings if Lightning is enabled + if (beam.hasLightningEffect()) { + y += 24; + + // Row 5: Density + Radius + addLabel(new GuiNpcLabel(206, "ability.lightningDensity", labelX, y + 5)); + GuiNpcTextField densityField = new GuiNpcTextField(206, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(beam.getLightningDensity())); + densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); + addTextField(densityField); + + addLabel(new GuiNpcLabel(207, "ability.lightningRadius", col2LabelX, y + 5)); + GuiNpcTextField radiusField = new GuiNpcTextField(207, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(beam.getLightningRadius())); + radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); + addTextField(radiusField); + } } @Override @@ -158,6 +180,10 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { beam.setOuterColorEnabled(value == 1); initGui(); break; + case 205: + beam.setLightningEffect(value == 1); + initGui(); + break; } } @@ -223,6 +249,12 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { case 204: beam.setRotationSpeed(parseFloat(field, beam.getRotationSpeed())); break; + case 206: + beam.setLightningDensity(parseFloat(field, beam.getLightningDensity())); + break; + case 207: + beam.setLightningRadius(parseFloat(field, beam.getLightningRadius())); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java index 47385f7cb..f83e93baf 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java @@ -111,6 +111,28 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, laser.getOuterColorWidth()); widthField.setEnabled(laser.isOuterColorEnabled()); addTextField(widthField); + + y += 24; + + // Row 3: Lightning Effect Enabled + addLabel(new GuiNpcLabel(204, "ability.lightning", labelX, y + 5)); + addButton(new GuiNpcButton(204, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, laser.hasLightningEffect() ? 1 : 0)); + + // Only show Lightning settings if Lightning is enabled + if (laser.hasLightningEffect()) { + y += 24; + + // Row 4: Density + Radius + addLabel(new GuiNpcLabel(205, "ability.lightningDensity", labelX, y + 5)); + GuiNpcTextField densityField = new GuiNpcTextField(205, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(laser.getLightningDensity())); + densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); + addTextField(densityField); + + addLabel(new GuiNpcLabel(206, "ability.lightningRadius", col2LabelX, y + 5)); + GuiNpcTextField radiusField = new GuiNpcTextField(206, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(laser.getLightningRadius())); + radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); + addTextField(radiusField); + } } @Override @@ -139,6 +161,10 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { laser.setOuterColorEnabled(value == 1); initGui(); break; + case 204: + laser.setLightningEffect(value == 1); + initGui(); + break; } } @@ -165,6 +191,12 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { case 203: laser.setOuterColorWidth(parseFloat(field, laser.getOuterColorWidth())); break; + case 205: + laser.setLightningDensity(parseFloat(field, laser.getLightningDensity())); + break; + case 206: + laser.setLightningRadius(parseFloat(field, laser.getLightningRadius())); + break; } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java index 9f5ac4caf..42dd6a773 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -87,36 +87,58 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 180; int col2FieldX = guiLeft + 260; - // Row 1: Inner Color + Outer Color + // Row 1: Inner Color + Rotation Speed addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); String innerHex = String.format("%06X", orb.getInnerColor() & 0xFFFFFF); GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); innerColorBtn.setTextColor(orb.getInnerColor() & 0xFFFFFF); addButton(innerColorBtn); - addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); - String outerHex = String.format("%06X", orb.getOuterColor() & 0xFFFFFF); - GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); - outerColorBtn.setTextColor(orb.getOuterColor() & 0xFFFFFF); - outerColorBtn.setEnabled(orb.isOuterColorEnabled()); - addButton(outerColorBtn); + addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", col2LabelX, y + 5)); + addTextField(createFloatField(204, col2FieldX, y, 55, orb.getRotationSpeed())); y += 24; - // Row 2: Outer Color Enabled + Outer Color Width + // Row 2: Outer Glow Enabled addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isOuterColorEnabled() ? 1 : 0)); - addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); - GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, orb.getOuterColorWidth()); - widthField.setEnabled(orb.isOuterColorEnabled()); - addTextField(widthField); + // Only show Outer Color and Width if Outer Glow is enabled + if (orb.isOuterColorEnabled()) { + y += 24; + + // Row 3: Outer Color + Outer Width + addLabel(new GuiNpcLabel(201, "ability.outerColor", labelX, y + 5)); + String outerHex = String.format("%06X", orb.getOuterColor() & 0xFFFFFF); + GuiNpcButton outerColorBtn = new GuiNpcButton(201, fieldX, y, 55, 20, outerHex); + outerColorBtn.setTextColor(orb.getOuterColor() & 0xFFFFFF); + addButton(outerColorBtn); + + addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); + addTextField(createFloatField(203, col2FieldX, y, 55, orb.getOuterColorWidth())); + } y += 24; - // Row 3: Rotation Speed - addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", labelX, y + 5)); - addTextField(createFloatField(204, fieldX, y, 50, orb.getRotationSpeed())); + // Row 4: Lightning Effect Enabled + addLabel(new GuiNpcLabel(205, "ability.lightning", labelX, y + 5)); + addButton(new GuiNpcButton(205, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.hasLightningEffect() ? 1 : 0)); + + // Only show Lightning settings if Lightning is enabled + if (orb.hasLightningEffect()) { + y += 24; + + // Row 5: Density + Radius + addLabel(new GuiNpcLabel(206, "ability.lightningDensity", labelX, y + 5)); + GuiNpcTextField densityField = new GuiNpcTextField(206, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(orb.getLightningDensity())); + densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); + addTextField(densityField); + + addLabel(new GuiNpcLabel(207, "ability.lightningRadius", col2LabelX, y + 5)); + GuiNpcTextField radiusField = new GuiNpcTextField(207, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(orb.getLightningRadius())); + radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); + addTextField(radiusField); + } } @Override @@ -148,6 +170,10 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { orb.setOuterColorEnabled(value == 1); initGui(); break; + case 205: + orb.setLightningEffect(value == 1); + initGui(); + break; } } @@ -207,6 +233,12 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { case 204: orb.setRotationSpeed(parseFloat(field, orb.getRotationSpeed())); break; + case 206: + orb.setLightningDensity(parseFloat(field, orb.getLightningDensity())); + break; + case 207: + orb.setLightningRadius(parseFloat(field, orb.getLightningRadius())); + break; } } } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index f3bba0c1d..b7e33a029 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1686,6 +1686,9 @@ ability.innerColor=Inner Color ability.outerColor=Outer Color ability.outerEnabled=Outer Glow ability.outerWidth=Outer Width +ability.lightning=Enable Lightning +ability.lightningDensity=Density +ability.lightningRadius=Radius ability.noVisualSettings=No visual settings for this ability type # Laser Shot Settings From d6d29613e8c1d2f6381b8833e3db042ecee6ad21 Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 26 Jan 2026 19:03:48 -0500 Subject: [PATCH 286/358] Lock View Rotation --- .../client/renderer/RenderAbilityBeam.java | 6 +- .../renderer/quadric/QuadricRenderer.java | 138 ------------------ .../renderer/quadric/SphereRenderer.java | 37 ----- .../npcs/entity/EntityAbilityBeam.java | 12 +- src/main/java/noppes/npcs/DataAbilities.java | 83 ++++++++++- .../npcs/entity/EntityNPCInterface.java | 3 + 6 files changed, 90 insertions(+), 189 deletions(-) delete mode 100644 src/main/java/kamkeel/npcs/client/renderer/quadric/QuadricRenderer.java delete mode 100644 src/main/java/kamkeel/npcs/client/renderer/quadric/SphereRenderer.java diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java index 554b07e31..2fcb58d54 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java @@ -68,8 +68,12 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { GL11.glPushMatrix(); GL11.glTranslated(renderOriginX, renderOriginY, renderOriginZ); + // Calculate head distance from origin (for overlap check) + double headDistFromOrigin = Math.sqrt(headOffsetX * headOffsetX + headOffsetY * headOffsetY + headOffsetZ * headOffsetZ); + // Render tail orb at origin (0,0,0) - only in anchored mode - if (beam.shouldRenderTailOrb()) { + // Don't render if head is too close to origin (would cause overlap/extra orb appearance) + if (beam.shouldRenderTailOrb() && headDistFromOrigin > headSize * 0.5) { renderTailOrb(headSize * 0.8f, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth()); } diff --git a/src/main/java/kamkeel/npcs/client/renderer/quadric/QuadricRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/quadric/QuadricRenderer.java deleted file mode 100644 index 13c994adc..000000000 --- a/src/main/java/kamkeel/npcs/client/renderer/quadric/QuadricRenderer.java +++ /dev/null @@ -1,138 +0,0 @@ -package kamkeel.npcs.client.renderer.quadric; - -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; -import net.minecraft.client.renderer.GLAllocation; -import org.lwjgl.opengl.GL11; - -/** - * Base class for rendering GLU quadric shapes (spheres, cylinders, etc.) - * Uses display lists for efficient rendering. - * - * Design inspired by LouisXIV's quadric rendering system. - */ -@SideOnly(Side.CLIENT) -public abstract class QuadricRenderer { - protected int displayList; - private boolean compiled; - - public final float radius; - public final int slices; - public final int stacks; - - // Current render state - protected float scaleX = 1.0f; - protected float scaleY = 1.0f; - protected float scaleZ = 1.0f; - - protected float rotationX = 0.0f; - protected float rotationY = 0.0f; - protected float rotationZ = 0.0f; - - protected int color = 0xFFFFFF; - protected float alpha = 1.0f; - - public QuadricRenderer(float radius, int slices, int stacks) { - this.radius = radius; - this.slices = slices; - this.stacks = stacks; - } - - public QuadricRenderer(float radius, int resolution) { - this(radius, resolution, resolution); - } - - public QuadricRenderer() { - this(0.5f, 16); - } - - /** - * Render the quadric shape with current transform and color settings. - */ - public void render(float partialTicks) { - if (!compiled) { - displayList = GLAllocation.generateDisplayLists(1); - GL11.glNewList(displayList, GL11.GL_COMPILE); - compile(); - GL11.glEndList(); - compiled = true; - } - - GL11.glPushMatrix(); - - // Apply scale - GL11.glScalef(scaleX, scaleY, scaleZ); - - // Apply rotation - GL11.glRotatef(rotationX, 1.0f, 0.0f, 0.0f); - GL11.glRotatef(rotationY, 0.0f, 1.0f, 0.0f); - GL11.glRotatef(rotationZ, 0.0f, 0.0f, 1.0f); - - // Apply color - float r = ((color >> 16) & 0xFF) / 255.0f; - float g = ((color >> 8) & 0xFF) / 255.0f; - float b = (color & 0xFF) / 255.0f; - GL11.glColor4f(r, g, b, alpha); - - GL11.glCallList(displayList); - - GL11.glPopMatrix(); - } - - /** - * Compile the quadric geometry into the display list. - * Called once, then cached. - */ - protected abstract void compile(); - - // Setters for render state - public void setScale(float scale) { - this.scaleX = this.scaleY = this.scaleZ = scale; - } - - public void setScale(float x, float y, float z) { - this.scaleX = x; - this.scaleY = y; - this.scaleZ = z; - } - - public void setRotation(float x, float y, float z) { - this.rotationX = x; - this.rotationY = y; - this.rotationZ = z; - } - - public void setColor(int color) { - this.color = color; - } - - public void setAlpha(float alpha) { - this.alpha = alpha; - } - - /** - * Linear interpolation helper. - */ - public static float lerp(float current, float target, float factor) { - return current + (target - current) * factor; - } - - /** - * Color interpolation helper. - */ - public static int lerpColor(int from, int to, float factor) { - int r1 = (from >> 16) & 0xFF; - int g1 = (from >> 8) & 0xFF; - int b1 = from & 0xFF; - - int r2 = (to >> 16) & 0xFF; - int g2 = (to >> 8) & 0xFF; - int b2 = to & 0xFF; - - int r = (int) (r1 + (r2 - r1) * factor); - int g = (int) (g1 + (g2 - g1) * factor); - int b = (int) (b1 + (b2 - b1) * factor); - - return (r << 16) | (g << 8) | b; - } -} diff --git a/src/main/java/kamkeel/npcs/client/renderer/quadric/SphereRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/quadric/SphereRenderer.java deleted file mode 100644 index dc72779a8..000000000 --- a/src/main/java/kamkeel/npcs/client/renderer/quadric/SphereRenderer.java +++ /dev/null @@ -1,37 +0,0 @@ -package kamkeel.npcs.client.renderer.quadric; - -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; -import org.lwjgl.util.glu.GLU; -import org.lwjgl.util.glu.Sphere; - -/** - * Renders a sphere using GLU quadrics. - * - * Design inspired by LouisXIV's quadric rendering system. - */ -@SideOnly(Side.CLIENT) -public class SphereRenderer extends QuadricRenderer { - - public SphereRenderer(float radius, int slices, int stacks) { - super(radius, slices, stacks); - } - - public SphereRenderer(float radius, int resolution) { - super(radius, resolution); - } - - public SphereRenderer() { - super(); - } - - @Override - protected void compile() { - Sphere sphere = new Sphere(); - sphere.setTextureFlag(true); // Enable texture coordinates - sphere.setDrawStyle(GLU.GLU_FILL); - sphere.setNormals(GLU.GLU_NONE); // No normals for self-illuminated rendering - sphere.setOrientation(GLU.GLU_OUTSIDE); - sphere.draw(radius, slices, stacks); - } -} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index ab62c4ca9..34030ab3f 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -360,18 +360,10 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { // Client: interpolate entity position handleClientInterpolation(); - // Client: update origin if attached to owner (mirrors server logic) - if (attachedToOwner) { - Entity owner = getOwner(); - if (owner != null) { - startX = owner.posX; - startY = owner.posY + owner.getEyeHeight() * 0.7; - startZ = owner.posZ; - } - } - // Client: derive head offset from entity position and origin // Entity position = head world position, so headOffset = pos - origin + // Note: During charging, updateCharging() handles origin positioning + // After firing, origin is fixed at the charged position headOffsetX = posX - startX; headOffsetY = posY - startY; headOffsetZ = posZ - startZ; diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 8875508b1..246ebed46 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -78,6 +78,16 @@ public class DataAbilities { */ private transient List recentHitTimes = new ArrayList<>(); + /** + * Locked rotation values for ACTIVE phase with lockMovement. + * Stored when entering ACTIVE phase, applied every tick to prevent rotation changes. + */ + private transient boolean rotationLocked = false; + private transient float lockedYaw = 0; + private transient float lockedYawHead = 0; + private transient float lockedRenderYawOffset = 0; + private transient float lockedPitch = 0; + // ═══════════════════════════════════════════════════════════════════ // CONSTRUCTOR // ═══════════════════════════════════════════════════════════════════ @@ -136,6 +146,12 @@ public class DataAbilities { // Remove telegraph - it has served its purpose removeTelegraph(currentAbility); + // Lock rotation if lockMovement is true + // Capture current rotation values to force them every tick + if (currentAbility.isLockMovement()) { + captureLockedRotation(); + } + // Play active sound and animation playAbilitySound(currentAbility.getActiveSound()); playAbilityAnimation(currentAbility.getActiveAnimationId()); @@ -186,9 +202,9 @@ public class DataAbilities { } } - // Lock look direction at target during ACTIVE phase only - if (shouldLockLookDirection() && target != null && target.isEntityAlive()) { - npc.getLookHelper().setLookPositionWithEntity(target, 30.0F, 30.0F); + // Unlock rotation when leaving ACTIVE phase + if (rotationLocked && currentAbility.getPhase() != AbilityPhase.ACTIVE) { + releaseLockedRotation(); } } } @@ -202,6 +218,9 @@ public class DataAbilities { // Stop any ability animation stopAbilityAnimation(); + // Release rotation lock + releaseLockedRotation(); + // Calculate cooldown: random(min, max) + ability offset int baseCooldown = minCooldown; if (maxCooldown > minCooldown) { @@ -526,6 +545,9 @@ public class DataAbilities { // Stop any ability animation stopAbilityAnimation(); + // Release rotation lock + releaseLockedRotation(); + // Fire interrupt event AbilityEvent.InterruptEvent interruptEvent = new AbilityEvent.InterruptEvent( npc.wrappedNPC, currentAbility, lastTarget, source, damage); @@ -546,6 +568,7 @@ public class DataAbilities { if (currentAbility != null) { removeTelegraph(currentAbility); stopAbilityAnimation(); + releaseLockedRotation(); currentAbility.interrupt(); currentAbility = null; lastTarget = null; @@ -737,6 +760,60 @@ public class DataAbilities { return phase == AbilityPhase.WINDUP || phase == AbilityPhase.ACTIVE; } + // ═══════════════════════════════════════════════════════════════════ + // ROTATION LOCKING + // ═══════════════════════════════════════════════════════════════════ + + /** + * Capture current rotation values to lock NPC's look direction. + * Called when entering ACTIVE phase with lockMovement enabled. + */ + private void captureLockedRotation() { + lockedYaw = npc.rotationYaw; + lockedYawHead = npc.rotationYawHead; + lockedRenderYawOffset = npc.renderYawOffset; + lockedPitch = npc.rotationPitch; + rotationLocked = true; + } + + /** + * Release the rotation lock. + * Called when leaving ACTIVE phase or ability completes. + */ + private void releaseLockedRotation() { + rotationLocked = false; + } + + /** + * Apply locked rotation values to the NPC. + * Called AFTER super.onLivingUpdate() in EntityNPCInterface to override + * any rotation changes made by the look helper or AI tasks. + */ + public void applyLockedRotation() { + if (!rotationLocked) { + return; + } + + // Force rotation back to locked values + npc.rotationYaw = lockedYaw; + npc.rotationYawHead = lockedYawHead; + npc.renderYawOffset = lockedRenderYawOffset; + npc.rotationPitch = lockedPitch; + + // Also set prev values to prevent interpolation jitter + npc.prevRotationYaw = lockedYaw; + npc.prevRotationYawHead = lockedYawHead; + npc.prevRenderYawOffset = lockedRenderYawOffset; + npc.prevRotationPitch = lockedPitch; + } + + /** + * Check if rotation is currently locked. + */ + public boolean isRotationLocked() { + return rotationLocked; + } + // ═══════════════════════════════════════════════════════════════════ // NBT SERIALIZATION // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index 20d2f6d5c..ae292d59d 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -481,6 +481,9 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti super.onLivingUpdate(); + // Apply locked rotation after super.onLivingUpdate() to override look helper changes + abilities.applyLockedRotation(); + handleMountRiderState(); if (worldObj.isRemote) { From 563b6f7a26ab73bf73b6af9ef2faf0883040c76c Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 26 Jan 2026 22:02:51 -0500 Subject: [PATCH 287/358] Ability Effects --- .../controllers/data/ability/Ability.java | 63 +++++- .../data/ability/AbilityEffect.java | 146 +++++++++++++ .../data/ability/type/AbilityCutter.java | 45 +--- .../data/ability/type/AbilityHeavyHit.java | 46 +--- .../data/ability/type/AbilityShockwave.java | 20 +- .../data/ability/type/AbilityTeleport.java | 17 +- .../data/ability/type/AbilityTrap.java | 87 +------- .../data/ability/type/AbilityVortex.java | 54 ++--- .../advanced/ability/SubGuiAbilityCharge.java | 27 ++- .../advanced/ability/SubGuiAbilityCutter.java | 42 ++-- .../advanced/ability/SubGuiAbilityDisc.java | 17 ++ .../ability/SubGuiAbilityEffects.java | 197 ++++++++++++++++++ .../ability/SubGuiAbilityEnergyBeam.java | 17 ++ .../advanced/ability/SubGuiAbilityGuard.java | 26 +++ .../advanced/ability/SubGuiAbilityHazard.java | 24 +++ .../ability/SubGuiAbilityHeavyHit.java | 50 +++-- .../ability/SubGuiAbilityLaserShot.java | 17 ++ .../advanced/ability/SubGuiAbilityOrb.java | 17 ++ .../ability/SubGuiAbilityProjectile.java | 24 +++ .../ability/SubGuiAbilityShockwave.java | 36 +++- .../advanced/ability/SubGuiAbilitySlam.java | 28 ++- .../ability/SubGuiAbilitySweeper.java | 17 ++ .../ability/SubGuiAbilityTeleport.java | 24 +++ .../advanced/ability/SubGuiAbilityTrap.java | 37 ++-- .../advanced/ability/SubGuiAbilityVortex.java | 33 +-- .../assets/customnpcs/lang/en_US.lang | 12 +- 26 files changed, 799 insertions(+), 324 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEffects.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 3f2165718..d7a8b9eab 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -78,6 +78,9 @@ public abstract class Ability implements IAbility { // Custom data for external mods protected NBTTagCompound customData = new NBTTagCompound(); + // Configurable potion effects + protected List effects = new ArrayList<>(); + // ═══════════════════════════════════════════════════════════════════ // EXECUTION STATE (not saved, reset each combat) // ═══════════════════════════════════════════════════════════════════ @@ -224,6 +227,21 @@ public abstract class Ability implements IAbility { hitEntity.velocityChanged = true; } + /** + * Applies all configured effects to the given entity. + * Call this after damage is applied to the target. + * + * @param entity The entity to apply effects to + */ + protected void applyEffects(EntityLivingBase entity) { + if (entity == null || effects.isEmpty()) { + return; + } + for (AbilityEffect effect : effects) { + effect.apply(entity); + } + } + public float getTelegraphRadius() { return 5.0f; } @@ -662,6 +680,13 @@ public abstract class Ability implements IAbility { for (Condition c : conditions) condList.appendTag(c.writeNBT()); nbt.setTag("conditions", condList); + // Effects + NBTTagList effectList = new NBTTagList(); + for (AbilityEffect effect : effects) { + effectList.appendTag(effect.writeNBT()); + } + nbt.setTag("effects", effectList); + // Type-specific NBTTagCompound typeNBT = new NBTTagCompound(); writeTypeNBT(typeNBT); @@ -735,6 +760,18 @@ public abstract class Ability implements IAbility { if (c != null) conditions.add(c); } + // Effects + effects.clear(); + if (nbt.hasKey("effects")) { + NBTTagList effectList = nbt.getTagList("effects", 10); + for (int i = 0; i < effectList.tagCount(); i++) { + AbilityEffect effect = AbilityEffect.fromNBT(effectList.getCompoundTagAt(i)); + if (effect != null && effect.isValid()) { + effects.add(effect); + } + } + } + readTypeNBT(nbt.getCompoundTag("typeData")); } @@ -807,7 +844,7 @@ public abstract class Ability implements IAbility { } public void setCooldownTicks(int cooldownTicks) { - this.cooldownTicks = cooldownTicks; + this.cooldownTicks = Math.max(0, cooldownTicks); } public int getWindUpTicks() { @@ -815,7 +852,7 @@ public abstract class Ability implements IAbility { } public void setWindUpTicks(int windUpTicks) { - this.windUpTicks = windUpTicks; + this.windUpTicks = Math.max(0, windUpTicks); } public int getActiveTicks() { @@ -823,7 +860,7 @@ public abstract class Ability implements IAbility { } public void setActiveTicks(int activeTicks) { - this.activeTicks = activeTicks; + this.activeTicks = Math.max(1, activeTicks); } public int getRecoveryTicks() { @@ -831,7 +868,7 @@ public abstract class Ability implements IAbility { } public void setRecoveryTicks(int recoveryTicks) { - this.recoveryTicks = recoveryTicks; + this.recoveryTicks = Math.max(0, recoveryTicks); } public boolean isInterruptible() { @@ -963,6 +1000,24 @@ public abstract class Ability implements IAbility { conditions.add(c); } + public List getEffects() { + return effects; + } + + public void setEffects(List effects) { + this.effects = effects != null ? effects : new ArrayList<>(); + } + + public void addEffect(AbilityEffect effect) { + if (effect != null && effect.isValid()) { + effects.add(effect); + } + } + + public void clearEffects() { + effects.clear(); + } + // ═══════════════════════════════════════════════════════════════════ // IAbility API METHODS // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java new file mode 100644 index 000000000..47e03f10d --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java @@ -0,0 +1,146 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.potion.Potion; +import net.minecraft.potion.PotionEffect; + +/** + * Represents a configurable potion effect that can be applied by abilities. + * Provides a unified system for managing debuffs like slowness, weakness, poison, etc. + */ +public class AbilityEffect { + + public enum EffectType { + NONE("gui.none", -1), + SLOWNESS("potion.moveSlowdown", Potion.moveSlowdown.id), + WEAKNESS("potion.weakness", Potion.weakness.id), + POISON("potion.poison", Potion.poison.id), + WITHER("potion.wither", Potion.wither.id), + BLINDNESS("potion.blindness", Potion.blindness.id), + NAUSEA("potion.confusion", Potion.confusion.id), + HUNGER("potion.hunger", Potion.hunger.id), + MINING_FATIGUE("potion.digSlowDown", Potion.digSlowdown.id); + + private final String langKey; + private final int potionId; + + EffectType(String langKey, int potionId) { + this.langKey = langKey; + this.potionId = potionId; + } + + public String getLangKey() { + return langKey; + } + + public int getPotionId() { + return potionId; + } + + public static EffectType fromOrdinal(int ordinal) { + EffectType[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return NONE; + } + } + + private EffectType type = EffectType.NONE; + private int durationTicks = 60; + private int amplifier = 0; // 0-10 + + public AbilityEffect() { + } + + public AbilityEffect(EffectType type, int durationTicks, int amplifier) { + this.type = type; + this.durationTicks = Math.max(1, durationTicks); + this.amplifier = Math.max(0, Math.min(10, amplifier)); + } + + /** + * Creates a copy of this effect. + */ + public AbilityEffect copy() { + return new AbilityEffect(type, durationTicks, amplifier); + } + + /** + * Applies this effect to the given entity. + * Does nothing if type is NONE. + */ + public void apply(EntityLivingBase entity) { + if (entity == null || type == EffectType.NONE) { + return; + } + + int potionId = type.getPotionId(); + if (potionId >= 0) { + entity.addPotionEffect(new PotionEffect(potionId, durationTicks, amplifier)); + } + } + + /** + * Writes this effect to NBT. + */ + public NBTTagCompound writeNBT() { + NBTTagCompound nbt = new NBTTagCompound(); + nbt.setInteger("type", type.ordinal()); + nbt.setInteger("duration", durationTicks); + nbt.setInteger("amplifier", amplifier); + return nbt; + } + + /** + * Reads this effect from NBT. + */ + public void readNBT(NBTTagCompound nbt) { + this.type = EffectType.fromOrdinal(nbt.getInteger("type")); + this.durationTicks = nbt.hasKey("duration") ? nbt.getInteger("duration") : 60; + this.amplifier = nbt.hasKey("amplifier") ? nbt.getInteger("amplifier") : 0; + } + + /** + * Creates an AbilityEffect from NBT. + */ + public static AbilityEffect fromNBT(NBTTagCompound nbt) { + AbilityEffect effect = new AbilityEffect(); + effect.readNBT(nbt); + return effect; + } + + // Getters & Setters + + public EffectType getType() { + return type; + } + + public void setType(EffectType type) { + this.type = type != null ? type : EffectType.NONE; + } + + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + + public int getAmplifier() { + return amplifier; + } + + public void setAmplifier(int amplifier) { + this.amplifier = Math.max(0, Math.min(10, amplifier)); + } + + /** + * Returns true if this effect is configured (not NONE). + */ + public boolean isValid() { + return type != EffectType.NONE; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index 611d0a4a6..8274b4d79 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -7,8 +7,6 @@ import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; @@ -41,9 +39,6 @@ public class AbilityCutter extends Ability { private SweepMode sweepMode = SweepMode.SWIPE; private float sweepSpeed = 6.0f; - private int stunDuration = 0; - private int poisonDurationSeconds = 0; - private int poisonLevel = 0; private boolean piercing = true; private float innerRadius = 0.0f; @@ -161,15 +156,9 @@ public class AbilityCutter extends Ability { // Apply damage with scripted event support boolean wasHit = applyAbilityDamage(npc, entity, actualDamage, knockback); - // Only apply effects if hit wasn't cancelled + // Apply effects if hit wasn't cancelled if (wasHit) { - if (stunDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); - entity.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); - } - if (poisonDurationSeconds > 0) { - entity.addPotionEffect(new PotionEffect(Potion.poison.id, poisonDurationSeconds * 20, poisonLevel)); - } + applyEffects(entity); } } } @@ -206,9 +195,6 @@ public class AbilityCutter extends Ability { nbt.setFloat("knockback", knockback); nbt.setString("sweepMode", sweepMode.name()); nbt.setFloat("sweepSpeed", sweepSpeed); - nbt.setInteger("stunDuration", stunDuration); - nbt.setInteger("poisonDurationSeconds", poisonDurationSeconds); - nbt.setInteger("poisonLevel", poisonLevel); nbt.setBoolean("piercing", piercing); nbt.setFloat("innerRadius", innerRadius); } @@ -225,9 +211,6 @@ public class AbilityCutter extends Ability { this.sweepMode = SweepMode.SWIPE; } this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 6.0f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; - this.poisonDurationSeconds = nbt.hasKey("poisonDurationSeconds") ? nbt.getInteger("poisonDurationSeconds") : 0; - this.poisonLevel = nbt.hasKey("poisonLevel") ? nbt.getInteger("poisonLevel") : 0; this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing"); this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f; } @@ -281,30 +264,6 @@ public class AbilityCutter extends Ability { this.sweepSpeed = sweepSpeed; } - public int getStunDuration() { - return stunDuration; - } - - public void setStunDuration(int stunDuration) { - this.stunDuration = stunDuration; - } - - public int getPoisonDurationSeconds() { - return poisonDurationSeconds; - } - - public void setPoisonDurationSeconds(int poisonDurationSeconds) { - this.poisonDurationSeconds = poisonDurationSeconds; - } - - public int getPoisonLevel() { - return poisonLevel; - } - - public void setPoisonLevel(int poisonLevel) { - this.poisonLevel = poisonLevel; - } - public boolean isPiercing() { return piercing; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index abdcc6f86..fc68200b7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -7,8 +7,6 @@ import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeavyHit; @@ -23,9 +21,6 @@ public class AbilityHeavyHit extends Ability { private float damage = 8.0f; private float knockback = 2.0f; - private int slownessLevel = 1; - private int weaknessLevel = 0; - private int potionDurationSeconds = 2; public AbilityHeavyHit() { this.typeId = "ability.cnpc.heavy_hit"; @@ -73,14 +68,9 @@ public class AbilityHeavyHit extends Ability { // Apply damage with scripted event support boolean wasHit = applyAbilityDamage(npc, target, damage, knockback); - if (wasHit && potionDurationSeconds > 0) { - int durationTicks = potionDurationSeconds * 20; - if (slownessLevel >= 0) { - target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, durationTicks, slownessLevel)); - } - if (weaknessLevel >= 0) { - target.addPotionEffect(new PotionEffect(Potion.weakness.id, durationTicks, weaknessLevel)); - } + // Apply effects if hit wasn't cancelled + if (wasHit) { + applyEffects(target); } } @@ -93,18 +83,12 @@ public class AbilityHeavyHit extends Ability { public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("damage", damage); nbt.setFloat("knockback", knockback); - nbt.setInteger("slownessLevel", slownessLevel); - nbt.setInteger("weaknessLevel", weaknessLevel); - nbt.setInteger("potionDurationSeconds", potionDurationSeconds); } @Override public void readTypeNBT(NBTTagCompound nbt) { this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 2.0f; - this.slownessLevel = nbt.hasKey("slownessLevel") ? nbt.getInteger("slownessLevel") : 1; - this.weaknessLevel = nbt.hasKey("weaknessLevel") ? nbt.getInteger("weaknessLevel") : 0; - this.potionDurationSeconds = nbt.hasKey("potionDurationSeconds") ? nbt.getInteger("potionDurationSeconds") : 2; } // Getters & Setters @@ -123,28 +107,4 @@ public class AbilityHeavyHit extends Ability { public void setKnockback(float knockback) { this.knockback = knockback; } - - public int getSlownessLevel() { - return slownessLevel; - } - - public void setSlownessLevel(int slownessLevel) { - this.slownessLevel = slownessLevel; - } - - public int getWeaknessLevel() { - return weaknessLevel; - } - - public void setWeaknessLevel(int weaknessLevel) { - this.weaknessLevel = weaknessLevel; - } - - public int getPotionDurationSeconds() { - return potionDurationSeconds; - } - - public void setPotionDurationSeconds(int potionDurationSeconds) { - this.potionDurationSeconds = potionDurationSeconds; - } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index 1708b49bd..b5a35831f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -7,8 +7,6 @@ import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; @@ -28,7 +26,6 @@ public class AbilityShockwave extends Ability { private float pushRadius = 8.0f; private float pushStrength = 1.5f; private float damage = 8.0f; - private int stunDuration = 0; private int maxTargets = 10; // Runtime state @@ -122,10 +119,9 @@ public class AbilityShockwave extends Ability { // Apply damage with custom knockback direction boolean wasHit = applyAbilityDamageWithDirection(npc, entity, damage * distFactor, finalPush, dx, dz); - // Apply stun if hit connected - if (wasHit && stunDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); - entity.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); + // Apply effects if hit connected + if (wasHit) { + applyEffects(entity); } } } @@ -145,7 +141,6 @@ public class AbilityShockwave extends Ability { nbt.setFloat("pushRadius", pushRadius); nbt.setFloat("pushStrength", pushStrength); nbt.setFloat("damage", damage); - nbt.setInteger("stunDuration", stunDuration); nbt.setInteger("maxTargets", maxTargets); } @@ -154,7 +149,6 @@ public class AbilityShockwave extends Ability { this.pushRadius = nbt.hasKey("pushRadius") ? nbt.getFloat("pushRadius") : 8.0f; this.pushStrength = nbt.hasKey("pushStrength") ? nbt.getFloat("pushStrength") : 1.5f; this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; this.maxTargets = nbt.hasKey("maxTargets") ? nbt.getInteger("maxTargets") : 10; } @@ -183,14 +177,6 @@ public class AbilityShockwave extends Ability { this.damage = damage; } - public int getStunDuration() { - return stunDuration; - } - - public void setStunDuration(int stunDuration) { - this.stunDuration = stunDuration; - } - public int getMaxTargets() { return maxTargets; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index ad5989f25..a79223d5d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -261,8 +261,13 @@ public class AbilityTeleport extends Ability { int blockY = MathHelper.floor_double(checkY); int blockZ = MathHelper.floor_double(checkZ); + // Skip out-of-world coordinates + if (blockY < 0 || blockY >= 256) { + continue; + } + Block block = world.getBlock(blockX, blockY, blockZ); - if (block.getMaterial().isSolid() && block.isOpaqueCube()) { + if (block != null && block.getMaterial().isSolid() && block.isOpaqueCube()) { return false; } } @@ -327,10 +332,20 @@ public class AbilityTeleport extends Ability { } private boolean isSafeLocation(World world, int x, int y, int z) { + // Bounds check - ensure we're within valid world coordinates + if (y < 1 || y >= 255) { + return false; + } + Block groundBlock = world.getBlock(x, y - 1, z); Block feetBlock = world.getBlock(x, y, z); Block headBlock = world.getBlock(x, y + 1, z); + // Defensive null checks (shouldn't happen in MC 1.7.10, but safe) + if (groundBlock == null || feetBlock == null || headBlock == null) { + return false; + } + return groundBlock.getMaterial().isSolid() && !feetBlock.getMaterial().isSolid() && !headBlock.getMaterial().isSolid(); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index fa2fefb4c..77ac4a5fe 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -8,8 +8,6 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; @@ -45,12 +43,6 @@ public class AbilityTrap extends Ability { private float damage = 6.0f; private float damageRadius = 0.0f; private float knockback = 0.5f; - private int stunDuration = 0; - private int rootDuration = 40; - private int slowDuration = 0; - private int slowLevel = 1; - private int poisonDuration = 0; - private int poisonLevel = 0; private boolean visible = true; // Offset parameters - trap spawns near target, not exactly on them @@ -262,24 +254,9 @@ public class AbilityTrap extends Ability { // Apply damage with scripted event support boolean wasHit = applyAbilityDamage(npc, entity, damage, knockback); - // Only apply effects if the hit wasn't cancelled + // Apply effects if the hit wasn't cancelled if (wasHit) { - if (stunDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); - entity.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); - } - - if (rootDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, rootDuration, 127)); - } - - if (slowDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, slowDuration, slowLevel)); - } - - if (poisonDuration > 0 && poisonLevel >= 0) { - entity.addPotionEffect(new PotionEffect(Potion.poison.id, poisonDuration, poisonLevel)); - } + applyEffects(entity); } } } @@ -309,12 +286,6 @@ public class AbilityTrap extends Ability { nbt.setFloat("damage", damage); nbt.setFloat("damageRadius", damageRadius); nbt.setFloat("knockback", knockback); - nbt.setInteger("stunDuration", stunDuration); - nbt.setInteger("rootDuration", rootDuration); - nbt.setInteger("slowDuration", slowDuration); - nbt.setInteger("slowLevel", slowLevel); - nbt.setInteger("poisonDuration", poisonDuration); - nbt.setInteger("poisonLevel", poisonLevel); nbt.setBoolean("visible", visible); nbt.setFloat("minOffset", minOffset); nbt.setFloat("maxOffset", maxOffset); @@ -336,12 +307,6 @@ public class AbilityTrap extends Ability { this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 6.0f; this.damageRadius = nbt.hasKey("damageRadius") ? nbt.getFloat("damageRadius") : 0.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; - this.rootDuration = nbt.hasKey("rootDuration") ? nbt.getInteger("rootDuration") : 40; - this.slowDuration = nbt.hasKey("slowDuration") ? nbt.getInteger("slowDuration") : 0; - this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 1; - this.poisonDuration = nbt.hasKey("poisonDuration") ? nbt.getInteger("poisonDuration") : 0; - this.poisonLevel = nbt.hasKey("poisonLevel") ? nbt.getInteger("poisonLevel") : 0; this.visible = !nbt.hasKey("visible") || nbt.getBoolean("visible"); this.minOffset = nbt.hasKey("minOffset") ? nbt.getFloat("minOffset") : 0.0f; this.maxOffset = nbt.hasKey("maxOffset") ? nbt.getFloat("maxOffset") : 1.5f; @@ -421,54 +386,6 @@ public class AbilityTrap extends Ability { this.knockback = knockback; } - public int getStunDuration() { - return stunDuration; - } - - public void setStunDuration(int stunDuration) { - this.stunDuration = stunDuration; - } - - public int getRootDuration() { - return rootDuration; - } - - public void setRootDuration(int rootDuration) { - this.rootDuration = rootDuration; - } - - public int getSlowDuration() { - return slowDuration; - } - - public void setSlowDuration(int slowDuration) { - this.slowDuration = slowDuration; - } - - public int getSlowLevel() { - return slowLevel; - } - - public void setSlowLevel(int slowLevel) { - this.slowLevel = slowLevel; - } - - public int getPoisonDuration() { - return poisonDuration; - } - - public void setPoisonDuration(int poisonDuration) { - this.poisonDuration = poisonDuration; - } - - public int getPoisonLevel() { - return poisonLevel; - } - - public void setPoisonLevel(int poisonLevel) { - this.poisonLevel = poisonLevel; - } - public boolean isVisible() { return visible; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index f0e08dd56..666b19bbc 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -7,8 +7,6 @@ import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; @@ -32,8 +30,6 @@ public class AbilityVortex extends Ability { private float pullStrength = 0.8f; private float damage = 0.0f; private float knockback = 0.0f; - private int stunDuration = 0; - private int rootDuration = 0; private boolean aoe = false; private int maxTargets = 5; private boolean damageOnPull = false; @@ -134,7 +130,7 @@ public class AbilityVortex extends Ability { ticksSincePullDamage++; for (UUID uuid : new HashSet<>(pulledEntities)) { - EntityLivingBase entity = findEntity(world, uuid); + EntityLivingBase entity = findEntity(npc, world, uuid); if (entity == null || entity.isDead) { pulledEntities.remove(uuid); continue; @@ -187,25 +183,23 @@ public class AbilityVortex extends Ability { // Only apply effects if hit wasn't cancelled if (wasHit) { - if (stunDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); - entity.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); - } - - if (rootDuration > 0) { - entity.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, rootDuration, 127)); - } + applyEffects(entity); } } + /** + * Find an entity by UUID within the vortex pull area. + * Uses AABB search instead of iterating all loaded entities. + */ @SuppressWarnings("unchecked") - private EntityLivingBase findEntity(World world, UUID uuid) { - for (Object obj : world.loadedEntityList) { - if (obj instanceof EntityLivingBase) { - EntityLivingBase entity = (EntityLivingBase) obj; - if (entity.getUniqueID().equals(uuid)) { - return entity; - } + private EntityLivingBase findEntity(EntityNPCInterface npc, World world, UUID uuid) { + // Search within pull radius (entities being pulled should be within this area) + AxisAlignedBB searchBox = npc.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); + List nearbyEntities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); + + for (EntityLivingBase entity : nearbyEntities) { + if (entity.getUniqueID().equals(uuid)) { + return entity; } } return null; @@ -231,8 +225,6 @@ public class AbilityVortex extends Ability { nbt.setFloat("pullStrength", pullStrength); nbt.setFloat("damage", damage); nbt.setFloat("knockback", knockback); - nbt.setInteger("stunDuration", stunDuration); - nbt.setInteger("rootDuration", rootDuration); nbt.setBoolean("aoe", aoe); nbt.setInteger("maxTargets", maxTargets); nbt.setBoolean("damageOnPull", damageOnPull); @@ -245,8 +237,6 @@ public class AbilityVortex extends Ability { this.pullStrength = nbt.hasKey("pullStrength") ? nbt.getFloat("pullStrength") : 0.8f; this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 0.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.0f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; - this.rootDuration = nbt.hasKey("rootDuration") ? nbt.getInteger("rootDuration") : 0; this.aoe = nbt.hasKey("aoe") && nbt.getBoolean("aoe"); this.maxTargets = nbt.hasKey("maxTargets") ? nbt.getInteger("maxTargets") : 5; this.damageOnPull = nbt.hasKey("damageOnPull") && nbt.getBoolean("damageOnPull"); @@ -286,22 +276,6 @@ public class AbilityVortex extends Ability { this.knockback = knockback; } - public int getStunDuration() { - return stunDuration; - } - - public void setStunDuration(int stunDuration) { - this.stunDuration = stunDuration; - } - - public int getRootDuration() { - return rootDuration; - } - - public void setRootDuration(int rootDuration) { - this.rootDuration = rootDuration; - } - public boolean isAoe() { return aoe; } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java index 4d7486c5e..ea35797c4 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java @@ -1,10 +1,15 @@ package noppes.npcs.client.gui.advanced.ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.type.AbilityCharge; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.List; /** * GUI for configuring Charge ability type-specific settings. @@ -41,10 +46,30 @@ public class SubGuiAbilityCharge extends SubGuiAbilityConfig { y += 24; - // Row 3: Hit Width + // Row 3: Hit Width + Effects addLabel(new GuiNpcLabel(104, "ability.hitWidth", labelX, y + 5)); addTextField(createFloatField(104, fieldX, y, 50, charge.getHitWidth())); + addButton(new GuiNpcButton(150, col2LabelX, y, 80, 20, "ability.effects")); + } + + @Override + protected void handleTypeButton(int id, GuiNpcButton button) { + if (id == 150) { + setSubGui(new SubGuiAbilityEffects(charge.getEffects())); + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + super.subGuiClosed(subgui); + if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + charge.setEffects(result); + } + } } @Override diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java index 514391f8a..10f5ab8c1 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java @@ -1,11 +1,15 @@ package noppes.npcs.client.gui.advanced.ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.type.AbilityCutter; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.List; /** * GUI for configuring Cutter ability type-specific settings. @@ -64,18 +68,8 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { y += 24; - // Row 5: Stun Duration + Poison Time - addLabel(new GuiNpcLabel(108, "ability.stunDuration", labelX, y + 5)); - addTextField(createIntField(108, fieldX, y, 50, cutter.getStunDuration())); - - addLabel(new GuiNpcLabel(109, "ability.poisonTime", col2LabelX, y + 5)); - addTextField(createIntField(109, col2FieldX, y, 50, cutter.getPoisonDurationSeconds())); - - y += 24; - - // Row 6: Poison Level - addLabel(new GuiNpcLabel(110, "ability.poisonLvl", labelX, y + 5)); - addTextField(createIntField(110, fieldX, y, 50, cutter.getPoisonLevel())); + // Row 5: Effects button + addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); } @Override @@ -88,6 +82,21 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { case 107: cutter.setPiercing(value == 1); break; + case 150: + setSubGui(new SubGuiAbilityEffects(cutter.getEffects())); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + super.subGuiClosed(subgui); + if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + cutter.setEffects(result); + } } } @@ -112,15 +121,6 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { case 105: cutter.setSweepSpeed(parseFloat(field, cutter.getSweepSpeed())); break; - case 108: - cutter.setStunDuration(field.getInteger()); - break; - case 109: - cutter.setPoisonDurationSeconds(field.getInteger()); - break; - case 110: - cutter.setPoisonLevel(field.getInteger()); - break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java index 373752fa6..2a3439af1 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.advanced.ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.type.AbilityDisc; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -9,6 +10,8 @@ import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.util.SubGuiInterface; +import java.util.List; + /** * GUI for configuring Disc ability type-specific settings. */ @@ -86,6 +89,11 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { addLabel(new GuiNpcLabel(111, "ability.maxDist", col2LabelX, y + 5)); addTextField(createFloatField(111, col2FieldX, y, 50, disc.getMaxDistance())); + + y += 24; + + // Row 7: Effects button + addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); } @Override @@ -163,6 +171,9 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { case 108: disc.setExplosive(value == 1); break; + case 150: + setSubGui(new SubGuiAbilityEffects(disc.getEffects())); + break; } } @@ -201,6 +212,12 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { } editingVisualColorId = 0; initGui(); + } else if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + disc.setEffects(result); + } } else { super.subGuiClosed(subgui); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEffects.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEffects.java new file mode 100644 index 000000000..103d87224 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEffects.java @@ -0,0 +1,197 @@ +package noppes.npcs.client.gui.advanced.ability; + +import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.ITextfieldListener; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.ArrayList; +import java.util.List; + +/** + * SubGui for editing ability effects (potion effects that apply to targets). + * Allows adding/removing effects and configuring type, duration, and amplifier. + */ +public class SubGuiAbilityEffects extends SubGuiInterface implements ITextfieldListener { + + private static final int MAX_EFFECTS = 5; + + // Effect type names for display + private static final String[] EFFECT_TYPE_NAMES = { + "gui.none", + "potion.moveSlowdown", + "potion.weakness", + "potion.poison", + "potion.wither", + "potion.blindness", + "potion.confusion", + "potion.hunger", + "potion.digSlowDown" + }; + + // Working copy of effects + private List effects; + private boolean cancelled = false; + + public SubGuiAbilityEffects(List existingEffects) { + // Create working copies of effects + this.effects = new ArrayList<>(); + if (existingEffects != null) { + for (AbilityEffect effect : existingEffects) { + this.effects.add(effect.copy()); + } + } + + setBackground("menubg.png"); + xSize = 280; + ySize = 180; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 8; + int labelX = guiLeft + 8; + + // Title + addLabel(new GuiNpcLabel(0, "ability.effects", labelX, y)); + y += 16; + + // Column headers + addLabel(new GuiNpcLabel(1, "gui.type", guiLeft + 10, y)); + addLabel(new GuiNpcLabel(2, "gui.duration", guiLeft + 115, y)); + addLabel(new GuiNpcLabel(3, "gui.amp", guiLeft + 175, y)); + y += 14; + + // Effect rows + for (int i = 0; i < effects.size() && i < MAX_EFFECTS; i++) { + AbilityEffect effect = effects.get(i); + int baseId = 1 + i * 10; + + // Type selector button + int typeIndex = effect.getType().ordinal(); + addButton(new GuiNpcButton(baseId, guiLeft + 8, y, 100, 20, EFFECT_TYPE_NAMES, typeIndex)); + + // Duration text field + GuiNpcTextField durField = new GuiNpcTextField(baseId + 1, this, fontRendererObj, + guiLeft + 112, y, 50, 20, String.valueOf(effect.getDurationTicks())); + durField.setIntegersOnly(); + durField.setMinMaxDefault(1, 12000, 60); + addTextField(durField); + + // Amplifier selector (0-10) + String[] ampValues = new String[11]; + for (int a = 0; a <= 10; a++) { + ampValues[a] = String.valueOf(a); + } + addButton(new GuiNpcButton(baseId + 2, guiLeft + 168, y, 40, 20, ampValues, effect.getAmplifier())); + + // Delete button + addButton(new GuiNpcButton(baseId + 3, guiLeft + 215, y, 20, 20, "X")); + + y += 24; + } + + // Add button (if space available) + if (effects.size() < MAX_EFFECTS) { + addButton(new GuiNpcButton(100, guiLeft + 8, y, 50, 20, "gui.add")); + } + + // Bottom buttons + addButton(new GuiNpcButton(65, guiLeft + 8, guiTop + ySize - 28, 60, 20, "gui.cancel")); + addButton(new GuiNpcButton(66, guiLeft + xSize - 68, guiTop + ySize - 28, 60, 20, "gui.done")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + int id = guibutton.id; + + // Add button + if (id == 100) { + if (effects.size() < MAX_EFFECTS) { + // Add new effect with default values (Slowness, 60 ticks, amp 0) + effects.add(new AbilityEffect(AbilityEffect.EffectType.SLOWNESS, 60, 0)); + initGui(); + } + return; + } + + // Cancel button + if (id == 65) { + cancelled = true; + close(); + return; + } + + // Done button + if (id == 66) { + // Remove any NONE effects before saving + effects.removeIf(e -> !e.isValid()); + cancelled = false; + close(); + return; + } + + // Effect row buttons (id >= 1 and < 100) + if (id >= 1 && id < 100) { + int effectIndex = (id - 1) / 10; + int action = (id - 1) % 10; + + if (effectIndex >= 0 && effectIndex < effects.size()) { + AbilityEffect effect = effects.get(effectIndex); + + switch (action) { + case 0: // Type selector + int typeIndex = ((GuiNpcButton) guibutton).getValue(); + effect.setType(AbilityEffect.EffectType.fromOrdinal(typeIndex)); + break; + case 2: // Amplifier selector + int amp = ((GuiNpcButton) guibutton).getValue(); + effect.setAmplifier(amp); + break; + case 3: // Delete button + effects.remove(effectIndex); + initGui(); + break; + } + } + } + } + + @Override + public void unFocused(GuiNpcTextField textField) { + int id = textField.id; + + // Duration text fields (baseId + 1) + if (id >= 2 && id < 100) { + int effectIndex = (id - 1) / 10; + int action = (id - 1) % 10; + + if (action == 1 && effectIndex >= 0 && effectIndex < effects.size()) { + effects.get(effectIndex).setDurationTicks(textField.getInteger()); + } + } + } + + /** + * Returns the resulting effects list after the dialog closes. + * Returns null if cancelled. + */ + public List getResult() { + if (cancelled) { + return null; + } + return effects; + } + + /** + * Returns true if the dialog was cancelled. + */ + public boolean wasCancelled() { + return cancelled; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java index ad15bb576..1c7179deb 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.advanced.ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.type.AbilityEnergyBeam; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -9,6 +10,8 @@ import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.util.SubGuiInterface; +import java.util.List; + /** * GUI for configuring Energy Beam ability type-specific settings. * Energy Beam is a homing head with trailing path (DBZ-style). @@ -87,6 +90,11 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { addLabel(new GuiNpcLabel(111, "ability.lifetime", col2LabelX, y + 5)); addTextField(createIntField(111, col2FieldX, y, 50, beam.getMaxLifetime())); + + y += 24; + + // Row 7: Effects button + addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); } @Override @@ -161,6 +169,9 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { case 108: beam.setExplosive(value == 1); break; + case 150: + setSubGui(new SubGuiAbilityEffects(beam.getEffects())); + break; } } @@ -199,6 +210,12 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { } editingVisualColorId = 0; initGui(); + } else if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + beam.setEffects(result); + } } else { super.subGuiClosed(subgui); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java index c5240d862..25383c926 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java @@ -1,11 +1,15 @@ package noppes.npcs.client.gui.advanced.ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.List; /** * GUI for configuring Guard ability type-specific settings. @@ -68,6 +72,13 @@ public class SubGuiAbilityGuard extends SubGuiAbilityConfig { GuiNpcTextField counterAnimField = createIntField(106, col2FieldX, y, 50, guard.getCounterAnimationId()); counterAnimField.setEnabled(guard.isCanCounter()); addTextField(counterAnimField); + + y += 24; + + // Row 5: Effects button (applies on counter) + GuiNpcButton effectsBtn = new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects"); + effectsBtn.setEnabled(guard.isCanCounter()); + addButton(effectsBtn); } @Override @@ -80,6 +91,21 @@ public class SubGuiAbilityGuard extends SubGuiAbilityConfig { case 102: guard.setCounterType(AbilityGuard.CounterType.values()[button.getValue()]); break; + case 150: + setSubGui(new SubGuiAbilityEffects(guard.getEffects())); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + super.subGuiClosed(subgui); + if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + guard.setEffects(result); + } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java index 1ec615d7a..8a91798d0 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java @@ -1,11 +1,15 @@ package noppes.npcs.client.gui.advanced.ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.type.AbilityHazard; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.List; /** * GUI for configuring Hazard ability type-specific settings. @@ -71,6 +75,11 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { addLabel(new GuiNpcLabel(109, "ability.affectsCaster", col2LabelX, y + 5)); addButton(new GuiNpcButton(109, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, hazard.isAffectsCaster() ? 1 : 0)); + + y += 24; + + // Row 6: Effects button + addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); } @Override @@ -86,6 +95,21 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { case 109: hazard.setAffectsCaster(value == 1); break; + case 150: + setSubGui(new SubGuiAbilityEffects(hazard.getEffects())); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + super.subGuiClosed(subgui); + if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + hazard.setEffects(result); + } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java index ea21127dc..897a4af1f 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java @@ -1,10 +1,15 @@ package noppes.npcs.client.gui.advanced.ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.type.AbilityHeavyHit; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.List; /** * GUI for configuring Heavy Hit ability type-specific settings. @@ -24,29 +29,37 @@ public class SubGuiAbilityHeavyHit extends SubGuiAbilityConfig { int labelX = guiLeft + 8; int fieldX = guiLeft + 85; int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - // Row 1: Damage + Potion Time + // Row 1: Damage + Knockback addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, heavyHit.getDamage())); - addLabel(new GuiNpcLabel(101, "ability.potionTime", col2LabelX, y + 5)); - addTextField(createIntField(101, col2FieldX, y, 50, heavyHit.getPotionDurationSeconds())); + addLabel(new GuiNpcLabel(102, "ability.knockback", col2LabelX, y + 5)); + addTextField(createFloatField(102, guiLeft + 205, y, 50, heavyHit.getKnockback())); y += 24; - // Row 2: Knockback + Slowness Level - addLabel(new GuiNpcLabel(102, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, heavyHit.getKnockback())); + // Row 2: Effects button + addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); + } - addLabel(new GuiNpcLabel(103, "ability.slownessLvl", col2LabelX, y + 5)); - addTextField(createIntField(103, col2FieldX, y, 50, heavyHit.getSlownessLevel())); + @Override + protected void handleTypeButton(int id, GuiNpcButton button) { + if (id == 150) { + setSubGui(new SubGuiAbilityEffects(heavyHit.getEffects())); + } + } - y += 24; - - // Row 3: Weakness Level - addLabel(new GuiNpcLabel(104, "ability.weaknessLvl", labelX, y + 5)); - addTextField(createIntField(104, fieldX, y, 50, heavyHit.getWeaknessLevel())); + @Override + public void subGuiClosed(SubGuiInterface subgui) { + super.subGuiClosed(subgui); + if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + heavyHit.setEffects(result); + } + } } @Override @@ -55,18 +68,9 @@ public class SubGuiAbilityHeavyHit extends SubGuiAbilityConfig { case 100: heavyHit.setDamage(parseFloat(field, heavyHit.getDamage())); break; - case 101: - heavyHit.setPotionDurationSeconds(field.getInteger()); - break; case 102: heavyHit.setKnockback(parseFloat(field, heavyHit.getKnockback())); break; - case 103: - heavyHit.setSlownessLevel(field.getInteger()); - break; - case 104: - heavyHit.setWeaknessLevel(field.getInteger()); - break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java index f83e93baf..614a8bce0 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.advanced.ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.type.AbilityLaserShot; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -9,6 +10,8 @@ import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.util.SubGuiInterface; +import java.util.List; + /** * GUI for configuring Laser Shot ability type-specific settings. */ @@ -77,6 +80,11 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { addLabel(new GuiNpcLabel(109, "ability.knockbackUp", col2LabelX, y + 5)); addTextField(createFloatField(109, col2FieldX, y, 50, laser.getKnockbackUp())); + + y += 24; + + // Row 6: Effects button + addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); } @Override @@ -142,6 +150,9 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { case 106: laser.setExplosive(value == 1); break; + case 150: + setSubGui(new SubGuiAbilityEffects(laser.getEffects())); + break; } } @@ -180,6 +191,12 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { } editingVisualColorId = 0; initGui(); + } else if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + laser.setEffects(result); + } } else { super.subGuiClosed(subgui); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java index 42dd6a773..6c5c598ba 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.advanced.ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -9,6 +10,8 @@ import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.util.SubGuiInterface; +import java.util.List; + /** * GUI for configuring Orb ability type-specific settings. */ @@ -77,6 +80,11 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { addLabel(new GuiNpcLabel(109, "ability.lifetime", col2LabelX, y + 5)); addTextField(createIntField(109, col2FieldX, y, 50, orb.getMaxLifetime())); + + y += 24; + + // Row 6: Effects button + addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); } @Override @@ -151,6 +159,9 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { case 106: orb.setExplosive(value == 1); break; + case 150: + setSubGui(new SubGuiAbilityEffects(orb.getEffects())); + break; } } @@ -189,6 +200,12 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { } editingVisualColorId = 0; initGui(); + } else if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + orb.setEffects(result); + } } else { super.subGuiClosed(subgui); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java index c9be00e38..52bf7a092 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java @@ -1,11 +1,15 @@ package noppes.npcs.client.gui.advanced.ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.type.AbilityProjectile; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.List; /** * GUI for configuring Projectile ability type-specific settings. @@ -69,6 +73,11 @@ public class SubGuiAbilityProjectile extends SubGuiAbilityConfig { addLabel(new GuiNpcLabel(107, "ability.homingStr", col2LabelX, y + 5)); addTextField(createFloatField(107, col2FieldX, y, 50, projectile.getHomingStrength())); + + y += 24; + + // Row 5: Effects button + addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); } @Override @@ -85,6 +94,21 @@ public class SubGuiAbilityProjectile extends SubGuiAbilityConfig { case 106: projectile.setHoming(value == 1); break; + case 150: + setSubGui(new SubGuiAbilityEffects(projectile.getEffects())); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + super.subGuiClosed(subgui); + if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + projectile.setEffects(result); + } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java index a03a85736..48c2c5295 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java @@ -1,10 +1,15 @@ package noppes.npcs.client.gui.advanced.ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.List; /** * GUI for configuring Shockwave ability type-specific settings. @@ -41,17 +46,35 @@ public class SubGuiAbilityShockwave extends SubGuiAbilityConfig { y += 24; - // Row 3: Stun Duration + Max Targets - addLabel(new GuiNpcLabel(103, "ability.stunDuration", labelX, y + 5)); - addTextField(createIntField(103, fieldX, y, 50, shockwave.getStunDuration())); + // Row 3: Max Targets + Effects button + addLabel(new GuiNpcLabel(104, "ability.maxTargets", labelX, y + 5)); + addTextField(createIntField(104, fieldX, y, 50, shockwave.getMaxTargets())); - addLabel(new GuiNpcLabel(104, "ability.maxTargets", col2LabelX, y + 5)); - addTextField(createIntField(104, col2FieldX, y, 50, shockwave.getMaxTargets())); + addButton(new GuiNpcButton(150, col2LabelX, y, 80, 20, "ability.effects")); y += 24; addLabel(new GuiNpcLabel(105, "ability.shockwaveFalloff", labelX, y + 5)); } + @Override + protected void handleTypeButton(int id, GuiNpcButton button) { + if (id == 150) { + setSubGui(new SubGuiAbilityEffects(shockwave.getEffects())); + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + super.subGuiClosed(subgui); + if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + shockwave.setEffects(result); + } + } + } + @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { @@ -64,9 +87,6 @@ public class SubGuiAbilityShockwave extends SubGuiAbilityConfig { case 102: shockwave.setPushStrength(parseFloat(field, shockwave.getPushStrength())); break; - case 103: - shockwave.setStunDuration(field.getInteger()); - break; case 104: shockwave.setMaxTargets(field.getInteger()); break; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java index 989202037..4e020829b 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java @@ -1,10 +1,15 @@ package noppes.npcs.client.gui.advanced.ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.type.AbilitySlam; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.List; /** * GUI for configuring Slam ability type-specific settings. @@ -44,9 +49,30 @@ public class SubGuiAbilitySlam extends SubGuiAbilityConfig { y += 24; - // Row 3: Leap Height + // Row 3: Leap Height + Effects addLabel(new GuiNpcLabel(104, "ability.leapHeight", labelX, y + 5)); addTextField(createFloatField(104, fieldX, y, 50, slam.getLeapHeight())); + + addButton(new GuiNpcButton(150, col2LabelX, y, 80, 20, "ability.effects")); + } + + @Override + protected void handleTypeButton(int id, GuiNpcButton button) { + if (id == 150) { + setSubGui(new SubGuiAbilityEffects(slam.getEffects())); + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + super.subGuiClosed(subgui); + if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + slam.setEffects(result); + } + } } @Override diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java index 95f54619b..9a3dc2fe8 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java @@ -1,5 +1,6 @@ package noppes.npcs.client.gui.advanced.ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.type.AbilitySweeper; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -9,6 +10,8 @@ import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.util.SubGuiInterface; +import java.util.List; + /** * GUI for configuring Sweeper ability type-specific settings. * Sweeper is a low rotating beam that players can jump over. @@ -75,6 +78,11 @@ public class SubGuiAbilitySweeper extends SubGuiAbilityConfig { // Row 5: Lock On Target addLabel(new GuiNpcLabel(108, "ability.lockTarget", labelX, y + 5)); addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, sweeper.isLockOnTarget() ? 1 : 0)); + + y += 24; + + // Row 6: Effects button + addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); } @Override @@ -121,6 +129,9 @@ public class SubGuiAbilitySweeper extends SubGuiAbilityConfig { case 108: sweeper.setLockOnTarget(value == 1); break; + case 150: + setSubGui(new SubGuiAbilityEffects(sweeper.getEffects())); + break; } } @@ -155,6 +166,12 @@ public class SubGuiAbilitySweeper extends SubGuiAbilityConfig { } editingVisualColorId = 0; initGui(); + } else if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + sweeper.setEffects(result); + } } else { super.subGuiClosed(subgui); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java index 6173f9ded..1dfb12d22 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java @@ -1,11 +1,15 @@ package noppes.npcs.client.gui.advanced.ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.List; /** * GUI for configuring Teleport ability type-specific settings. @@ -85,6 +89,11 @@ public class SubGuiAbilityTeleport extends SubGuiAbilityConfig { addLabel(new GuiNpcLabel(109, "ability.dmgAtEnd", col2LabelX, y + 5)); addButton(new GuiNpcButton(109, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isDamageAtEnd() ? 1 : 0)); + + y += 24; + + // Effects button + addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); } @Override @@ -104,6 +113,21 @@ public class SubGuiAbilityTeleport extends SubGuiAbilityConfig { case 109: teleport.setDamageAtEnd(value == 1); break; + case 150: + setSubGui(new SubGuiAbilityEffects(teleport.getEffects())); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + super.subGuiClosed(subgui); + if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + teleport.setEffects(result); + } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java index 781d0665a..2c0fc3a68 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java @@ -1,11 +1,15 @@ package noppes.npcs.client.gui.advanced.ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.type.AbilityTrap; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.List; /** * GUI for configuring Trap ability type-specific settings. @@ -55,24 +59,31 @@ public class SubGuiAbilityTrap extends SubGuiAbilityConfig { y += 24; - // Row 4: Root Duration + Stun Duration - addLabel(new GuiNpcLabel(106, "ability.rootDuration", labelX, y + 5)); - addTextField(createIntField(106, fieldX, y, 50, trap.getRootDuration())); - - addLabel(new GuiNpcLabel(107, "ability.stunDuration", col2LabelX, y + 5)); - addTextField(createIntField(107, col2FieldX, y, 50, trap.getStunDuration())); - - y += 24; - - // Row 5: Knockback + // Row 4: Knockback + Effects button addLabel(new GuiNpcLabel(108, "ability.knockback", labelX, y + 5)); addTextField(createFloatField(108, fieldX, y, 50, trap.getKnockback())); + + addButton(new GuiNpcButton(150, col2LabelX, y, 80, 20, "ability.effects")); } @Override protected void handleTypeButton(int id, GuiNpcButton button) { if (id == 100) { trap.setPlacement(AbilityTrap.TrapPlacement.values()[button.getValue()]); + } else if (id == 150) { + setSubGui(new SubGuiAbilityEffects(trap.getEffects())); + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + super.subGuiClosed(subgui); + if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + trap.setEffects(result); + } } } @@ -94,12 +105,6 @@ public class SubGuiAbilityTrap extends SubGuiAbilityConfig { case 105: trap.setMaxTriggers(field.getInteger()); break; - case 106: - trap.setRootDuration(field.getInteger()); - break; - case 107: - trap.setStunDuration(field.getInteger()); - break; case 108: trap.setKnockback(parseFloat(field, trap.getKnockback())); break; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java index 003cef5c6..4cc7bc89a 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java @@ -1,11 +1,15 @@ package noppes.npcs.client.gui.advanced.ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.type.AbilityVortex; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.List; /** * GUI for configuring Vortex ability type-specific settings. @@ -54,12 +58,8 @@ public class SubGuiAbilityVortex extends SubGuiAbilityConfig { y += 24; - // Row 4: Stun Duration + Root Duration - addLabel(new GuiNpcLabel(106, "ability.stunDuration", labelX, y + 5)); - addTextField(createIntField(106, fieldX, y, 50, vortex.getStunDuration())); - - addLabel(new GuiNpcLabel(107, "ability.rootDuration", col2LabelX, y + 5)); - addTextField(createIntField(107, col2FieldX, y, 50, vortex.getRootDuration())); + // Row 4: Effects button + addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); y += 24; @@ -81,6 +81,21 @@ public class SubGuiAbilityVortex extends SubGuiAbilityConfig { case 108: vortex.setDamageOnPull(value == 1); break; + case 150: + setSubGui(new SubGuiAbilityEffects(vortex.getEffects())); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + super.subGuiClosed(subgui); + if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + vortex.setEffects(result); + } } } @@ -102,12 +117,6 @@ public class SubGuiAbilityVortex extends SubGuiAbilityConfig { case 105: vortex.setMaxTargets(field.getInteger()); break; - case 106: - vortex.setStunDuration(field.getInteger()); - break; - case 107: - vortex.setRootDuration(field.getInteger()); - break; case 109: vortex.setPullDamage(parseFloat(field, vortex.getPullDamage())); break; diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index b7e33a029..d5dc945ce 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1575,17 +1575,11 @@ ability.minDist=Min Dist ability.distance=Distance ability.lifetime=Lifetime ability.stunTicks=Stun -ability.stunDur=Stun Dur -ability.rootDur=Root Dur -ability.rootDuration=Root Duration -ability.stunDuration=Stun Duration ability.leapHeight=Leap Height ability.hitWidth=Hit Width -ability.potionTime=Potion Time (s) -ability.weaknessLvl=Weakness Lvl -ability.poisonTime=Poison Time (s) -ability.slownessLvl=Slowness Lvl -ability.poisonLvl=Poison Lvl +ability.effects=Effects +gui.duration=Duration +gui.amp=Amp # Slam Settings ability.leapSpeed=Leap Speed From c858f24e677f13e9a1ee155b6e28f98cf9b7932f Mon Sep 17 00:00:00 2001 From: Vidal Date: Tue, 27 Jan 2026 01:33:12 -0300 Subject: [PATCH 288/358] Implemented built in animations for abilities --- .../data/ability/AbilityAnimation.java | 79 +++++++++++++++++++ .../data/ability/AbilityController.java | 73 +++++++++++++++-- 2 files changed, 147 insertions(+), 5 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java new file mode 100644 index 000000000..1a6c907ea --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java @@ -0,0 +1,79 @@ +package kamkeel.npcs.controllers.data.ability; + +import noppes.npcs.CustomNpcs; +import noppes.npcs.LogWriter; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.InputStream; +import java.util.HashMap; + +import static kamkeel.npcs.controllers.data.ability.AbilityPhase.*; + +public enum AbilityAnimation {; + + private final HashMap animations = new HashMap<>(); + + AbilityAnimation(String idle, String windUp, String active, String recovery) { + animations.put(IDLE, idle); + animations.put(WINDUP, windUp); + animations.put(ACTIVE, active); + animations.put(RECOVERY, recovery); + } + + AbilityAnimation(String idle, String windUp, String active) { + animations.put(IDLE, idle); + animations.put(WINDUP, windUp); + animations.put(ACTIVE, active); + } + + AbilityAnimation(String windUp, String active) { + animations.put(WINDUP, windUp); + animations.put(ACTIVE, active); + } + + AbilityAnimation(String anim, AbilityPhase phase) { + animations.put(phase, anim); + } + + public String fileName(AbilityPhase phase) { + return animations.get(phase); + } + + public File get(AbilityPhase phase) { + if (animations.get(phase).isEmpty() || animations.get(phase).equals("N/A")) + return null; + + return getFile(animations.get(phase)); + } + + public File idle() { + return get(IDLE); + } + + public File windUp() { + return get(WINDUP); + } + + public File active() { + return get(ACTIVE); + } + + public File recovery() { + return get(RECOVERY); + } + + private File getFile(String fileName) { + try (InputStream stream = CustomNpcs.class.getResourceAsStream("/internal/data/animations/" + name() + "/" + fileName + ".json")) { + + File file = new File("temp"); + + FileUtils.copyInputStreamToFile(stream, file); + + return file; + } catch (Exception i) { + LogWriter.except(i); + } + return null; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 41c20a085..3eb8283f8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -22,14 +22,13 @@ import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; import noppes.npcs.api.handler.IAbilityHandler; +import noppes.npcs.api.handler.data.IAnimation; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; import noppes.npcs.util.NBTJsonUtil; import java.io.File; -import java.util.Collection; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.function.Supplier; /** @@ -49,6 +48,7 @@ public class AbilityController implements IAbilityHandler { public AbilityController() { Instance = this; registerBuiltins(); + new Animations(); } /** @@ -321,4 +321,67 @@ public class AbilityController implements IAbilityHandler { registerType("cnpc:hazard", AbilityHazard::new); registerType("cnpc:trap", AbilityTrap::new); } + + public static class Animations { + public HashMap> animations; + + public static Animations Instance; + + public Animations() { + Instance = this; + animations = new HashMap<>(); + load(); + } + + public void load() { + registerBuiltins(); + } + + private void registerBuiltins() { + animations.clear(); + + try { + for (AbilityPhase phase : AbilityPhase.values()) { + for (AbilityAnimation anim : AbilityAnimation.values()) { + File file = anim.get(phase); + + if (file == null) { +// LogWriter.info("AnimationController: Ability Animations for " + anim.name() + " not found."); + continue; + } + + Animation animation = new Animation(); + animation.readFromNBT(NBTJsonUtil.LoadFile(file)); + + animations + .computeIfAbsent(anim.ordinal(), k -> new EnumMap<>(AbilityPhase.class)) + .put(phase, animation); + } + } + } catch (Exception e) { + LogWriter.except(e); + } + } + + public IAnimation get(AbilityAnimation ability, AbilityPhase phase) { + return get(ability.ordinal(), phase); + } + + public IAnimation get(int id, AbilityPhase phase) { + return animations.get(id).get(phase); + } + + public boolean has(AbilityAnimation ability, AbilityPhase phase) { + return get(ability.ordinal(), phase) != null; + } + + public boolean has(int id, AbilityPhase phase) { + return get(id, phase) != null; + } + + public IAnimation[] getAnimations(AbilityAnimation ability) { + ArrayList animations = new ArrayList<>(this.animations.get(ability.ordinal()).values()); + return animations.toArray(new IAnimation[0]); + } + } } From 39b6304bedeb549a723bcdaa6b885b34da042002 Mon Sep 17 00:00:00 2001 From: Vidal Date: Tue, 27 Jan 2026 01:34:34 -0300 Subject: [PATCH 289/358] Updated imports --- .../kamkeel/npcs/controllers/data/ability/AbilityController.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 3eb8283f8..be9ce355d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -23,7 +23,6 @@ import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; import noppes.npcs.api.handler.IAbilityHandler; import noppes.npcs.api.handler.data.IAnimation; -import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.data.Animation; import noppes.npcs.util.NBTJsonUtil; From 246baa9f55b424c6520705fb8f7648cbf3804078 Mon Sep 17 00:00:00 2001 From: Vidal Date: Tue, 27 Jan 2026 01:49:08 -0300 Subject: [PATCH 290/358] Removed static class from AbilityController and made animations built into the enum --- src/api | 2 +- .../data/ability/AbilityAnimation.java | 45 +++++++++---- .../data/ability/AbilityController.java | 64 ------------------- 3 files changed, 34 insertions(+), 77 deletions(-) diff --git a/src/api b/src/api index b84763a8e..ae73e1b88 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit b84763a8eefdc0be7fb909888ac5ad6d22e3f987 +Subproject commit ae73e1b885415701c1ee20b64c1d0ae9c9089efc diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java index 1a6c907ea..cb27dcd3f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java @@ -2,6 +2,9 @@ package kamkeel.npcs.controllers.data.ability; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; +import noppes.npcs.api.handler.data.IAnimation; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.util.NBTJsonUtil; import org.apache.commons.io.FileUtils; import java.io.File; @@ -11,7 +14,6 @@ import java.util.HashMap; import static kamkeel.npcs.controllers.data.ability.AbilityPhase.*; public enum AbilityAnimation {; - private final HashMap animations = new HashMap<>(); AbilityAnimation(String idle, String windUp, String active, String recovery) { @@ -40,30 +42,49 @@ public enum AbilityAnimation {; return animations.get(phase); } - public File get(AbilityPhase phase) { + public File getFile(AbilityPhase phase) { if (animations.get(phase).isEmpty() || animations.get(phase).equals("N/A")) return null; - return getFile(animations.get(phase)); + return getFileFromName(animations.get(phase)); } - public File idle() { - return get(IDLE); + public IAnimation getAnimation(AbilityPhase phase) { + try { + File file = getFile(phase); + + if (file == null) { + return null; + } + + Animation animation = new Animation(); + animation.readFromNBT(NBTJsonUtil.LoadFile(file)); + + return animation; + } catch (Exception e) { + LogWriter.except(e); + } + + return null; } - public File windUp() { - return get(WINDUP); + public IAnimation idle() { + return getAnimation(IDLE); } - public File active() { - return get(ACTIVE); + public IAnimation windUp() { + return getAnimation(WINDUP); } - public File recovery() { - return get(RECOVERY); + public IAnimation active() { + return getAnimation(ACTIVE); } - private File getFile(String fileName) { + public IAnimation recovery() { + return getAnimation(RECOVERY); + } + + private File getFileFromName(String fileName) { try (InputStream stream = CustomNpcs.class.getResourceAsStream("/internal/data/animations/" + name() + "/" + fileName + ".json")) { File file = new File("temp"); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index be9ce355d..0b88f1a42 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -47,7 +47,6 @@ public class AbilityController implements IAbilityHandler { public AbilityController() { Instance = this; registerBuiltins(); - new Animations(); } /** @@ -320,67 +319,4 @@ public class AbilityController implements IAbilityHandler { registerType("cnpc:hazard", AbilityHazard::new); registerType("cnpc:trap", AbilityTrap::new); } - - public static class Animations { - public HashMap> animations; - - public static Animations Instance; - - public Animations() { - Instance = this; - animations = new HashMap<>(); - load(); - } - - public void load() { - registerBuiltins(); - } - - private void registerBuiltins() { - animations.clear(); - - try { - for (AbilityPhase phase : AbilityPhase.values()) { - for (AbilityAnimation anim : AbilityAnimation.values()) { - File file = anim.get(phase); - - if (file == null) { -// LogWriter.info("AnimationController: Ability Animations for " + anim.name() + " not found."); - continue; - } - - Animation animation = new Animation(); - animation.readFromNBT(NBTJsonUtil.LoadFile(file)); - - animations - .computeIfAbsent(anim.ordinal(), k -> new EnumMap<>(AbilityPhase.class)) - .put(phase, animation); - } - } - } catch (Exception e) { - LogWriter.except(e); - } - } - - public IAnimation get(AbilityAnimation ability, AbilityPhase phase) { - return get(ability.ordinal(), phase); - } - - public IAnimation get(int id, AbilityPhase phase) { - return animations.get(id).get(phase); - } - - public boolean has(AbilityAnimation ability, AbilityPhase phase) { - return get(ability.ordinal(), phase) != null; - } - - public boolean has(int id, AbilityPhase phase) { - return get(id, phase) != null; - } - - public IAnimation[] getAnimations(AbilityAnimation ability) { - ArrayList animations = new ArrayList<>(this.animations.get(ability.ordinal()).values()); - return animations.toArray(new IAnimation[0]); - } - } } From 0484596a91c7e5d6f01f7ede189b5e151de4e630 Mon Sep 17 00:00:00 2001 From: Vidal Date: Tue, 27 Jan 2026 01:51:49 -0300 Subject: [PATCH 291/358] Updated imports and fields --- .../data/ability/AbilityAnimation.java | 28 +++++++++---------- .../data/ability/AbilityController.java | 2 -- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java index cb27dcd3f..4b7f2ba3e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java @@ -14,39 +14,39 @@ import java.util.HashMap; import static kamkeel.npcs.controllers.data.ability.AbilityPhase.*; public enum AbilityAnimation {; - private final HashMap animations = new HashMap<>(); + private final HashMap animationNames = new HashMap<>(); AbilityAnimation(String idle, String windUp, String active, String recovery) { - animations.put(IDLE, idle); - animations.put(WINDUP, windUp); - animations.put(ACTIVE, active); - animations.put(RECOVERY, recovery); + animationNames.put(IDLE, idle); + animationNames.put(WINDUP, windUp); + animationNames.put(ACTIVE, active); + animationNames.put(RECOVERY, recovery); } AbilityAnimation(String idle, String windUp, String active) { - animations.put(IDLE, idle); - animations.put(WINDUP, windUp); - animations.put(ACTIVE, active); + animationNames.put(IDLE, idle); + animationNames.put(WINDUP, windUp); + animationNames.put(ACTIVE, active); } AbilityAnimation(String windUp, String active) { - animations.put(WINDUP, windUp); - animations.put(ACTIVE, active); + animationNames.put(WINDUP, windUp); + animationNames.put(ACTIVE, active); } AbilityAnimation(String anim, AbilityPhase phase) { - animations.put(phase, anim); + animationNames.put(phase, anim); } public String fileName(AbilityPhase phase) { - return animations.get(phase); + return animationNames.get(phase); } public File getFile(AbilityPhase phase) { - if (animations.get(phase).isEmpty() || animations.get(phase).equals("N/A")) + if (animationNames.get(phase).isEmpty() || animationNames.get(phase).equals("N/A")) return null; - return getFileFromName(animations.get(phase)); + return getFileFromName(animationNames.get(phase)); } public IAnimation getAnimation(AbilityPhase phase) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 0b88f1a42..a61df06fa 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -22,8 +22,6 @@ import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; import noppes.npcs.api.handler.IAbilityHandler; -import noppes.npcs.api.handler.data.IAnimation; -import noppes.npcs.controllers.data.Animation; import noppes.npcs.util.NBTJsonUtil; import java.io.File; From 0f44a3c6c64906608ec9945b9923d6df20fbf620 Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 26 Jan 2026 23:55:07 -0500 Subject: [PATCH 292/358] Ability Anchor Points + Orb / Disc Summoning --- .../controllers/data/ability/AnchorPoint.java | 55 ++++ .../data/ability/type/AbilityDisc.java | 73 +++-- .../data/ability/type/AbilityEnergyBeam.java | 13 +- .../data/ability/type/AbilityOrb.java | 76 +++-- .../npcs/entity/EntityAbilityBeam.java | 54 ++-- .../npcs/entity/EntityAbilityDisc.java | 275 ++++++++++++++-- .../kamkeel/npcs/entity/EntityAbilityOrb.java | 183 +++++++++++ .../kamkeel/npcs/util/AnchorPointHelper.java | 303 ++++++++++++++++++ .../advanced/ability/SubGuiAbilityDisc.java | 22 +- .../ability/SubGuiAbilityEnergyBeam.java | 12 +- .../advanced/ability/SubGuiAbilityOrb.java | 12 +- .../assets/customnpcs/lang/en_US.lang | 1 + 12 files changed, 984 insertions(+), 95 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java create mode 100644 src/main/java/kamkeel/npcs/util/AnchorPointHelper.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java new file mode 100644 index 000000000..9bf408c27 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java @@ -0,0 +1,55 @@ +package kamkeel.npcs.controllers.data.ability; + +/** + * Defines anchor points for ability charging effects. + * Determines where the charging visual (orb, disc, etc.) appears relative to the entity. + */ +public enum AnchorPoint { + FRONT(0, "Front"), // In front of entity face (default) + CENTER(1, "Center"), // Entity center + RIGHT_HAND(2, "Right Hand"), // Right arm/hand position + LEFT_HAND(3, "Left Hand"), // Left arm/hand position + ABOVE_HEAD(4, "Above Head"), // Above the entity + CHEST(5, "Chest"); // Chest level, centered + + private final int id; + private final String displayName; + + AnchorPoint(int id, String displayName) { + this.id = id; + this.displayName = displayName; + } + + public int getId() { + return id; + } + + public String getDisplayName() { + return displayName; + } + + /** + * Get AnchorPoint from NBT id value. + * Returns FRONT if id is invalid. + */ + public static AnchorPoint fromId(int id) { + for (AnchorPoint anchor : values()) { + if (anchor.id == id) { + return anchor; + } + } + return FRONT; + } + + /** + * Get all display names for GUI dropdowns. + */ + public static String[] getDisplayNames() { + AnchorPoint[] values = values(); + String[] names = new String[values.length]; + for (int i = 0; i < values.length; i++) { + names[i] = values[i].displayName; + } + return names; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 5aa34a969..95dcd4afb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -3,11 +3,13 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityDisc; +import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; @@ -28,7 +30,7 @@ public class AbilityDisc extends Ability { private float discRadius = 1.0f; private float discThickness = 0.2f; private float maxDistance = 35.0f; - private int maxLifetime = 250; + private int maxLifetime = 200; // Combat properties private float damage = 8.0f; @@ -66,6 +68,12 @@ public class AbilityDisc extends Ability { private float lightningDensity = 0.15f; private float lightningRadius = 0.5f; + // Anchor point for charging position + private AnchorPoint anchorPoint = AnchorPoint.FRONT; + + // Transient state for disc entity (used during windup charging) + private transient int discEntityId = -1; + public AbilityDisc() { this.typeId = "ability.cnpc.disc"; this.name = "Disc"; @@ -105,24 +113,41 @@ public class AbilityDisc extends Ability { public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { if (world.isRemote) return; - double spawnX = npc.posX; - double spawnY = npc.posY + npc.getEyeHeight() * 0.8; - double spawnZ = npc.posZ; + // Find and start moving the disc that was spawned during windup + if (discEntityId != -1) { + Entity entity = world.getEntityByID(discEntityId); + if (entity instanceof EntityAbilityDisc) { + EntityAbilityDisc disc = (EntityAbilityDisc) entity; + disc.startMoving(target); + } + discEntityId = -1; + } + } - EntityAbilityDisc disc = new EntityAbilityDisc( - world, npc, target, - spawnX, spawnY, spawnZ, - discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, - damage, knockback, knockbackUp, - speed, homing, homingStrength, homingRange, - boomerang, boomerangDelay, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime, - lightningEffect, lightningDensity, lightningRadius - ); + @Override + public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; - world.spawnEntityInWorld(disc); + // Spawn disc in charging mode on first tick of windup + if (tick == 1) { + // Create disc in charging mode - follows NPC based on anchor point during windup + EntityAbilityDisc disc = EntityAbilityDisc.createCharging( + world, npc, target, + discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, + speed, homing, homingStrength, homingRange, + boomerang, boomerangDelay, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, + anchorPoint, // Anchor point for charging position + windUpTicks // Charge duration = windup duration + ); + + world.spawnEntityInWorld(disc); + discEntityId = disc.getEntityId(); + } } @Override @@ -135,6 +160,14 @@ public class AbilityDisc extends Ability { @Override public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + // Despawn charging disc if interrupted during windup + if (discEntityId != -1) { + Entity entity = npc.worldObj.getEntityByID(discEntityId); + if (entity != null) { + entity.setDead(); + } + discEntityId = -1; + } } @Override @@ -193,6 +226,7 @@ public class AbilityDisc extends Ability { nbt.setBoolean("lightningEffect", lightningEffect); nbt.setFloat("lightningDensity", lightningDensity); nbt.setFloat("lightningRadius", lightningRadius); + nbt.setInteger("anchorPoint", anchorPoint.getId()); } @Override @@ -201,7 +235,7 @@ public class AbilityDisc extends Ability { this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f; this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 35.0f; - this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 250; + this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200; this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.2f; this.knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.15f; @@ -224,6 +258,7 @@ public class AbilityDisc extends Ability { this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; + this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromId(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; } // Getters & Setters @@ -281,4 +316,6 @@ public class AbilityDisc extends Ability { public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } public float getLightningRadius() { return lightningRadius; } public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } + public AnchorPoint getAnchorPoint() { return anchorPoint; } + public void setAnchorPoint(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 9a5345435..18969b43c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; @@ -66,6 +67,9 @@ public class AbilityEnergyBeam extends Ability { private float lightningDensity = 0.15f; private float lightningRadius = 0.5f; + // Anchor point for charging position + private AnchorPoint anchorPoint = AnchorPoint.FRONT; + // Transient state for beam entity (used during windup charging) private transient int beamEntityId = -1; @@ -127,7 +131,7 @@ public class AbilityEnergyBeam extends Ability { if (tick == 1) { float offsetDist = 1.0f; - // Create beam in charging mode - follows NPC's look direction during windup + // Create beam in charging mode - follows NPC based on anchor point during windup EntityAbilityBeam beam = EntityAbilityBeam.createCharging( world, npc, target, beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, @@ -139,7 +143,8 @@ public class AbilityEnergyBeam extends Ability { lightningEffect, lightningDensity, lightningRadius, lockMovement, // Anchored mode windUpTicks, // Charge duration = windup duration - offsetDist // Offset distance in front of NPC + offsetDist, // Offset distance (used by FRONT anchor) + anchorPoint // Anchor point for charging position ); world.spawnEntityInWorld(beam); @@ -221,6 +226,7 @@ public class AbilityEnergyBeam extends Ability { nbt.setBoolean("lightningEffect", lightningEffect); nbt.setFloat("lightningDensity", lightningDensity); nbt.setFloat("lightningRadius", lightningRadius); + nbt.setInteger("anchorPoint", anchorPoint.getId()); } @Override @@ -250,6 +256,7 @@ public class AbilityEnergyBeam extends Ability { this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; + this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromId(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; } // Getters & Setters @@ -303,4 +310,6 @@ public class AbilityEnergyBeam extends Ability { public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } public float getLightningRadius() { return lightningRadius; } public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } + public AnchorPoint getAnchorPoint() { return anchorPoint; } + public void setAnchorPoint(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 358b82896..38ffd8db4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -3,11 +3,13 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityOrb; +import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; @@ -63,6 +65,12 @@ public class AbilityOrb extends Ability { private float lightningRadius = 0.5f; // Max distance lightning arcs - scales with orbSize private int lightningFadeTime = 6; // Ticks before lightning fades out + // Anchor point for charging position + private AnchorPoint anchorPoint = AnchorPoint.FRONT; + + // Transient state for orb entity (used during windup charging) + private transient int orbEntityId = -1; + public AbilityOrb() { this.typeId = "ability.cnpc.orb"; this.name = "Orb"; @@ -102,25 +110,40 @@ public class AbilityOrb extends Ability { public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { if (world.isRemote) return; - // Spawn position at NPC's eye level - double spawnX = npc.posX; - double spawnY = npc.posY + npc.getEyeHeight() * 0.8; - double spawnZ = npc.posZ; + // Find and start moving the orb that was spawned during windup + if (orbEntityId != -1) { + Entity entity = world.getEntityByID(orbEntityId); + if (entity instanceof EntityAbilityOrb) { + EntityAbilityOrb orb = (EntityAbilityOrb) entity; + orb.startMoving(target); + } + orbEntityId = -1; + } + } - // Create and spawn the self-managing orb entity - EntityAbilityOrb orb = new EntityAbilityOrb( - world, npc, target, - spawnX, spawnY, spawnZ, - orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, - damage, knockback, knockbackUp, - orbSpeed, homing, homingStrength, homingRange, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime, - lightningEffect, lightningDensity, lightningRadius, lightningFadeTime - ); + @Override + public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; - world.spawnEntityInWorld(orb); + // Spawn orb in charging mode on first tick of windup + if (tick == 1) { + // Create orb in charging mode - follows NPC based on anchor point during windup + EntityAbilityOrb orb = EntityAbilityOrb.createCharging( + world, npc, target, + orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, + orbSpeed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, + maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, lightningFadeTime, + anchorPoint, // Anchor point for charging position + windUpTicks // Charge duration = windup duration + ); + + world.spawnEntityInWorld(orb); + orbEntityId = orb.getEntityId(); + } } @Override @@ -135,7 +158,14 @@ public class AbilityOrb extends Ability { @Override public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - // Nothing to clean up - entity manages itself + // Despawn charging orb if interrupted during windup + if (orbEntityId != -1) { + Entity entity = npc.worldObj.getEntityByID(orbEntityId); + if (entity != null) { + entity.setDead(); + } + orbEntityId = -1; + } } @Override @@ -192,6 +222,7 @@ public class AbilityOrb extends Ability { nbt.setFloat("lightningDensity", lightningDensity); nbt.setFloat("lightningRadius", lightningRadius); nbt.setInteger("lightningFadeTime", lightningFadeTime); + nbt.setInteger("anchorPoint", anchorPoint.getId()); } @Override @@ -221,6 +252,7 @@ public class AbilityOrb extends Ability { this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; this.lightningFadeTime = nbt.hasKey("lightningFadeTime") ? nbt.getInteger("lightningFadeTime") : 6; + this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromId(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; } // Getters & Setters @@ -423,4 +455,12 @@ public class AbilityOrb extends Ability { public void setLightningFadeTime(int lightningFadeTime) { this.lightningFadeTime = lightningFadeTime; } + + public AnchorPoint getAnchorPoint() { + return anchorPoint; + } + + public void setAnchorPoint(AnchorPoint anchorPoint) { + this.anchorPoint = anchorPoint; + } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 34030ab3f..70aaf5254 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -1,5 +1,7 @@ package kamkeel.npcs.entity; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -55,6 +57,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { private int chargeDuration = 40; private int chargeTick = 0; private float chargeOffsetDistance = 1.0f; + private AnchorPoint anchorPoint = AnchorPoint.FRONT; // Trail fading for non-anchored beams (comet effect) private boolean fadeTrail = false; @@ -214,7 +217,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { /** * Create a beam in charging mode (for windup phase). * The beam will grow from 0 to headSize over chargeDuration ticks. - * Position follows the owner's look direction. + * Position follows the owner based on anchor point. */ public static EntityAbilityBeam createCharging(World world, EntityNPCInterface owner, EntityLivingBase target, float beamWidth, float headSize, int innerColor, int outerColor, @@ -225,12 +228,13 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime, boolean lightningEffect, float lightningDensity, float lightningRadius, - boolean anchoredMode, int chargeDuration, float chargeOffsetDistance) { - // Calculate initial position in front of owner's look direction - float yaw = (float) Math.toRadians(owner.rotationYawHead); - double spawnX = owner.posX - Math.sin(yaw) * chargeOffsetDistance; - double spawnY = owner.posY + owner.getEyeHeight() * 0.7; - double spawnZ = owner.posZ + Math.cos(yaw) * chargeOffsetDistance; + boolean anchoredMode, int chargeDuration, float chargeOffsetDistance, + AnchorPoint anchorPoint) { + // Calculate initial position based on anchor point + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint, chargeOffsetDistance); + double spawnX = spawnPos.xCoord; + double spawnY = spawnPos.yCoord; + double spawnZ = spawnPos.zCoord; EntityAbilityBeam beam = new EntityAbilityBeam( world, owner, target, @@ -247,6 +251,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { beam.chargeDuration = chargeDuration; beam.chargeTick = 0; beam.chargeOffsetDistance = chargeOffsetDistance; + beam.anchorPoint = anchorPoint; // Non-anchored beams have fading trail (comet effect) beam.fadeTrail = !anchoredMode; @@ -420,7 +425,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { } /** - * Update during charging state - follow owner's look direction. + * Update during charging state - follow owner based on anchor point. */ private void updateCharging() { chargeTick++; @@ -431,27 +436,28 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { return; } - // Follow owner's head/look direction (rotationYawHead for NPCs/players) - float yaw; + // Calculate position based on anchor point + Vec3 pos; if (owner instanceof EntityLivingBase) { - yaw = (float) Math.toRadians(((EntityLivingBase) owner).rotationYawHead); + pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorPoint, chargeOffsetDistance); } else { - yaw = (float) Math.toRadians(owner.rotationYaw); + // Fallback for non-living entities (shouldn't happen normally) + float yaw = (float) Math.toRadians(owner.rotationYaw); + double offsetX = -Math.sin(yaw) * chargeOffsetDistance; + double offsetZ = Math.cos(yaw) * chargeOffsetDistance; + pos = Vec3.createVectorHelper( + owner.posX + offsetX, + owner.posY + owner.getEyeHeight() * 0.7, + owner.posZ + offsetZ + ); } - double offsetX = -Math.sin(yaw) * chargeOffsetDistance; - double offsetZ = Math.cos(yaw) * chargeOffsetDistance; - - double newX = owner.posX + offsetX; - double newY = owner.posY + owner.getEyeHeight() * 0.7; - double newZ = owner.posZ + offsetZ; - - setPosition(newX, newY, newZ); + setPosition(pos.xCoord, pos.yCoord, pos.zCoord); // Also update origin for when firing starts - startX = newX; - startY = newY; - startZ = newZ; + startX = pos.xCoord; + startY = pos.yCoord; + startZ = pos.zCoord; } /** @@ -726,6 +732,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; this.chargeOffsetDistance = nbt.hasKey("ChargeOffsetDistance") ? nbt.getFloat("ChargeOffsetDistance") : 1.0f; + this.anchorPoint = nbt.hasKey("AnchorPoint") ? AnchorPoint.fromId(nbt.getInteger("AnchorPoint")) : AnchorPoint.FRONT; this.fadeTrail = nbt.hasKey("FadeTrail") && nbt.getBoolean("FadeTrail"); this.trailFadeTime = nbt.hasKey("TrailFadeTime") ? nbt.getInteger("TrailFadeTime") : 20; @@ -761,6 +768,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { nbt.setInteger("ChargeDuration", chargeDuration); nbt.setInteger("ChargeTick", chargeTick); nbt.setFloat("ChargeOffsetDistance", chargeOffsetDistance); + nbt.setInteger("AnchorPoint", anchorPoint.getId()); nbt.setBoolean("FadeTrail", fadeTrail); nbt.setInteger("TrailFadeTime", trailFadeTime); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index c0127286f..b1f57f9c9 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -1,5 +1,7 @@ package kamkeel.npcs.entity; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -35,10 +37,45 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { private float discRadius = 1.0f; // Width of disc private float discThickness = 0.2f; // Height of disc + // Charging state (during windup) + private boolean charging = false; + private int chargeDuration = 40; + private int chargeTick = 0; + private AnchorPoint anchorPoint = AnchorPoint.FRONT; + private float targetDiscRadius = 1.0f; // Full radius to grow to during charging + private float targetDiscThickness = 0.2f; // Full thickness to grow to during charging + + // Data watcher index for charging state (synced to clients) + private static final int DW_CHARGING = 20; + public EntityAbilityDisc(World world) { super(world); } + @Override + protected void entityInit() { + super.entityInit(); + // Register data watcher for charging state + this.dataWatcher.addObject(DW_CHARGING, (byte) 0); + } + + /** + * Check if disc is in charging state (synced via data watcher). + */ + public boolean isCharging() { + return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; + } + + /** + * Set charging state (server only, synced to clients via data watcher). + */ + private void setCharging(boolean value) { + this.charging = value; + if (!worldObj.isRemote) { + this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); + } + } + /** * Full constructor with all parameters (no lightning). */ @@ -117,6 +154,97 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { } } + /** + * Create a disc in charging mode (for windup phase). + * The disc will grow from 0 to discRadius over chargeDuration ticks. + * Position follows the owner based on anchor point. + */ + public static EntityAbilityDisc createCharging(World world, EntityNPCInterface owner, EntityLivingBase target, + float discRadius, float discThickness, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean boomerang, int boomerangDelay, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius, + AnchorPoint anchorPoint, int chargeDuration) { + // Calculate initial position based on anchor point + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint); + double spawnX = spawnPos.xCoord; + double spawnY = spawnPos.yCoord; + double spawnZ = spawnPos.zCoord; + + EntityAbilityDisc disc = new EntityAbilityDisc( + world, owner, target, + spawnX, spawnY, spawnZ, + discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, + boomerang, boomerangDelay, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius); + + // Set charging state (uses data watcher for client sync) + disc.setCharging(true); + disc.chargeDuration = chargeDuration; + disc.chargeTick = 0; + disc.anchorPoint = anchorPoint; + + // Store target size and start at 0 for grow effect + disc.targetDiscRadius = disc.discRadius; + disc.targetDiscThickness = disc.discThickness; + disc.discRadius = 0.01f; // Start very small + disc.discThickness = 0.01f; + disc.size = 0.01f; // Base size used for rendering + disc.renderCurrentSize = 0.01f; + disc.prevRenderSize = 0.01f; + + // Clear motion while charging + disc.motionX = 0; + disc.motionY = 0; + disc.motionZ = 0; + + return disc; + } + + /** + * Start the disc moving (exit charging mode). + * Called by ability when windup ends. + */ + public void startMoving(EntityLivingBase target) { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Calculate velocity toward target + Entity owner = getOwner(); + + if (target != null) { + double dx = target.posX - posX; + double dy = (target.posY + target.height * 0.5) - posY; + double dz = target.posZ - posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + motionX = (dx / len) * speed; + motionY = (dy / len) * speed; + motionZ = (dz / len) * speed; + } + } else if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + } + @Override protected void updateRotation() { // Disc spins ONLY on Y axis (flat spin like a saw blade) @@ -127,6 +255,12 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { @Override protected void updateProjectile() { + // Handle charging state (windup phase) + if (isCharging()) { + updateCharging(); + return; + } + if (worldObj.isRemote) { handleClientInterpolation(); } else { @@ -141,17 +275,8 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { checkEntityCollision(); this.moveEntity(motionX, motionY, motionZ); - // Boomerang: if we've traveled far without hitting, start returning + // Track ticks for boomerang delay (checkMaxDistance handles the actual return trigger) if (boomerang && !returning && !hasHit) { - double distTraveled = Math.sqrt( - (posX - startX) * (posX - startX) + - (posY - startY) * (posY - startY) + - (posZ - startZ) * (posZ - startZ) - ); - // Start returning at 80% of max distance or after delay - if (distTraveled >= maxDistance * 0.8 || ticksSinceMiss >= boomerangDelay) { - returning = true; - } ticksSinceMiss++; } } @@ -170,20 +295,41 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { @Override protected boolean checkMaxDistance() { - // If boomerang and returning, don't die from max distance - if (boomerang && returning) { - // Die if we get close to owner - Entity owner = getOwner(); - if (owner != null) { - double distToOwner = Math.sqrt( - (posX - owner.posX) * (posX - owner.posX) + - (posY - owner.posY) * (posY - owner.posY) + - (posZ - owner.posZ) * (posZ - owner.posZ) - ); - return distToOwner < 1.0; + double distTraveled = Math.sqrt( + (posX - startX) * (posX - startX) + + (posY - startY) * (posY - startY) + + (posZ - startZ) * (posZ - startZ) + ); + + if (boomerang) { + // If not returning yet, check if we should start returning + if (!returning && !hasHit) { + if (distTraveled >= maxDistance * 0.8 || ticksSinceMiss >= boomerangDelay) { + returning = true; + } } + + // If returning, only die when close to owner + if (returning) { + Entity owner = getOwner(); + if (owner != null) { + double distToOwner = Math.sqrt( + (posX - owner.posX) * (posX - owner.posX) + + (posY - owner.posY) * (posY - owner.posY) + + (posZ - owner.posZ) * (posZ - owner.posZ) + ); + return distToOwner < 1.5; + } + // Owner gone but returning - keep going toward last known position + return false; + } + + // Not returning yet - don't die from distance (we'll trigger return above) + return false; } - return super.checkMaxDistance(); + + // Non-boomerang: standard max distance check + return distTraveled >= maxDistance; } private void updateHoming() { @@ -225,19 +371,28 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { private void updateReturnToOwner() { Entity owner = getOwner(); - if (owner == null) { - this.setDead(); - return; + + // If owner is gone (unloaded/dead), head back toward start position + double targetX, targetY, targetZ; + if (owner != null) { + targetX = owner.posX; + targetY = owner.posY + owner.height * 0.5; + targetZ = owner.posZ; + } else { + // Return to start position if owner not available + targetX = startX; + targetY = startY; + targetZ = startZ; } - double dx = owner.posX - posX; - double dy = (owner.posY + owner.height * 0.5) - posY; - double dz = owner.posZ - posZ; + double dx = targetX - posX; + double dy = targetY - posY; + double dz = targetZ - posZ; double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); if (dist > 0) { // Stronger homing when returning - double returnStrength = homingStrength * 2.0; + double returnStrength = Math.min(1.0, homingStrength * 2.5); double desiredVX = (dx / dist) * speed; double desiredVY = (dy / dist) * speed; double desiredVZ = (dz / dist) * speed; @@ -297,6 +452,52 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { } } + /** + * Update during charging state - follow owner based on anchor point. + */ + private void updateCharging() { + chargeTick++; + + Entity owner = getOwner(); + if (owner == null || owner.isDead) { + setDead(); + return; + } + + // Grow size based on charge progress + float progress = getChargeProgress(); + this.discRadius = targetDiscRadius * progress; + this.discThickness = targetDiscThickness * progress; + this.size = this.discRadius; // Base size for interpolation + + // Calculate position based on anchor point + if (owner instanceof EntityLivingBase) { + Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorPoint); + setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + } + + // Update rotation during charging (disc still spins) + updateRotation(); + } + + /** + * Get the charge progress (0-1). + */ + public float getChargeProgress() { + if (chargeDuration <= 0) return 1.0f; + return Math.min(1.0f, (float) chargeTick / chargeDuration); + } + + /** + * Get interpolated charge progress for smooth rendering. + */ + public float getInterpolatedChargeProgress(float partialTicks) { + if (chargeDuration <= 0) return 1.0f; + float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration); + float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration); + return prevProgress + (currProgress - prevProgress) * partialTicks; + } + // ==================== GETTERS ==================== public float getSpeed() { @@ -336,6 +537,15 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { this.discRadius = nbt.hasKey("DiscRadius") ? nbt.getFloat("DiscRadius") : 1.0f; this.discThickness = nbt.hasKey("DiscThickness") ? nbt.getFloat("DiscThickness") : 0.2f; this.returning = nbt.hasKey("Returning") && nbt.getBoolean("Returning"); + // Charging state + boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); + this.charging = isCharging; + this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); + this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; + this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; + this.anchorPoint = nbt.hasKey("AnchorPoint") ? AnchorPoint.fromId(nbt.getInteger("AnchorPoint")) : AnchorPoint.FRONT; + this.targetDiscRadius = nbt.hasKey("TargetDiscRadius") ? nbt.getFloat("TargetDiscRadius") : this.discRadius; + this.targetDiscThickness = nbt.hasKey("TargetDiscThickness") ? nbt.getFloat("TargetDiscThickness") : this.discThickness; } @Override @@ -349,5 +559,12 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { nbt.setFloat("DiscRadius", discRadius); nbt.setFloat("DiscThickness", discThickness); nbt.setBoolean("Returning", returning); + // Charging state + nbt.setBoolean("Charging", isCharging()); + nbt.setInteger("ChargeDuration", chargeDuration); + nbt.setInteger("ChargeTick", chargeTick); + nbt.setInteger("AnchorPoint", anchorPoint.getId()); + nbt.setFloat("TargetDiscRadius", targetDiscRadius); + nbt.setFloat("TargetDiscThickness", targetDiscThickness); } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index 38e3741f9..18a0c8503 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -1,5 +1,7 @@ package kamkeel.npcs.entity; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -25,10 +27,44 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { private float homingStrength = 0.35f; // Increased from 0.15 for better tracking private float homingRange = 20.0f; + // Charging state (during windup) + private boolean charging = false; + private int chargeDuration = 40; + private int chargeTick = 0; + private AnchorPoint anchorPoint = AnchorPoint.FRONT; + private float targetSize = 1.0f; // Full size to grow to during charging + + // Data watcher index for charging state (synced to clients) + private static final int DW_CHARGING = 20; + public EntityAbilityOrb(World world) { super(world); } + @Override + protected void entityInit() { + super.entityInit(); + // Register data watcher for charging state + this.dataWatcher.addObject(DW_CHARGING, (byte) 0); + } + + /** + * Check if orb is in charging state (synced via data watcher). + */ + public boolean isCharging() { + return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; + } + + /** + * Set charging state (server only, synced to clients via data watcher). + */ + private void setCharging(boolean value) { + this.charging = value; + if (!worldObj.isRemote) { + this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); + } + } + /** * Full constructor with all parameters. */ @@ -102,8 +138,100 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { } } + /** + * Create an orb in charging mode (for windup phase). + * The orb will grow from 0 to orbSize over chargeDuration ticks. + * Position follows the owner based on anchor point. + */ + public static EntityAbilityOrb createCharging(World world, EntityNPCInterface owner, EntityLivingBase target, + float orbSize, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + float damage, float knockback, float knockbackUp, + float speed, boolean homing, float homingStrength, float homingRange, + boolean explosive, float explosionRadius, float explosionDamageFalloff, + int stunDuration, int slowDuration, int slowLevel, + float maxDistance, int maxLifetime, + boolean lightningEffect, float lightningDensity, float lightningRadius, int lightningFadeTime, + AnchorPoint anchorPoint, int chargeDuration) { + // Calculate initial position based on anchor point + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint); + double spawnX = spawnPos.xCoord; + double spawnY = spawnPos.yCoord; + double spawnZ = spawnPos.zCoord; + + EntityAbilityOrb orb = new EntityAbilityOrb( + world, owner, target, + spawnX, spawnY, spawnZ, + orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, + explosive, explosionRadius, explosionDamageFalloff, + stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, + lightningEffect, lightningDensity, lightningRadius, lightningFadeTime); + + // Set charging state (uses data watcher for client sync) + orb.setCharging(true); + orb.chargeDuration = chargeDuration; + orb.chargeTick = 0; + orb.anchorPoint = anchorPoint; + + // Store target size and start at 0 for grow effect + orb.targetSize = orb.size; + orb.size = 0.01f; // Start very small + orb.renderCurrentSize = 0.01f; + orb.prevRenderSize = 0.01f; + + // Clear motion while charging + orb.motionX = 0; + orb.motionY = 0; + orb.motionZ = 0; + + return orb; + } + + /** + * Start the orb moving (exit charging mode). + * Called by ability when windup ends. + */ + public void startMoving(EntityLivingBase target) { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Calculate velocity toward target + Entity owner = getOwner(); + + if (target != null) { + double dx = target.posX - posX; + double dy = (target.posY + target.height * 0.5) - posY; + double dz = target.posZ - posZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + motionX = (dx / len) * speed; + motionY = (dy / len) * speed; + motionZ = (dz / len) * speed; + } + } else if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } + } + @Override protected void updateProjectile() { + // Handle charging state (windup phase) + if (isCharging()) { + updateCharging(); + return; + } + if (worldObj.isRemote) { handleClientInterpolation(); } else { @@ -207,6 +335,47 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { } } + /** + * Update during charging state - follow owner based on anchor point. + */ + private void updateCharging() { + chargeTick++; + + Entity owner = getOwner(); + if (owner == null || owner.isDead) { + setDead(); + return; + } + + // Grow size based on charge progress + float progress = getChargeProgress(); + this.size = targetSize * progress; + + // Calculate position based on anchor point + if (owner instanceof EntityLivingBase) { + Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorPoint); + setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + } + } + + /** + * Get the charge progress (0-1). + */ + public float getChargeProgress() { + if (chargeDuration <= 0) return 1.0f; + return Math.min(1.0f, (float) chargeTick / chargeDuration); + } + + /** + * Get interpolated charge progress for smooth rendering. + */ + public float getInterpolatedChargeProgress(float partialTicks) { + if (chargeDuration <= 0) return 1.0f; + float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration); + float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration); + return prevProgress + (currProgress - prevProgress) * partialTicks; + } + // ==================== GETTERS ==================== public float getSpeed() { @@ -238,6 +407,14 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { this.homing = !nbt.hasKey("Homing") || nbt.getBoolean("Homing"); this.homingStrength = nbt.hasKey("HomingStrength") ? nbt.getFloat("HomingStrength") : 0.15f; this.homingRange = nbt.hasKey("HomingRange") ? nbt.getFloat("HomingRange") : 20.0f; + // Charging state + boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); + this.charging = isCharging; + this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); + this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; + this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; + this.anchorPoint = nbt.hasKey("AnchorPoint") ? AnchorPoint.fromId(nbt.getInteger("AnchorPoint")) : AnchorPoint.FRONT; + this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size; } @Override @@ -246,5 +423,11 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { nbt.setBoolean("Homing", homing); nbt.setFloat("HomingStrength", homingStrength); nbt.setFloat("HomingRange", homingRange); + // Charging state + nbt.setBoolean("Charging", isCharging()); + nbt.setInteger("ChargeDuration", chargeDuration); + nbt.setInteger("ChargeTick", chargeTick); + nbt.setInteger("AnchorPoint", anchorPoint.getId()); + nbt.setFloat("TargetSize", targetSize); } } diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java new file mode 100644 index 000000000..7ad4dc581 --- /dev/null +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -0,0 +1,303 @@ +package kamkeel.npcs.util; + +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.util.Vec3; +import noppes.npcs.constants.EnumAnimationPart; +import noppes.npcs.controllers.data.AnimationData; +import noppes.npcs.controllers.data.Frame; +import noppes.npcs.controllers.data.FramePart; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Utility class for calculating world positions based on anchor points. + * Used for positioning charging effects on different body parts. + * Animation-aware: reads arm positions from NPC animation data when available. + */ +public class AnchorPointHelper { + + // Model coordinate scale factor (model coords to blocks) + private static final float MODEL_SCALE = 0.0625f; // 1/16 + + // Default offsets (in blocks) + private static final float ARM_LATERAL_OFFSET = 0.35f; // ~5.5 pixels lateral from center + private static final float ARM_FORWARD_OFFSET = 0.25f; // Arm extended slightly forward + private static final float FRONT_OFFSET = 1.0f; // Distance in front for FRONT anchor + + // Height multipliers relative to eye height + private static final float FRONT_HEIGHT_MULT = 0.7f; + private static final float CENTER_HEIGHT_MULT = 0.5f; + private static final float ARM_HEIGHT_MULT = 0.75f; + private static final float ABOVE_HEAD_HEIGHT_MULT = 1.3f; + private static final float CHEST_HEIGHT_MULT = 0.5f; + + /** + * Calculate the world position for a given anchor point on an entity. + * + * @param entity The entity to calculate the anchor position for + * @param anchor The anchor point type + * @return Vec3 containing the world coordinates for the anchor point + */ + public static Vec3 calculateAnchorPosition(EntityLivingBase entity, AnchorPoint anchor) { + return calculateAnchorPosition(entity, anchor, FRONT_OFFSET); + } + + /** + * Calculate the world position for a given anchor point on an entity. + * For NPCs with active animations, this will read the actual arm positions. + * + * @param entity The entity to calculate the anchor position for + * @param anchor The anchor point type + * @param frontOffset Custom offset distance for FRONT anchor point + * @return Vec3 containing the world coordinates for the anchor point + */ + public static Vec3 calculateAnchorPosition(EntityLivingBase entity, AnchorPoint anchor, float frontOffset) { + double x = entity.posX; + double y = entity.posY; + double z = entity.posZ; + float eyeHeight = entity.getEyeHeight(); + + // Get yaw in radians (use body yaw for arm positioning, head yaw for facing) + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); + float headYaw = (float) Math.toRadians(entity.rotationYawHead); + + switch (anchor) { + case FRONT: + // In front of entity face (use head yaw) + x -= Math.sin(headYaw) * frontOffset; + y += eyeHeight * FRONT_HEIGHT_MULT; + z += Math.cos(headYaw) * frontOffset; + break; + + case CENTER: + // Entity center + y += eyeHeight * CENTER_HEIGHT_MULT; + break; + + case RIGHT_HAND: + // Try to get animated arm position + Vec3 rightArmPos = getAnimatedArmPosition(entity, true); + if (rightArmPos != null) { + return rightArmPos; + } + // Fallback: calculated position using body yaw + x += calculateHandOffsetX(bodyYaw, true); + y += eyeHeight * ARM_HEIGHT_MULT; + z += calculateHandOffsetZ(bodyYaw, true); + break; + + case LEFT_HAND: + // Try to get animated arm position + Vec3 leftArmPos = getAnimatedArmPosition(entity, false); + if (leftArmPos != null) { + return leftArmPos; + } + // Fallback: calculated position using body yaw + x += calculateHandOffsetX(bodyYaw, false); + y += eyeHeight * ARM_HEIGHT_MULT; + z += calculateHandOffsetZ(bodyYaw, false); + break; + + case ABOVE_HEAD: + // Above the entity + y += eyeHeight * ABOVE_HEAD_HEIGHT_MULT; + break; + + case CHEST: + // Chest level, centered + y += eyeHeight * CHEST_HEIGHT_MULT; + break; + } + + return Vec3.createVectorHelper(x, y, z); + } + + /** + * Get the animated arm position from NPC animation data. + * Uses Minecraft's hand positioning logic from RenderNPCHumanMale. + * Hand offset values: (-0.0625, 0.4375, 0.0625) for right hand after arm postRender. + * + * @param entity The entity (must be EntityNPCInterface for animation support) + * @param rightArm True for right arm, false for left arm + * @return Vec3 world position if animation data available, null otherwise + */ + private static Vec3 getAnimatedArmPosition(EntityLivingBase entity, boolean rightArm) { + if (!(entity instanceof EntityNPCInterface)) { + return null; + } + + EntityNPCInterface npc = (EntityNPCInterface) entity; + AnimationData animData = npc.display.animationData; + + if (animData == null || !animData.isActive() || animData.animation == null) { + return null; + } + + Frame frame = (Frame) animData.animation.currentFrame(); + if (frame == null) { + return null; + } + + EnumAnimationPart armPart = rightArm ? EnumAnimationPart.RIGHT_ARM : EnumAnimationPart.LEFT_ARM; + if (!frame.frameParts.containsKey(armPart)) { + return null; + } + + FramePart part = frame.frameParts.get(armPart); + if (part == null) { + return null; + } + + // Ensure interpolation is up to date + part.interpolateOffset(); + part.interpolateAngles(); + + // Get body yaw for world rotation + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); + + // === SHOULDER POSITION (model coordinates, in model units) === + // Right arm pivot: X = -5 (right side), Y = -2 (below head), Z = 0 + // Left arm pivot: X = 5 (left side), Y = -2, Z = 0 + // Note: In Minecraft model coords, Y=0 is at head level, shoulder is at Y=-2 + float shoulderModelX = rightArm ? -5.0f : 5.0f; + float shoulderModelY = -2.0f; + float shoulderModelZ = 0.0f; + + // Add animation pivot offsets (if any) + if (part.prevPivots != null) { + shoulderModelX += part.prevPivots[0]; + shoulderModelY += part.prevPivots[1]; + shoulderModelZ += part.prevPivots[2]; + } + + // === HAND OFFSET IN ARM'S LOCAL SPACE (from RenderNPCHumanMale) === + // These values are in blocks, applied AFTER postRender transforms + // Right: (-0.0625, 0.4375, 0.0625), Left: (0.0625, 0.4375, 0.0625) + float handLocalX = rightArm ? -0.0625f : 0.0625f; + float handLocalY = 0.4375f; // 7 model units down the arm + float handLocalZ = 0.0625f; // Slightly forward + + // === ARM ROTATION ANGLES === + float rotX = part.prevRotations != null ? part.prevRotations[0] : 0; + float rotY = part.prevRotations != null ? part.prevRotations[1] : 0; + float rotZ = part.prevRotations != null ? part.prevRotations[2] : 0; + + // === APPLY ARM ROTATIONS TO HAND OFFSET === + // Rotation order matches ModelRenderer.postRender: Z, Y, X + double hx = handLocalX; + double hy = handLocalY; + double hz = handLocalZ; + + // Rotate around Z axis + double cosZ = Math.cos(rotZ); + double sinZ = Math.sin(rotZ); + double tempX = hx * cosZ - hy * sinZ; + double tempY = hx * sinZ + hy * cosZ; + hx = tempX; + hy = tempY; + + // Rotate around Y axis + double cosY = Math.cos(rotY); + double sinY = Math.sin(rotY); + tempX = hx * cosY + hz * sinY; + double tempZ = -hx * sinY + hz * cosY; + hx = tempX; + hz = tempZ; + + // Rotate around X axis + double cosX = Math.cos(rotX); + double sinX = Math.sin(rotX); + tempY = hy * cosX - hz * sinX; + tempZ = hy * sinX + hz * cosX; + hy = tempY; + hz = tempZ; + + // === COMBINE SHOULDER + ROTATED HAND OFFSET === + // Convert shoulder from model units to blocks + double worldOffsetX = shoulderModelX * MODEL_SCALE + hx; + double worldOffsetY = shoulderModelY * MODEL_SCALE + hy; + double worldOffsetZ = shoulderModelZ * MODEL_SCALE + hz; + + // === ROTATE BY BODY YAW === + double cosYaw = Math.cos(bodyYaw); + double sinYaw = Math.sin(bodyYaw); + double rotatedX = worldOffsetX * cosYaw + worldOffsetZ * sinYaw; + double rotatedZ = -worldOffsetX * sinYaw + worldOffsetZ * cosYaw; + + // === FINAL WORLD POSITION === + // Entity posY is at feet, shoulder height is approximately eyeHeight * 0.9 + double shoulderWorldY = entity.posY + entity.getEyeHeight() * 0.9; + + double worldX = entity.posX + rotatedX; + double worldY = shoulderWorldY + worldOffsetY; + double worldZ = entity.posZ + rotatedZ; + + return Vec3.createVectorHelper(worldX, worldY, worldZ); + } + + /** + * Calculate the X offset for hand positions (fallback when no animation). + * + * @param yaw Entity's body yaw in radians + * @param rightHand True for right hand, false for left hand + * @return X offset in world coordinates + */ + private static double calculateHandOffsetX(float yaw, boolean rightHand) { + // Perpendicular direction (90 degrees from look direction) + // Right hand: +90 degrees, Left hand: -90 degrees + float perpYaw = yaw + (float) (rightHand ? Math.PI / 2 : -Math.PI / 2); + + // Lateral offset (perpendicular to look direction) + double lateralX = Math.sin(perpYaw) * ARM_LATERAL_OFFSET; + + // Forward offset (in look direction) + double forwardX = -Math.sin(yaw) * ARM_FORWARD_OFFSET; + + return lateralX + forwardX; + } + + /** + * Calculate the Z offset for hand positions (fallback when no animation). + * + * @param yaw Entity's body yaw in radians + * @param rightHand True for right hand, false for left hand + * @return Z offset in world coordinates + */ + private static double calculateHandOffsetZ(float yaw, boolean rightHand) { + // Perpendicular direction (90 degrees from look direction) + float perpYaw = yaw + (float) (rightHand ? Math.PI / 2 : -Math.PI / 2); + + // Lateral offset (perpendicular to look direction) + double lateralZ = -Math.cos(perpYaw) * ARM_LATERAL_OFFSET; + + // Forward offset (in look direction) + double forwardZ = Math.cos(yaw) * ARM_FORWARD_OFFSET; + + return lateralZ + forwardZ; + } + + /** + * Get the height multiplier for a given anchor point. + * Useful for external code that needs to know the Y position offset. + * + * @param anchor The anchor point type + * @return Height multiplier relative to eye height + */ + public static float getHeightMultiplier(AnchorPoint anchor) { + switch (anchor) { + case FRONT: + return FRONT_HEIGHT_MULT; + case CENTER: + return CENTER_HEIGHT_MULT; + case RIGHT_HAND: + case LEFT_HAND: + return ARM_HEIGHT_MULT; + case ABOVE_HEAD: + return ABOVE_HEAD_HEIGHT_MULT; + case CHEST: + return CHEST_HEIGHT_MULT; + default: + return FRONT_HEIGHT_MULT; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java index 2a3439af1..33c5372fd 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.type.AbilityDisc; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -92,8 +93,11 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { y += 24; - // Row 7: Effects button - addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); + // Row 7: Max Lifetime + Effects button + addLabel(new GuiNpcLabel(112, "ability.lifetime", labelX, y + 5)); + addTextField(createIntField(112, fieldX, y, 50, disc.getMaxLifetime())); + + addButton(new GuiNpcButton(150, col2LabelX, y, 80, 20, "ability.effects")); } @Override @@ -104,7 +108,13 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 180; int col2FieldX = guiLeft + 260; - // Row 1: Inner Color + Outer Color + // Row 1: Anchor Point + addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); + addButton(new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), disc.getAnchorPoint().getId())); + + y += 24; + + // Row 2: Inner Color + Outer Color addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); String innerHex = String.format("%06X", disc.getInnerColor() & 0xFFFFFF); GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); @@ -197,6 +207,9 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { disc.setLightningEffect(value == 1); initGui(); break; + case 210: + disc.setAnchorPoint(AnchorPoint.fromId(value)); + break; } } @@ -253,6 +266,9 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { case 111: disc.setMaxDistance(parseFloat(field, disc.getMaxDistance())); break; + case 112: + disc.setMaxLifetime(field.getInteger()); + break; } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java index 1c7179deb..626fac6b9 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.type.AbilityEnergyBeam; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -105,7 +106,13 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 180; int col2FieldX = guiLeft + 260; - // Row 1: Inner Color + Outer Color + // Row 1: Anchor Point + addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); + addButton(new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), beam.getAnchorPoint().getId())); + + y += 24; + + // Row 2: Inner Color + Outer Color addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); String innerHex = String.format("%06X", beam.getInnerColor() & 0xFFFFFF); GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); @@ -195,6 +202,9 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { beam.setLightningEffect(value == 1); initGui(); break; + case 210: + beam.setAnchorPoint(AnchorPoint.fromId(value)); + break; } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java index 6c5c598ba..c43569c46 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -95,7 +96,13 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 180; int col2FieldX = guiLeft + 260; - // Row 1: Inner Color + Rotation Speed + // Row 1: Anchor Point + addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); + addButton(new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), orb.getAnchorPoint().getId())); + + y += 24; + + // Row 2: Inner Color + Rotation Speed addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); String innerHex = String.format("%06X", orb.getInnerColor() & 0xFFFFFF); GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); @@ -185,6 +192,9 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { orb.setLightningEffect(value == 1); initGui(); break; + case 210: + orb.setAnchorPoint(AnchorPoint.fromId(value)); + break; } } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index d5dc945ce..01e3ae541 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1683,6 +1683,7 @@ ability.outerWidth=Outer Width ability.lightning=Enable Lightning ability.lightningDensity=Density ability.lightningRadius=Radius +ability.anchorPoint=Anchor Point ability.noVisualSettings=No visual settings for this ability type # Laser Shot Settings From 39b53b0be7424507bbc99c775aefa0450c160285 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 27 Jan 2026 02:03:54 -0500 Subject: [PATCH 293/358] Anchor Points --- .../kamkeel/npcs/util/AnchorPointHelper.java | 256 ++++++++---------- 1 file changed, 115 insertions(+), 141 deletions(-) diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java index 7ad4dc581..5a7e32d28 100644 --- a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -16,13 +16,32 @@ import noppes.npcs.entity.EntityNPCInterface; */ public class AnchorPointHelper { - // Model coordinate scale factor (model coords to blocks) - private static final float MODEL_SCALE = 0.0625f; // 1/16 + // ==================== TUNABLE CONSTANTS ==================== + // These can be adjusted to fine-tune positioning - // Default offsets (in blocks) - private static final float ARM_LATERAL_OFFSET = 0.35f; // ~5.5 pixels lateral from center - private static final float ARM_FORWARD_OFFSET = 0.25f; // Arm extended slightly forward - private static final float FRONT_OFFSET = 1.0f; // Distance in front for FRONT anchor + // Shoulder position in model units (default biped arm rotation points) + private static final float SHOULDER_X_RIGHT = -5.0f; // Right arm is on the right side (-X in model space) + private static final float SHOULDER_X_LEFT = 5.0f; // Left arm is on the left side (+X in model space) + private static final float SHOULDER_Y = 2.0f; // Shoulder height from model origin + private static final float SHOULDER_Z = 0.0f; // Shoulder depth + + // Hand offset in model units (where items are held, relative to shoulder after rotation) + // In arm's local space: X = toward/away from body, Y = down the arm, Z = forward/back + private static float HAND_OFFSET_X_RIGHT = -2.0f; // Slightly toward body for right + private static float HAND_OFFSET_X_LEFT = 2.0f; // Slightly toward body for left + private static float HAND_OFFSET_Y = 14.0f; // Down the arm (arm is ~12 units long) + private static float HAND_OFFSET_Z = 0.0f; // Forward from arm + + // Height of shoulder relative to entity position (feet) + private static final float SHOULDER_HEIGHT_BLOCKS = 1.35f; + + // Model scale factor (1/16 - converts model units to blocks) + private static final float MODEL_SCALE = 0.0625f; + + // Fallback offsets (in blocks) when no animation + private static final float ARM_LATERAL_OFFSET = 0.35f; + private static final float ARM_FORWARD_OFFSET = 0.3f; + private static final float FRONT_OFFSET = 1.0f; // Height multipliers relative to eye height private static final float FRONT_HEIGHT_MULT = 0.7f; @@ -33,10 +52,6 @@ public class AnchorPointHelper { /** * Calculate the world position for a given anchor point on an entity. - * - * @param entity The entity to calculate the anchor position for - * @param anchor The anchor point type - * @return Vec3 containing the world coordinates for the anchor point */ public static Vec3 calculateAnchorPosition(EntityLivingBase entity, AnchorPoint anchor) { return calculateAnchorPosition(entity, anchor, FRONT_OFFSET); @@ -44,12 +59,6 @@ public class AnchorPointHelper { /** * Calculate the world position for a given anchor point on an entity. - * For NPCs with active animations, this will read the actual arm positions. - * - * @param entity The entity to calculate the anchor position for - * @param anchor The anchor point type - * @param frontOffset Custom offset distance for FRONT anchor point - * @return Vec3 containing the world coordinates for the anchor point */ public static Vec3 calculateAnchorPosition(EntityLivingBase entity, AnchorPoint anchor, float frontOffset) { double x = entity.posX; @@ -57,54 +66,44 @@ public class AnchorPointHelper { double z = entity.posZ; float eyeHeight = entity.getEyeHeight(); - // Get yaw in radians (use body yaw for arm positioning, head yaw for facing) - float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); - float headYaw = (float) Math.toRadians(entity.rotationYawHead); + // Body yaw in radians (used for arm positioning) + // Minecraft: yaw 0 = south (+Z), 90 = west (-X), 180 = north (-Z), 270 = east (+X) + float bodyYawRad = (float) Math.toRadians(entity.renderYawOffset); + float headYawRad = (float) Math.toRadians(entity.rotationYawHead); switch (anchor) { case FRONT: // In front of entity face (use head yaw) - x -= Math.sin(headYaw) * frontOffset; + x -= Math.sin(headYawRad) * frontOffset; y += eyeHeight * FRONT_HEIGHT_MULT; - z += Math.cos(headYaw) * frontOffset; + z += Math.cos(headYawRad) * frontOffset; break; case CENTER: - // Entity center y += eyeHeight * CENTER_HEIGHT_MULT; break; case RIGHT_HAND: - // Try to get animated arm position - Vec3 rightArmPos = getAnimatedArmPosition(entity, true); - if (rightArmPos != null) { - return rightArmPos; + Vec3 rightPos = getAnimatedHandPosition(entity, true); + if (rightPos != null) { + return rightPos; } - // Fallback: calculated position using body yaw - x += calculateHandOffsetX(bodyYaw, true); - y += eyeHeight * ARM_HEIGHT_MULT; - z += calculateHandOffsetZ(bodyYaw, true); - break; + // Fallback + return calculateFallbackHandPosition(entity, bodyYawRad, true); case LEFT_HAND: - // Try to get animated arm position - Vec3 leftArmPos = getAnimatedArmPosition(entity, false); - if (leftArmPos != null) { - return leftArmPos; + Vec3 leftPos = getAnimatedHandPosition(entity, false); + if (leftPos != null) { + return leftPos; } - // Fallback: calculated position using body yaw - x += calculateHandOffsetX(bodyYaw, false); - y += eyeHeight * ARM_HEIGHT_MULT; - z += calculateHandOffsetZ(bodyYaw, false); - break; + // Fallback + return calculateFallbackHandPosition(entity, bodyYawRad, false); case ABOVE_HEAD: - // Above the entity y += eyeHeight * ABOVE_HEAD_HEIGHT_MULT; break; case CHEST: - // Chest level, centered y += eyeHeight * CHEST_HEIGHT_MULT; break; } @@ -113,15 +112,10 @@ public class AnchorPointHelper { } /** - * Get the animated arm position from NPC animation data. - * Uses Minecraft's hand positioning logic from RenderNPCHumanMale. - * Hand offset values: (-0.0625, 0.4375, 0.0625) for right hand after arm postRender. - * - * @param entity The entity (must be EntityNPCInterface for animation support) - * @param rightArm True for right arm, false for left arm - * @return Vec3 world position if animation data available, null otherwise + * Calculate hand position using NPC animation data. + * Uses the same transformation logic as the renderer. */ - private static Vec3 getAnimatedArmPosition(EntityLivingBase entity, boolean rightArm) { + private static Vec3 getAnimatedHandPosition(EntityLivingBase entity, boolean rightArm) { if (!(entity instanceof EntityNPCInterface)) { return null; } @@ -152,136 +146,116 @@ public class AnchorPointHelper { part.interpolateOffset(); part.interpolateAngles(); - // Get body yaw for world rotation - float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); + // === STEP 1: Get shoulder position in model space === + float shoulderX = rightArm ? SHOULDER_X_RIGHT : SHOULDER_X_LEFT; + float shoulderY = SHOULDER_Y; + float shoulderZ = SHOULDER_Z; - // === SHOULDER POSITION (model coordinates, in model units) === - // Right arm pivot: X = -5 (right side), Y = -2 (below head), Z = 0 - // Left arm pivot: X = 5 (left side), Y = -2, Z = 0 - // Note: In Minecraft model coords, Y=0 is at head level, shoulder is at Y=-2 - float shoulderModelX = rightArm ? -5.0f : 5.0f; - float shoulderModelY = -2.0f; - float shoulderModelZ = 0.0f; - - // Add animation pivot offsets (if any) + // Add animation pivot offsets if (part.prevPivots != null) { - shoulderModelX += part.prevPivots[0]; - shoulderModelY += part.prevPivots[1]; - shoulderModelZ += part.prevPivots[2]; + shoulderX += part.prevPivots[0]; + shoulderY += part.prevPivots[1]; + shoulderZ += part.prevPivots[2]; } - // === HAND OFFSET IN ARM'S LOCAL SPACE (from RenderNPCHumanMale) === - // These values are in blocks, applied AFTER postRender transforms - // Right: (-0.0625, 0.4375, 0.0625), Left: (0.0625, 0.4375, 0.0625) - float handLocalX = rightArm ? -0.0625f : 0.0625f; - float handLocalY = 0.4375f; // 7 model units down the arm - float handLocalZ = 0.0625f; // Slightly forward + // === STEP 2: Get hand offset in arm's local space === + float handX = rightArm ? HAND_OFFSET_X_RIGHT : HAND_OFFSET_X_LEFT; + float handY = HAND_OFFSET_Y; + float handZ = HAND_OFFSET_Z; - // === ARM ROTATION ANGLES === + // === STEP 3: Apply arm rotations to hand offset (Z → Y → X order) === + // Rotations are in radians (already converted by interpolateAngles) float rotX = part.prevRotations != null ? part.prevRotations[0] : 0; float rotY = part.prevRotations != null ? part.prevRotations[1] : 0; float rotZ = part.prevRotations != null ? part.prevRotations[2] : 0; - // === APPLY ARM ROTATIONS TO HAND OFFSET === - // Rotation order matches ModelRenderer.postRender: Z, Y, X - double hx = handLocalX; - double hy = handLocalY; - double hz = handLocalZ; + // Start with hand offset + double hx = handX; + double hy = handY; + double hz = handZ; - // Rotate around Z axis + // Apply Z rotation first double cosZ = Math.cos(rotZ); double sinZ = Math.sin(rotZ); - double tempX = hx * cosZ - hy * sinZ; - double tempY = hx * sinZ + hy * cosZ; - hx = tempX; - hy = tempY; + double newX = hx * cosZ - hy * sinZ; + double newY = hx * sinZ + hy * cosZ; + hx = newX; + hy = newY; - // Rotate around Y axis + // Apply Y rotation double cosY = Math.cos(rotY); double sinY = Math.sin(rotY); - tempX = hx * cosY + hz * sinY; - double tempZ = -hx * sinY + hz * cosY; - hx = tempX; - hz = tempZ; + newX = hx * cosY + hz * sinY; + double newZ = -hx * sinY + hz * cosY; + hx = newX; + hz = newZ; - // Rotate around X axis + // Apply X rotation double cosX = Math.cos(rotX); double sinX = Math.sin(rotX); - tempY = hy * cosX - hz * sinX; - tempZ = hy * sinX + hz * cosX; - hy = tempY; - hz = tempZ; + newY = hy * cosX - hz * sinX; + newZ = hy * sinX + hz * cosX; + hy = newY; + hz = newZ; - // === COMBINE SHOULDER + ROTATED HAND OFFSET === - // Convert shoulder from model units to blocks - double worldOffsetX = shoulderModelX * MODEL_SCALE + hx; - double worldOffsetY = shoulderModelY * MODEL_SCALE + hy; - double worldOffsetZ = shoulderModelZ * MODEL_SCALE + hz; + // === STEP 4: Combine shoulder + rotated hand offset (in model units) === + double modelX = shoulderX + hx; + double modelY = shoulderY + hy; + double modelZ = shoulderZ + hz; - // === ROTATE BY BODY YAW === + // === STEP 5: Convert to blocks === + double blockX = modelX * MODEL_SCALE; + double blockY = modelY * MODEL_SCALE; + double blockZ = modelZ * MODEL_SCALE; + + // === STEP 6: Apply body yaw rotation === + // In model space: +X = left, -X = right, +Z = back, -Z = front + // Need to rotate to align with entity's facing direction + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); double cosYaw = Math.cos(bodyYaw); double sinYaw = Math.sin(bodyYaw); - double rotatedX = worldOffsetX * cosYaw + worldOffsetZ * sinYaw; - double rotatedZ = -worldOffsetX * sinYaw + worldOffsetZ * cosYaw; - // === FINAL WORLD POSITION === - // Entity posY is at feet, shoulder height is approximately eyeHeight * 0.9 - double shoulderWorldY = entity.posY + entity.getEyeHeight() * 0.9; + // Rotate around Y axis (body rotation) + double worldOffsetX = blockX * cosYaw + blockZ * sinYaw; + double worldOffsetZ = -blockX * sinYaw + blockZ * cosYaw; - double worldX = entity.posX + rotatedX; - double worldY = shoulderWorldY + worldOffsetY; - double worldZ = entity.posZ + rotatedZ; + // === STEP 7: Final world position === + double worldX = entity.posX + worldOffsetX; + double worldY = entity.posY + SHOULDER_HEIGHT_BLOCKS - blockY; // Subtract because +Y in model is down + double worldZ = entity.posZ + worldOffsetZ; return Vec3.createVectorHelper(worldX, worldY, worldZ); } /** - * Calculate the X offset for hand positions (fallback when no animation). - * - * @param yaw Entity's body yaw in radians - * @param rightHand True for right hand, false for left hand - * @return X offset in world coordinates + * Calculate fallback hand position when no animation is active. */ - private static double calculateHandOffsetX(float yaw, boolean rightHand) { - // Perpendicular direction (90 degrees from look direction) - // Right hand: +90 degrees, Left hand: -90 degrees - float perpYaw = yaw + (float) (rightHand ? Math.PI / 2 : -Math.PI / 2); + private static Vec3 calculateFallbackHandPosition(EntityLivingBase entity, float bodyYawRad, boolean rightHand) { + double x = entity.posX; + double y = entity.posY; + double z = entity.posZ; + float eyeHeight = entity.getEyeHeight(); - // Lateral offset (perpendicular to look direction) - double lateralX = Math.sin(perpYaw) * ARM_LATERAL_OFFSET; + // Perpendicular direction for lateral offset + float perpYaw = bodyYawRad + (float) (rightHand ? Math.PI / 2 : -Math.PI / 2); - // Forward offset (in look direction) - double forwardX = -Math.sin(yaw) * ARM_FORWARD_OFFSET; + // Lateral offset (perpendicular to body facing) + double lateralX = -Math.sin(perpYaw) * ARM_LATERAL_OFFSET; + double lateralZ = Math.cos(perpYaw) * ARM_LATERAL_OFFSET; - return lateralX + forwardX; - } + // Forward offset (in facing direction) + double forwardX = -Math.sin(bodyYawRad) * ARM_FORWARD_OFFSET; + double forwardZ = Math.cos(bodyYawRad) * ARM_FORWARD_OFFSET; - /** - * Calculate the Z offset for hand positions (fallback when no animation). - * - * @param yaw Entity's body yaw in radians - * @param rightHand True for right hand, false for left hand - * @return Z offset in world coordinates - */ - private static double calculateHandOffsetZ(float yaw, boolean rightHand) { - // Perpendicular direction (90 degrees from look direction) - float perpYaw = yaw + (float) (rightHand ? Math.PI / 2 : -Math.PI / 2); + x += lateralX + forwardX; + y += eyeHeight * ARM_HEIGHT_MULT; + z += lateralZ + forwardZ; - // Lateral offset (perpendicular to look direction) - double lateralZ = -Math.cos(perpYaw) * ARM_LATERAL_OFFSET; - - // Forward offset (in look direction) - double forwardZ = Math.cos(yaw) * ARM_FORWARD_OFFSET; - - return lateralZ + forwardZ; + return Vec3.createVectorHelper(x, y, z); } /** * Get the height multiplier for a given anchor point. - * Useful for external code that needs to know the Y position offset. - * - * @param anchor The anchor point type - * @return Height multiplier relative to eye height */ public static float getHeightMultiplier(AnchorPoint anchor) { switch (anchor) { From 93486d306b86795d9f66a363a5234e40be6be2d0 Mon Sep 17 00:00:00 2001 From: Vidal Date: Tue, 27 Jan 2026 04:36:12 -0300 Subject: [PATCH 294/358] Made animation fetching methods return Animation instead of IDs --- .../controllers/data/ability/Ability.java | 14 +++++++--- src/main/java/noppes/npcs/DataAbilities.java | 27 +++++++++++-------- .../gui/advanced/SubGuiAbilityConfig.java | 4 +-- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index d7a8b9eab..02d2227ad 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -17,6 +17,8 @@ import noppes.npcs.api.INbt; import noppes.npcs.api.ability.IAbility; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; @@ -927,16 +929,20 @@ public abstract class Ability implements IAbility { this.activeSound = activeSound; } - public int getWindUpAnimationId() { - return windUpAnimationId; + public Animation getWindUpAnimation() { + if (AnimationController.Instance == null) return null; + + return (Animation) AnimationController.Instance.get(windUpAnimationId); } public void setWindUpAnimationId(int windUpAnimationId) { this.windUpAnimationId = windUpAnimationId; } - public int getActiveAnimationId() { - return activeAnimationId; + public Animation getActiveAnimation() { + if (AnimationController.Instance == null) return null; + + return (Animation) AnimationController.Instance.get(activeAnimationId); } public void setActiveAnimationId(int activeAnimationId) { diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 246ebed46..a9c670647 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -154,7 +154,7 @@ public class DataAbilities { // Play active sound and animation playAbilitySound(currentAbility.getActiveSound()); - playAbilityAnimation(currentAbility.getActiveAnimationId()); + playAbilityAnimation(currentAbility.getActiveAnimation()); // Fire execute event (cancelable) AbilityEvent.ExecuteEvent executeEvent = new AbilityEvent.ExecuteEvent( @@ -359,7 +359,7 @@ public class DataAbilities { playAbilitySound(ability.getWindUpSound()); // Play wind up animation if configured - playAbilityAnimation(ability.getWindUpAnimationId()); + playAbilityAnimation(ability.getWindUpAnimation()); return true; } @@ -377,18 +377,23 @@ public class DataAbilities { * Play an animation on the NPC by ID. * Public so abilities can trigger animations directly if needed. * - * @param animationId The global animation ID to play, or -1 for none + * @param animation The animation to play, or null if none */ - public void playAbilityAnimation(int animationId) { - if (animationId < 0) return; + public void playAbilityAnimation(Animation animation) { + if (animation == null || animation.id < 0) return; if (AnimationController.Instance == null) return; - Animation animation = AnimationController.Instance.animations.get(animationId); - if (animation != null) { - npc.display.animationData.setEnabled(true); - npc.display.animationData.setAnimation(animation); - npc.display.animationData.updateClient(); - } + npc.display.animationData.setEnabled(true); + npc.display.animationData.setAnimation(animation); + npc.display.animationData.updateClient(); + } + + public void playAbilityAnimation(int animation) { + if (animation < 0) return; + if (AnimationController.Instance == null) return; + if (AnimationController.Instance.get(animation) == null) return; + + playAbilityAnimation((Animation) AnimationController.Instance.get(animation)); } /** diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index 5f03cbd9f..6c5323707 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -127,8 +127,8 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Effects this.windUpSound = ability.getWindUpSound(); this.activeSound = ability.getActiveSound(); - this.windUpAnimationId = ability.getWindUpAnimationId(); - this.activeAnimationId = ability.getActiveAnimationId(); + this.windUpAnimationId = ability.getWindUpAnimation() != null ? ability.getWindUpAnimation().getID() : -1; + this.activeAnimationId = ability.getActiveAnimation() != null ? ability.getActiveAnimation().getID() : -1; // Telegraph this.showTelegraph = ability.isShowTelegraph(); From 747aae833749e346bd4bb8fa6151eb9272047f89 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 27 Jan 2026 04:07:02 -0500 Subject: [PATCH 295/358] Recovery -> Dazed. Better cooldown logic handling --- .../kamkeel/npcs/command/AbilityCommand.java | 5 +- .../controllers/data/ability/Ability.java | 89 ++--- .../data/ability/AbilityAnimation.java | 6 +- .../data/ability/AbilityPhase.java | 2 +- .../data/ability/type/AbilityCharge.java | 6 +- .../data/ability/type/AbilityCutter.java | 20 +- .../data/ability/type/AbilityDash.java | 6 +- .../data/ability/type/AbilityDisc.java | 46 ++- .../data/ability/type/AbilityEnergyBeam.java | 46 ++- .../data/ability/type/AbilityGuard.java | 20 +- .../data/ability/type/AbilityHazard.java | 21 +- .../data/ability/type/AbilityHeal.java | 31 +- .../data/ability/type/AbilityHeavyHit.java | 12 +- .../data/ability/type/AbilityLaserShot.java | 13 +- .../data/ability/type/AbilityOrb.java | 46 ++- .../data/ability/type/AbilityProjectile.java | 7 +- .../data/ability/type/AbilityShockwave.java | 27 +- .../data/ability/type/AbilitySlam.java | 5 +- .../data/ability/type/AbilitySweeper.java | 46 ++- .../data/ability/type/AbilityTeleport.java | 14 +- .../data/ability/type/AbilityTrap.java | 22 +- .../data/ability/type/AbilityVortex.java | 6 +- .../kamkeel/npcs/util/AnchorPointHelper.java | 317 ++++++++++-------- src/main/java/noppes/npcs/DataAbilities.java | 51 +-- .../gui/advanced/SubGuiAbilityConfig.java | 47 +-- .../advanced/ability/SubGuiAbilityCutter.java | 15 +- .../advanced/ability/SubGuiAbilityGuard.java | 14 +- .../advanced/ability/SubGuiAbilityHazard.java | 23 +- .../advanced/ability/SubGuiAbilityHeal.java | 26 +- .../advanced/ability/SubGuiAbilityTrap.java | 23 +- 30 files changed, 577 insertions(+), 435 deletions(-) diff --git a/src/main/java/kamkeel/npcs/command/AbilityCommand.java b/src/main/java/kamkeel/npcs/command/AbilityCommand.java index 4a783f551..cf5e1ca58 100644 --- a/src/main/java/kamkeel/npcs/command/AbilityCommand.java +++ b/src/main/java/kamkeel/npcs/command/AbilityCommand.java @@ -119,8 +119,9 @@ public class AbilityCommand extends CommandKamkeelBase { sendResult(sender, " Type: \u00A7d" + ability.getTypeId()); sendResult(sender, " Cooldown: \u00A7e" + ability.getCooldownTicks() + " ticks"); sendResult(sender, " Wind Up: \u00A7e" + ability.getWindUpTicks() + " ticks"); - sendResult(sender, " Active: \u00A7e" + ability.getActiveTicks() + " ticks"); - sendResult(sender, " Recovery: \u00A7e" + ability.getRecoveryTicks() + " ticks"); + if (ability.isInterruptible()) { + sendResult(sender, " Dazed: \u00A7e" + ability.getDazedTicks() + " ticks"); + } sendResult(sender, " Range: \u00A7e" + ability.getMinRange() + " - " + ability.getMaxRange()); sendResult(sender, " Telegraph: \u00A7e" + (ability.isShowTelegraph() ? "Yes" : "No")); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 02d2227ad..e5dc7e493 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -47,18 +47,13 @@ public abstract class Ability implements IAbility { protected float maxRange = 20; // Timing (ticks) - protected int cooldownTicks = 100; + protected int cooldownTicks = 0; // Added to global cooldown after ability completes protected int windUpTicks = 20; - protected int activeTicks = 10; - protected int recoveryTicks = 20; + protected int dazedTicks = 80; // Only used when interrupted during WINDUP (if interruptible) // Interruption protected boolean interruptible = true; - // Wait for completion - if true, ability controls when active phase ends (ignores activeTicks) - // Used for abilities like Slam that need to wait for landing, or Beam that runs until entity dies - protected boolean waitForCompletion = false; - // Feedback protected boolean lockMovement = true; protected int windUpColor = 0x80FF4400; // Telegraph color during wind up @@ -507,11 +502,8 @@ public abstract class Ability implements IAbility { /** * Tick the ability forward. Called every game tick while executing. *

- * Normal flow: WINDUP -> ACTIVE -> IDLE (no recovery in normal completion) - * Interrupted flow: WINDUP -> RECOVERY -> IDLE (recovery only on interrupt) - *

- * If waitForCompletion is true, the ability controls when active ends - * by calling signalCompletion() instead of using activeTicks. + * Normal flow: WINDUP -> ACTIVE -> IDLE + * Interrupted flow: WINDUP -> DAZED -> IDLE (dazed only on interrupt during WINDUP) * * @return true if phase changed this tick */ @@ -529,18 +521,13 @@ public abstract class Ability implements IAbility { } break; case ACTIVE: - // If waitForCompletion is true, ability must call signalCompletion() - // Otherwise, auto-complete when activeTicks is reached - if (!waitForCompletion && currentTick >= activeTicks) { - phase = AbilityPhase.IDLE; - currentTick = 0; - return true; - } + // Abilities control their own completion by calling signalCompletion() + // No auto-complete - each ability determines when it's done break; - case RECOVERY: - // Recovery phase is only entered on interrupt - // Count down recoveryTicks then go to IDLE - if (currentTick >= recoveryTicks) { + case DAZED: + // Dazed phase is only entered on interrupt during WINDUP + // NPC cannot attack during this time + if (currentTick >= dazedTicks) { phase = AbilityPhase.IDLE; currentTick = 0; return true; @@ -552,13 +539,14 @@ public abstract class Ability implements IAbility { /** * Signal that the ability has completed its active phase. - * Called by abilities with waitForCompletion=true when they are done. + * Called by abilities when they are done. * For example, AbilitySlam calls this when the NPC lands. * * @return true if phase changed to IDLE */ public boolean signalCompletion() { if (phase == AbilityPhase.ACTIVE) { + cleanup(); phase = AbilityPhase.IDLE; currentTick = 0; return true; @@ -567,13 +555,14 @@ public abstract class Ability implements IAbility { } /** - * Interrupt this ability. Transitions to RECOVERY phase if interruptible. + * Interrupt this ability. Transitions to DAZED phase if interruptible during WINDUP. * Called when NPC takes direct damage during WINDUP. */ public void interrupt() { + cleanup(); if (interruptible && phase == AbilityPhase.WINDUP) { - // Interrupted during windup - go to recovery (dazed state) - phase = AbilityPhase.RECOVERY; + // Interrupted during windup - go to dazed state + phase = AbilityPhase.DAZED; currentTick = 0; } else { // Force stop - go directly to IDLE @@ -624,15 +613,25 @@ public abstract class Ability implements IAbility { } /** - * Reset all execution state (called on combat end) + * Reset all execution state (called on combat end, NPC death, target lost, etc.) */ public void reset() { + cleanup(); phase = AbilityPhase.IDLE; currentTick = 0; currentTarget = null; telegraphInstance = null; } + /** + * Cleanup any resources created by this ability. + * Override this to kill spawned entities, cancel telegraphs, etc. + * Called when ability ends for any reason (completion, interrupt, reset, NPC death). + */ + public void cleanup() { + // Override in subclasses to clean up spawned entities, etc. + } + // ═══════════════════════════════════════════════════════════════════ // CONDITION CHECKING // ═══════════════════════════════════════════════════════════════════ @@ -661,10 +660,8 @@ public abstract class Ability implements IAbility { nbt.setFloat("maxRange", maxRange); nbt.setInteger("cooldown", cooldownTicks); nbt.setInteger("windUp", windUpTicks); - nbt.setInteger("active", activeTicks); - nbt.setInteger("recovery", recoveryTicks); + nbt.setInteger("recovery", dazedTicks); nbt.setBoolean("interruptible", interruptible); - nbt.setBoolean("waitForCompletion", waitForCompletion); nbt.setBoolean("lockMovement", lockMovement); nbt.setInteger("windUpColor", windUpColor); nbt.setInteger("activeColor", activeColor); @@ -708,10 +705,8 @@ public abstract class Ability implements IAbility { maxRange = nbt.getFloat("maxRange"); cooldownTicks = nbt.getInteger("cooldown"); windUpTicks = nbt.getInteger("windUp"); - activeTicks = nbt.getInteger("active"); - recoveryTicks = nbt.getInteger("recovery"); + dazedTicks = nbt.getInteger("recovery"); interruptible = nbt.getBoolean("interruptible"); - waitForCompletion = nbt.getBoolean("waitForCompletion"); lockMovement = nbt.getBoolean("lockMovement"); // Support old telegraphColor key for backwards compatibility if (nbt.hasKey("windUpColor")) { @@ -857,20 +852,12 @@ public abstract class Ability implements IAbility { this.windUpTicks = Math.max(0, windUpTicks); } - public int getActiveTicks() { - return activeTicks; + public int getDazedTicks() { + return dazedTicks; } - public void setActiveTicks(int activeTicks) { - this.activeTicks = Math.max(1, activeTicks); - } - - public int getRecoveryTicks() { - return recoveryTicks; - } - - public void setRecoveryTicks(int recoveryTicks) { - this.recoveryTicks = Math.max(0, recoveryTicks); + public void setDazedTicks(int dazedTicks) { + this.dazedTicks = Math.max(0, dazedTicks); } public boolean isInterruptible() { @@ -881,14 +868,6 @@ public abstract class Ability implements IAbility { this.interruptible = interruptible; } - public boolean isWaitForCompletion() { - return waitForCompletion; - } - - public void setWaitForCompletion(boolean waitForCompletion) { - this.waitForCompletion = waitForCompletion; - } - public boolean isLockMovement() { return lockMovement; } @@ -978,7 +957,7 @@ public abstract class Ability implements IAbility { } /** - * Get the current execution phase as int (0=IDLE, 1=WINDUP, 2=ACTIVE, 3=RECOVERY). + * Get the current execution phase as int (0=IDLE, 1=WINDUP, 2=ACTIVE, 3=DAZED). * Required for IAbility interface. */ @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java index 4b7f2ba3e..3aea051f3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java @@ -20,7 +20,7 @@ public enum AbilityAnimation {; animationNames.put(IDLE, idle); animationNames.put(WINDUP, windUp); animationNames.put(ACTIVE, active); - animationNames.put(RECOVERY, recovery); + animationNames.put(DAZED, recovery); } AbilityAnimation(String idle, String windUp, String active) { @@ -80,8 +80,8 @@ public enum AbilityAnimation {; return getAnimation(ACTIVE); } - public IAnimation recovery() { - return getAnimation(RECOVERY); + public IAnimation dazed() { + return getAnimation(DAZED); } private File getFileFromName(String fileName) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java index 6338279ba..0611a0274 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityPhase.java @@ -4,5 +4,5 @@ public enum AbilityPhase { IDLE, // Not executing WINDUP, // Charging up (telegraph visible) ACTIVE, // Executing (damage happens) - RECOVERY // Finishing animation + DAZED // Stunned after interrupt during WINDUP (cannot attack) } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 696367ce8..7a1d9babf 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -46,10 +46,8 @@ public class AbilityCharge extends Ability { this.maxRange = 20.0f; this.minRange = 4.0f; this.lockMovement = false; // Movement IS the ability - this.cooldownTicks = 80; + this.cooldownTicks = 0; this.windUpTicks = 20; - this.activeTicks = 40; - this.recoveryTicks = 10; // LINE telegraph showing charge path this.telegraphType = TelegraphType.LINE; this.showTelegraph = true; @@ -161,11 +159,13 @@ public class AbilityCharge extends Ability { npc.motionX = 0; npc.motionZ = 0; npc.velocityChanged = true; + signalCompletion(); return; } if (isChargeBlocked(npc)) { stopMomentum(npc); + signalCompletion(); return; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index 8274b4d79..b3052eeac 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -38,6 +38,7 @@ public class AbilityCutter extends Ability { private SweepMode sweepMode = SweepMode.SWIPE; private float sweepSpeed = 6.0f; + private int spinDurationTicks = 60; // Duration for SPIN mode private boolean piercing = true; private float innerRadius = 0.0f; @@ -52,10 +53,8 @@ public class AbilityCutter extends Ability { this.targetingMode = TargetingMode.AOE_SELF; this.maxRange = 8.0f; this.lockMovement = true; - this.cooldownTicks = 80; + this.cooldownTicks = 0; this.windUpTicks = 20; - this.activeTicks = 15; - this.recoveryTicks = 15; this.telegraphType = TelegraphType.CONE; this.windUpSound = "random.bow"; this.activeSound = "random.break"; @@ -110,6 +109,7 @@ public class AbilityCutter extends Ability { switch (sweepMode) { case SWIPE: if (currentRotation > arcAngle / 2.0f) { + signalCompletion(); // Swipe arc complete return; } performSweepDamage(npc, world, innerRadius, range, currentRotation); @@ -117,6 +117,10 @@ public class AbilityCutter extends Ability { break; case SPIN: + if (tick >= spinDurationTicks) { + signalCompletion(); // Spin duration complete + return; + } currentRotation = (currentRotation + sweepSpeed) % 360.0f; hitEntities.clear(); performSweepDamage(npc, world, innerRadius, range, currentRotation); @@ -195,6 +199,7 @@ public class AbilityCutter extends Ability { nbt.setFloat("knockback", knockback); nbt.setString("sweepMode", sweepMode.name()); nbt.setFloat("sweepSpeed", sweepSpeed); + nbt.setInteger("spinDurationTicks", spinDurationTicks); nbt.setBoolean("piercing", piercing); nbt.setFloat("innerRadius", innerRadius); } @@ -211,6 +216,7 @@ public class AbilityCutter extends Ability { this.sweepMode = SweepMode.SWIPE; } this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 6.0f; + this.spinDurationTicks = nbt.hasKey("spinDurationTicks") ? nbt.getInteger("spinDurationTicks") : 60; this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing"); this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f; } @@ -264,6 +270,14 @@ public class AbilityCutter extends Ability { this.sweepSpeed = sweepSpeed; } + public int getSpinDurationTicks() { + return spinDurationTicks; + } + + public void setSpinDurationTicks(int spinDurationTicks) { + this.spinDurationTicks = Math.max(1, spinDurationTicks); + } + public boolean isPiercing() { return piercing; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 4edc18f82..586e402a8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -87,10 +87,8 @@ public class AbilityDash extends Ability { this.maxRange = 20.0f; this.minRange = 0.0f; this.lockMovement = false; - this.cooldownTicks = 40; + this.cooldownTicks = 0; this.windUpTicks = 5; - this.activeTicks = 8; - this.recoveryTicks = 10; // No telegraph for dash - it's a quick evasive move this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; @@ -174,6 +172,7 @@ public class AbilityDash extends Ability { npc.motionX = 0; npc.motionZ = 0; npc.velocityChanged = true; + signalCompletion(); return; } @@ -181,6 +180,7 @@ public class AbilityDash extends Ability { npc.motionX = 0; npc.motionZ = 0; npc.velocityChanged = true; + signalCompletion(); return; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 95dcd4afb..67fa23cdf 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -72,7 +72,7 @@ public class AbilityDisc extends Ability { private AnchorPoint anchorPoint = AnchorPoint.FRONT; // Transient state for disc entity (used during windup charging) - private transient int discEntityId = -1; + private transient EntityAbilityDisc discEntity = null; public AbilityDisc() { this.typeId = "ability.cnpc.disc"; @@ -80,10 +80,8 @@ public class AbilityDisc extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 30.0f; this.minRange = 5.0f; - this.cooldownTicks = 80; + this.cooldownTicks = 0; this.windUpTicks = 20; - this.activeTicks = 1; - this.recoveryTicks = 10; this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; } @@ -111,17 +109,19 @@ public class AbilityDisc extends Ability { @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (world.isRemote) return; - - // Find and start moving the disc that was spawned during windup - if (discEntityId != -1) { - Entity entity = world.getEntityByID(discEntityId); - if (entity instanceof EntityAbilityDisc) { - EntityAbilityDisc disc = (EntityAbilityDisc) entity; - disc.startMoving(target); - } - discEntityId = -1; + if (world.isRemote) { + signalCompletion(); + return; } + + // Start moving the disc that was spawned during windup + if (discEntity != null && !discEntity.isDead) { + discEntity.startMoving(target); + } + discEntity = null; + + // Disc entity manages itself - ability is done + signalCompletion(); } @Override @@ -131,7 +131,7 @@ public class AbilityDisc extends Ability { // Spawn disc in charging mode on first tick of windup if (tick == 1) { // Create disc in charging mode - follows NPC based on anchor point during windup - EntityAbilityDisc disc = EntityAbilityDisc.createCharging( + discEntity = EntityAbilityDisc.createCharging( world, npc, target, discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, @@ -145,8 +145,7 @@ public class AbilityDisc extends Ability { windUpTicks // Charge duration = windup duration ); - world.spawnEntityInWorld(disc); - discEntityId = disc.getEntityId(); + world.spawnEntityInWorld(discEntity); } } @@ -159,15 +158,12 @@ public class AbilityDisc extends Ability { } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - // Despawn charging disc if interrupted during windup - if (discEntityId != -1) { - Entity entity = npc.worldObj.getEntityByID(discEntityId); - if (entity != null) { - entity.setDead(); - } - discEntityId = -1; + public void cleanup() { + // Despawn disc entity if still alive + if (discEntity != null && !discEntity.isDead) { + discEntity.setDead(); } + discEntity = null; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 18969b43c..3bcc5072c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -71,7 +71,7 @@ public class AbilityEnergyBeam extends Ability { private AnchorPoint anchorPoint = AnchorPoint.FRONT; // Transient state for beam entity (used during windup charging) - private transient int beamEntityId = -1; + private transient EntityAbilityBeam beamEntity = null; public AbilityEnergyBeam() { this.typeId = "ability.cnpc.beam"; @@ -79,10 +79,8 @@ public class AbilityEnergyBeam extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 20.0f; this.minRange = 5.0f; - this.cooldownTicks = 150; + this.cooldownTicks = 0; this.windUpTicks = 40; - this.activeTicks = 200; // Match maxLifetime to keep NPC locked while beam is active - this.recoveryTicks = 5; this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; } @@ -110,17 +108,19 @@ public class AbilityEnergyBeam extends Ability { @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (world.isRemote) return; - - // Find and start firing the beam that was spawned during windup - if (beamEntityId != -1) { - Entity entity = world.getEntityByID(beamEntityId); - if (entity instanceof EntityAbilityBeam) { - EntityAbilityBeam beam = (EntityAbilityBeam) entity; - beam.startFiring(target); - } - beamEntityId = -1; + if (world.isRemote) { + signalCompletion(); + return; } + + // Start firing the beam that was spawned during windup + if (beamEntity != null && !beamEntity.isDead) { + beamEntity.startFiring(target); + } + beamEntity = null; + + // Beam entity manages itself - ability is done + signalCompletion(); } @Override @@ -132,7 +132,7 @@ public class AbilityEnergyBeam extends Ability { float offsetDist = 1.0f; // Create beam in charging mode - follows NPC based on anchor point during windup - EntityAbilityBeam beam = EntityAbilityBeam.createCharging( + beamEntity = EntityAbilityBeam.createCharging( world, npc, target, beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, @@ -147,8 +147,7 @@ public class AbilityEnergyBeam extends Ability { anchorPoint // Anchor point for charging position ); - world.spawnEntityInWorld(beam); - beamEntityId = beam.getEntityId(); + world.spawnEntityInWorld(beamEntity); } } @@ -161,15 +160,12 @@ public class AbilityEnergyBeam extends Ability { } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - // Despawn charging beam if interrupted during windup - if (beamEntityId != -1) { - Entity entity = npc.worldObj.getEntityByID(beamEntityId); - if (entity != null) { - entity.setDead(); - } - beamEntityId = -1; + public void cleanup() { + // Despawn beam entity if still alive + if (beamEntity != null && !beamEntity.isDead) { + beamEntity.setDead(); } + beamEntity = null; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index 404589c6e..831ef97fd 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -25,6 +25,7 @@ public class AbilityGuard extends Ability { private static final Random RANDOM = new Random(); // Type-specific parameters + private int durationTicks = 60; private float damageReduction = 0.5f; private boolean canCounter = false; private CounterType counterType = CounterType.FLAT; @@ -48,10 +49,8 @@ public class AbilityGuard extends Ability { this.name = "Guard"; this.targetingMode = TargetingMode.SELF; this.lockMovement = true; - this.cooldownTicks = 120; + this.cooldownTicks = 0; this.windUpTicks = 10; - this.activeTicks = 60; - this.recoveryTicks = 10; this.interruptible = false; // Hard to interrupt while guarding // No telegraph for guard - it's a defensive stance this.telegraphType = TelegraphType.NONE; @@ -95,6 +94,11 @@ public class AbilityGuard extends Ability { if (canCounter && counterTriggered && lastAttacker != null && !world.isRemote) { performCounter(npc, world); } + + // Check if guard duration has ended + if (tick >= durationTicks) { + signalCompletion(); + } } /** @@ -175,6 +179,7 @@ public class AbilityGuard extends Ability { @Override public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); nbt.setFloat("damageReduction", damageReduction); nbt.setBoolean("canCounter", canCounter); nbt.setString("counterType", counterType.name()); @@ -186,6 +191,7 @@ public class AbilityGuard extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 60; this.damageReduction = nbt.hasKey("damageReduction") ? nbt.getFloat("damageReduction") : 0.5f; this.canCounter = nbt.hasKey("canCounter") && nbt.getBoolean("canCounter"); try { @@ -206,6 +212,14 @@ public class AbilityGuard extends Ability { } // Getters & Setters + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + public float getDamageReduction() { return damageReduction; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 3161c5dbe..3e9998f6f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -42,6 +42,7 @@ public class AbilityHazard extends Ability { FOLLOW_TARGET } + private int durationTicks = 100; private float radius = 4.0f; private float innerRadius = 0.0f; private float coneAngle = 45.0f; @@ -81,10 +82,8 @@ public class AbilityHazard extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 15.0f; this.lockMovement = true; - this.cooldownTicks = 200; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 100; - this.recoveryTicks = 10; this.telegraphType = TelegraphType.CIRCLE; } @@ -232,6 +231,12 @@ public class AbilityHazard extends Ability { public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { if (world.isRemote) return; + // Check if hazard duration has ended + if (tick >= durationTicks) { + signalCompletion(); + return; + } + damagedThisTick.clear(); ticksSinceDamage++; @@ -342,6 +347,7 @@ public class AbilityHazard extends Ability { @Override public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); nbt.setFloat("radius", radius); nbt.setFloat("innerRadius", innerRadius); nbt.setFloat("coneAngle", coneAngle); @@ -366,6 +372,7 @@ public class AbilityHazard extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 100; this.radius = nbt.hasKey("radius") ? nbt.getFloat("radius") : 4.0f; this.innerRadius = nbt.hasKey("innerRadius") ? nbt.getFloat("innerRadius") : 0.0f; this.coneAngle = nbt.hasKey("coneAngle") ? nbt.getFloat("coneAngle") : 45.0f; @@ -404,6 +411,14 @@ public class AbilityHazard extends Ability { } // Getters & Setters + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + public float getRadius() { return radius; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index 3cf04db2c..59ef4751a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -25,6 +25,7 @@ import java.util.List; public class AbilityHeal extends Ability { // Type-specific parameters + private int durationTicks = 60; private float healAmount = 10.0f; private float healPercent = 0.0f; private boolean healSelf = true; @@ -40,10 +41,8 @@ public class AbilityHeal extends Ability { this.name = "Heal"; this.targetingMode = TargetingMode.SELF; this.lockMovement = true; - this.cooldownTicks = 200; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 10; - this.recoveryTicks = 10; // No telegraph for heal - it's a self/ally buff this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; @@ -92,6 +91,9 @@ public class AbilityHeal extends Ability { healEntity(ally); spawnHealParticles(world, ally); } + + // Instant heal completes immediately + signalCompletion(); } // If not instant, HoT healing is applied in onActiveTick } @@ -100,16 +102,16 @@ public class AbilityHeal extends Ability { public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { if (world.isRemote || instantHeal) return; - // Heal over time - distribute heal across active ticks + // Heal over time - distribute heal across duration ticks if (tick % 10 == 0) { // Calculate tick-based heal for fixed amount - float tickHeal = (healAmount / (float) activeTicks) * 10; + float tickHeal = (healAmount / (float) durationTicks) * 10; if (healSelf) { float selfTickHeal = tickHeal; // Add percentage-based heal portion if (healPercent > 0) { - selfTickHeal += (npc.getMaxHealth() * healPercent / (float) activeTicks) * 10; + selfTickHeal += (npc.getMaxHealth() * healPercent / (float) durationTicks) * 10; } npc.heal(selfTickHeal); if (tick % 20 == 0) { @@ -123,7 +125,7 @@ public class AbilityHeal extends Ability { float allyTickHeal = tickHeal; // Add percentage-based heal portion for each ally if (healPercent > 0) { - allyTickHeal += (ally.getMaxHealth() * healPercent / (float) activeTicks) * 10; + allyTickHeal += (ally.getMaxHealth() * healPercent / (float) durationTicks) * 10; } ally.heal(allyTickHeal); if (tick % 20 == 0) { @@ -133,6 +135,11 @@ public class AbilityHeal extends Ability { } } } + + // Check if heal duration has ended + if (tick >= durationTicks) { + signalCompletion(); + } } /** @@ -202,6 +209,7 @@ public class AbilityHeal extends Ability { @Override public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); nbt.setFloat("healAmount", healAmount); nbt.setFloat("healPercent", healPercent); nbt.setBoolean("healSelf", healSelf); @@ -212,6 +220,7 @@ public class AbilityHeal extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 60; this.healAmount = nbt.hasKey("healAmount") ? nbt.getFloat("healAmount") : 10.0f; this.healPercent = nbt.hasKey("healPercent") ? nbt.getFloat("healPercent") : 0.0f; this.healSelf = !nbt.hasKey("healSelf") || nbt.getBoolean("healSelf"); @@ -221,6 +230,14 @@ public class AbilityHeal extends Ability { } // Getters & Setters + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + public float getHealAmount() { return healAmount; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index fc68200b7..32335daeb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -29,10 +29,8 @@ public class AbilityHeavyHit extends Ability { this.maxRange = 3.0f; this.minRange = 0.0f; this.lockMovement = true; - this.cooldownTicks = 80; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 5; - this.recoveryTicks = 10; this.telegraphType = TelegraphType.POINT; this.showTelegraph = false; this.windUpSound = "random.anvil_use"; @@ -63,7 +61,10 @@ public class AbilityHeavyHit extends Ability { @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (world.isRemote || target == null) return; + if (world.isRemote || target == null) { + signalCompletion(); + return; + } // Apply damage with scripted event support boolean wasHit = applyAbilityDamage(npc, target, damage, knockback); @@ -72,6 +73,9 @@ public class AbilityHeavyHit extends Ability { if (wasHit) { applyEffects(target); } + + // Heavy Hit is instant + signalCompletion(); } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index 77df6ff35..8084aaad4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -60,10 +60,8 @@ public class AbilityLaserShot extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 35.0f; this.minRange = 3.0f; - this.cooldownTicks = 60; + this.cooldownTicks = 0; this.windUpTicks = 15; - this.activeTicks = 1; - this.recoveryTicks = 5; this.telegraphType = TelegraphType.LINE; this.showTelegraph = true; } @@ -101,7 +99,10 @@ public class AbilityLaserShot extends Ability { @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (world.isRemote) return; + if (world.isRemote) { + signalCompletion(); + return; + } double spawnX = npc.posX; double spawnY = npc.posY + npc.getEyeHeight() * 0.8; @@ -120,10 +121,14 @@ public class AbilityLaserShot extends Ability { ); world.spawnEntityInWorld(laser); + + // Laser entity manages itself - ability is done + signalCompletion(); } @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + // Laser entity manages itself } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 38ffd8db4..6bc6da964 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -69,7 +69,7 @@ public class AbilityOrb extends Ability { private AnchorPoint anchorPoint = AnchorPoint.FRONT; // Transient state for orb entity (used during windup charging) - private transient int orbEntityId = -1; + private transient EntityAbilityOrb orbEntity = null; public AbilityOrb() { this.typeId = "ability.cnpc.orb"; @@ -77,10 +77,8 @@ public class AbilityOrb extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 25.0f; this.minRange = 5.0f; - this.cooldownTicks = 100; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 1; // Minimal - just spawn and done - this.recoveryTicks = 10; this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; } @@ -108,17 +106,19 @@ public class AbilityOrb extends Ability { @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (world.isRemote) return; - - // Find and start moving the orb that was spawned during windup - if (orbEntityId != -1) { - Entity entity = world.getEntityByID(orbEntityId); - if (entity instanceof EntityAbilityOrb) { - EntityAbilityOrb orb = (EntityAbilityOrb) entity; - orb.startMoving(target); - } - orbEntityId = -1; + if (world.isRemote) { + signalCompletion(); + return; } + + // Start moving the orb that was spawned during windup + if (orbEntity != null && !orbEntity.isDead) { + orbEntity.startMoving(target); + } + orbEntity = null; + + // Orb entity manages itself - ability is done + signalCompletion(); } @Override @@ -128,7 +128,7 @@ public class AbilityOrb extends Ability { // Spawn orb in charging mode on first tick of windup if (tick == 1) { // Create orb in charging mode - follows NPC based on anchor point during windup - EntityAbilityOrb orb = EntityAbilityOrb.createCharging( + orbEntity = EntityAbilityOrb.createCharging( world, npc, target, orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, @@ -141,8 +141,7 @@ public class AbilityOrb extends Ability { windUpTicks // Charge duration = windup duration ); - world.spawnEntityInWorld(orb); - orbEntityId = orb.getEntityId(); + world.spawnEntityInWorld(orbEntity); } } @@ -157,15 +156,12 @@ public class AbilityOrb extends Ability { } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - // Despawn charging orb if interrupted during windup - if (orbEntityId != -1) { - Entity entity = npc.worldObj.getEntityByID(orbEntityId); - if (entity != null) { - entity.setDead(); - } - orbEntityId = -1; + public void cleanup() { + // Despawn orb entity if still alive + if (orbEntity != null && !orbEntity.isDead) { + orbEntity.setDead(); } + orbEntity = null; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index 8ff9f5410..4a12ae4eb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -38,10 +38,8 @@ public class AbilityProjectile extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.minRange = 5.0f; this.maxRange = 20.0f; - this.cooldownTicks = 60; + this.cooldownTicks = 0; this.windUpTicks = 15; - this.activeTicks = 5; - this.recoveryTicks = 10; // No telegraph for projectile - it's a ranged attack this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; @@ -122,6 +120,9 @@ public class AbilityProjectile extends Ability { // Spawn projectile particles (visual trail) spawnProjectileParticles(world, npc, target); + + // Projectile is instant + signalCompletion(); } private void spawnProjectileParticles(World world, EntityNPCInterface npc, EntityLivingBase target) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index b5a35831f..71d888978 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -28,19 +28,14 @@ public class AbilityShockwave extends Ability { private float damage = 8.0f; private int maxTargets = 10; - // Runtime state - private transient boolean executed = false; - public AbilityShockwave() { this.typeId = "ability.cnpc.shockwave"; this.name = "Shockwave"; this.targetingMode = TargetingMode.AOE_SELF; this.maxRange = 15.0f; this.lockMovement = true; - this.cooldownTicks = 100; + this.cooldownTicks = 0; this.windUpTicks = 25; - this.activeTicks = 5; - this.recoveryTicks = 10; this.telegraphType = TelegraphType.CIRCLE; this.windUpSound = "random.explode"; this.activeSound = "random.explode"; @@ -74,13 +69,7 @@ public class AbilityShockwave extends Ability { @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - executed = false; - } - - @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - if (executed) return; - executed = true; + // Shockwave is instant - apply effect immediately after windup // Get all entities in radius AxisAlignedBB box = npc.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius); @@ -124,16 +113,14 @@ public class AbilityShockwave extends Ability { applyEffects(entity); } } + + // Shockwave completes instantly + signalCompletion(); } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - executed = false; - } - - @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - executed = false; + public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + // Shockwave is instant - nothing to do per-tick } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index d2c17a3e8..dcb914a96 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -49,11 +49,8 @@ public class AbilitySlam extends Ability { this.name = "Slam"; this.targetingMode = TargetingMode.AOE_SELF; // Can also be AOE_TARGET to leap to target this.windUpTicks = 30; - this.activeTicks = 60; // Max time in air (used as timeout if waitForCompletion fails) - this.recoveryTicks = 10; - this.cooldownTicks = 100; + this.cooldownTicks = 0; this.lockMovement = true; - this.waitForCompletion = true; // Ability completes when NPC lands, not by activeTicks this.minRange = 2.0f; this.maxRange = 15.0f; this.telegraphType = TelegraphType.CIRCLE; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index b7ca2aa22..ca41ed6e3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -54,10 +54,8 @@ public class AbilitySweeper extends Ability { this.maxRange = 15.0f; this.minRange = 0.0f; this.lockMovement = true; - this.cooldownTicks = 200; + this.cooldownTicks = 0; this.windUpTicks = 60; - this.activeTicks = 400; // Long duration - entity controls actual end - this.recoveryTicks = 5; // Short recovery // Circle telegraph to show range this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; @@ -86,36 +84,36 @@ public class AbilitySweeper extends Ability { @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (!world.isRemote) { - LogWriter.info("[Sweeper] onExecute called at NPC pos " + npc.posX + ", " + npc.posY + ", " + npc.posZ); - - // Spawn the entity that handles BOTH visuals AND damage - activeEntity = new EntityAbilitySweeper(world, npc, target, - beamLength, beamWidth, beamHeight, - innerColor, outerColor, outerColorEnabled, outerColorWidth, - sweepSpeed, numberOfRotations, - damage, damageInterval, piercing, - lockOnTarget); - world.spawnEntityInWorld(activeEntity); - - LogWriter.info("[Sweeper] Spawned sweeper entity"); + if (world.isRemote) { + signalCompletion(); + return; } + + LogWriter.info("[Sweeper] onExecute called at NPC pos " + npc.posX + ", " + npc.posY + ", " + npc.posZ); + + // Spawn the entity that handles BOTH visuals AND damage + activeEntity = new EntityAbilitySweeper(world, npc, target, + beamLength, beamWidth, beamHeight, + innerColor, outerColor, outerColorEnabled, outerColorWidth, + sweepSpeed, numberOfRotations, + damage, damageInterval, piercing, + lockOnTarget); + world.spawnEntityInWorld(activeEntity); + + LogWriter.info("[Sweeper] Spawned sweeper entity"); + // Entity manages itself - completion signaled from onActiveTick when entity dies } @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // Entity handles everything - nothing to do here - // Check if entity died early (entity manages its own death based on rotations) - if (activeEntity != null && activeEntity.isDead) { - // Entity finished, ability can end early - // The ability system will handle the recovery phase + // Entity handles everything - check if entity finished its rotations + if (activeEntity == null || activeEntity.isDead) { + signalCompletion(); } } @Override - public void reset() { - super.reset(); - + public void cleanup() { // Clean up entity if still alive if (activeEntity != null && !activeEntity.isDead) { activeEntity.setDead(); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index a79223d5d..668b2f04d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -59,10 +59,8 @@ public class AbilityTeleport extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 30.0f; this.minRange = 5.0f; - this.cooldownTicks = 100; + this.cooldownTicks = 0; this.windUpTicks = 10; - this.activeTicks = 40; - this.recoveryTicks = 10; // No telegraph for teleport - it's instant repositioning this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; @@ -101,7 +99,10 @@ public class AbilityTeleport extends Ability { @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1; - if (currentBlink >= blinkLimit) return; + if (currentBlink >= blinkLimit) { + signalCompletion(); + return; + } ticksSinceLastBlink++; @@ -109,6 +110,11 @@ public class AbilityTeleport extends Ability { performBlink(npc, target, world); currentBlink++; ticksSinceLastBlink = 0; + + // Check if this was the last blink + if (currentBlink >= blinkLimit) { + signalCompletion(); + } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 77ac4a5fe..dbb7273cd 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -34,6 +34,7 @@ public class AbilityTrap extends Ability { AHEAD_OF_CASTER } + private int durationTicks = 200; private TrapPlacement placement = TrapPlacement.AT_TARGET; private float placementDistance = 5.0f; private float triggerRadius = 2.0f; @@ -64,10 +65,8 @@ public class AbilityTrap extends Ability { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 15.0f; this.lockMovement = true; - this.cooldownTicks = 150; + this.cooldownTicks = 0; this.windUpTicks = 20; - this.activeTicks = 200; - this.recoveryTicks = 10; this.telegraphType = TelegraphType.CIRCLE; } @@ -180,6 +179,12 @@ public class AbilityTrap extends Ability { @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + // Check if trap duration has ended + if (tick >= durationTicks) { + signalCompletion(); + return; + } + if (!armed) { if (tick >= armTime) { armed = true; @@ -190,6 +195,7 @@ public class AbilityTrap extends Ability { ticksSinceLastTrigger++; if (maxTriggers > 0 && triggerCount >= maxTriggers) { + signalCompletion(); // All triggers used, trap is done return; } @@ -277,6 +283,7 @@ public class AbilityTrap extends Ability { @Override public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setInteger("durationTicks", durationTicks); nbt.setString("placement", placement.name()); nbt.setFloat("placementDistance", placementDistance); nbt.setFloat("triggerRadius", triggerRadius); @@ -294,6 +301,7 @@ public class AbilityTrap extends Ability { @Override public void readTypeNBT(NBTTagCompound nbt) { + this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 200; try { this.placement = TrapPlacement.valueOf(nbt.getString("placement")); } catch (Exception e) { @@ -314,6 +322,14 @@ public class AbilityTrap extends Ability { } // Getters & Setters + public int getDurationTicks() { + return durationTicks; + } + + public void setDurationTicks(int durationTicks) { + this.durationTicks = Math.max(1, durationTicks); + } + public TrapPlacement getPlacement() { return placement; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index 666b19bbc..70c1c96b7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -46,10 +46,8 @@ public class AbilityVortex extends Ability { this.targetingMode = TargetingMode.AOE_SELF; this.maxRange = 15.0f; this.lockMovement = true; - this.cooldownTicks = 120; + this.cooldownTicks = 0; this.windUpTicks = 30; - this.activeTicks = 40; - this.recoveryTicks = 5; this.telegraphType = TelegraphType.CIRCLE; this.windUpSound = "mob.ghast.charge"; this.activeSound = "mob.ghast.fireball"; @@ -119,6 +117,7 @@ public class AbilityVortex extends Ability { @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { if (pullComplete || pulledEntities.isEmpty()) { + signalCompletion(); return; } @@ -174,6 +173,7 @@ public class AbilityVortex extends Ability { if (!anyStillPulling && pulledEntities.isEmpty()) { pullComplete = true; + signalCompletion(); } } diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java index 5a7e32d28..593c8aa19 100644 --- a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -7,104 +7,87 @@ import noppes.npcs.constants.EnumAnimationPart; import noppes.npcs.controllers.data.AnimationData; import noppes.npcs.controllers.data.Frame; import noppes.npcs.controllers.data.FramePart; +import noppes.npcs.entity.EntityCustomNpc; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.entity.data.ModelScalePart; /** * Utility class for calculating world positions based on anchor points. - * Used for positioning charging effects on different body parts. - * Animation-aware: reads arm positions from NPC animation data when available. + * Supports both EntityNPCInterface and EntityPlayer with animation and model size awareness. */ public class AnchorPointHelper { - // ==================== TUNABLE CONSTANTS ==================== - // These can be adjusted to fine-tune positioning + // Default biped model constants (in model units, 1 unit = 1/16 block) + private static final float MODEL_SCALE = 1f / 16f; - // Shoulder position in model units (default biped arm rotation points) - private static final float SHOULDER_X_RIGHT = -5.0f; // Right arm is on the right side (-X in model space) - private static final float SHOULDER_X_LEFT = 5.0f; // Left arm is on the left side (+X in model space) - private static final float SHOULDER_Y = 2.0f; // Shoulder height from model origin - private static final float SHOULDER_Z = 0.0f; // Shoulder depth + // Shoulder position relative to model origin + private static final float SHOULDER_Y = 2f; + private static final float SHOULDER_X_OFFSET = 5f; // Distance from center to shoulder - // Hand offset in model units (where items are held, relative to shoulder after rotation) - // In arm's local space: X = toward/away from body, Y = down the arm, Z = forward/back - private static float HAND_OFFSET_X_RIGHT = -2.0f; // Slightly toward body for right - private static float HAND_OFFSET_X_LEFT = 2.0f; // Slightly toward body for left - private static float HAND_OFFSET_Y = 14.0f; // Down the arm (arm is ~12 units long) - private static float HAND_OFFSET_Z = 0.0f; // Forward from arm + // Palm position (in arm's local space, relative to shoulder) + // Arm is 12 units long; palm is near the end + private static final float PALM_DOWN = 11f; // Distance down the arm to palm center + private static final float PALM_FORWARD = 2f; // Forward offset to palm surface (not center of hand) + private static final float PALM_INWARD = 0f; // Lateral offset (0 = centered on arm) - // Height of shoulder relative to entity position (feet) - private static final float SHOULDER_HEIGHT_BLOCKS = 1.35f; + // Default height offsets as fraction of entity height + private static final float FRONT_HEIGHT = 0.7f; + private static final float CENTER_HEIGHT = 0.5f; + private static final float ARM_HEIGHT = 0.75f; + private static final float ABOVE_HEAD_HEIGHT = 1.2f; + private static final float CHEST_HEIGHT = 0.65f; - // Model scale factor (1/16 - converts model units to blocks) - private static final float MODEL_SCALE = 0.0625f; + // Distance in front for FRONT anchor + private static final float DEFAULT_FRONT_DISTANCE = 1.0f; - // Fallback offsets (in blocks) when no animation - private static final float ARM_LATERAL_OFFSET = 0.35f; - private static final float ARM_FORWARD_OFFSET = 0.3f; - private static final float FRONT_OFFSET = 1.0f; - - // Height multipliers relative to eye height - private static final float FRONT_HEIGHT_MULT = 0.7f; - private static final float CENTER_HEIGHT_MULT = 0.5f; - private static final float ARM_HEIGHT_MULT = 0.75f; - private static final float ABOVE_HEAD_HEIGHT_MULT = 1.3f; - private static final float CHEST_HEIGHT_MULT = 0.5f; + // Fallback arm positioning (when no animation) + private static final float FALLBACK_LATERAL = 0.35f; + private static final float FALLBACK_FORWARD = 0.3f; /** - * Calculate the world position for a given anchor point on an entity. + * Calculate world position for an anchor point on an entity. */ public static Vec3 calculateAnchorPosition(EntityLivingBase entity, AnchorPoint anchor) { - return calculateAnchorPosition(entity, anchor, FRONT_OFFSET); + return calculateAnchorPosition(entity, anchor, DEFAULT_FRONT_DISTANCE); } /** - * Calculate the world position for a given anchor point on an entity. + * Calculate world position for an anchor point with custom front offset. */ public static Vec3 calculateAnchorPosition(EntityLivingBase entity, AnchorPoint anchor, float frontOffset) { + float scale = getModelScale(entity); + float height = entity.height; + double x = entity.posX; double y = entity.posY; double z = entity.posZ; - float eyeHeight = entity.getEyeHeight(); - // Body yaw in radians (used for arm positioning) - // Minecraft: yaw 0 = south (+Z), 90 = west (-X), 180 = north (-Z), 270 = east (+X) - float bodyYawRad = (float) Math.toRadians(entity.renderYawOffset); - float headYawRad = (float) Math.toRadians(entity.rotationYawHead); + float headYaw = (float) Math.toRadians(entity.rotationYawHead); + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); switch (anchor) { case FRONT: - // In front of entity face (use head yaw) - x -= Math.sin(headYawRad) * frontOffset; - y += eyeHeight * FRONT_HEIGHT_MULT; - z += Math.cos(headYawRad) * frontOffset; + x -= Math.sin(headYaw) * frontOffset * scale; + y += height * FRONT_HEIGHT; + z += Math.cos(headYaw) * frontOffset * scale; break; case CENTER: - y += eyeHeight * CENTER_HEIGHT_MULT; + y += height * CENTER_HEIGHT; break; case RIGHT_HAND: - Vec3 rightPos = getAnimatedHandPosition(entity, true); - if (rightPos != null) { - return rightPos; - } - // Fallback - return calculateFallbackHandPosition(entity, bodyYawRad, true); + return calculateHandPosition(entity, true, scale); case LEFT_HAND: - Vec3 leftPos = getAnimatedHandPosition(entity, false); - if (leftPos != null) { - return leftPos; - } - // Fallback - return calculateFallbackHandPosition(entity, bodyYawRad, false); + return calculateHandPosition(entity, false, scale); case ABOVE_HEAD: - y += eyeHeight * ABOVE_HEAD_HEIGHT_MULT; + y += height * ABOVE_HEAD_HEIGHT; break; case CHEST: - y += eyeHeight * CHEST_HEIGHT_MULT; + y += height * CHEST_HEIGHT; break; } @@ -112,17 +95,51 @@ public class AnchorPointHelper { } /** - * Calculate hand position using NPC animation data. - * Uses the same transformation logic as the renderer. + * Get model scale factor for an entity. + * NPCs have configurable model size, players use default scale. */ - private static Vec3 getAnimatedHandPosition(EntityLivingBase entity, boolean rightArm) { - if (!(entity instanceof EntityNPCInterface)) { - return null; + private static float getModelScale(EntityLivingBase entity) { + if (entity instanceof EntityNPCInterface) { + int modelSize = ((EntityNPCInterface) entity).display.modelSize; + return modelSize / 5f; // modelSize 5 = 100% scale + } + return 1f; + } + + // Default arm scale (no scaling) + private static final ModelScalePart DEFAULT_ARM_SCALE = new ModelScalePart(); + + /** + * Get arm scale for an entity. + * EntityCustomNpc has per-part scaling, others use default (1,1,1). + */ + private static ModelScalePart getArmScale(EntityLivingBase entity) { + if (entity instanceof EntityCustomNpc) { + return ((EntityCustomNpc) entity).modelData.modelScale.arms; + } + return DEFAULT_ARM_SCALE; + } + + /** + * Calculate hand position using animation data if available. + */ + private static Vec3 calculateHandPosition(EntityLivingBase entity, boolean rightHand, float scale) { + // Try to get animated position first + Vec3 animatedPos = getAnimatedHandPosition(entity, rightHand, scale); + if (animatedPos != null) { + return animatedPos; } - EntityNPCInterface npc = (EntityNPCInterface) entity; - AnimationData animData = npc.display.animationData; + // Fallback to static position + return calculateFallbackHandPosition(entity, rightHand, scale); + } + /** + * Calculate hand position from animation data. + * Works for both NPCs and Players. + */ + private static Vec3 getAnimatedHandPosition(EntityLivingBase entity, boolean rightHand, float scale) { + AnimationData animData = AnimationData.getData(entity); if (animData == null || !animData.isActive() || animData.animation == null) { return null; } @@ -132,146 +149,172 @@ public class AnchorPointHelper { return null; } - EnumAnimationPart armPart = rightArm ? EnumAnimationPart.RIGHT_ARM : EnumAnimationPart.LEFT_ARM; - if (!frame.frameParts.containsKey(armPart)) { - return null; - } - + EnumAnimationPart armPart = rightHand ? EnumAnimationPart.RIGHT_ARM : EnumAnimationPart.LEFT_ARM; FramePart part = frame.frameParts.get(armPart); if (part == null) { return null; } - // Ensure interpolation is up to date - part.interpolateOffset(); - part.interpolateAngles(); + // Get arm scale for EntityCustomNpc + ModelScalePart armScale = getArmScale(entity); - // === STEP 1: Get shoulder position in model space === - float shoulderX = rightArm ? SHOULDER_X_RIGHT : SHOULDER_X_LEFT; + // Get rotation and pivot values + float[] rotations = getRotations(part, entity); + float[] pivots = getPivots(part, entity); + + // Shoulder position in model space + float shoulderX = rightHand ? -SHOULDER_X_OFFSET : SHOULDER_X_OFFSET; float shoulderY = SHOULDER_Y; - float shoulderZ = SHOULDER_Z; + float shoulderZ = 0f; - // Add animation pivot offsets - if (part.prevPivots != null) { - shoulderX += part.prevPivots[0]; - shoulderY += part.prevPivots[1]; - shoulderZ += part.prevPivots[2]; - } + // Add pivot offsets + shoulderX += pivots[0]; + shoulderY += pivots[1]; + shoulderZ += pivots[2]; - // === STEP 2: Get hand offset in arm's local space === - float handX = rightArm ? HAND_OFFSET_X_RIGHT : HAND_OFFSET_X_LEFT; - float handY = HAND_OFFSET_Y; - float handZ = HAND_OFFSET_Z; + // Palm offset in arm's local space (before rotation) + // Apply arm scaling to the palm position + float palmX = (rightHand ? -PALM_INWARD : PALM_INWARD) * armScale.scaleX; + float palmY = PALM_DOWN * armScale.scaleY; + float palmZ = PALM_FORWARD * armScale.scaleZ; - // === STEP 3: Apply arm rotations to hand offset (Z → Y → X order) === - // Rotations are in radians (already converted by interpolateAngles) - float rotX = part.prevRotations != null ? part.prevRotations[0] : 0; - float rotY = part.prevRotations != null ? part.prevRotations[1] : 0; - float rotZ = part.prevRotations != null ? part.prevRotations[2] : 0; + // Apply arm rotations (order: Z -> Y -> X) + double hx = palmX, hy = palmY, hz = palmZ; - // Start with hand offset - double hx = handX; - double hy = handY; - double hz = handZ; - - // Apply Z rotation first - double cosZ = Math.cos(rotZ); - double sinZ = Math.sin(rotZ); + // Z rotation + double cosZ = Math.cos(rotations[2]); + double sinZ = Math.sin(rotations[2]); double newX = hx * cosZ - hy * sinZ; double newY = hx * sinZ + hy * cosZ; hx = newX; hy = newY; - // Apply Y rotation - double cosY = Math.cos(rotY); - double sinY = Math.sin(rotY); + // Y rotation + double cosY = Math.cos(rotations[1]); + double sinY = Math.sin(rotations[1]); newX = hx * cosY + hz * sinY; double newZ = -hx * sinY + hz * cosY; hx = newX; hz = newZ; - // Apply X rotation - double cosX = Math.cos(rotX); - double sinX = Math.sin(rotX); + // X rotation + double cosX = Math.cos(rotations[0]); + double sinX = Math.sin(rotations[0]); newY = hy * cosX - hz * sinX; newZ = hy * sinX + hz * cosX; hy = newY; hz = newZ; - // === STEP 4: Combine shoulder + rotated hand offset (in model units) === + // Combine shoulder + rotated hand offset double modelX = shoulderX + hx; double modelY = shoulderY + hy; double modelZ = shoulderZ + hz; - // === STEP 5: Convert to blocks === - double blockX = modelX * MODEL_SCALE; - double blockY = modelY * MODEL_SCALE; - double blockZ = modelZ * MODEL_SCALE; + // Convert to blocks and apply model scale + double blockX = modelX * MODEL_SCALE * scale; + double blockY = modelY * MODEL_SCALE * scale; + double blockZ = modelZ * MODEL_SCALE * scale; - // === STEP 6: Apply body yaw rotation === - // In model space: +X = left, -X = right, +Z = back, -Z = front - // Need to rotate to align with entity's facing direction + // Apply body yaw rotation float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); double cosYaw = Math.cos(bodyYaw); double sinYaw = Math.sin(bodyYaw); - // Rotate around Y axis (body rotation) double worldOffsetX = blockX * cosYaw + blockZ * sinYaw; double worldOffsetZ = -blockX * sinYaw + blockZ * cosYaw; - // === STEP 7: Final world position === + // Calculate shoulder height based on entity and scale + float shoulderHeight = entity.height * ARM_HEIGHT; + + // Final world position double worldX = entity.posX + worldOffsetX; - double worldY = entity.posY + SHOULDER_HEIGHT_BLOCKS - blockY; // Subtract because +Y in model is down + double worldY = entity.posY + shoulderHeight - blockY; double worldZ = entity.posZ + worldOffsetZ; return Vec3.createVectorHelper(worldX, worldY, worldZ); } + /** + * Get rotation values from FramePart. + * Uses interpolated values on client, raw values on server. + */ + private static float[] getRotations(FramePart part, EntityLivingBase entity) { + if (entity.worldObj.isRemote) { + // Client side - use interpolated values (already in radians) + part.interpolateAngles(); + return part.prevRotations; + } else { + // Server side - convert raw degrees to radians + float pi = (float) Math.PI / 180f; + return new float[]{ + part.rotation[0] * pi, + part.rotation[1] * pi, + part.rotation[2] * pi + }; + } + } + + /** + * Get pivot values from FramePart. + * Uses interpolated values on client, raw values on server. + */ + private static float[] getPivots(FramePart part, EntityLivingBase entity) { + if (entity.worldObj.isRemote) { + // Client side - use interpolated values + part.interpolateOffset(); + return part.prevPivots; + } else { + // Server side - use raw values + return part.pivot; + } + } + /** * Calculate fallback hand position when no animation is active. */ - private static Vec3 calculateFallbackHandPosition(EntityLivingBase entity, float bodyYawRad, boolean rightHand) { - double x = entity.posX; - double y = entity.posY; - double z = entity.posZ; - float eyeHeight = entity.getEyeHeight(); + private static Vec3 calculateFallbackHandPosition(EntityLivingBase entity, boolean rightHand, float scale) { + float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); + + // Get arm scale for EntityCustomNpc + ModelScalePart armScale = getArmScale(entity); // Perpendicular direction for lateral offset - float perpYaw = bodyYawRad + (float) (rightHand ? Math.PI / 2 : -Math.PI / 2); + float perpYaw = bodyYaw + (rightHand ? (float) (Math.PI / 2) : (float) (-Math.PI / 2)); - // Lateral offset (perpendicular to body facing) - double lateralX = -Math.sin(perpYaw) * ARM_LATERAL_OFFSET; - double lateralZ = Math.cos(perpYaw) * ARM_LATERAL_OFFSET; + // Calculate offsets scaled by model size and arm scale + double lateralX = -Math.sin(perpYaw) * FALLBACK_LATERAL * scale * armScale.scaleX; + double lateralZ = Math.cos(perpYaw) * FALLBACK_LATERAL * scale * armScale.scaleX; + double forwardX = -Math.sin(bodyYaw) * FALLBACK_FORWARD * scale * armScale.scaleZ; + double forwardZ = Math.cos(bodyYaw) * FALLBACK_FORWARD * scale * armScale.scaleZ; - // Forward offset (in facing direction) - double forwardX = -Math.sin(bodyYawRad) * ARM_FORWARD_OFFSET; - double forwardZ = Math.cos(bodyYawRad) * ARM_FORWARD_OFFSET; + // Arm length affects vertical position + double armLengthOffset = (1 - armScale.scaleY) * 0.3; - x += lateralX + forwardX; - y += eyeHeight * ARM_HEIGHT_MULT; - z += lateralZ + forwardZ; + double x = entity.posX + lateralX + forwardX; + double y = entity.posY + entity.height * ARM_HEIGHT - armLengthOffset; + double z = entity.posZ + lateralZ + forwardZ; return Vec3.createVectorHelper(x, y, z); } /** - * Get the height multiplier for a given anchor point. + * Get default height multiplier for an anchor point. */ public static float getHeightMultiplier(AnchorPoint anchor) { switch (anchor) { case FRONT: - return FRONT_HEIGHT_MULT; + return FRONT_HEIGHT; case CENTER: - return CENTER_HEIGHT_MULT; + return CENTER_HEIGHT; case RIGHT_HAND: case LEFT_HAND: - return ARM_HEIGHT_MULT; + return ARM_HEIGHT; case ABOVE_HEAD: - return ABOVE_HEAD_HEIGHT_MULT; + return ABOVE_HEAD_HEIGHT; case CHEST: - return CHEST_HEIGHT_MULT; + return CHEST_HEIGHT; default: - return FRONT_HEIGHT_MULT; + return FRONT_HEIGHT; } } } diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index a9c670647..92bb1241c 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -170,8 +170,8 @@ public class DataAbilities { currentAbility.onActiveTick(npc, target, npc.worldObj, currentAbility.getCurrentTick()); break; - case RECOVERY: - // Recovery phase - just wait + case DAZED: + // Dazed phase - NPC cannot attack, just wait for daze to end break; case IDLE: @@ -222,12 +222,10 @@ public class DataAbilities { releaseLockedRotation(); // Calculate cooldown: random(min, max) + ability offset - int baseCooldown = minCooldown; - if (maxCooldown > minCooldown) { - baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1); - } - int totalCooldown = baseCooldown + currentAbility.getCooldownTicks(); - cooldownEndTime = npc.worldObj.getTotalWorldTime() + totalCooldown; + int abilityCooldownOffset = currentAbility.getCooldownTicks(); + rollCooldown(); + // Add ability-specific cooldown offset + cooldownEndTime += abilityCooldownOffset; currentAbility = null; lastTarget = null; @@ -539,8 +537,8 @@ public class DataAbilities { /** * Interrupt the currently executing ability. - * The ability will transition to RECOVERY phase (dazed state) and continue ticking. - * When RECOVERY completes, the ability will move to IDLE and onAbilityComplete() is called. + * If interrupted during WINDUP, the ability will transition to DAZED phase. + * When DAZED completes, the ability will move to IDLE and onAbilityComplete() is called. */ public void interruptCurrentAbility(DamageSource source, float damage) { if (currentAbility != null) { @@ -559,10 +557,10 @@ public class DataAbilities { NpcAPI.EVENT_BUS.post(interruptEvent); currentAbility.onInterrupt(npc, source, damage); - currentAbility.interrupt(); // This now transitions to RECOVERY, not IDLE + currentAbility.interrupt(); // Transitions to DAZED if interrupted during WINDUP - // Don't clear currentAbility - let it tick through RECOVERY phase - // When RECOVERY ends and phase becomes IDLE, onAbilityComplete() will be called + // Don't clear currentAbility - let it tick through DAZED phase + // When DAZED ends and phase becomes IDLE, onAbilityComplete() will be called } } @@ -631,10 +629,13 @@ public class DataAbilities { /** * Reset all runtime state (cooldowns, current ability). * Called when NPC respawns or combat ends. + * Rolls cooldown so NPC doesn't immediately use an ability. */ public void reset() { stopCurrentAbility(); - resetCooldown(); + + // Roll cooldown so NPC doesn't immediately attack after reset + rollCooldown(); // Reset execution state on all abilities for (Ability ability : abilities) { @@ -642,6 +643,18 @@ public class DataAbilities { } } + /** + * Roll a new cooldown using the min/max range. + * Called after ability completes and on reset. + */ + private void rollCooldown() { + int baseCooldown = minCooldown; + if (maxCooldown > minCooldown) { + baseCooldown = minCooldown + random.nextInt(maxCooldown - minCooldown + 1); + } + cooldownEndTime = npc.worldObj.getTotalWorldTime() + baseCooldown; + } + // ═══════════════════════════════════════════════════════════════════ // ABILITY LIST MANAGEMENT // ═══════════════════════════════════════════════════════════════════ @@ -706,7 +719,7 @@ public class DataAbilities { * Rules: * - WINDUP phase: Block AI if lockMovement is true * - ACTIVE phase: Block AI if lockMovement OR hasAbilityMovement is true - * - RECOVERY phase: Block AI if lockMovement is true + * - DAZED phase: Always block AI (NPC is stunned) */ public boolean isAbilityControllingMovement() { if (currentAbility == null || !currentAbility.isExecuting()) { @@ -727,9 +740,9 @@ public class DataAbilities { // - hasAbilityMovement is true (ability is moving the NPC itself) return currentAbility.isLockMovement() || currentAbility.hasAbilityMovement(); - case RECOVERY: - // During recovery, block if lockMovement is set - return currentAbility.isLockMovement(); + case DAZED: + // During dazed phase, always block - NPC is stunned from interrupt + return true; default: return false; @@ -739,7 +752,7 @@ public class DataAbilities { /** * Check if NPC's look direction should be locked to target. * Only locks during ACTIVE phase when lockMovement is true. - * This allows the NPC to freely track target during WINDUP and RECOVERY. + * This allows the NPC to freely track target during WINDUP and DAZED. */ public boolean shouldLockLookDirection() { if (currentAbility == null || !currentAbility.isExecuting()) { diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index 6c5323707..040c999a4 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -61,8 +61,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Timing protected int cooldownTicks; protected int windUpTicks; - protected int activeTicks; - protected int recoveryTicks; + protected int dazedTicks; // ═══════════════════════════════════════════════════════════════════════════ // CACHED VALUES - Target Tab @@ -115,8 +114,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Timing this.cooldownTicks = ability.getCooldownTicks(); this.windUpTicks = ability.getWindUpTicks(); - this.activeTicks = ability.getActiveTicks(); - this.recoveryTicks = ability.getRecoveryTicks(); + this.dazedTicks = ability.getDazedTicks(); // Target this.minRange = ability.getMinRange(); @@ -254,42 +252,36 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi y += 24; - // Row 3: Interruptible + // Row 3: Interruptible + Dazed Ticks (only if interruptible) addLabel(new GuiNpcLabel(6, "ability.interruptible", col1LabelX, y + 5)); addButton(new GuiNpcButton(14, guiLeft + 85, y, 40, 20, new String[]{"gui.no", "gui.yes"}, interruptible ? 1 : 0)); + // Only show dazed ticks if interruptible is enabled + if (interruptible) { + addLabel(new GuiNpcLabel(12, "ability.dazed", col2LabelX, y + 5)); + GuiNpcTextField dazedField = new GuiNpcTextField(13, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(dazedTicks)); + dazedField.setIntegersOnly(); + dazedField.setMinMaxDefault(0, 1000, 80); + addTextField(dazedField); + } + y += 28; // Separator - Timing section header addLabel(new GuiNpcLabel(7, "ability.timing", col1LabelX, y)); y += 14; - // Row 4: Windup Ticks + Active Ticks + // Row 4: Windup Ticks + Cooldown Ticks addLabel(new GuiNpcLabel(10, "ability.windup", col1LabelX, y + 5)); GuiNpcTextField windupField = new GuiNpcTextField(11, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(windUpTicks)); windupField.setIntegersOnly(); windupField.setMinMaxDefault(0, 1000, 20); addTextField(windupField); - addLabel(new GuiNpcLabel(11, "ability.active", col2LabelX, y + 5)); - GuiNpcTextField activeField = new GuiNpcTextField(12, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(activeTicks)); - activeField.setIntegersOnly(); - activeField.setMinMaxDefault(1, 1000, 10); - addTextField(activeField); - - y += 24; - - // Row 5: Recovery Ticks + Cooldown Ticks - addLabel(new GuiNpcLabel(12, "ability.recovery", col1LabelX, y + 5)); - GuiNpcTextField recoveryField = new GuiNpcTextField(13, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(recoveryTicks)); - recoveryField.setIntegersOnly(); - recoveryField.setMinMaxDefault(0, 1000, 20); - addTextField(recoveryField); - addLabel(new GuiNpcLabel(13, "ability.cooldown", col2LabelX, y + 5)); GuiNpcTextField cooldownField = new GuiNpcTextField(10, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(cooldownTicks)); cooldownField.setIntegersOnly(); - cooldownField.setMinMaxDefault(0, 10000, 100); + cooldownField.setMinMaxDefault(0, 10000, 0); addTextField(cooldownField); } @@ -632,6 +624,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // General tab buttons else if (id == 14) { interruptible = ((GuiNpcButton) guibutton).getValue() == 1; + initGui(); // Refresh to show/hide dazed ticks field } else if (id == 16) { lockMovement = ((GuiNpcButton) guibutton).getValue() == 1; } @@ -764,10 +757,8 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi cooldownTicks = textField.getInteger(); } else if (id == 11) { windUpTicks = textField.getInteger(); - } else if (id == 12) { - activeTicks = textField.getInteger(); } else if (id == 13) { - recoveryTicks = textField.getInteger(); + dazedTicks = textField.getInteger(); } // Visual-specific fields (200+) - delegate to subclass @@ -865,8 +856,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Timing ability.setCooldownTicks(cooldownTicks); ability.setWindUpTicks(windUpTicks); - ability.setActiveTicks(activeTicks); - ability.setRecoveryTicks(recoveryTicks); + ability.setDazedTicks(dazedTicks); // Target ability.setMinRange(minRange); @@ -914,8 +904,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi this.cooldownTicks = ability.getCooldownTicks(); this.windUpTicks = ability.getWindUpTicks(); - this.activeTicks = ability.getActiveTicks(); - this.recoveryTicks = ability.getRecoveryTicks(); + this.dazedTicks = ability.getDazedTicks(); this.minRange = ability.getMinRange(); this.maxRange = ability.getMaxRange(); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java index 10f5ab8c1..f3dbc64c7 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java @@ -68,8 +68,15 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { y += 24; - // Row 5: Effects button - addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); + // Row 5: Spin Duration (only for SPIN mode) + Effects button + if (cutter.getSweepMode() == AbilityCutter.SweepMode.SPIN) { + addLabel(new GuiNpcLabel(108, "ability.duration", labelX, y + 5)); + GuiNpcTextField durationField = createIntField(108, fieldX, y, 50, cutter.getSpinDurationTicks()); + durationField.setMinMaxDefault(1, 1000, 60); + addTextField(durationField); + } + + addButton(new GuiNpcButton(150, col2LabelX, y, 80, 20, "ability.effects")); } @Override @@ -78,6 +85,7 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { switch (id) { case 106: cutter.setSweepMode(AbilityCutter.SweepMode.values()[value]); + initGui(); // Refresh to show/hide duration field break; case 107: cutter.setPiercing(value == 1); @@ -121,6 +129,9 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { case 105: cutter.setSweepSpeed(parseFloat(field, cutter.getSweepSpeed())); break; + case 108: + cutter.setSpinDurationTicks(field.getInteger()); + break; } } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java index 25383c926..183d66814 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java @@ -31,9 +31,14 @@ public class SubGuiAbilityGuard extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 165; int col2FieldX = guiLeft + 225; - // Row 1: Damage Reduction - addLabel(new GuiNpcLabel(100, "ability.dmgReduce", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, guard.getDamageReduction())); + // Row 1: Duration + Damage Reduction + addLabel(new GuiNpcLabel(99, "ability.duration", labelX, y + 5)); + GuiNpcTextField durationField = createIntField(99, fieldX, y, 50, guard.getDurationTicks()); + durationField.setMinMaxDefault(1, 1000, 60); + addTextField(durationField); + + addLabel(new GuiNpcLabel(100, "ability.dmgReduce", col2LabelX, y + 5)); + addTextField(createFloatField(100, col2FieldX, y, 50, guard.getDamageReduction())); y += 24; @@ -112,6 +117,9 @@ public class SubGuiAbilityGuard extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { + case 99: + guard.setDurationTicks(field.getInteger()); + break; case 100: guard.setDamageReduction(parseFloat(field, guard.getDamageReduction())); break; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java index 8a91798d0..c9633c0f9 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java @@ -31,14 +31,22 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 145; int col2FieldX = guiLeft + 205; - // Row 1: Shape + Placement - addLabel(new GuiNpcLabel(100, "ability.shape", labelX, y + 5)); - String[] shapes = {"Circle", "Ring", "Cone"}; - addButton(new GuiNpcButton(100, fieldX, y, 55, 20, shapes, hazard.getShape().ordinal())); + // Row 1: Duration + Shape + addLabel(new GuiNpcLabel(99, "ability.duration", labelX, y + 5)); + GuiNpcTextField durationField = createIntField(99, fieldX, y, 50, hazard.getDurationTicks()); + durationField.setMinMaxDefault(1, 2000, 100); + addTextField(durationField); - addLabel(new GuiNpcLabel(101, "ability.placement", col2LabelX, y + 5)); + addLabel(new GuiNpcLabel(100, "ability.shape", col2LabelX, y + 5)); + String[] shapes = {"Circle", "Ring", "Cone"}; + addButton(new GuiNpcButton(100, col2FieldX, y, 55, 20, shapes, hazard.getShape().ordinal())); + + y += 24; + + // Row 2: Placement + addLabel(new GuiNpcLabel(101, "ability.placement", labelX, y + 5)); String[] placements = {"Caster", "Target", "FollowCast", "FollowTgt"}; - addButton(new GuiNpcButton(101, col2FieldX, y, 60, 20, placements, hazard.getPlacement().ordinal())); + addButton(new GuiNpcButton(101, fieldX, y, 70, 20, placements, hazard.getPlacement().ordinal())); y += 24; @@ -116,6 +124,9 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { + case 99: + hazard.setDurationTicks(field.getInteger()); + break; case 102: hazard.setRadius(parseFloat(field, hazard.getRadius())); break; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java index 8b679fb25..010da51cf 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java @@ -27,7 +27,20 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 145; int col2FieldX = guiLeft + 205; - // Row 1: Heal Amount + Heal Percent + // Row 1: Instant Heal + Duration (only shown if not instant) + addLabel(new GuiNpcLabel(103, "dialog.instant", labelX, y + 5)); + addButton(new GuiNpcButton(103, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isInstantHeal() ? 1 : 0)); + + if (!heal.isInstantHeal()) { + addLabel(new GuiNpcLabel(99, "ability.duration", col2LabelX, y + 5)); + GuiNpcTextField durationField = createIntField(99, col2FieldX, y, 50, heal.getDurationTicks()); + durationField.setMinMaxDefault(1, 1000, 60); + addTextField(durationField); + } + + y += 24; + + // Row 2: Heal Amount + Heal Percent addLabel(new GuiNpcLabel(100, "ability.healAmount", labelX, y + 5)); addTextField(createFloatField(100, fieldX, y, 50, heal.getHealAmount())); @@ -36,16 +49,13 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig { y += 24; - // Row 2: Heal Radius + Instant Heal + // Row 3: Heal Radius addLabel(new GuiNpcLabel(102, "ability.healRadius", labelX, y + 5)); addTextField(createFloatField(102, fieldX, y, 50, heal.getHealRadius())); - addLabel(new GuiNpcLabel(103, "dialog.instant", col2LabelX, y + 5)); - addButton(new GuiNpcButton(103, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isInstantHeal() ? 1 : 0)); - y += 24; - // Row 3: Heal Self + Heal Allies + // Row 4: Heal Self + Heal Allies addLabel(new GuiNpcLabel(104, "ability.healSelf", labelX, y + 5)); addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isHealSelf() ? 1 : 0)); @@ -59,6 +69,7 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig { switch (id) { case 103: heal.setInstantHeal(value == 1); + initGui(); // Refresh to show/hide duration field break; case 104: heal.setHealSelf(value == 1); @@ -72,6 +83,9 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { + case 99: + heal.setDurationTicks(field.getInteger()); + break; case 100: heal.setHealAmount(parseFloat(field, heal.getHealAmount())); break; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java index 2c0fc3a68..c846565c4 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java @@ -31,13 +31,21 @@ public class SubGuiAbilityTrap extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 145; int col2FieldX = guiLeft + 205; - // Row 1: Placement + Trigger Radius - addLabel(new GuiNpcLabel(100, "ability.placement", labelX, y + 5)); - String[] placements = {"Caster", "Target", "Ahead"}; - addButton(new GuiNpcButton(100, fieldX, y, 55, 20, placements, trap.getPlacement().ordinal())); + // Row 1: Duration + Placement + addLabel(new GuiNpcLabel(99, "ability.duration", labelX, y + 5)); + GuiNpcTextField durationField = createIntField(99, fieldX, y, 50, trap.getDurationTicks()); + durationField.setMinMaxDefault(1, 2000, 200); + addTextField(durationField); - addLabel(new GuiNpcLabel(101, "ability.triggerRad", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, trap.getTriggerRadius())); + addLabel(new GuiNpcLabel(100, "ability.placement", col2LabelX, y + 5)); + String[] placements = {"Caster", "Target", "Ahead"}; + addButton(new GuiNpcButton(100, col2FieldX, y, 55, 20, placements, trap.getPlacement().ordinal())); + + y += 24; + + // Row 2: Trigger Radius + addLabel(new GuiNpcLabel(101, "ability.triggerRad", labelX, y + 5)); + addTextField(createFloatField(101, fieldX, y, 50, trap.getTriggerRadius())); y += 24; @@ -90,6 +98,9 @@ public class SubGuiAbilityTrap extends SubGuiAbilityConfig { @Override protected void handleTypeTextField(int id, GuiNpcTextField field) { switch (id) { + case 99: + trap.setDurationTicks(field.getInteger()); + break; case 101: trap.setTriggerRadius(parseFloat(field, trap.getTriggerRadius())); break; From 4330405f961283fab1058973f6c22f0a3721dbb7 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 27 Jan 2026 04:12:39 -0500 Subject: [PATCH 296/358] Fix Cooldown Logic --- src/api | 2 +- src/main/java/noppes/npcs/DataAbilities.java | 44 ++++++++++++++++---- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/src/api b/src/api index ae73e1b88..2054012fa 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit ae73e1b885415701c1ee20b64c1d0ae9c9089efc +Subproject commit 2054012fae799fa233b99824b4f649d311eff86f diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 92bb1241c..0ff0aa8a1 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -166,8 +166,20 @@ public class DataAbilities { // Call onExecute currentAbility.onExecute(npc, target, npc.worldObj); + + // Check if ability completed during onExecute (signalCompletion was called) + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(target); + return; + } } currentAbility.onActiveTick(npc, target, npc.worldObj, currentAbility.getCurrentTick()); + + // Check if ability completed during onActiveTick (signalCompletion was called) + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(target); + return; + } break; case DAZED: @@ -175,17 +187,31 @@ public class DataAbilities { break; case IDLE: - // Ability completed - currentAbility.onComplete(npc, target); - - // Fire complete event - AbilityEvent.CompleteEvent completeEvent = new AbilityEvent.CompleteEvent( - npc.wrappedNPC, currentAbility, target); - NpcAPI.EVENT_BUS.post(completeEvent); - - onAbilityComplete(); + // Ability completed (reached via tick() phase transition from DAZED) + handleAbilityCompletion(target); break; } + } + + /** + * Handle ability completion - called when ability phase becomes IDLE. + * This can happen from: + * - signalCompletion() called in onExecute() or onActiveTick() + * - tick() transitioning from DAZED to IDLE + */ + private void handleAbilityCompletion(EntityLivingBase target) { + if (currentAbility == null) return; + + // Call onComplete callback + currentAbility.onComplete(npc, target); + + // Fire complete event + AbilityEvent.CompleteEvent completeEvent = new AbilityEvent.CompleteEvent( + npc.wrappedNPC, currentAbility, target); + NpcAPI.EVENT_BUS.post(completeEvent); + + // Clean up and roll cooldown + onAbilityComplete(); // Clear AI pathfinding if ability is controlling movement // But DON'T zero motion - let the ability set it if needed From 76fb9ee52fdf194d2102967ab246ac1f83eb5f53 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 27 Jan 2026 14:46:17 -0500 Subject: [PATCH 297/358] Fix Dazed Ticks and other smaller issues --- .../data/ability/type/AbilityDash.java | 14 +++---- .../data/ability/type/AbilityGuard.java | 6 +-- .../data/ability/type/AbilityHeal.java | 27 ++++++------- .../data/ability/type/AbilityTeleport.java | 5 ++- .../data/ability/type/AbilityVortex.java | 39 +++++++++---------- .../assets/customnpcs/lang/en_US.lang | 3 +- 6 files changed, 44 insertions(+), 50 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 586e402a8..cd4d42ac7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -199,8 +199,6 @@ public class AbilityDash extends Ability { npc.motionX = 0; npc.motionZ = 0; npc.velocityChanged = true; - dashDirection = null; - chosenDirection = null; } @Override @@ -208,23 +206,21 @@ public class AbilityDash extends Ability { npc.motionX = 0; npc.motionZ = 0; npc.velocityChanged = true; + } + + @Override + public void cleanup() { dashDirection = null; chosenDirection = null; } private boolean isDashBlocked(EntityNPCInterface npc) { + if (dashDirection == null) return true; double nextX = dashDirection.xCoord * dashSpeed; double nextZ = dashDirection.zCoord * dashSpeed; return !npc.worldObj.getCollidingBoundingBoxes(npc, npc.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty(); } - @Override - public void reset() { - super.reset(); - dashDirection = null; - chosenDirection = null; - } - @Override public float getTelegraphRadius() { return 0; // No telegraph for dash diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index 831ef97fd..fd9f25a24 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -116,11 +116,7 @@ public class AbilityGuard extends Ability { lastAttacker = attacker; lastDamageTaken = damage; counterTriggered = true; - - if (interruptible) { - performCounter(npc, npc.worldObj); - npc.abilities.interruptCurrentAbility(source, damage); - } + // Counter will be performed in onActiveTick - don't self-interrupt } private void performCounter(EntityNPCInterface npc, World world) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index 59ef4751a..f19a7cb02 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -34,7 +34,14 @@ public class AbilityHeal extends Ability { private boolean instantHeal = true; // Runtime state - private transient List healedAllies = new ArrayList<>(); + private transient List healedAllies; + + private List getHealedAllies() { + if (healedAllies == null) { + healedAllies = new ArrayList<>(); + } + return healedAllies; + } public AbilityHeal() { this.typeId = "ability.cnpc.heal"; @@ -73,7 +80,7 @@ public class AbilityHeal extends Ability { public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { if (world.isRemote) return; - healedAllies.clear(); + getHealedAllies().clear(); // Always find allies if we're healing them (needed for both instant and HoT) if (healAllies && healRadius > 0) { @@ -87,7 +94,7 @@ public class AbilityHeal extends Ability { spawnHealParticles(world, npc); } - for (EntityLivingBase ally : healedAllies) { + for (EntityLivingBase ally : getHealedAllies()) { healEntity(ally); spawnHealParticles(world, ally); } @@ -120,7 +127,7 @@ public class AbilityHeal extends Ability { } if (healAllies && healRadius > 0) { - for (EntityLivingBase ally : healedAllies) { + for (EntityLivingBase ally : getHealedAllies()) { if (!ally.isDead) { float allyTickHeal = tickHeal; // Add percentage-based heal portion for each ally @@ -164,7 +171,7 @@ public class AbilityHeal extends Ability { if (living instanceof EntityNPCInterface) { float dist = npc.getDistanceToEntity(living); if (dist <= healRadius) { - healedAllies.add(living); + getHealedAllies().add(living); } } } @@ -192,14 +199,8 @@ public class AbilityHeal extends Ability { } @Override - public void onInterrupt(EntityNPCInterface npc, net.minecraft.util.DamageSource source, float damage) { - healedAllies.clear(); - } - - @Override - public void reset() { - super.reset(); - healedAllies.clear(); + public void cleanup() { + getHealedAllies().clear(); } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index 668b2f04d..218432fc9 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -374,7 +374,10 @@ public class AbilityTeleport extends Ability { double dist = Math.sqrt(Math.pow(living.posX - x, 2) + Math.pow(living.posZ - z, 2)); if (dist <= damageRadius) { // Apply damage with scripted event support (no knockback for teleport damage) - applyAbilityDamage(npc, living, damage, 0); + boolean wasHit = applyAbilityDamage(npc, living, damage, 0); + if (wasHit) { + applyEffects(living); + } } } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index 70c1c96b7..fef61779e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -8,7 +8,6 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; -import net.minecraft.util.DamageSource; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityVortex; @@ -36,10 +35,17 @@ public class AbilityVortex extends Ability { private float pullDamage = 0.0f; // Runtime state - private transient Set pulledEntities = new HashSet<>(); + private transient Set pulledEntities; private transient boolean pullComplete = false; private transient int ticksSincePullDamage = 0; + private Set getPulledEntities() { + if (pulledEntities == null) { + pulledEntities = new HashSet<>(); + } + return pulledEntities; + } + public AbilityVortex() { this.typeId = "ability.cnpc.vortex"; this.name = "Vortex"; @@ -82,7 +88,7 @@ public class AbilityVortex extends Ability { @Override public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - pulledEntities.clear(); + getPulledEntities().clear(); pullComplete = false; ticksSincePullDamage = 0; @@ -98,7 +104,7 @@ public class AbilityVortex extends Ability { double dist = npc.getDistanceToEntity(entity); if (dist <= pullRadius) { - pulledEntities.add(entity.getUniqueID()); + getPulledEntities().add(entity.getUniqueID()); count++; if (count >= maxTargets) break; } @@ -108,7 +114,7 @@ public class AbilityVortex extends Ability { if (target != null && !target.isDead) { double dist = npc.getDistanceToEntity(target); if (dist <= pullRadius) { - pulledEntities.add(target.getUniqueID()); + getPulledEntities().add(target.getUniqueID()); } } } @@ -116,7 +122,7 @@ public class AbilityVortex extends Ability { @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - if (pullComplete || pulledEntities.isEmpty()) { + if (pullComplete || getPulledEntities().isEmpty()) { signalCompletion(); return; } @@ -128,10 +134,10 @@ public class AbilityVortex extends Ability { boolean anyStillPulling = false; ticksSincePullDamage++; - for (UUID uuid : new HashSet<>(pulledEntities)) { + for (UUID uuid : new HashSet<>(getPulledEntities())) { EntityLivingBase entity = findEntity(npc, world, uuid); if (entity == null || entity.isDead) { - pulledEntities.remove(uuid); + getPulledEntities().remove(uuid); continue; } @@ -141,7 +147,7 @@ public class AbilityVortex extends Ability { double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); if (dist <= 1.0f) { - pulledEntities.remove(uuid); + getPulledEntities().remove(uuid); onTargetArrived(npc, entity, world); continue; } @@ -155,7 +161,7 @@ public class AbilityVortex extends Ability { AxisAlignedBB nextBox = entity.boundingBox.copy().offset(nextX, nextY, nextZ); if (!world.getCollidingBoundingBoxes(entity, nextBox).isEmpty()) { - pulledEntities.remove(uuid); + getPulledEntities().remove(uuid); continue; } @@ -171,7 +177,7 @@ public class AbilityVortex extends Ability { } } - if (!anyStillPulling && pulledEntities.isEmpty()) { + if (!anyStillPulling && getPulledEntities().isEmpty()) { pullComplete = true; signalCompletion(); } @@ -206,15 +212,8 @@ public class AbilityVortex extends Ability { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - pulledEntities.clear(); - pullComplete = false; - ticksSincePullDamage = 0; - } - - @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - pulledEntities.clear(); + public void cleanup() { + getPulledEntities().clear(); pullComplete = false; ticksSincePullDamage = 0; } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 01e3ae541..8ac2d99c0 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1512,8 +1512,7 @@ ability.maxRange=Max Range ability.timing=Timing (ticks) ability.cooldown=Cooldown ability.windup=Wind Up -ability.active=Active -ability.recovery=Recovery +ability.dazed=Dazed Ticks ability.interruptible=Interruptible ability.threshold=Threshold ability.lockMove=Lock Move From 6d39b3c0267cb78a0b6488e548061a61b503fcc5 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 27 Jan 2026 15:21:59 -0500 Subject: [PATCH 298/358] Fix Anchor Points for Animations --- .../kamkeel/npcs/util/AnchorPointHelper.java | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java index 593c8aa19..79ad2cea4 100644 --- a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -178,33 +178,33 @@ public class AnchorPointHelper { float palmY = PALM_DOWN * armScale.scaleY; float palmZ = PALM_FORWARD * armScale.scaleZ; - // Apply arm rotations (order: Z -> Y -> X) + // Apply arm rotations (order: X -> Y -> Z, matching GL's reverse application) double hx = palmX, hy = palmY, hz = palmZ; - // Z rotation - double cosZ = Math.cos(rotations[2]); - double sinZ = Math.sin(rotations[2]); - double newX = hx * cosZ - hy * sinZ; - double newY = hx * sinZ + hy * cosZ; - hx = newX; - hy = newY; - - // Y rotation - double cosY = Math.cos(rotations[1]); - double sinY = Math.sin(rotations[1]); - newX = hx * cosY + hz * sinY; - double newZ = -hx * sinY + hz * cosY; - hx = newX; - hz = newZ; - - // X rotation + // X rotation first double cosX = Math.cos(rotations[0]); double sinX = Math.sin(rotations[0]); - newY = hy * cosX - hz * sinX; - newZ = hy * sinX + hz * cosX; + double newY = hy * cosX - hz * sinX; + double newZ = hy * sinX + hz * cosX; hy = newY; hz = newZ; + // Y rotation second + double cosY = Math.cos(rotations[1]); + double sinY = Math.sin(rotations[1]); + double newX = hx * cosY + hz * sinY; + newZ = -hx * sinY + hz * cosY; + hx = newX; + hz = newZ; + + // Z rotation last + double cosZ = Math.cos(rotations[2]); + double sinZ = Math.sin(rotations[2]); + newX = hx * cosZ - hy * sinZ; + newY = hx * sinZ + hy * cosZ; + hx = newX; + hy = newY; + // Combine shoulder + rotated hand offset double modelX = shoulderX + hx; double modelY = shoulderY + hy; @@ -216,12 +216,15 @@ public class AnchorPointHelper { double blockZ = modelZ * MODEL_SCALE * scale; // Apply body yaw rotation + // Renderer uses glRotate(180 - yaw), so: cos(180-yaw) = -cos(yaw), sin(180-yaw) = sin(yaw) + // Y-axis rotation: x' = x*cos + z*sin, z' = -x*sin + z*cos + // Substituting: x' = -x*cos(yaw) + z*sin(yaw), z' = -x*sin(yaw) - z*cos(yaw) float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); double cosYaw = Math.cos(bodyYaw); double sinYaw = Math.sin(bodyYaw); double worldOffsetX = blockX * cosYaw + blockZ * sinYaw; - double worldOffsetZ = -blockX * sinYaw + blockZ * cosYaw; + double worldOffsetZ = blockX * sinYaw - blockZ * cosYaw; // Calculate shoulder height based on entity and scale float shoulderHeight = entity.height * ARM_HEIGHT; From c6810c05eae419986345ff31853c108913f5be4e Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 27 Jan 2026 16:43:47 -0500 Subject: [PATCH 299/358] More Ability Fixes. --- .../data/ability/type/AbilitySweeper.java | 4 +- .../kamkeel/npcs/util/AnchorPointHelper.java | 29 ++++--- .../npcs/client/gui/SubGuiColorSelector.java | 6 +- .../client/gui/advanced/GuiNPCAbilities.java | 29 ++++++- .../gui/advanced/SubGuiAbilityConfig.java | 85 +++++++++++++++---- .../advanced/ability/SubGuiAbilityCutter.java | 8 +- .../advanced/ability/SubGuiAbilityDash.java | 4 +- .../advanced/ability/SubGuiAbilityDisc.java | 47 +++++++--- .../ability/SubGuiAbilityEnergyBeam.java | 46 ++++++---- .../advanced/ability/SubGuiAbilityGuard.java | 5 +- .../advanced/ability/SubGuiAbilityHazard.java | 12 ++- .../advanced/ability/SubGuiAbilityHeal.java | 12 ++- .../ability/SubGuiAbilityLaserShot.java | 17 ++-- .../advanced/ability/SubGuiAbilityOrb.java | 34 +++++--- .../ability/SubGuiAbilityProjectile.java | 26 ++++-- .../ability/SubGuiAbilitySweeper.java | 8 +- .../ability/SubGuiAbilityTeleport.java | 16 +++- .../advanced/ability/SubGuiAbilityTrap.java | 4 +- .../advanced/ability/SubGuiAbilityVortex.java | 8 +- .../gui/select/GuiAnimationSelection.java | 5 +- .../npcs/client/gui/util/GuiNPCInterface.java | 47 +++++++--- .../assets/customnpcs/lang/en_US.lang | 49 ++++++++++- 22 files changed, 378 insertions(+), 123 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index ca41ed6e3..5a61c5dce 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -30,7 +30,7 @@ public class AbilitySweeper extends Ability { // Type-specific parameters private float beamLength = 10.0f; private float beamWidth = 0.3f; // Thin like beam trail - private float beamHeight = 0.8f; // Height above ground (low enough to jump over) + private float beamHeight = 0.5f; // Height above ground (low enough to jump over) private float damage = 5.0f; private int damageInterval = 5; private boolean piercing = true; @@ -172,7 +172,7 @@ public class AbilitySweeper extends Ability { public void readTypeNBT(NBTTagCompound nbt) { this.beamLength = nbt.hasKey("beamLength") ? nbt.getFloat("beamLength") : 10.0f; this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.3f; - this.beamHeight = nbt.hasKey("beamHeight") ? nbt.getFloat("beamHeight") : 0.8f; + this.beamHeight = nbt.hasKey("beamHeight") ? nbt.getFloat("beamHeight") : 0.5f; this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 5.0f; this.damageInterval = nbt.hasKey("damageInterval") ? nbt.getInteger("damageInterval") : 5; this.piercing = !nbt.hasKey("piercing") || nbt.getBoolean("piercing"); diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java index 79ad2cea4..45e4f3eab 100644 --- a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -40,9 +40,10 @@ public class AnchorPointHelper { // Distance in front for FRONT anchor private static final float DEFAULT_FRONT_DISTANCE = 1.0f; - // Fallback arm positioning (when no animation) - private static final float FALLBACK_LATERAL = 0.35f; - private static final float FALLBACK_FORWARD = 0.3f; + // Fallback arm positioning (when no animation, arms hang at sides) + private static final float FALLBACK_LATERAL = 0.35f; // Distance from body center to arm + private static final float FALLBACK_FORWARD = 0.1f; // Slight forward offset + private static final float FALLBACK_ARM_HEIGHT = 0.45f; // Hand height when arms hang down (near hip) /** * Calculate world position for an anchor point on an entity. @@ -274,6 +275,7 @@ public class AnchorPointHelper { /** * Calculate fallback hand position when no animation is active. + * Represents arms hanging at sides in default idle pose. */ private static Vec3 calculateFallbackHandPosition(EntityLivingBase entity, boolean rightHand, float scale) { float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); @@ -281,20 +283,23 @@ public class AnchorPointHelper { // Get arm scale for EntityCustomNpc ModelScalePart armScale = getArmScale(entity); - // Perpendicular direction for lateral offset + // Perpendicular direction for lateral offset (right = +90°, left = -90° from body facing) float perpYaw = bodyYaw + (rightHand ? (float) (Math.PI / 2) : (float) (-Math.PI / 2)); - // Calculate offsets scaled by model size and arm scale - double lateralX = -Math.sin(perpYaw) * FALLBACK_LATERAL * scale * armScale.scaleX; - double lateralZ = Math.cos(perpYaw) * FALLBACK_LATERAL * scale * armScale.scaleX; - double forwardX = -Math.sin(bodyYaw) * FALLBACK_FORWARD * scale * armScale.scaleZ; - double forwardZ = Math.cos(bodyYaw) * FALLBACK_FORWARD * scale * armScale.scaleZ; + // Calculate lateral offset - arms hang at sides + // Scale by model size, but not by arm scale (lateral distance is fixed by skeleton) + double lateralX = -Math.sin(perpYaw) * FALLBACK_LATERAL * scale; + double lateralZ = Math.cos(perpYaw) * FALLBACK_LATERAL * scale; - // Arm length affects vertical position - double armLengthOffset = (1 - armScale.scaleY) * 0.3; + // Small forward offset + double forwardX = -Math.sin(bodyYaw) * FALLBACK_FORWARD * scale; + double forwardZ = Math.cos(bodyYaw) * FALLBACK_FORWARD * scale; + + // Arm length affects vertical position - longer arms = lower hands + double armLengthOffset = armScale.scaleY * 0.3 * scale; double x = entity.posX + lateralX + forwardX; - double y = entity.posY + entity.height * ARM_HEIGHT - armLengthOffset; + double y = entity.posY + entity.height * FALLBACK_ARM_HEIGHT + armLengthOffset; double z = entity.posZ + lateralZ + forwardZ; return Vec3.createVectorHelper(x, y, z); diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiColorSelector.java b/src/main/java/noppes/npcs/client/gui/SubGuiColorSelector.java index 947825f5a..be8c07f8a 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiColorSelector.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiColorSelector.java @@ -25,10 +25,10 @@ public class SubGuiColorSelector extends SubGuiInterface implements ITextfieldLi public int color; public SubGuiColorSelector(int color) { - xSize = 176; + xSize = 182; ySize = 222; this.color = color; - setBackground("smallbg.png"); + setBackground("menubg.png"); } @Override @@ -40,7 +40,7 @@ public class SubGuiColorSelector extends SubGuiInterface implements ITextfieldLi this.addTextField(textfield = new GuiNpcTextField(0, this, guiLeft + 53, guiTop + 20, 70, 20, getColor())); textfield.setTextColor(color); - addButton(new GuiNpcButton(66, guiLeft + 112, guiTop + 198, 60, 20, "gui.done")); + addButton(new GuiNpcButton(66, guiLeft + 112, guiTop + 175, 60, 20, "gui.done")); } public String getColor() { diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java index c0fb15596..9abcebbe7 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -73,15 +73,15 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC addLabel(new GuiNpcLabel(100, "gui.enabled", guiLeft + 5, y + 5)); addButton(new GuiNpcButton(100, guiLeft + 55, y, 50, 20, new String[]{"gui.no", "gui.yes"}, abilitiesEnabled ? 1 : 0)); - // Cooldown range: Min Cooldown and Max Cooldown + // Cooldown range: Min Cooldown and Max Cooldown (spaced apart more) addLabel(new GuiNpcLabel(101, "ability.minCooldown", guiLeft + 115, y + 5)); - GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 185, y, 40, 20, String.valueOf(minCooldown)); + GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 165, y, 40, 20, String.valueOf(minCooldown)); minField.setIntegersOnly(); minField.setMinMaxDefault(0, 10000, 20); addTextField(minField); - addLabel(new GuiNpcLabel(102, "ability.maxCooldown", guiLeft + 235, y + 5)); - GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 305, y, 40, 20, String.valueOf(maxCooldown)); + addLabel(new GuiNpcLabel(102, "ability.maxCooldown", guiLeft + 260, y + 5)); + GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 310, y, 40, 20, String.valueOf(maxCooldown)); maxField.setIntegersOnly(); maxField.setMinMaxDefault(0, 10000, 60); addTextField(maxField); @@ -123,6 +123,14 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Load button (under the add/remove arrows) addButton(new GuiNpcButton(75, centerX, y + 90, 40, 20, "gui.load")); + // On/Off toggle for selected ability (under Load button) + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { + Ability selectedAbility = npcAbilities.get(selectedAbilityIndex); + GuiNpcButton toggleBtn = new GuiNpcButton(77, centerX, y + 112, 40, 20, + new String[]{"gui.off", "gui.on"}, selectedAbility.isEnabled() ? 1 : 0); + addButton(toggleBtn); + } + // Right side buttons: Edit and Up/Down carrots with Save button addButton(new GuiNpcButton(72, guiLeft + 210, y + 145, 55, 20, "gui.edit")); getButton(72).setEnabled(selectedAbilityIndex >= 0); @@ -243,6 +251,19 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC return; } + // Toggle selected ability on/off + if (id == 77) { + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { + Ability ability = npcAbilities.get(selectedAbilityIndex); + ability.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); + updateNpcAbilitiesList(); + selectAbilityByIndex(selectedAbilityIndex); + initGui(); + save(); + } + return; + } + initGui(); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index 040c999a4..831420473 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -4,6 +4,9 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.Condition; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.Frame; +import noppes.npcs.controllers.AnimationController; import net.minecraft.client.gui.GuiButton; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.StatCollector; @@ -140,7 +143,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi this.hasTypeSettings = ability.hasTypeSettings(); this.hasVisualSettings = hasVisualSettings(); - setBackground("menubg.png"); + setBackground("menubg.png", 217); xSize = 356; ySize = 200; } @@ -248,13 +251,17 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi addTextField(weightField); addLabel(new GuiNpcLabel(5, "ability.lockMove", col2LabelX, y + 5)); - addButton(new GuiNpcButton(16, col2FieldX, y, 40, 20, new String[]{"gui.no", "gui.yes"}, lockMovement ? 1 : 0)); + GuiNpcButton lockMoveBtn = new GuiNpcButton(16, col2FieldX, y, 40, 20, new String[]{"gui.no", "gui.yes"}, lockMovement ? 1 : 0); + lockMoveBtn.setHoverText("ability.hover.lockMove"); + addButton(lockMoveBtn); y += 24; // Row 3: Interruptible + Dazed Ticks (only if interruptible) addLabel(new GuiNpcLabel(6, "ability.interruptible", col1LabelX, y + 5)); - addButton(new GuiNpcButton(14, guiLeft + 85, y, 40, 20, new String[]{"gui.no", "gui.yes"}, interruptible ? 1 : 0)); + GuiNpcButton interruptBtn = new GuiNpcButton(14, guiLeft + 85, y, 40, 20, new String[]{"gui.no", "gui.yes"}, interruptible ? 1 : 0); + interruptBtn.setHoverText("ability.hover.interruptible"); + addButton(interruptBtn); // Only show dazed ticks if interruptible is enabled if (interruptible) { @@ -278,6 +285,13 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi windupField.setMinMaxDefault(0, 1000, 20); addTextField(windupField); + // Sync button - only show if windup animation is selected + if (windUpAnimationId >= 0) { + GuiNpcButton syncBtn = new GuiNpcButton(17, guiLeft + 115, y, 40, 20, "gui.sync"); + syncBtn.setHoverText("ability.hover.sync"); + addButton(syncBtn); + } + addLabel(new GuiNpcLabel(13, "ability.cooldown", col2LabelX, y + 5)); GuiNpcTextField cooldownField = new GuiNpcTextField(10, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(cooldownTicks)); cooldownField.setIntegersOnly(); @@ -387,7 +401,9 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi if (!targetingModeLocked) { String[] targetingModes = getAvailableTargetingModes(); int selectedIndex = getTargetingModeIndex(targetingMode); - addButton(new GuiNpcButton(4, col1FieldX, y, 90, 20, targetingModes, selectedIndex)); + GuiNpcButton targetBtn = new GuiNpcButton(4, col1FieldX, y, 90, 20, targetingModes, selectedIndex); + targetBtn.setHoverText("ability.hover.targeting"); + addButton(targetBtn); } else { String modeName = "ability.target." + targetingMode.name().toLowerCase(); addLabel(new GuiNpcLabel(23, modeName, col1FieldX, y + 5)); @@ -534,7 +550,9 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Show Telegraph addLabel(new GuiNpcLabel(41, "ability.showTelegraph", col1LabelX, y + 5)); - addButton(new GuiNpcButton(20, guiLeft + 95, y, 40, 20, new String[]{"gui.no", "gui.yes"}, showTelegraph ? 1 : 0)); + GuiNpcButton telegraphBtn = new GuiNpcButton(20, guiLeft + 95, y, 40, 20, new String[]{"gui.no", "gui.yes"}, showTelegraph ? 1 : 0); + telegraphBtn.setHoverText("ability.hover.showTelegraph"); + addButton(telegraphBtn); // Telegraph Type (separate column) String typeKey = ability.getTelegraphType().name().toLowerCase(); @@ -544,20 +562,20 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi y += 24; - // Colors - addLabel(new GuiNpcLabel(44, "ability.windUpColor", col1LabelX, y + 5)); - String windUpHex = String.format("%06X", windUpColor & 0xFFFFFF); - GuiNpcButton windUpColorBtn = new GuiNpcButton(22, guiLeft + 95, y, 55, 20, windUpHex); - windUpColorBtn.setEnabled(showTelegraph); - windUpColorBtn.setTextColor(windUpColor & 0xFFFFFF); - addButton(windUpColorBtn); + // Colors - only show when telegraph is enabled + if (showTelegraph) { + addLabel(new GuiNpcLabel(44, "ability.windUpColor", col1LabelX, y + 5)); + String windUpHex = String.format("%06X", windUpColor & 0xFFFFFF); + GuiNpcButton windUpColorBtn = new GuiNpcButton(22, guiLeft + 95, y, 55, 20, windUpHex); + windUpColorBtn.setTextColor(windUpColor & 0xFFFFFF); + addButton(windUpColorBtn); - addLabel(new GuiNpcLabel(45, "ability.activeColor", guiLeft + 200, y + 5)); - String activeHex = String.format("%06X", activeColor & 0xFFFFFF); - GuiNpcButton activeColorBtn = new GuiNpcButton(24, guiLeft + 280, y, 55, 20, activeHex); - activeColorBtn.setEnabled(showTelegraph); - activeColorBtn.setTextColor(activeColor & 0xFFFFFF); - addButton(activeColorBtn); + addLabel(new GuiNpcLabel(45, "ability.activeColor", guiLeft + 200, y + 5)); + String activeHex = String.format("%06X", activeColor & 0xFFFFFF); + GuiNpcButton activeColorBtn = new GuiNpcButton(24, guiLeft + 280, y, 55, 20, activeHex); + activeColorBtn.setTextColor(activeColor & 0xFFFFFF); + addButton(activeColorBtn); + } } } @@ -627,6 +645,9 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi initGui(); // Refresh to show/hide dazed ticks field } else if (id == 16) { lockMovement = ((GuiNpcButton) guibutton).getValue() == 1; + } else if (id == 17) { + // Sync windup ticks with animation duration + syncWindupWithAnimation(); } // Target tab - Targeting mode @@ -918,6 +939,34 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi initGui(); } + // ═══════════════════════════════════════════════════════════════════════════ + // SYNC ANIMATION + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Syncs windup ticks with the selected windup animation's total duration. + */ + private void syncWindupWithAnimation() { + if (windUpAnimationId < 0 || AnimationController.Instance == null) { + return; + } + + Animation animation = (Animation) AnimationController.Instance.get(windUpAnimationId); + if (animation == null || animation.frames.isEmpty()) { + return; + } + + // Calculate total duration by summing all frame durations + int totalDuration = 0; + for (Frame frame : animation.frames) { + totalDuration += frame.getDuration(); + } + + // Update the cached value and refresh GUI + windUpTicks = totalDuration; + initGui(); + } + // ═══════════════════════════════════════════════════════════════════════════ // HELPER METHODS FOR SUBCLASSES // ═══════════════════════════════════════════════════════════════════════════ diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java index f3dbc64c7..f10c63eef 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java @@ -61,10 +61,14 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { // Row 4: Sweep Mode + Piercing addLabel(new GuiNpcLabel(106, "ability.sweepMode", labelX, y + 5)); String[] sweepModes = {"Swipe", "Spin"}; - addButton(new GuiNpcButton(106, fieldX, y, 70, 20, sweepModes, cutter.getSweepMode().ordinal())); + GuiNpcButton sweepModeBtn = new GuiNpcButton(106, fieldX, y, 70, 20, sweepModes, cutter.getSweepMode().ordinal()); + sweepModeBtn.setHoverText("ability.hover.sweepMode"); + addButton(sweepModeBtn); addLabel(new GuiNpcLabel(107, "ability.piercing", col2LabelX, y + 5)); - addButton(new GuiNpcButton(107, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, cutter.isPiercing() ? 1 : 0)); + GuiNpcButton piercingBtn = new GuiNpcButton(107, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, cutter.isPiercing() ? 1 : 0); + piercingBtn.setHoverText("ability.hover.piercing"); + addButton(piercingBtn); y += 24; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java index 26bfd6610..f113b3c1c 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java @@ -30,7 +30,9 @@ public class SubGuiAbilityDash extends SubGuiAbilityConfig { // Row 1: Dash Mode addLabel(new GuiNpcLabel(100, "ability.mode", labelX, y + 5)); String[] dashModes = {"Aggressive", "Defensive"}; - addButton(new GuiNpcButton(100, fieldX, y, 80, 20, dashModes, dash.getDashMode().ordinal())); + GuiNpcButton modeBtn = new GuiNpcButton(100, fieldX, y, 80, 20, dashModes, dash.getDashMode().ordinal()); + modeBtn.setHoverText("ability.hover.dashMode"); + addButton(modeBtn); y += 24; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java index 33c5372fd..2b804d99b 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java @@ -57,30 +57,42 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { y += 24; - // Row 3: Homing + Homing Strength + // Row 3: Homing + Homing Strength (only show strength if homing) addLabel(new GuiNpcLabel(104, "ability.homing", labelX, y + 5)); - addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isHoming() ? 1 : 0)); + GuiNpcButton homingBtn = new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isHoming() ? 1 : 0); + homingBtn.setHoverText("ability.hover.homing"); + addButton(homingBtn); - addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, disc.getHomingStrength())); + if (disc.isHoming()) { + addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); + addTextField(createFloatField(105, col2FieldX, y, 50, disc.getHomingStrength())); + } y += 24; - // Row 4: Boomerang + Boomerang Delay + // Row 4: Boomerang + Boomerang Delay (only show delay if boomerang) addLabel(new GuiNpcLabel(106, "ability.boomerang", labelX, y + 5)); - addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isBoomerang() ? 1 : 0)); + GuiNpcButton boomerangBtn = new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isBoomerang() ? 1 : 0); + boomerangBtn.setHoverText("ability.hover.boomerang"); + addButton(boomerangBtn); - addLabel(new GuiNpcLabel(107, "ability.boomerangDelay", col2LabelX, y + 5)); - addTextField(createIntField(107, col2FieldX, y, 50, disc.getBoomerangDelay())); + if (disc.isBoomerang()) { + addLabel(new GuiNpcLabel(107, "ability.boomerangDelay", col2LabelX, y + 5)); + addTextField(createIntField(107, col2FieldX, y, 50, disc.getBoomerangDelay())); + } y += 24; - // Row 5: Explosive + Explosion Radius + // Row 5: Explosive + Explosion Radius (only show radius if explosive) addLabel(new GuiNpcLabel(108, "ability.explosive", labelX, y + 5)); - addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isExplosive() ? 1 : 0)); + GuiNpcButton explosiveBtn = new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isExplosive() ? 1 : 0); + explosiveBtn.setHoverText("ability.hover.explosive"); + addButton(explosiveBtn); - addLabel(new GuiNpcLabel(109, "ability.explosionRad", col2LabelX, y + 5)); - addTextField(createFloatField(109, col2FieldX, y, 50, disc.getExplosionRadius())); + if (disc.isExplosive()) { + addLabel(new GuiNpcLabel(109, "ability.explosionRad", col2LabelX, y + 5)); + addTextField(createFloatField(109, col2FieldX, y, 50, disc.getExplosionRadius())); + } y += 24; @@ -110,7 +122,9 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { // Row 1: Anchor Point addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); - addButton(new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), disc.getAnchorPoint().getId())); + GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), disc.getAnchorPoint().getId()); + anchorBtn.setHoverText("ability.hover.anchorPoint"); + addButton(anchorBtn); y += 24; @@ -149,7 +163,9 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { // Row 4: Lightning Effect Enabled addLabel(new GuiNpcLabel(205, "ability.lightning", labelX, y + 5)); - addButton(new GuiNpcButton(205, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.hasLightningEffect() ? 1 : 0)); + GuiNpcButton lightningBtn = new GuiNpcButton(205, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.hasLightningEffect() ? 1 : 0); + lightningBtn.setHoverText("ability.hover.lightning"); + addButton(lightningBtn); // Only show Lightning settings if Lightning is enabled if (disc.hasLightningEffect()) { @@ -174,12 +190,15 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { switch (id) { case 104: disc.setHoming(value == 1); + initGui(); break; case 106: disc.setBoomerang(value == 1); + initGui(); break; case 108: disc.setExplosive(value == 1); + initGui(); break; case 150: setSubGui(new SubGuiAbilityEffects(disc.getEffects())); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java index 626fac6b9..6e90428ce 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java @@ -58,30 +58,40 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { y += 24; - // Row 3: Homing + Homing Strength + // Row 3: Homing + Homing Strength (only show strength if homing) addLabel(new GuiNpcLabel(104, "ability.homing", labelX, y + 5)); - addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isHoming() ? 1 : 0)); + GuiNpcButton homingBtn = new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isHoming() ? 1 : 0); + homingBtn.setHoverText("ability.hover.homing"); + addButton(homingBtn); - addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, beam.getHomingStrength())); + if (beam.isHoming()) { + addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); + addTextField(createFloatField(105, col2FieldX, y, 50, beam.getHomingStrength())); + } y += 24; - // Row 4: Homing Range + Max Distance - addLabel(new GuiNpcLabel(106, "ability.homingRange", labelX, y + 5)); - addTextField(createFloatField(106, fieldX, y, 50, beam.getHomingRange())); + // Row 4: Max Distance (+ Homing Range only if homing) + if (beam.isHoming()) { + addLabel(new GuiNpcLabel(106, "ability.homingRange", labelX, y + 5)); + addTextField(createFloatField(106, fieldX, y, 50, beam.getHomingRange())); + } - addLabel(new GuiNpcLabel(107, "ability.maxDist", col2LabelX, y + 5)); - addTextField(createFloatField(107, col2FieldX, y, 50, beam.getMaxDistance())); + addLabel(new GuiNpcLabel(107, "ability.maxDist", beam.isHoming() ? col2LabelX : labelX, y + 5)); + addTextField(createFloatField(107, beam.isHoming() ? col2FieldX : fieldX, y, 50, beam.getMaxDistance())); y += 24; - // Row 5: Explosive + Explosion Radius + // Row 5: Explosive + Explosion Radius (only show radius if explosive) addLabel(new GuiNpcLabel(108, "ability.explosive", labelX, y + 5)); - addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isExplosive() ? 1 : 0)); + GuiNpcButton explosiveBtn = new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isExplosive() ? 1 : 0); + explosiveBtn.setHoverText("ability.hover.explosive"); + addButton(explosiveBtn); - addLabel(new GuiNpcLabel(109, "ability.explosionRad", col2LabelX, y + 5)); - addTextField(createFloatField(109, col2FieldX, y, 50, beam.getExplosionRadius())); + if (beam.isExplosive()) { + addLabel(new GuiNpcLabel(109, "ability.explosionRad", col2LabelX, y + 5)); + addTextField(createFloatField(109, col2FieldX, y, 50, beam.getExplosionRadius())); + } y += 24; @@ -108,7 +118,9 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { // Row 1: Anchor Point addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); - addButton(new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), beam.getAnchorPoint().getId())); + GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), beam.getAnchorPoint().getId()); + anchorBtn.setHoverText("ability.hover.anchorPoint"); + addButton(anchorBtn); y += 24; @@ -147,7 +159,9 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { // Row 4: Lightning Effect Enabled (only affects head) addLabel(new GuiNpcLabel(205, "ability.lightning", labelX, y + 5)); - addButton(new GuiNpcButton(205, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.hasLightningEffect() ? 1 : 0)); + GuiNpcButton lightningBtn = new GuiNpcButton(205, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.hasLightningEffect() ? 1 : 0); + lightningBtn.setHoverText("ability.hover.lightning"); + addButton(lightningBtn); // Only show Lightning settings if Lightning is enabled if (beam.hasLightningEffect()) { @@ -172,9 +186,11 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { switch (id) { case 104: beam.setHoming(value == 1); + initGui(); break; case 108: beam.setExplosive(value == 1); + initGui(); break; case 150: setSubGui(new SubGuiAbilityEffects(beam.getEffects())); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java index 183d66814..a975a97fc 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java @@ -44,11 +44,14 @@ public class SubGuiAbilityGuard extends SubGuiAbilityConfig { // Row 2: Can Counter + Counter Type addLabel(new GuiNpcLabel(101, "ability.canCounter", labelX, y + 5)); - addButton(new GuiNpcButton(101, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, guard.isCanCounter() ? 1 : 0)); + GuiNpcButton canCounterBtn = new GuiNpcButton(101, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, guard.isCanCounter() ? 1 : 0); + canCounterBtn.setHoverText("ability.hover.canCounter"); + addButton(canCounterBtn); addLabel(new GuiNpcLabel(102, "ability.counterType", col2LabelX, y + 5)); String[] types = {"Flat", "Percent"}; GuiNpcButton counterTypeButton = new GuiNpcButton(102, col2FieldX, y, 60, 20, types, guard.getCounterType().ordinal()); + counterTypeButton.setHoverText("ability.hover.counterType"); counterTypeButton.setEnabled(guard.isCanCounter()); addButton(counterTypeButton); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java index c9633c0f9..9822d9f90 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java @@ -39,14 +39,18 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { addLabel(new GuiNpcLabel(100, "ability.shape", col2LabelX, y + 5)); String[] shapes = {"Circle", "Ring", "Cone"}; - addButton(new GuiNpcButton(100, col2FieldX, y, 55, 20, shapes, hazard.getShape().ordinal())); + GuiNpcButton shapeBtn = new GuiNpcButton(100, col2FieldX, y, 55, 20, shapes, hazard.getShape().ordinal()); + shapeBtn.setHoverText("ability.hover.shape"); + addButton(shapeBtn); y += 24; // Row 2: Placement addLabel(new GuiNpcLabel(101, "ability.placement", labelX, y + 5)); String[] placements = {"Caster", "Target", "FollowCast", "FollowTgt"}; - addButton(new GuiNpcButton(101, fieldX, y, 70, 20, placements, hazard.getPlacement().ordinal())); + GuiNpcButton placementBtn = new GuiNpcButton(101, fieldX, y, 70, 20, placements, hazard.getPlacement().ordinal()); + placementBtn.setHoverText("ability.hover.placement"); + addButton(placementBtn); y += 24; @@ -82,7 +86,9 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { addTextField(createIntField(108, fieldX, y, 50, hazard.getPoisonLevel())); addLabel(new GuiNpcLabel(109, "ability.affectsCaster", col2LabelX, y + 5)); - addButton(new GuiNpcButton(109, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, hazard.isAffectsCaster() ? 1 : 0)); + GuiNpcButton affectsCasterBtn = new GuiNpcButton(109, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, hazard.isAffectsCaster() ? 1 : 0); + affectsCasterBtn.setHoverText("ability.hover.affectsCaster"); + addButton(affectsCasterBtn); y += 24; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java index 010da51cf..490b4773a 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java @@ -29,7 +29,9 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig { // Row 1: Instant Heal + Duration (only shown if not instant) addLabel(new GuiNpcLabel(103, "dialog.instant", labelX, y + 5)); - addButton(new GuiNpcButton(103, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isInstantHeal() ? 1 : 0)); + GuiNpcButton instantBtn = new GuiNpcButton(103, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isInstantHeal() ? 1 : 0); + instantBtn.setHoverText("ability.hover.instant"); + addButton(instantBtn); if (!heal.isInstantHeal()) { addLabel(new GuiNpcLabel(99, "ability.duration", col2LabelX, y + 5)); @@ -57,10 +59,14 @@ public class SubGuiAbilityHeal extends SubGuiAbilityConfig { // Row 4: Heal Self + Heal Allies addLabel(new GuiNpcLabel(104, "ability.healSelf", labelX, y + 5)); - addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isHealSelf() ? 1 : 0)); + GuiNpcButton healSelfBtn = new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isHealSelf() ? 1 : 0); + healSelfBtn.setHoverText("ability.hover.healSelf"); + addButton(healSelfBtn); addLabel(new GuiNpcLabel(105, "ability.healAllies", col2LabelX, y + 5)); - addButton(new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isHealAllies() ? 1 : 0)); + GuiNpcButton healAlliesBtn = new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isHealAllies() ? 1 : 0); + healAlliesBtn.setHoverText("ability.hover.healAllies"); + addButton(healAlliesBtn); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java index 614a8bce0..50e8fa92c 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java @@ -65,12 +65,16 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { y += 24; - // Row 4: Explosive + Explosion Radius + // Row 4: Explosive + Explosion Radius (only show radius if explosive) addLabel(new GuiNpcLabel(106, "ability.explosive", labelX, y + 5)); - addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, laser.isExplosive() ? 1 : 0)); + GuiNpcButton explosiveBtn = new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, laser.isExplosive() ? 1 : 0); + explosiveBtn.setHoverText("ability.hover.explosive"); + addButton(explosiveBtn); - addLabel(new GuiNpcLabel(107, "ability.explosionRad", col2LabelX, y + 5)); - addTextField(createFloatField(107, col2FieldX, y, 50, laser.getExplosionRadius())); + if (laser.isExplosive()) { + addLabel(new GuiNpcLabel(107, "ability.explosionRad", col2LabelX, y + 5)); + addTextField(createFloatField(107, col2FieldX, y, 50, laser.getExplosionRadius())); + } y += 24; @@ -124,7 +128,9 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { // Row 3: Lightning Effect Enabled addLabel(new GuiNpcLabel(204, "ability.lightning", labelX, y + 5)); - addButton(new GuiNpcButton(204, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, laser.hasLightningEffect() ? 1 : 0)); + GuiNpcButton lightningBtn = new GuiNpcButton(204, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, laser.hasLightningEffect() ? 1 : 0); + lightningBtn.setHoverText("ability.hover.lightning"); + addButton(lightningBtn); // Only show Lightning settings if Lightning is enabled if (laser.hasLightningEffect()) { @@ -149,6 +155,7 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { switch (id) { case 106: laser.setExplosive(value == 1); + initGui(); break; case 150: setSubGui(new SubGuiAbilityEffects(laser.getEffects())); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java index c43569c46..858eeefa7 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -57,21 +57,29 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { y += 24; - // Row 3: Homing + Homing Strength + // Row 3: Homing + Homing Strength (only show strength if homing) addLabel(new GuiNpcLabel(104, "ability.homing", labelX, y + 5)); - addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isHoming() ? 1 : 0)); + GuiNpcButton homingBtn = new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isHoming() ? 1 : 0); + homingBtn.setHoverText("ability.hover.homing"); + addButton(homingBtn); - addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, orb.getHomingStrength())); + if (orb.isHoming()) { + addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); + addTextField(createFloatField(105, col2FieldX, y, 50, orb.getHomingStrength())); + } y += 24; - // Row 4: Explosive + Explosion Radius + // Row 4: Explosive + Explosion Radius (only show radius if explosive) addLabel(new GuiNpcLabel(106, "ability.explosive", labelX, y + 5)); - addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isExplosive() ? 1 : 0)); + GuiNpcButton explosiveBtn = new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isExplosive() ? 1 : 0); + explosiveBtn.setHoverText("ability.hover.explosive"); + addButton(explosiveBtn); - addLabel(new GuiNpcLabel(107, "ability.explosionRad", col2LabelX, y + 5)); - addTextField(createFloatField(107, col2FieldX, y, 50, orb.getExplosionRadius())); + if (orb.isExplosive()) { + addLabel(new GuiNpcLabel(107, "ability.explosionRad", col2LabelX, y + 5)); + addTextField(createFloatField(107, col2FieldX, y, 50, orb.getExplosionRadius())); + } y += 24; @@ -98,7 +106,9 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { // Row 1: Anchor Point addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); - addButton(new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), orb.getAnchorPoint().getId())); + GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), orb.getAnchorPoint().getId()); + anchorBtn.setHoverText("ability.hover.anchorPoint"); + addButton(anchorBtn); y += 24; @@ -137,7 +147,9 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { // Row 4: Lightning Effect Enabled addLabel(new GuiNpcLabel(205, "ability.lightning", labelX, y + 5)); - addButton(new GuiNpcButton(205, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.hasLightningEffect() ? 1 : 0)); + GuiNpcButton lightningBtn = new GuiNpcButton(205, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.hasLightningEffect() ? 1 : 0); + lightningBtn.setHoverText("ability.hover.lightning"); + addButton(lightningBtn); // Only show Lightning settings if Lightning is enabled if (orb.hasLightningEffect()) { @@ -162,9 +174,11 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { switch (id) { case 104: orb.setHoming(value == 1); + initGui(); break; case 106: orb.setExplosive(value == 1); + initGui(); break; case 150: setSubGui(new SubGuiAbilityEffects(orb.getEffects())); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java index 52bf7a092..fe66fa242 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java @@ -58,21 +58,29 @@ public class SubGuiAbilityProjectile extends SubGuiAbilityConfig { y += 24; - // Row 3: Explosive + Explosion Radius + // Row 3: Explosive + Explosion Radius (only show radius if explosive) addLabel(new GuiNpcLabel(104, "ability.explosive", labelX, y + 5)); - addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, projectile.isExplosive() ? 1 : 0)); + GuiNpcButton explosiveBtn = new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, projectile.isExplosive() ? 1 : 0); + explosiveBtn.setHoverText("ability.hover.explosive"); + addButton(explosiveBtn); - addLabel(new GuiNpcLabel(105, "ability.explosionRad", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, projectile.getExplosionRadius())); + if (projectile.isExplosive()) { + addLabel(new GuiNpcLabel(105, "ability.explosionRad", col2LabelX, y + 5)); + addTextField(createFloatField(105, col2FieldX, y, 50, projectile.getExplosionRadius())); + } y += 24; - // Row 4: Homing + Homing Strength + // Row 4: Homing + Homing Strength (only show strength if homing) addLabel(new GuiNpcLabel(106, "ability.homing", labelX, y + 5)); - addButton(new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, projectile.isHoming() ? 1 : 0)); + GuiNpcButton homingBtn = new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, projectile.isHoming() ? 1 : 0); + homingBtn.setHoverText("ability.hover.homing"); + addButton(homingBtn); - addLabel(new GuiNpcLabel(107, "ability.homingStr", col2LabelX, y + 5)); - addTextField(createFloatField(107, col2FieldX, y, 50, projectile.getHomingStrength())); + if (projectile.isHoming()) { + addLabel(new GuiNpcLabel(107, "ability.homingStr", col2LabelX, y + 5)); + addTextField(createFloatField(107, col2FieldX, y, 50, projectile.getHomingStrength())); + } y += 24; @@ -90,9 +98,11 @@ public class SubGuiAbilityProjectile extends SubGuiAbilityConfig { break; case 104: projectile.setExplosive(value == 1); + initGui(); break; case 106: projectile.setHoming(value == 1); + initGui(); break; case 150: setSubGui(new SubGuiAbilityEffects(projectile.getEffects())); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java index 9a3dc2fe8..1561b408a 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java @@ -71,13 +71,17 @@ public class SubGuiAbilitySweeper extends SubGuiAbilityConfig { addTextField(createIntField(106, fieldX, y, 50, sweeper.getNumberOfRotations())); addLabel(new GuiNpcLabel(107, "ability.piercing", col2LabelX, y + 5)); - addButton(new GuiNpcButton(107, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, sweeper.isPiercing() ? 1 : 0)); + GuiNpcButton piercingBtn = new GuiNpcButton(107, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, sweeper.isPiercing() ? 1 : 0); + piercingBtn.setHoverText("ability.hover.piercing"); + addButton(piercingBtn); y += 24; // Row 5: Lock On Target addLabel(new GuiNpcLabel(108, "ability.lockTarget", labelX, y + 5)); - addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, sweeper.isLockOnTarget() ? 1 : 0)); + GuiNpcButton lockTargetBtn = new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, sweeper.isLockOnTarget() ? 1 : 0); + lockTargetBtn.setHoverText("ability.hover.lockTarget"); + addButton(lockTargetBtn); y += 24; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java index 1dfb12d22..e39918d2d 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java @@ -54,7 +54,9 @@ public class SubGuiAbilityTeleport extends SubGuiAbilityConfig { addTextField(createIntField(103, fieldX, y, 50, teleport.getBlinkDelayTicks())); addLabel(new GuiNpcLabel(105, "ability.lineOfSight", col2LabelX, y + 5)); - addButton(new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isRequireLineOfSight() ? 1 : 0)); + GuiNpcButton losBtn1 = new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isRequireLineOfSight() ? 1 : 0); + losBtn1.setHoverText("ability.hover.lineOfSight"); + addButton(losBtn1); y += 24; break; case SINGLE: @@ -63,7 +65,9 @@ public class SubGuiAbilityTeleport extends SubGuiAbilityConfig { addTextField(createFloatField(101, fieldX, y, 50, teleport.getBlinkRadius())); addLabel(new GuiNpcLabel(105, "ability.lineOfSight", col2LabelX, y + 5)); - addButton(new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isRequireLineOfSight() ? 1 : 0)); + GuiNpcButton losBtn2 = new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isRequireLineOfSight() ? 1 : 0); + losBtn2.setHoverText("ability.hover.lineOfSight"); + addButton(losBtn2); y += 24; break; case BEHIND: @@ -85,10 +89,14 @@ public class SubGuiAbilityTeleport extends SubGuiAbilityConfig { // Damage at Start/End addLabel(new GuiNpcLabel(108, "ability.dmgAtStart", labelX, y + 5)); - addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isDamageAtStart() ? 1 : 0)); + GuiNpcButton dmgStartBtn = new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isDamageAtStart() ? 1 : 0); + dmgStartBtn.setHoverText("ability.hover.dmgAtStart"); + addButton(dmgStartBtn); addLabel(new GuiNpcLabel(109, "ability.dmgAtEnd", col2LabelX, y + 5)); - addButton(new GuiNpcButton(109, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isDamageAtEnd() ? 1 : 0)); + GuiNpcButton dmgEndBtn = new GuiNpcButton(109, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isDamageAtEnd() ? 1 : 0); + dmgEndBtn.setHoverText("ability.hover.dmgAtEnd"); + addButton(dmgEndBtn); y += 24; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java index c846565c4..edeb59635 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java @@ -39,7 +39,9 @@ public class SubGuiAbilityTrap extends SubGuiAbilityConfig { addLabel(new GuiNpcLabel(100, "ability.placement", col2LabelX, y + 5)); String[] placements = {"Caster", "Target", "Ahead"}; - addButton(new GuiNpcButton(100, col2FieldX, y, 55, 20, placements, trap.getPlacement().ordinal())); + GuiNpcButton placementBtn = new GuiNpcButton(100, col2FieldX, y, 55, 20, placements, trap.getPlacement().ordinal()); + placementBtn.setHoverText("ability.hover.placement"); + addButton(placementBtn); y += 24; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java index 4cc7bc89a..b56828470 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java @@ -51,7 +51,9 @@ public class SubGuiAbilityVortex extends SubGuiAbilityConfig { // Row 3: AOE + Max Targets addLabel(new GuiNpcLabel(104, "ability.aoe", labelX, y + 5)); - addButton(new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, vortex.isAoe() ? 1 : 0)); + GuiNpcButton aoeBtn = new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, vortex.isAoe() ? 1 : 0); + aoeBtn.setHoverText("ability.hover.aoe"); + addButton(aoeBtn); addLabel(new GuiNpcLabel(105, "ability.maxTargets", col2LabelX, y + 5)); addTextField(createIntField(105, col2FieldX, y, 50, vortex.getMaxTargets())); @@ -65,7 +67,9 @@ public class SubGuiAbilityVortex extends SubGuiAbilityConfig { // Row 5: Damage on Pull + Pull Damage addLabel(new GuiNpcLabel(108, "ability.dmgOnPull", labelX, y + 5)); - addButton(new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, vortex.isDamageOnPull() ? 1 : 0)); + GuiNpcButton dmgOnPullBtn = new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, vortex.isDamageOnPull() ? 1 : 0); + dmgOnPullBtn.setHoverText("ability.hover.dmgOnPull"); + addButton(dmgOnPullBtn); addLabel(new GuiNpcLabel(109, "ability.pullDamage", col2LabelX, y + 5)); addTextField(createFloatField(109, col2FieldX, y, 50, vortex.getPullDamage())); diff --git a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java index 882f5817b..cc3e06cca 100644 --- a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java +++ b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java @@ -58,7 +58,7 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr if (scrollAnimations == null) { scrollAnimations = new GuiCustomScroll(this, 0, 0); - scrollAnimations.setSize(212, 163); + scrollAnimations.setSize(212, 150); } scrollAnimations.setList(getSearchList()); if (selectedName != null) { @@ -68,7 +68,8 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr scrollAnimations.guiTop = guiTop + 14; this.addScroll(scrollAnimations); - addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 4, guiTop + 179, 212, 20, search)); + // Search textbox - moved up to avoid overlapping with buttons + addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 4, guiTop + 166, 212, 20, search)); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 8444c5c1d..11bbaf451 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -57,6 +57,7 @@ public abstract class GuiNPCInterface extends GuiScreen { public float bgScaleX = 1; public float bgScaleY = 1; public float bgScaleZ = 1; + public int bgTextureHeight = 256; // Actual content height in texture (for bottom border stitching) public GuiNPCInterface(EntityNPCInterface npc) { this.player = Minecraft.getMinecraft().thePlayer; @@ -74,6 +75,11 @@ public abstract class GuiNPCInterface extends GuiScreen { background = new ResourceLocation("customnpcs", "textures/gui/" + texture); } + public void setBackground(String texture, int textureHeight) { + background = new ResourceLocation("customnpcs", "textures/gui/" + texture); + bgTextureHeight = textureHeight; + } + public ResourceLocation getResource(String texture) { return new ResourceLocation("customnpcs", "textures/gui/" + texture); } @@ -421,23 +427,44 @@ public abstract class GuiNPCInterface extends GuiScreen { GL11.glTranslatef(guiLeft, guiTop, 0); GL11.glScalef(bgScale * bgScaleX, bgScale * bgScaleY, bgScale * bgScaleZ); mc.renderEngine.bindTexture(background); + + // Handle Y dimension: if ySize < texture content height, stitch bottom border + int topHeight = ySize; + int bottomHeight = 0; + int bottomTextureV = 0; + if (ySize < bgTextureHeight) { + // Keep bottom border from texture bottom (last 6 pixels of actual content) + bottomHeight = 6; + topHeight = ySize - bottomHeight; + bottomTextureV = bgTextureHeight - bottomHeight; + } + if (xSize > 256) { // GUI wider than texture: draw left portion, then right portion from texture edge - drawTexturedModalRect(0, 0, 0, 0, 250, ySize); - drawTexturedModalRect(250, 0, 256 - (xSize - 250), 0, xSize - 250, ySize); + drawTexturedModalRect(0, 0, 0, 0, 250, topHeight); + drawTexturedModalRect(250, 0, 256 - (xSize - 250), 0, xSize - 250, topHeight); + if (bottomHeight > 0) { + drawTexturedModalRect(0, topHeight, 0, bottomTextureV, 250, bottomHeight); + drawTexturedModalRect(250, topHeight, 256 - (xSize - 250), bottomTextureV, xSize - 250, bottomHeight); + } } else if (xSize < 256) { // GUI narrower than texture: stitch left and right edges together - // Left portion (first half of GUI) int leftWidth = xSize / 2; - // Right portion width (remaining) int rightWidth = xSize - leftWidth; - // Draw left side from texture start - drawTexturedModalRect(0, 0, 0, 0, leftWidth, ySize); - // Draw right side from texture end (256 - rightWidth) - drawTexturedModalRect(leftWidth, 0, 256 - rightWidth, 0, rightWidth, ySize); + // Draw top portion + drawTexturedModalRect(0, 0, 0, 0, leftWidth, topHeight); + drawTexturedModalRect(leftWidth, 0, 256 - rightWidth, 0, rightWidth, topHeight); + // Draw bottom border strip + if (bottomHeight > 0) { + drawTexturedModalRect(0, topHeight, 0, bottomTextureV, leftWidth, bottomHeight); + drawTexturedModalRect(leftWidth, topHeight, 256 - rightWidth, bottomTextureV, rightWidth, bottomHeight); + } } else { - // GUI exactly 256px: draw full texture - drawTexturedModalRect(0, 0, 0, 0, xSize, ySize); + // GUI exactly 256px wide + drawTexturedModalRect(0, 0, 0, 0, xSize, topHeight); + if (bottomHeight > 0) { + drawTexturedModalRect(0, topHeight, 0, bottomTextureV, xSize, bottomHeight); + } } GL11.glPopMatrix(); } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 8ac2d99c0..750a75f54 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -115,6 +115,7 @@ gui.play=Play gui.selectSound=Select Sound gui.selectnpc=Select NPC gui.select=Select +gui.sync=Sync gui.range=Range gui.partly=Partially gui.copy=Copy @@ -1504,6 +1505,8 @@ menu.abilities=Abilities ability.availableTypes=Available Types ability.npcAbilities=NPC Abilities ability.globalCooldown=Global Cooldown +ability.minCooldown=Min CD +ability.maxCooldown=Max CD ability.edit=Edit Ability ability.weight=Weight ability.targeting=Targeting @@ -1679,7 +1682,7 @@ ability.innerColor=Inner Color ability.outerColor=Outer Color ability.outerEnabled=Outer Glow ability.outerWidth=Outer Width -ability.lightning=Enable Lightning +ability.lightning=Lightning ability.lightningDensity=Density ability.lightningRadius=Radius ability.anchorPoint=Anchor Point @@ -1725,6 +1728,50 @@ ability.typeId=Type ID ability.conditions=Conditions ability.noTypeSettings=No type-specific settings +ability.hover.weight=Higher weight = more likely to be chosen when multiple abilities available +ability.hover.lockMove=Prevents NPC from moving while ability is charging or active +ability.hover.interruptible=If enabled, ability can be interrupted by taking damage +ability.hover.dazed=Stun duration in ticks if ability is interrupted (20 ticks = 1 second) +ability.hover.windup=Charge-up time before ability activates (20 ticks = 1 second) +ability.hover.cooldown=Time before ability can be used again (20 ticks = 1 second) +ability.hover.sync=Set windup ticks to match animation duration +ability.hover.minRange=Target must be at least this far away +ability.hover.maxRange=Target must be within this distance +ability.hover.targeting=How the ability selects its target +ability.hover.conditions=Requirements that must be met for ability to activate +ability.hover.showTelegraph=Show visual warning indicator during windup +ability.hover.windUpColor=Color of telegraph during charge-up phase +ability.hover.activeColor=Color of telegraph when ability is active +ability.hover.homing=Projectile tracks toward target +ability.hover.homingStr=How aggressively the projectile tracks (higher = tighter turns) +ability.hover.explosive=Creates explosion on impact +ability.hover.explosionRad=Size of explosion radius +ability.hover.piercing=Hits multiple targets instead of stopping on first hit +ability.hover.lightning=Add electrical effect to visuals +ability.hover.outerGlow=Enable outer glow layer around projectile +ability.hover.anchorPoint=Where on the NPC the ability spawns from +ability.hover.damage=Base damage dealt to targets +ability.hover.speed=Movement speed of projectile +ability.hover.lifetime=Maximum ticks before despawning +ability.hover.knockback=Push force applied to targets +ability.hover.boomerang=Projectile returns to caster after reaching max distance +ability.hover.dashMode=Aggressive dashes toward target, Defensive dashes away +ability.hover.aoe=Affects all valid targets in range instead of just one +ability.hover.dmgOnPull=Deal damage continuously while pulling targets +ability.hover.lineOfSight=Requires clear line of sight to the target +ability.hover.dmgAtStart=Deal damage at the teleport origin location +ability.hover.dmgAtEnd=Deal damage at the teleport destination +ability.hover.instant=Heals instantly instead of over time +ability.hover.healSelf=Ability can heal the casting NPC +ability.hover.healAllies=Ability can heal nearby allied NPCs +ability.hover.placement=Where the ability effect is placed (Caster, Target, etc.) +ability.hover.lockTarget=Sweeper beam tracks the target's position +ability.hover.canCounter=Enable counterattack when blocking damage +ability.hover.counterType=Flat = fixed damage, Percent = % of blocked damage +ability.hover.shape=Shape of the hazard zone (Circle, Ring, Cone) +ability.hover.affectsCaster=Hazard can damage the NPC that created it +ability.hover.sweepMode=Swipe = single arc attack, Spin = continuous rotation + # Conditions condition.edit=Edit Condition condition.hp_above=NPC HP Above From aeb0093b4d9ded959e0327c2d70214d3c1f40712 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 27 Jan 2026 18:04:46 -0500 Subject: [PATCH 300/358] Add Tooltips --- .../gui/advanced/SubGuiConditionEdit.java | 7 +- .../ability/SubGuiAbilityEffects.java | 9 +- .../npcs/client/gui/util/GuiNPCInterface.java | 5 + .../npcs/client/gui/util/GuiNpcButton.java | 27 ++- .../npcs/client/gui/util/GuiNpcTextField.java | 157 ++++++++++++++++++ .../assets/customnpcs/lang/en_US.lang | 16 ++ 6 files changed, 210 insertions(+), 11 deletions(-) diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java index 3d2874dbc..f45858a99 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java @@ -88,7 +88,9 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi // Condition Type selector addLabel(new GuiNpcLabel(1, "gui.type", labelX, y + 5)); - addButton(new GuiNpcButton(1, fieldX, y, 115, 20, CONDITION_TYPES, selectedTypeIndex)); + GuiNpcButton typeBtn = new GuiNpcButton(1, fieldX, y, 115, 20, CONDITION_TYPES, selectedTypeIndex); + typeBtn.setHoverText("condition.hover.type"); + addButton(typeBtn); y += 26; // Type-specific fields @@ -99,6 +101,7 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi GuiNpcTextField hitsField = new GuiNpcTextField(2, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(requiredHits)); hitsField.setIntegersOnly(); hitsField.setMinMaxDefault(1, 100, 3); + hitsField.setHoverText("condition.hover.hits"); addTextField(hitsField); y += 24; @@ -106,6 +109,7 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi GuiNpcTextField ticksField = new GuiNpcTextField(3, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(withinTicks)); ticksField.setIntegersOnly(); ticksField.setMinMaxDefault(1, 1200, 60); + ticksField.setHoverText("condition.hover.within"); addTextField(ticksField); } else { // HP threshold field (as percentage) @@ -114,6 +118,7 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi GuiNpcTextField thresholdField = new GuiNpcTextField(4, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(percent)); thresholdField.setIntegersOnly(); thresholdField.setMinMaxDefault(1, 99, 50); + thresholdField.setHoverText("condition.hover.threshold"); addTextField(thresholdField); addLabel(new GuiNpcLabel(4, "%", fieldX + 45, y + 5)); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEffects.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEffects.java index 103d87224..b4eced168 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEffects.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEffects.java @@ -74,13 +74,16 @@ public class SubGuiAbilityEffects extends SubGuiInterface implements ITextfieldL // Type selector button int typeIndex = effect.getType().ordinal(); - addButton(new GuiNpcButton(baseId, guiLeft + 8, y, 100, 20, EFFECT_TYPE_NAMES, typeIndex)); + GuiNpcButton typeBtn = new GuiNpcButton(baseId, guiLeft + 8, y, 100, 20, EFFECT_TYPE_NAMES, typeIndex); + typeBtn.setHoverText("effect.hover.type"); + addButton(typeBtn); // Duration text field GuiNpcTextField durField = new GuiNpcTextField(baseId + 1, this, fontRendererObj, guiLeft + 112, y, 50, 20, String.valueOf(effect.getDurationTicks())); durField.setIntegersOnly(); durField.setMinMaxDefault(1, 12000, 60); + durField.setHoverText("effect.hover.duration"); addTextField(durField); // Amplifier selector (0-10) @@ -88,7 +91,9 @@ public class SubGuiAbilityEffects extends SubGuiInterface implements ITextfieldL for (int a = 0; a <= 10; a++) { ampValues[a] = String.valueOf(a); } - addButton(new GuiNpcButton(baseId + 2, guiLeft + 168, y, 40, 20, ampValues, effect.getAmplifier())); + GuiNpcButton ampBtn = new GuiNpcButton(baseId + 2, guiLeft + 168, y, 40, 20, ampValues, effect.getAmplifier()); + ampBtn.setHoverText("effect.hover.amplifier"); + addButton(ampBtn); // Delete button addButton(new GuiNpcButton(baseId + 3, guiLeft + 215, y, 20, 20, "X")); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java index 11bbaf451..4ddd820c0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNPCInterface.java @@ -409,6 +409,11 @@ public abstract class GuiNPCInterface extends GuiScreen { button.drawHover(i, j, subGui); } } + for (GuiNpcTextField textField : textfields.values()) { + if (textField.hasHoverText()) { + textField.drawHover(i, j, subGui); + } + } for (GuiScreen gui : extra.values()) gui.drawScreen(i, j, f); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java index c14d0da83..d09bddecf 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java @@ -3,6 +3,7 @@ package noppes.npcs.client.gui.util; import kamkeel.npcs.util.TextSplitter; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.ScaledResolution; import net.minecraft.client.renderer.RenderHelper; import net.minecraft.util.ResourceLocation; import net.minecraft.util.StatCollector; @@ -217,17 +218,27 @@ public class GuiNpcButton extends GuiButton { maxHeight += 2 + (textLines.size() - 1) * 10; } - int gameWidth = mc.displayWidth; - int maxTooltipX = gameWidth - maxWidth - 4; + // Get scaled screen dimensions + ScaledResolution scaledRes = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int screenWidth = scaledRes.getScaledWidth(); + int screenHeight = scaledRes.getScaledHeight(); - if (j2 > maxTooltipX) { - int diff = j2 - maxTooltipX; - j2 -= diff; - GL11.glTranslatef(-300, 0, 0); + // If tooltip would go off right edge, draw on left side of mouse + if (j2 + maxWidth + 6 > screenWidth) { + j2 = x - maxWidth - 16; } - if (k2 + maxHeight + 6 > mc.displayHeight) { - k2 = mc.displayHeight - maxHeight - 6; + // Keep tooltip on screen horizontally + if (j2 < 4) { + j2 = 4; + } + + // Keep tooltip on screen vertically + if (k2 + maxHeight + 6 > screenHeight) { + k2 = screenHeight - maxHeight - 6; + } + if (k2 < 4) { + k2 = 4; } this.zLevel = 300.0F; diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java index 91f3ca142..436b47bb5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcTextField.java @@ -4,6 +4,14 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.GuiScreen; import net.minecraft.client.gui.GuiTextField; +import net.minecraft.util.StatCollector; +import net.minecraft.client.gui.ScaledResolution; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; +import net.minecraft.client.renderer.RenderHelper; +import kamkeel.npcs.util.TextSplitter; +import java.util.List; +import java.util.Iterator; public class GuiNpcTextField extends GuiTextField { public boolean enabled = true; @@ -18,6 +26,9 @@ public class GuiNpcTextField extends GuiTextField { public float minFloat = 0, maxFloat = Float.MAX_VALUE, defFloat = 0; protected static GuiNpcTextField activeTextfield = null; public boolean canEdit = true; + public String hoverableText = ""; + private boolean wasHovered = false; + private int hoverCount = 0; private final int[] allowedSpecialChars = {14, 211, 203, 205}; @@ -203,5 +214,151 @@ public class GuiNpcTextField extends GuiTextField { return this; } + public void setHoverText(String text) { + this.hoverableText = StatCollector.translateToLocal(text); + } + public boolean hasHoverText() { + return hoverableText != null && !hoverableText.isEmpty(); + } + + public boolean isMouseOver(int mouseX, int mouseY) { + return mouseX >= xPosition && mouseX < xPosition + width && + mouseY >= yPosition && mouseY < yPosition + height; + } + + public void drawHover(int mouseX, int mouseY, boolean hasSubGui) { + if (hasSubGui || !enabled || hoverableText.isEmpty()) + return; + + boolean isHovered = isMouseOver(mouseX, mouseY); + if (!isHovered) { + wasHovered = false; + hoverCount = 0; + return; + } + + if (!wasHovered) { + wasHovered = true; + hoverCount = 0; + } else if (hoverCount < 65) { + hoverCount++; + } + + if (hoverCount > 60) { + GL11.glPushMatrix(); + Minecraft mc = Minecraft.getMinecraft(); + GL11.glColor4f(1, 1, 1, 1); + List lines = TextSplitter.splitText(hoverableText, 30); + drawHoveringText(lines, mouseX, mouseY, mc); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glPopMatrix(); + } + } + + protected void drawHoveringText(List textLines, int x, int y, Minecraft mc) { + if (mc.fontRenderer == null || textLines.isEmpty()) + return; + GL11.glDisable(GL12.GL_RESCALE_NORMAL); + RenderHelper.disableStandardItemLighting(); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_DEPTH_TEST); + + int maxWidth = 0; + for (String s : textLines) { + int lineWidth = mc.fontRenderer.getStringWidth(s); + if (lineWidth > maxWidth) { + maxWidth = lineWidth; + } + } + + int j2 = x + 12; + int k2 = y - 12; + int maxHeight = 8; + + if (textLines.size() > 1) { + maxHeight += 2 + (textLines.size() - 1) * 10; + } + + // Get scaled screen dimensions + ScaledResolution scaledRes = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int screenWidth = scaledRes.getScaledWidth(); + int screenHeight = scaledRes.getScaledHeight(); + + // If tooltip would go off right edge, draw on left side of mouse + if (j2 + maxWidth + 6 > screenWidth) { + j2 = x - maxWidth - 16; + } + + // Keep tooltip on screen horizontally + if (j2 < 4) { + j2 = 4; + } + + // Keep tooltip on screen vertically + if (k2 + maxHeight + 6 > screenHeight) { + k2 = screenHeight - maxHeight - 6; + } + if (k2 < 4) { + k2 = 4; + } + + int zLevel = 300; + int bgColor = 0xF0100010; + int borderColor1 = 0x505000FF; + int borderColor2 = (borderColor1 & 0xFEFEFE) >> 1 | borderColor1 & 0xFF000000; + + drawGradientRect(j2 - 3, k2 - 4, j2 + maxWidth + 3, k2 - 3, bgColor, bgColor, zLevel); + drawGradientRect(j2 - 3, k2 + maxHeight + 3, j2 + maxWidth + 3, k2 + maxHeight + 4, bgColor, bgColor, zLevel); + drawGradientRect(j2 - 3, k2 - 3, j2 + maxWidth + 3, k2 + maxHeight + 3, bgColor, bgColor, zLevel); + drawGradientRect(j2 - 4, k2 - 3, j2 - 3, k2 + maxHeight + 3, bgColor, bgColor, zLevel); + drawGradientRect(j2 + maxWidth + 3, k2 - 3, j2 + maxWidth + 4, k2 + maxHeight + 3, bgColor, bgColor, zLevel); + drawGradientRect(j2 - 3, k2 - 3 + 1, j2 - 3 + 1, k2 + maxHeight + 3 - 1, borderColor1, borderColor2, zLevel); + drawGradientRect(j2 + maxWidth + 2, k2 - 3 + 1, j2 + maxWidth + 3, k2 + maxHeight + 3 - 1, borderColor1, borderColor2, zLevel); + drawGradientRect(j2 - 3, k2 - 3, j2 + maxWidth + 3, k2 - 3 + 1, borderColor1, borderColor1, zLevel); + drawGradientRect(j2 - 3, k2 + maxHeight + 2, j2 + maxWidth + 3, k2 + maxHeight + 3, borderColor2, borderColor2, zLevel); + + for (int l = 0; l < textLines.size(); ++l) { + String s1 = textLines.get(l); + mc.fontRenderer.drawStringWithShadow(s1, j2, k2, -1); + if (l == 0) { + k2 += 2; + } + k2 += 10; + } + + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glEnable(GL11.GL_DEPTH_TEST); + RenderHelper.enableStandardItemLighting(); + GL11.glEnable(GL12.GL_RESCALE_NORMAL); + } + + protected void drawGradientRect(int left, int top, int right, int bottom, int startColor, int endColor, int zLevel) { + float f = (float)(startColor >> 24 & 255) / 255.0F; + float f1 = (float)(startColor >> 16 & 255) / 255.0F; + float f2 = (float)(startColor >> 8 & 255) / 255.0F; + float f3 = (float)(startColor & 255) / 255.0F; + float f4 = (float)(endColor >> 24 & 255) / 255.0F; + float f5 = (float)(endColor >> 16 & 255) / 255.0F; + float f6 = (float)(endColor >> 8 & 255) / 255.0F; + float f7 = (float)(endColor & 255) / 255.0F; + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glEnable(GL11.GL_BLEND); + GL11.glDisable(GL11.GL_ALPHA_TEST); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glShadeModel(GL11.GL_SMOOTH); + net.minecraft.client.renderer.Tessellator tessellator = net.minecraft.client.renderer.Tessellator.instance; + tessellator.startDrawingQuads(); + tessellator.setColorRGBA_F(f1, f2, f3, f); + tessellator.addVertex((double)right, (double)top, (double)zLevel); + tessellator.addVertex((double)left, (double)top, (double)zLevel); + tessellator.setColorRGBA_F(f5, f6, f7, f4); + tessellator.addVertex((double)left, (double)bottom, (double)zLevel); + tessellator.addVertex((double)right, (double)bottom, (double)zLevel); + tessellator.draw(); + GL11.glShadeModel(GL11.GL_FLAT); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_ALPHA_TEST); + GL11.glEnable(GL11.GL_TEXTURE_2D); + } } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 750a75f54..d4f75d883 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1783,6 +1783,22 @@ condition.percent=Threshold condition.hits=Required Hits condition.within=Within (ticks) +# Condition Hover Tooltips +condition.hover.type=Condition type that must be met for ability to activate +condition.hover.hpAbove=Activates when NPC health is above this percentage +condition.hover.hpBelow=Activates when NPC health is below this percentage +condition.hover.targetHpAbove=Activates when target's health is above this percentage +condition.hover.targetHpBelow=Activates when target's health is below this percentage +condition.hover.hitCount=Activates after receiving X hits within the time window +condition.hover.hits=Number of hits required to trigger condition +condition.hover.within=Time window in ticks (20 ticks = 1 second) +condition.hover.threshold=Health percentage threshold (1-99%) + +# Effect Hover Tooltips +effect.hover.type=Potion effect type to apply to targets +effect.hover.duration=Effect duration in ticks (20 ticks = 1 second) +effect.hover.amplifier=Effect strength level (higher = stronger) + # ========================================= # Auction House # ========================================= From e599ed67f5a9186b47db3be7168af592b67499e3 Mon Sep 17 00:00:00 2001 From: Vidal Date: Tue, 27 Jan 2026 22:19:26 -0300 Subject: [PATCH 301/358] Added animations to these abilities --- .../data/ability/AbilityAnimation.java | 17 +- .../data/ability/type/AbilityEnergyBeam.java | 20 ++ .../data/ability/type/AbilityOrb.java | 12 + .../ABILITYBEAM/Ability_Beam_Active.json | 230 ++++++++++++++++++ .../ABILITYBEAM/Ability_Beam_Windup.json | 158 ++++++++++++ .../ABILITYORB/Ability_Orb_Windup.json | 192 +++++++++++++++ 6 files changed, 621 insertions(+), 8 deletions(-) create mode 100644 src/main/resources/internal/animations/ABILITYBEAM/Ability_Beam_Active.json create mode 100644 src/main/resources/internal/animations/ABILITYBEAM/Ability_Beam_Windup.json create mode 100644 src/main/resources/internal/animations/ABILITYORB/Ability_Orb_Windup.json diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java index 3aea051f3..190be70d3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java @@ -2,7 +2,6 @@ package kamkeel.npcs.controllers.data.ability; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; -import noppes.npcs.api.handler.data.IAnimation; import noppes.npcs.controllers.data.Animation; import noppes.npcs.util.NBTJsonUtil; import org.apache.commons.io.FileUtils; @@ -13,7 +12,9 @@ import java.util.HashMap; import static kamkeel.npcs.controllers.data.ability.AbilityPhase.*; -public enum AbilityAnimation {; +public enum AbilityAnimation { + ABILITYORB("Ability_Orb_Windup", WINDUP), + ABILITYBEAM("Ability_Beam_Windup", "Ability_Beam_Active"); private final HashMap animationNames = new HashMap<>(); AbilityAnimation(String idle, String windUp, String active, String recovery) { @@ -49,7 +50,7 @@ public enum AbilityAnimation {; return getFileFromName(animationNames.get(phase)); } - public IAnimation getAnimation(AbilityPhase phase) { + public Animation getAnimation(AbilityPhase phase) { try { File file = getFile(phase); @@ -68,24 +69,24 @@ public enum AbilityAnimation {; return null; } - public IAnimation idle() { + public Animation idle() { return getAnimation(IDLE); } - public IAnimation windUp() { + public Animation windUp() { return getAnimation(WINDUP); } - public IAnimation active() { + public Animation active() { return getAnimation(ACTIVE); } - public IAnimation dazed() { + public Animation dazed() { return getAnimation(DAZED); } private File getFileFromName(String fileName) { - try (InputStream stream = CustomNpcs.class.getResourceAsStream("/internal/data/animations/" + name() + "/" + fileName + ".json")) { + try (InputStream stream = CustomNpcs.class.getResourceAsStream("/internal/animations/" + name() + "/" + fileName + ".json")) { File file = new File("temp"); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 3bcc5072c..2dea02f5a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityAnimation; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; @@ -17,6 +18,7 @@ import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityEnergyBeam; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.controllers.data.Animation; import noppes.npcs.entity.EntityNPCInterface; /** @@ -70,6 +72,8 @@ public class AbilityEnergyBeam extends Ability { // Anchor point for charging position private AnchorPoint anchorPoint = AnchorPoint.FRONT; + private final AbilityAnimation animation = AbilityAnimation.ABILITYBEAM; + // Transient state for beam entity (used during windup charging) private transient EntityAbilityBeam beamEntity = null; @@ -308,4 +312,20 @@ public class AbilityEnergyBeam extends Ability { public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } public AnchorPoint getAnchorPoint() { return anchorPoint; } public void setAnchorPoint(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } + + @Override + public Animation getWindUpAnimation() { + if (windUpAnimationId == -1) + return animation.windUp(); + + return super.getWindUpAnimation(); + } + + @Override + public Animation getActiveAnimation() { + if (activeAnimationId == -1) + return animation.active(); + + return super.getActiveAnimation(); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 6bc6da964..4832c2281 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityAnimation; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; @@ -17,6 +18,7 @@ import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityOrb; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.controllers.data.Animation; import noppes.npcs.entity.EntityNPCInterface; /** @@ -71,6 +73,8 @@ public class AbilityOrb extends Ability { // Transient state for orb entity (used during windup charging) private transient EntityAbilityOrb orbEntity = null; + private final AbilityAnimation animation = AbilityAnimation.ABILITYORB; + public AbilityOrb() { this.typeId = "ability.cnpc.orb"; this.name = "Orb"; @@ -459,4 +463,12 @@ public class AbilityOrb extends Ability { public void setAnchorPoint(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } + + @Override + public Animation getWindUpAnimation() { + if (windUpAnimationId != -1) + return super.getWindUpAnimation(); + + return animation.windUp(); + } } diff --git a/src/main/resources/internal/animations/ABILITYBEAM/Ability_Beam_Active.json b/src/main/resources/internal/animations/ABILITYBEAM/Ability_Beam_Active.json new file mode 100644 index 000000000..eb9516f38 --- /dev/null +++ b/src/main/resources/internal/animations/ABILITYBEAM/Ability_Beam_Active.json @@ -0,0 +1,230 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": 107.0f, + "Rotation0": -94.0f, + "Pivot0": -5.0f, + "Pivot1": 3.0f, + "Rotation2": -31.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 88.0f, + "Rotation0": -56.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 12.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 56.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": 25.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 5 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": 12.0f, + "Rotation0": -75.0f, + "Pivot0": -2.0f, + "Pivot1": 3.0f, + "Rotation2": 50.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": -18.0f, + "Rotation0": -107.0f, + "Pivot0": -1.0f, + "Pivot1": 2.0f, + "Rotation2": 50.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -4.0f, + "Part": "RIGHT_LEG", + "Rotation1": 56.0f, + "Rotation0": 18.0f, + "Pivot0": -4.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 5 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.75f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": 12.0f, + "Rotation0": -75.0f, + "Pivot0": -2.0f, + "Pivot1": 3.0f, + "Rotation2": 50.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": -18.0f, + "Rotation0": -107.0f, + "Pivot0": -1.0f, + "Pivot1": 2.0f, + "Rotation2": 50.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -4.0f, + "Part": "RIGHT_LEG", + "Rotation1": 56.0f, + "Rotation0": 18.0f, + "Pivot0": -4.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 20 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 2, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Beam_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/internal/animations/ABILITYBEAM/Ability_Beam_Windup.json b/src/main/resources/internal/animations/ABILITYBEAM/Ability_Beam_Windup.json new file mode 100644 index 000000000..6c53e1eab --- /dev/null +++ b/src/main/resources/internal/animations/ABILITYBEAM/Ability_Beam_Windup.json @@ -0,0 +1,158 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": 94.0f, + "Rotation0": -94.0f, + "Pivot0": -4.0f, + "Pivot1": 3.0f, + "Rotation2": -31.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 75.0f, + "Rotation0": -56.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 50.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 5 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": 107.0f, + "Rotation0": -94.0f, + "Pivot0": -5.0f, + "Pivot1": 3.0f, + "Rotation2": -31.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 88.0f, + "Rotation0": -56.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 12.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 56.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": 25.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 35 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 1, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Beam_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/internal/animations/ABILITYORB/Ability_Orb_Windup.json b/src/main/resources/internal/animations/ABILITYORB/Ability_Orb_Windup.json new file mode 100644 index 000000000..77f21f941 --- /dev/null +++ b/src/main/resources/internal/animations/ABILITYORB/Ability_Orb_Windup.json @@ -0,0 +1,192 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 31.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 3.0f, + "Part": "RIGHT_LEG", + "Rotation1": 69.0f, + "Rotation0": 12.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 37.0f, + "Rotation0": -101.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -25.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": -88.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -90.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Duration": 20 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": -44.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": -37.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -50.0f, + "Rotation0": 50.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": -56.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_LEG", + "Rotation1": -75.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 5 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": -44.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": -37.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -50.0f, + "Rotation0": 50.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": -56.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_LEG", + "Rotation1": -75.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 5 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 3, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Orb_Windup", + "WhileStanding": 1b +} From 83ccd25b33949c4a597651cc401dfa8734188e8c Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 28 Jan 2026 14:44:19 -0500 Subject: [PATCH 302/358] Merge Conflicts + Scripting for Abilities --- src/api | 2 +- .../data/ability/type/AbilityCharge.java | 4 +- .../data/ability/type/AbilityCutter.java | 19 +- .../data/ability/type/AbilityDash.java | 19 +- .../data/ability/type/AbilityDisc.java | 13 +- .../data/ability/type/AbilityEnergyBeam.java | 18 +- .../data/ability/type/AbilityGuard.java | 24 ++- .../data/ability/type/AbilityHazard.java | 34 +++- .../data/ability/type/AbilityHeal.java | 4 +- .../data/ability/type/AbilityHeavyHit.java | 3 +- .../data/ability/type/AbilityLaserShot.java | 3 +- .../data/ability/type/AbilityOrb.java | 16 +- .../data/ability/type/AbilityProjectile.java | 4 +- .../data/ability/type/AbilityShockwave.java | 4 +- .../data/ability/type/AbilitySlam.java | 4 +- .../data/ability/type/AbilitySweeper.java | 3 +- .../data/ability/type/AbilityTeleport.java | 19 +- .../data/ability/type/AbilityTrap.java | 19 +- .../data/ability/type/AbilityVortex.java | 4 +- src/main/java/noppes/npcs/DataAbilities.java | 44 +++++ .../advanced/ability/SubGuiAbilityCutter.java | 6 +- .../advanced/ability/SubGuiAbilityDash.java | 4 +- .../advanced/ability/SubGuiAbilityDisc.java | 4 +- .../ability/SubGuiAbilityEnergyBeam.java | 4 +- .../advanced/ability/SubGuiAbilityGuard.java | 4 +- .../advanced/ability/SubGuiAbilityHazard.java | 8 +- .../advanced/ability/SubGuiAbilityOrb.java | 4 +- .../ability/SubGuiAbilityTeleport.java | 6 +- .../advanced/ability/SubGuiAbilityTrap.java | 4 +- .../npcs/scripted/entity/ScriptNpc.java | 4 + .../scripted/wrapper/ScriptDataAbilities.java | 163 ++++++++++++++++++ 31 files changed, 408 insertions(+), 63 deletions(-) create mode 100644 src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java diff --git a/src/api b/src/api index 2054012fa..3050910ef 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 2054012fae799fa233b99824b4f649d311eff86f +Subproject commit 3050910efbc6e301e4ef18fefc20529c507a7bae diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 7a1d9babf..d87ed05ee 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -18,6 +18,8 @@ import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityCharge; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityCharge; + import java.util.HashSet; import java.util.List; import java.util.Set; @@ -25,7 +27,7 @@ import java.util.Set; /** * Charge ability: Rush attack where NPC charges in a line, damaging all targets hit. */ -public class AbilityCharge extends Ability { +public class AbilityCharge extends Ability implements IAbilityCharge { // Type-specific parameters private float chargeSpeed = 0.8f; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index b3052eeac..1d77e9219 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -15,6 +15,8 @@ import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityCutter; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityCutter; + import java.util.HashSet; import java.util.List; import java.util.Set; @@ -24,7 +26,7 @@ import java.util.Set; * Deals damage to all entities in a cone/arc in front of the caster. * Can sweep outward over time or hit instantly. */ -public class AbilityCutter extends Ability { +public class AbilityCutter extends Ability implements IAbilityCutter { public enum SweepMode { SWIPE, @@ -254,14 +256,25 @@ public class AbilityCutter extends Ability { this.knockback = knockback; } - public SweepMode getSweepMode() { + public SweepMode getSweepModeEnum() { return sweepMode; } - public void setSweepMode(SweepMode sweepMode) { + public void setSweepModeEnum(SweepMode sweepMode) { this.sweepMode = sweepMode; } + @Override + public int getSweepMode() { + return sweepMode.ordinal(); + } + + @Override + public void setSweepMode(int mode) { + SweepMode[] values = SweepMode.values(); + this.sweepMode = mode >= 0 && mode < values.length ? values[mode] : SweepMode.SWIPE; + } + public float getSweepSpeed() { return sweepSpeed; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index cd4d42ac7..677578745 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -14,13 +14,15 @@ import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDash; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityDash; + import java.util.Random; /** * Dash ability: Quick evasive sidestep with NO telegraph. * Defensive repositioning move to evade attacks. */ -public class AbilityDash extends Ability { +public class AbilityDash extends Ability implements IAbilityDash { /** * Dash behavior mode. @@ -245,14 +247,25 @@ public class AbilityDash extends Ability { } // Getters & Setters - public DashMode getDashMode() { + public DashMode getDashModeEnum() { return dashMode; } - public void setDashMode(DashMode dashMode) { + public void setDashModeEnum(DashMode dashMode) { this.dashMode = dashMode; } + @Override + public int getDashMode() { + return dashMode.ordinal(); + } + + @Override + public void setDashMode(int mode) { + DashMode[] values = DashMode.values(); + this.dashMode = mode >= 0 && mode < values.length ? values[mode] : DashMode.AGGRESSIVE; + } + public float getDashDistance() { return dashDistance; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 67fa23cdf..2c7b8e43c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -17,13 +17,14 @@ import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDisc; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.api.ability.type.IAbilityDisc; import noppes.npcs.entity.EntityNPCInterface; /** * Disc ability: Spawns a flat spinning disc projectile. * Has optional boomerang behavior to return to owner. */ -public class AbilityDisc extends Ability { +public class AbilityDisc extends Ability implements IAbilityDisc { // Movement properties private float speed = 0.6f; @@ -312,6 +313,12 @@ public class AbilityDisc extends Ability { public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } public float getLightningRadius() { return lightningRadius; } public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } - public AnchorPoint getAnchorPoint() { return anchorPoint; } - public void setAnchorPoint(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } + public AnchorPoint getAnchorPointEnum() { return anchorPoint; } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } + + @Override + public int getAnchorPoint() { return anchorPoint.getId(); } + + @Override + public void setAnchorPoint(int point) { this.anchorPoint = AnchorPoint.fromId(point); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 2dea02f5a..790489b2c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -12,12 +12,12 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityBeam; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; -import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.DamageSource; +import net.minecraft.nbt.NBTTagCompound;; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityEnergyBeam; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.api.ability.type.IAbilityEnergyBeam; import noppes.npcs.controllers.data.Animation; import noppes.npcs.entity.EntityNPCInterface; @@ -26,7 +26,7 @@ import noppes.npcs.entity.EntityNPCInterface; * The beam stays attached to the origin and curves as it homes in on target. * Cannot cross over itself (self-intersection prevention). */ -public class AbilityEnergyBeam extends Ability { +public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { // Movement properties private float speed = 0.4f; @@ -310,9 +310,8 @@ public class AbilityEnergyBeam extends Ability { public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } public float getLightningRadius() { return lightningRadius; } public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } - public AnchorPoint getAnchorPoint() { return anchorPoint; } - public void setAnchorPoint(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } + @Override public Animation getWindUpAnimation() { if (windUpAnimationId == -1) @@ -328,4 +327,13 @@ public class AbilityEnergyBeam extends Ability { return super.getActiveAnimation(); } + + public AnchorPoint getAnchorPointEnum() { return anchorPoint; } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } + + @Override + public int getAnchorPoint() { return anchorPoint.getId(); } + + @Override + public void setAnchorPoint(int point) { this.anchorPoint = AnchorPoint.fromId(point); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index fd9f25a24..84701b79e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -14,13 +14,15 @@ import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityGuard; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityGuard; + import java.util.Random; /** * Guard ability: Defensive stance that reduces incoming damage. * Can optionally counter-attack when hit. */ -public class AbilityGuard extends Ability { +public class AbilityGuard extends Ability implements IAbilityGuard { private static final Random RANDOM = new Random(); @@ -228,18 +230,34 @@ public class AbilityGuard extends Ability { return canCounter; } + @Override + public boolean canCounter() { + return canCounter; + } + public void setCanCounter(boolean canCounter) { this.canCounter = canCounter; } - public CounterType getCounterType() { + public CounterType getCounterTypeEnum() { return counterType; } - public void setCounterType(CounterType counterType) { + public void setCounterTypeEnum(CounterType counterType) { this.counterType = counterType; } + @Override + public int getCounterType() { + return counterType.ordinal(); + } + + @Override + public void setCounterType(int type) { + CounterType[] values = CounterType.values(); + this.counterType = type >= 0 && type < values.length ? values[type] : CounterType.FLAT; + } + public float getCounterValue() { return counterValue; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 3e9998f6f..79e43e2cc 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -18,6 +18,8 @@ import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHazard; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityHazard; + import java.util.HashSet; import java.util.List; import java.util.Random; @@ -27,7 +29,7 @@ import java.util.Set; * Hazard ability: Creates persistent ground effect zones. * Deals damage and/or applies debuffs to entities in the zone over time. */ -public class AbilityHazard extends Ability { +public class AbilityHazard extends Ability implements IAbilityHazard { public enum HazardShape { CIRCLE, @@ -443,22 +445,44 @@ public class AbilityHazard extends Ability { this.coneAngle = coneAngle; } - public HazardShape getShape() { + public HazardShape getShapeEnum() { return shape; } - public void setShape(HazardShape shape) { + public void setShapeEnum(HazardShape shape) { this.shape = shape; } - public PlacementMode getPlacement() { + @Override + public int getShape() { + return shape.ordinal(); + } + + @Override + public void setShape(int shape) { + HazardShape[] values = HazardShape.values(); + this.shape = shape >= 0 && shape < values.length ? values[shape] : HazardShape.CIRCLE; + } + + public PlacementMode getPlacementEnum() { return placement; } - public void setPlacement(PlacementMode placement) { + public void setPlacementEnum(PlacementMode placement) { this.placement = placement; } + @Override + public int getPlacement() { + return placement.ordinal(); + } + + @Override + public void setPlacement(int placement) { + PlacementMode[] values = PlacementMode.values(); + this.placement = placement >= 0 && placement < values.length ? values[placement] : PlacementMode.AT_CASTER; + } + public float getDamagePerSecond() { return damagePerSecond; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index f19a7cb02..11aed6c47 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -15,6 +15,8 @@ import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeal; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityHeal; + import java.util.ArrayList; import java.util.List; @@ -22,7 +24,7 @@ import java.util.List; * Heal ability: Restore health to self or allies. * Can heal a fixed amount or percentage of max health. */ -public class AbilityHeal extends Ability { +public class AbilityHeal extends Ability implements IAbilityHeal { // Type-specific parameters private int durationTicks = 60; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index 32335daeb..82091c041 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -11,13 +11,14 @@ import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeavyHit; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.api.ability.type.IAbilityHeavyHit; import noppes.npcs.entity.EntityNPCInterface; /** * Heavy Hit ability: Single-target melee attack with optional stun. * Deals high damage to one target and can stun them. */ -public class AbilityHeavyHit extends Ability { +public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { private float damage = 8.0f; private float knockback = 2.0f; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index 8084aaad4..44b6bc0b5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -13,13 +13,14 @@ import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityLaserShot; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.api.ability.type.IAbilityLaserShot; import noppes.npcs.entity.EntityNPCInterface; /** * Laser Shot ability: Fast expanding thin line that pierces through targets. * Travels in a straight line from origin, damaging all entities it passes through. */ -public class AbilityLaserShot extends Ability { +public class AbilityLaserShot extends Ability implements IAbilityLaserShot { // Laser properties private float laserWidth = 0.3f; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 4832c2281..fabdf6ab0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -19,6 +19,7 @@ import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityOrb; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.controllers.data.Animation; +import noppes.npcs.api.ability.type.IAbilityOrb; import noppes.npcs.entity.EntityNPCInterface; /** @@ -26,7 +27,7 @@ import noppes.npcs.entity.EntityNPCInterface; * The EntityAbilityOrb handles all movement, collision, and damage logic. * This ability just configures and spawns the orb entity. */ -public class AbilityOrb extends Ability { +public class AbilityOrb extends Ability implements IAbilityOrb { // Movement properties private float orbSpeed = 0.5f; @@ -456,11 +457,11 @@ public class AbilityOrb extends Ability { this.lightningFadeTime = lightningFadeTime; } - public AnchorPoint getAnchorPoint() { + public AnchorPoint getAnchorPointEnum() { return anchorPoint; } - public void setAnchorPoint(AnchorPoint anchorPoint) { + public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } @@ -471,4 +472,13 @@ public class AbilityOrb extends Ability { return animation.windUp(); } + + public int getAnchorPoint() { + return anchorPoint.getId(); + } + + @Override + public void setAnchorPoint(int point) { + this.anchorPoint = AnchorPoint.fromId(point); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index 4a12ae4eb..56b12028e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -14,13 +14,15 @@ import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityProjectile; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityProjectile; + import java.util.List; /** * Projectile ability: Ranged attack that deals damage to target. * Currently instant damage - can be enhanced with custom projectile entities. */ -public class AbilityProjectile extends Ability { +public class AbilityProjectile extends Ability implements IAbilityProjectile { // Type-specific parameters private float damage = 6.0f; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index 71d888978..c6b93134a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -15,13 +15,15 @@ import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityShockwave; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityShockwave; + import java.util.List; /** * Shockwave ability: Pushes targets away from the caster with damage. * Opposite of Vortex - instant knockback rather than gradual pull. */ -public class AbilityShockwave extends Ability { +public class AbilityShockwave extends Ability implements IAbilityShockwave { private float pushRadius = 8.0f; private float pushStrength = 1.5f; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index dcb914a96..ba49ffb2c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -17,6 +17,8 @@ import noppes.npcs.client.gui.advanced.ability.SubGuiAbilitySlam; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilitySlam; + import java.util.List; /** @@ -28,7 +30,7 @@ import java.util.List; * - Landing: When NPC hits ground, deal AOE damage * - RECOVERY: Landing recovery animation */ -public class AbilitySlam extends Ability { +public class AbilitySlam extends Ability implements IAbilitySlam { // Type-specific config private float damage = 10.0f; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index 5a61c5dce..66f589a00 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -15,6 +15,7 @@ import noppes.npcs.LogWriter; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilitySweeper; import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.api.ability.type.IAbilitySweeper; import noppes.npcs.entity.EntityNPCInterface; /** @@ -25,7 +26,7 @@ import noppes.npcs.entity.EntityNPCInterface; * All damage logic is handled by the EntityAbilitySweeper entity to ensure * visual and damage hitbox alignment. */ -public class AbilitySweeper extends Ability { +public class AbilitySweeper extends Ability implements IAbilitySweeper { // Type-specific parameters private float beamLength = 10.0f; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index 218432fc9..05293a467 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -18,13 +18,15 @@ import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityTeleport; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityTeleport; + import java.util.List; import java.util.Random; /** * Teleport ability: Instant reposition with wall/line-of-sight checks. */ -public class AbilityTeleport extends Ability { +public class AbilityTeleport extends Ability implements IAbilityTeleport { /** * Teleport behavior mode. @@ -440,14 +442,25 @@ public class AbilityTeleport extends Ability { } // Getters & Setters - public TeleportMode getMode() { + public TeleportMode getModeEnum() { return mode; } - public void setMode(TeleportMode mode) { + public void setModeEnum(TeleportMode mode) { this.mode = mode; } + @Override + public int getMode() { + return mode.ordinal(); + } + + @Override + public void setMode(int mode) { + TeleportMode[] values = TeleportMode.values(); + this.mode = mode >= 0 && mode < values.length ? values[mode] : TeleportMode.BLINK; + } + public int getBlinkCount() { return blinkCount; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index dbb7273cd..b14a9ad67 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -16,6 +16,8 @@ import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityTrap; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityTrap; + import java.util.HashSet; import java.util.List; import java.util.Random; @@ -26,7 +28,7 @@ import java.util.UUID; * Trap ability: Places a proximity-triggered trap. * Features arm time, trigger radius, and various effects on trigger. */ -public class AbilityTrap extends Ability { +public class AbilityTrap extends Ability implements IAbilityTrap { public enum TrapPlacement { AT_CASTER, @@ -330,14 +332,25 @@ public class AbilityTrap extends Ability { this.durationTicks = Math.max(1, durationTicks); } - public TrapPlacement getPlacement() { + public TrapPlacement getPlacementEnum() { return placement; } - public void setPlacement(TrapPlacement placement) { + public void setPlacementEnum(TrapPlacement placement) { this.placement = placement; } + @Override + public int getPlacement() { + return placement.ordinal(); + } + + @Override + public void setPlacement(int placement) { + TrapPlacement[] values = TrapPlacement.values(); + this.placement = placement >= 0 && placement < values.length ? values[placement] : TrapPlacement.AT_TARGET; + } + public float getPlacementDistance() { return placementDistance; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index fef61779e..a85bbc442 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -14,6 +14,8 @@ import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityVortex; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.ability.type.IAbilityVortex; + import java.util.HashSet; import java.util.List; import java.util.Set; @@ -23,7 +25,7 @@ import java.util.UUID; * Vortex ability: Pulls targets toward the caster. * Can pull single target or AOE, with optional damage and stun on arrival. */ -public class AbilityVortex extends Ability { +public class AbilityVortex extends Ability implements IAbilityVortex { private float pullRadius = 8.0f; private float pullStrength = 0.8f; diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 0ff0aa8a1..2ddabc588 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -604,6 +604,50 @@ public class DataAbilities { } } + /** + * Force start a specific ability, bypassing normal selection and cooldown. + * If an ability is currently executing, it will be cancelled. + * + * @param ability The ability to start + * @param target The target entity (can be null for self-targeted abilities) + * @return true if the ability was started successfully + */ + public boolean forceStartAbility(Ability ability, EntityLivingBase target) { + if (ability == null || npc.worldObj.isRemote) { + return false; + } + + // Stop any currently executing ability + stopCurrentAbility(); + + // Reset the ability state + ability.reset(); + + // Start the ability directly + return startAbility(ability, target); + } + + /** + * Execute a preset ability on this NPC. + * The NPC does NOT need to have this ability assigned. + * + * @param presetName The name of the saved ability preset + * @param target The target entity (can be null for self-targeted abilities) + * @return true if the ability was started successfully + */ + public boolean executePresetAbility(String presetName, EntityLivingBase target) { + if (presetName == null || presetName.isEmpty() || npc.worldObj.isRemote) { + return false; + } + + Ability preset = AbilityController.Instance.getSavedAbility(presetName); + if (preset == null) { + return false; + } + + return forceStartAbility(preset, target); + } + // ═══════════════════════════════════════════════════════════════════ // SCRIPT EVENTS // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java index f10c63eef..a1539536d 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java @@ -61,7 +61,7 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { // Row 4: Sweep Mode + Piercing addLabel(new GuiNpcLabel(106, "ability.sweepMode", labelX, y + 5)); String[] sweepModes = {"Swipe", "Spin"}; - GuiNpcButton sweepModeBtn = new GuiNpcButton(106, fieldX, y, 70, 20, sweepModes, cutter.getSweepMode().ordinal()); + GuiNpcButton sweepModeBtn = new GuiNpcButton(106, fieldX, y, 70, 20, sweepModes, cutter.getSweepModeEnum().ordinal()); sweepModeBtn.setHoverText("ability.hover.sweepMode"); addButton(sweepModeBtn); @@ -73,7 +73,7 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { y += 24; // Row 5: Spin Duration (only for SPIN mode) + Effects button - if (cutter.getSweepMode() == AbilityCutter.SweepMode.SPIN) { + if (cutter.getSweepModeEnum() == AbilityCutter.SweepMode.SPIN) { addLabel(new GuiNpcLabel(108, "ability.duration", labelX, y + 5)); GuiNpcTextField durationField = createIntField(108, fieldX, y, 50, cutter.getSpinDurationTicks()); durationField.setMinMaxDefault(1, 1000, 60); @@ -88,7 +88,7 @@ public class SubGuiAbilityCutter extends SubGuiAbilityConfig { int value = button.getValue(); switch (id) { case 106: - cutter.setSweepMode(AbilityCutter.SweepMode.values()[value]); + cutter.setSweepModeEnum(AbilityCutter.SweepMode.values()[value]); initGui(); // Refresh to show/hide duration field break; case 107: diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java index f113b3c1c..9d7bd63b6 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java @@ -30,7 +30,7 @@ public class SubGuiAbilityDash extends SubGuiAbilityConfig { // Row 1: Dash Mode addLabel(new GuiNpcLabel(100, "ability.mode", labelX, y + 5)); String[] dashModes = {"Aggressive", "Defensive"}; - GuiNpcButton modeBtn = new GuiNpcButton(100, fieldX, y, 80, 20, dashModes, dash.getDashMode().ordinal()); + GuiNpcButton modeBtn = new GuiNpcButton(100, fieldX, y, 80, 20, dashModes, dash.getDashModeEnum().ordinal()); modeBtn.setHoverText("ability.hover.dashMode"); addButton(modeBtn); @@ -47,7 +47,7 @@ public class SubGuiAbilityDash extends SubGuiAbilityConfig { @Override protected void handleTypeButton(int id, GuiNpcButton button) { if (id == 100) { - dash.setDashMode(AbilityDash.DashMode.values()[button.getValue()]); + dash.setDashModeEnum(AbilityDash.DashMode.values()[button.getValue()]); } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java index 2b804d99b..00383853f 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java @@ -122,7 +122,7 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { // Row 1: Anchor Point addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); - GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), disc.getAnchorPoint().getId()); + GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), disc.getAnchorPointEnum().getId()); anchorBtn.setHoverText("ability.hover.anchorPoint"); addButton(anchorBtn); @@ -227,7 +227,7 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { initGui(); break; case 210: - disc.setAnchorPoint(AnchorPoint.fromId(value)); + disc.setAnchorPointEnum(AnchorPoint.fromId(value)); break; } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java index 6e90428ce..d878c9381 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java @@ -118,7 +118,7 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { // Row 1: Anchor Point addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); - GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), beam.getAnchorPoint().getId()); + GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), beam.getAnchorPointEnum().getId()); anchorBtn.setHoverText("ability.hover.anchorPoint"); addButton(anchorBtn); @@ -219,7 +219,7 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { initGui(); break; case 210: - beam.setAnchorPoint(AnchorPoint.fromId(value)); + beam.setAnchorPointEnum(AnchorPoint.fromId(value)); break; } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java index a975a97fc..0d2ec667b 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java @@ -50,7 +50,7 @@ public class SubGuiAbilityGuard extends SubGuiAbilityConfig { addLabel(new GuiNpcLabel(102, "ability.counterType", col2LabelX, y + 5)); String[] types = {"Flat", "Percent"}; - GuiNpcButton counterTypeButton = new GuiNpcButton(102, col2FieldX, y, 60, 20, types, guard.getCounterType().ordinal()); + GuiNpcButton counterTypeButton = new GuiNpcButton(102, col2FieldX, y, 60, 20, types, guard.getCounterTypeEnum().ordinal()); counterTypeButton.setHoverText("ability.hover.counterType"); counterTypeButton.setEnabled(guard.isCanCounter()); addButton(counterTypeButton); @@ -97,7 +97,7 @@ public class SubGuiAbilityGuard extends SubGuiAbilityConfig { initGui(); break; case 102: - guard.setCounterType(AbilityGuard.CounterType.values()[button.getValue()]); + guard.setCounterTypeEnum(AbilityGuard.CounterType.values()[button.getValue()]); break; case 150: setSubGui(new SubGuiAbilityEffects(guard.getEffects())); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java index 9822d9f90..5b8be7976 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java @@ -39,7 +39,7 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { addLabel(new GuiNpcLabel(100, "ability.shape", col2LabelX, y + 5)); String[] shapes = {"Circle", "Ring", "Cone"}; - GuiNpcButton shapeBtn = new GuiNpcButton(100, col2FieldX, y, 55, 20, shapes, hazard.getShape().ordinal()); + GuiNpcButton shapeBtn = new GuiNpcButton(100, col2FieldX, y, 55, 20, shapes, hazard.getShapeEnum().ordinal()); shapeBtn.setHoverText("ability.hover.shape"); addButton(shapeBtn); @@ -48,7 +48,7 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { // Row 2: Placement addLabel(new GuiNpcLabel(101, "ability.placement", labelX, y + 5)); String[] placements = {"Caster", "Target", "FollowCast", "FollowTgt"}; - GuiNpcButton placementBtn = new GuiNpcButton(101, fieldX, y, 70, 20, placements, hazard.getPlacement().ordinal()); + GuiNpcButton placementBtn = new GuiNpcButton(101, fieldX, y, 70, 20, placements, hazard.getPlacementEnum().ordinal()); placementBtn.setHoverText("ability.hover.placement"); addButton(placementBtn); @@ -101,10 +101,10 @@ public class SubGuiAbilityHazard extends SubGuiAbilityConfig { int value = button.getValue(); switch (id) { case 100: - hazard.setShape(AbilityHazard.HazardShape.values()[value]); + hazard.setShapeEnum(AbilityHazard.HazardShape.values()[value]); break; case 101: - hazard.setPlacement(AbilityHazard.PlacementMode.values()[value]); + hazard.setPlacementEnum(AbilityHazard.PlacementMode.values()[value]); break; case 109: hazard.setAffectsCaster(value == 1); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java index 858eeefa7..40cff112f 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -106,7 +106,7 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { // Row 1: Anchor Point addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); - GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), orb.getAnchorPoint().getId()); + GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), orb.getAnchorPointEnum().getId()); anchorBtn.setHoverText("ability.hover.anchorPoint"); addButton(anchorBtn); @@ -207,7 +207,7 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { initGui(); break; case 210: - orb.setAnchorPoint(AnchorPoint.fromId(value)); + orb.setAnchorPointEnum(AnchorPoint.fromId(value)); break; } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java index e39918d2d..4b23b0ca0 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java @@ -34,11 +34,11 @@ public class SubGuiAbilityTeleport extends SubGuiAbilityConfig { // Row 1: Mode addLabel(new GuiNpcLabel(100, "ability.mode", labelX, y + 5)); String[] modes = {"Blink", "Behind", "Single"}; - addButton(new GuiNpcButton(100, fieldX, y, 80, 20, modes, teleport.getMode().ordinal())); + addButton(new GuiNpcButton(100, fieldX, y, 80, 20, modes, teleport.getModeEnum().ordinal())); y += 24; - switch (teleport.getMode()) { + switch (teleport.getModeEnum()) { case BLINK: // Row 2: Blink Radius + Blink Count addLabel(new GuiNpcLabel(101, "ability.blinkRadius", labelX, y + 5)); @@ -109,7 +109,7 @@ public class SubGuiAbilityTeleport extends SubGuiAbilityConfig { int value = button.getValue(); switch (id) { case 100: - teleport.setMode(AbilityTeleport.TeleportMode.values()[value]); + teleport.setModeEnum(AbilityTeleport.TeleportMode.values()[value]); initGui(); break; case 105: diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java index edeb59635..5c9c52558 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java @@ -39,7 +39,7 @@ public class SubGuiAbilityTrap extends SubGuiAbilityConfig { addLabel(new GuiNpcLabel(100, "ability.placement", col2LabelX, y + 5)); String[] placements = {"Caster", "Target", "Ahead"}; - GuiNpcButton placementBtn = new GuiNpcButton(100, col2FieldX, y, 55, 20, placements, trap.getPlacement().ordinal()); + GuiNpcButton placementBtn = new GuiNpcButton(100, col2FieldX, y, 55, 20, placements, trap.getPlacementEnum().ordinal()); placementBtn.setHoverText("ability.hover.placement"); addButton(placementBtn); @@ -79,7 +79,7 @@ public class SubGuiAbilityTrap extends SubGuiAbilityConfig { @Override protected void handleTypeButton(int id, GuiNpcButton button) { if (id == 100) { - trap.setPlacement(AbilityTrap.TrapPlacement.values()[button.getValue()]); + trap.setPlacementEnum(AbilityTrap.TrapPlacement.values()[button.getValue()]); } else if (id == 150) { setSubGui(new SubGuiAbilityEffects(trap.getEffects())); } diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java b/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java index 8b632f7ce..26f7a1ba7 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java @@ -1457,4 +1457,8 @@ public class ScriptNpc extends ScriptLiving imp public IMagicData getMagicData() { return npc.stats.magicData; } + + public noppes.npcs.api.ability.IDataAbilities getAbilityData() { + return new noppes.npcs.scripted.wrapper.ScriptDataAbilities(npc); + } } diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java new file mode 100644 index 000000000..d097a892c --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java @@ -0,0 +1,163 @@ +package noppes.npcs.scripted.wrapper; + +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; +import net.minecraft.entity.EntityLivingBase; +import noppes.npcs.DataAbilities; +import noppes.npcs.api.ability.IAbility; +import noppes.npcs.api.ability.IDataAbilities; +import noppes.npcs.entity.EntityNPCInterface; + +/** + * Script wrapper for DataAbilities providing API access to NPC abilities. + */ +public class ScriptDataAbilities implements IDataAbilities { + + private final DataAbilities data; + private final EntityNPCInterface npc; + + public ScriptDataAbilities(EntityNPCInterface npc) { + this.npc = npc; + this.data = npc.abilities; + } + + @Override + public boolean isEnabled() { + return data.enabled; + } + + @Override + public void setEnabled(boolean enabled) { + data.enabled = enabled; + } + + @Override + public IAbility[] getAbilities() { + return data.getAbilities().toArray(new IAbility[0]); + } + + @Override + public void addAbility(IAbility ability) { + if (ability instanceof Ability) { + data.addAbility((Ability) ability); + } + } + + @Override + public void removeAbility(String abilityId) { + data.removeAbility(abilityId); + } + + @Override + public IAbility getAbility(String abilityId) { + return data.getAbility(abilityId); + } + + @Override + public boolean hasAbility(String abilityId) { + return data.getAbility(abilityId) != null; + } + + @Override + public void clearAbilities() { + data.clearAbilities(); + } + + @Override + public IAbility getCurrentAbility() { + return data.getCurrentAbility(); + } + + @Override + public boolean isExecutingAbility() { + return data.isExecutingAbility(); + } + + @Override + public void interruptCurrentAbility() { + data.interruptCurrentAbility(null, 0); + } + + @Override + public int getGlobalCooldown() { + return (int) data.getRemainingCooldown(); + } + + @Override + public void setGlobalCooldown(int ticks) { + // Set the cooldown by manipulating the internal cooldown end time + // Not directly exposed, but we can reset and let it tick down + data.resetCooldown(); + // This is a best-effort implementation + } + + @Override + public void resetCooldowns() { + data.resetCooldown(); + } + + @Override + public boolean forceStartAbility(String abilityId) { + return forceStartAbility(abilityId, null); + } + + @Override + public boolean forceStartAbility(String abilityId, Object target) { + // Find the ability + Ability ability = data.getAbility(abilityId); + if (ability == null) { + return false; + } + + // Resolve target + EntityLivingBase targetEntity = resolveTarget(target); + + // Use the direct force start method + return data.forceStartAbility(ability, targetEntity); + } + + @Override + public boolean executePresetAbility(String presetName) { + return executePresetAbility(presetName, null); + } + + @Override + public boolean executePresetAbility(String presetName, Object target) { + // Resolve target + EntityLivingBase targetEntity = resolveTarget(target); + + // Use the direct preset execution method + return data.executePresetAbility(presetName, targetEntity); + } + + @Override + public IAbility createAbility(String typeId) { + if (AbilityController.Instance == null) { + return null; + } + return AbilityController.Instance.create(typeId); + } + + /** + * Resolve a target object to an EntityLivingBase. + */ + private EntityLivingBase resolveTarget(Object target) { + if (target == null) { + // Try to use the NPC's current attack target + return npc.getAttackTarget(); + } + + if (target instanceof EntityLivingBase) { + return (EntityLivingBase) target; + } + + if (target instanceof noppes.npcs.api.entity.IEntityLivingBase) { + Object mcEntity = ((noppes.npcs.api.entity.IEntityLivingBase) target).getMCEntity(); + if (mcEntity instanceof EntityLivingBase) { + return (EntityLivingBase) mcEntity; + } + } + + return null; + } +} From 7d2bba7b95e9cda41dcb6c20033c0f428377cab5 Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 28 Jan 2026 15:16:28 -0500 Subject: [PATCH 303/358] Better Lock Movement handling --- src/api | 2 +- .../controllers/data/ability/Ability.java | 58 +++++++++++-- .../data/ability/LockMovementType.java | 83 +++++++++++++++++++ .../data/ability/type/AbilityCharge.java | 3 +- .../data/ability/type/AbilityCutter.java | 3 +- .../data/ability/type/AbilityDash.java | 3 +- .../data/ability/type/AbilityDisc.java | 2 + .../data/ability/type/AbilityEnergyBeam.java | 6 +- .../data/ability/type/AbilityGuard.java | 3 +- .../data/ability/type/AbilityHazard.java | 3 +- .../data/ability/type/AbilityHeal.java | 3 +- .../data/ability/type/AbilityHeavyHit.java | 3 +- .../data/ability/type/AbilityLaserShot.java | 2 + .../data/ability/type/AbilityOrb.java | 2 + .../data/ability/type/AbilityProjectile.java | 2 + .../data/ability/type/AbilityShockwave.java | 3 +- .../data/ability/type/AbilitySlam.java | 3 +- .../data/ability/type/AbilitySweeper.java | 3 +- .../data/ability/type/AbilityTeleport.java | 2 + .../data/ability/type/AbilityTrap.java | 3 +- .../data/ability/type/AbilityVortex.java | 3 +- src/main/java/noppes/npcs/DataAbilities.java | 78 +++++++++-------- .../gui/advanced/SubGuiAbilityConfig.java | 11 +-- .../assets/customnpcs/lang/en_US.lang | 3 + 24 files changed, 227 insertions(+), 60 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/LockMovementType.java diff --git a/src/api b/src/api index 3050910ef..2b21264ad 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 3050910efbc6e301e4ef18fefc20529c507a7bae +Subproject commit 2b21264ada69ec5e800977a000e892bc82d7c61f diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index e5dc7e493..a82a8fc6e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -55,7 +55,7 @@ public abstract class Ability implements IAbility { protected boolean interruptible = true; // Feedback - protected boolean lockMovement = true; + protected LockMovementType lockMovement = LockMovementType.WINDUP; protected int windUpColor = 0x80FF4400; // Telegraph color during wind up protected int activeColor = 0xC0FF0000; // Telegraph warning/active color @@ -662,7 +662,7 @@ public abstract class Ability implements IAbility { nbt.setInteger("windUp", windUpTicks); nbt.setInteger("recovery", dazedTicks); nbt.setBoolean("interruptible", interruptible); - nbt.setBoolean("lockMovement", lockMovement); + nbt.setInteger("lockMovement", lockMovement.ordinal()); nbt.setInteger("windUpColor", windUpColor); nbt.setInteger("activeColor", activeColor); nbt.setString("windUpSound", windUpSound); @@ -707,7 +707,7 @@ public abstract class Ability implements IAbility { windUpTicks = nbt.getInteger("windUp"); dazedTicks = nbt.getInteger("recovery"); interruptible = nbt.getBoolean("interruptible"); - lockMovement = nbt.getBoolean("lockMovement"); + lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement")); // Support old telegraphColor key for backwards compatibility if (nbt.hasKey("windUpColor")) { windUpColor = nbt.getInteger("windUpColor"); @@ -868,14 +868,62 @@ public abstract class Ability implements IAbility { this.interruptible = interruptible; } - public boolean isLockMovement() { + public LockMovementType getLockMovement() { return lockMovement; } - public void setLockMovement(boolean lockMovement) { + public void setLockMovement(LockMovementType lockMovement) { this.lockMovement = lockMovement; } + /** + * API method: Get lock movement type as integer. + * @return 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE + */ + @Override + public int getLockMovementType() { + return lockMovement.ordinal(); + } + + /** + * API method: Set lock movement type from integer. + * @param type 0=NO, 1=WINDUP, 2=ACTIVE, 3=WINDUP_AND_ACTIVE + */ + @Override + public void setLockMovementType(int type) { + this.lockMovement = LockMovementType.fromOrdinal(type); + } + + /** + * Check if movement should be locked during WINDUP phase. + */ + @Override + public boolean isMovementLockedDuringWindup() { + return lockMovement.locksWindup(); + } + + /** + * Check if movement should be locked during ACTIVE phase. + */ + @Override + public boolean isMovementLockedDuringActive() { + return lockMovement.locksActive(); + } + + /** + * Check if movement should be locked during the current phase. + */ + public boolean isMovementLockedForCurrentPhase() { + switch (phase) { + case WINDUP: + return lockMovement.locksWindup(); + case ACTIVE: + return lockMovement.locksActive(); + default: + return false; + } + } + public int getWindUpColor() { return windUpColor; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/LockMovementType.java b/src/main/java/kamkeel/npcs/controllers/data/ability/LockMovementType.java new file mode 100644 index 000000000..1b81a3bb3 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/LockMovementType.java @@ -0,0 +1,83 @@ +package kamkeel.npcs.controllers.data.ability; + +/** + * Defines when an ability locks NPC movement and rotation. + */ +public enum LockMovementType { + /** + * No movement lock - NPC can move freely during both phases. + */ + NO, + + /** + * Lock movement during WINDUP phase only. + * NPC can move during ACTIVE phase. + */ + WINDUP, + + /** + * Lock movement during ACTIVE phase only. + * NPC can move during WINDUP phase. + */ + ACTIVE, + + /** + * Lock movement during both WINDUP and ACTIVE phases. + */ + WINDUP_AND_ACTIVE; + + /** + * Check if movement should be locked during WINDUP phase. + */ + public boolean locksWindup() { + return this == WINDUP || this == WINDUP_AND_ACTIVE; + } + + /** + * Check if movement should be locked during ACTIVE phase. + */ + public boolean locksActive() { + return this == ACTIVE || this == WINDUP_AND_ACTIVE; + } + + /** + * Get display name for GUI. + */ + public String getDisplayKey() { + switch (this) { + case NO: + return "gui.no"; + case WINDUP: + return "ability.lockMove.windup"; + case ACTIVE: + return "ability.lockMove.active"; + case WINDUP_AND_ACTIVE: + return "ability.lockMove.both"; + default: + return "gui.no"; + } + } + + /** + * Get all display keys for GUI dropdown. + */ + public static String[] getDisplayKeys() { + return new String[]{ + "gui.no", + "ability.lockMove.windup", + "ability.lockMove.active", + "ability.lockMove.both" + }; + } + + /** + * Get LockMovementType from ordinal, with fallback to WINDUP. + */ + public static LockMovementType fromOrdinal(int ordinal) { + LockMovementType[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return WINDUP; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index d87ed05ee..1f6f31250 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; @@ -47,7 +48,7 @@ public class AbilityCharge extends Ability implements IAbilityCharge { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 20.0f; this.minRange = 4.0f; - this.lockMovement = false; // Movement IS the ability + this.lockMovement = LockMovementType.WINDUP; this.cooldownTicks = 0; this.windUpTicks = 20; // LINE telegraph showing charge path diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index 1d77e9219..9db9d7700 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; @@ -54,7 +55,7 @@ public class AbilityCutter extends Ability implements IAbilityCutter { this.name = "Cutter"; this.targetingMode = TargetingMode.AOE_SELF; this.maxRange = 8.0f; - this.lockMovement = true; + this.lockMovement = LockMovementType.NO; this.cooldownTicks = 0; this.windUpTicks = 20; this.telegraphType = TelegraphType.CONE; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 677578745..2c03de4c2 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; @@ -88,7 +89,7 @@ public class AbilityDash extends Ability implements IAbilityDash { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 20.0f; this.minRange = 0.0f; - this.lockMovement = false; + this.lockMovement = LockMovementType.NO; this.cooldownTicks = 0; this.windUpTicks = 5; // No telegraph for dash - it's a quick evasive move diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 2c7b8e43c..a68e38cd6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -4,6 +4,7 @@ import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; @@ -83,6 +84,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { this.minRange = 5.0f; this.cooldownTicks = 0; this.windUpTicks = 20; + this.lockMovement = LockMovementType.WINDUP; this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 790489b2c..b8946306a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityAnimation; import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; @@ -85,6 +86,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { this.minRange = 5.0f; this.cooldownTicks = 0; this.windUpTicks = 40; + this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; } @@ -145,7 +147,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, lightningEffect, lightningDensity, lightningRadius, - lockMovement, // Anchored mode + lockMovement.locksActive(), // Anchored mode during firing windUpTicks, // Charge duration = windup duration offsetDist, // Offset distance (used by FRONT anchor) anchorPoint // Anchor point for charging position @@ -311,7 +313,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { public float getLightningRadius() { return lightningRadius; } public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } - + @Override public Animation getWindUpAnimation() { if (windUpAnimationId == -1) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index 84701b79e..92c7ca58e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; @@ -50,7 +51,7 @@ public class AbilityGuard extends Ability implements IAbilityGuard { this.typeId = "ability.cnpc.guard"; this.name = "Guard"; this.targetingMode = TargetingMode.SELF; - this.lockMovement = true; + this.lockMovement = LockMovementType.ACTIVE; this.cooldownTicks = 0; this.windUpTicks = 10; this.interruptible = false; // Hard to interrupt while guarding diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 79e43e2cc..61841ded4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; @@ -83,7 +84,7 @@ public class AbilityHazard extends Ability implements IAbilityHazard { this.name = "Hazard"; this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 15.0f; - this.lockMovement = true; + this.lockMovement = LockMovementType.WINDUP; this.cooldownTicks = 0; this.windUpTicks = 30; this.telegraphType = TelegraphType.CIRCLE; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index 11aed6c47..406be2f3a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; @@ -49,7 +50,7 @@ public class AbilityHeal extends Ability implements IAbilityHeal { this.typeId = "ability.cnpc.heal"; this.name = "Heal"; this.targetingMode = TargetingMode.SELF; - this.lockMovement = true; + this.lockMovement = LockMovementType.WINDUP; this.cooldownTicks = 0; this.windUpTicks = 30; // No telegraph for heal - it's a self/ally buff diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index 82091c041..4ed4a0e78 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; @@ -29,7 +30,7 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 3.0f; this.minRange = 0.0f; - this.lockMovement = true; + this.lockMovement = LockMovementType.NO; this.cooldownTicks = 0; this.windUpTicks = 30; this.telegraphType = TelegraphType.POINT; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index 44b6bc0b5..da9f07780 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityLaser; @@ -63,6 +64,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { this.minRange = 3.0f; this.cooldownTicks = 0; this.windUpTicks = 15; + this.lockMovement = LockMovementType.WINDUP; this.telegraphType = TelegraphType.LINE; this.showTelegraph = true; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index fabdf6ab0..336f54e7c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityAnimation; import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; @@ -84,6 +85,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { this.minRange = 5.0f; this.cooldownTicks = 0; this.windUpTicks = 30; + this.lockMovement = LockMovementType.WINDUP; this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index 56b12028e..6b81fabe7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.Entity; @@ -42,6 +43,7 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { this.maxRange = 20.0f; this.cooldownTicks = 0; this.windUpTicks = 15; + this.lockMovement = LockMovementType.NO; // No telegraph for projectile - it's a ranged attack this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index c6b93134a..006084f5e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; @@ -35,7 +36,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { this.name = "Shockwave"; this.targetingMode = TargetingMode.AOE_SELF; this.maxRange = 15.0f; - this.lockMovement = true; + this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; this.cooldownTicks = 0; this.windUpTicks = 25; this.telegraphType = TelegraphType.CIRCLE; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index ba49ffb2c..b9da81dc5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; @@ -52,7 +53,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam { this.targetingMode = TargetingMode.AOE_SELF; // Can also be AOE_TARGET to leap to target this.windUpTicks = 30; this.cooldownTicks = 0; - this.lockMovement = true; + this.lockMovement = LockMovementType.WINDUP; this.minRange = 2.0f; this.maxRange = 15.0f; this.telegraphType = TelegraphType.CIRCLE; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index 66f589a00..2abbbbb48 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; @@ -54,7 +55,7 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 15.0f; this.minRange = 0.0f; - this.lockMovement = true; + this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; this.cooldownTicks = 0; this.windUpTicks = 60; // Circle telegraph to show range diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index 05293a467..1fef00a55 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.block.Block; @@ -63,6 +64,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { this.minRange = 5.0f; this.cooldownTicks = 0; this.windUpTicks = 10; + this.lockMovement = LockMovementType.NO; // No telegraph for teleport - it's instant repositioning this.telegraphType = TelegraphType.NONE; this.showTelegraph = false; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index b14a9ad67..6bf69a60b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; @@ -66,7 +67,7 @@ public class AbilityTrap extends Ability implements IAbilityTrap { this.name = "Trap"; this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 15.0f; - this.lockMovement = true; + this.lockMovement = LockMovementType.WINDUP; this.cooldownTicks = 0; this.windUpTicks = 20; this.telegraphType = TelegraphType.CIRCLE; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index a85bbc442..1bcfb7cbc 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -3,6 +3,7 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; @@ -53,7 +54,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex { this.name = "Vortex"; this.targetingMode = TargetingMode.AOE_SELF; this.maxRange = 15.0f; - this.lockMovement = true; + this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; this.cooldownTicks = 0; this.windUpTicks = 30; this.telegraphType = TelegraphType.CIRCLE; diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 2ddabc588..538829cef 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -79,7 +79,7 @@ public class DataAbilities { private transient List recentHitTimes = new ArrayList<>(); /** - * Locked rotation values for ACTIVE phase with lockMovement. + * Locked rotation values for ACTIVE phase when movement is locked. * Stored when entering ACTIVE phase, applied every tick to prevent rotation changes. */ private transient boolean rotationLocked = false; @@ -146,9 +146,9 @@ public class DataAbilities { // Remove telegraph - it has served its purpose removeTelegraph(currentAbility); - // Lock rotation if lockMovement is true + // Lock rotation if movement is locked during ACTIVE phase // Capture current rotation values to force them every tick - if (currentAbility.isLockMovement()) { + if (currentAbility.isMovementLockedDuringActive()) { captureLockedRotation(); } @@ -191,6 +191,32 @@ public class DataAbilities { handleAbilityCompletion(target); break; } + + // Apply movement control if ability is still executing + if (currentAbility != null && currentAbility.isExecuting()) { + applyMovementControl(); + } + } + + /** + * Apply movement control based on lock movement settings. + * Called every tick during ability execution. + */ + private void applyMovementControl() { + if (currentAbility == null) return; + + // Check if movement should be locked for the current phase + if (currentAbility.isMovementLockedForCurrentPhase()) { + // Clear navigator to prevent AI pathfinding + npc.getNavigator().clearPathEntity(); + + // Zero motion if ability doesn't have its own movement + // This allows abilities like Charge/Slam to control their own motion + if (!currentAbility.hasAbilityMovement()) { + npc.motionX = 0; + npc.motionZ = 0; + } + } } /** @@ -210,29 +236,11 @@ public class DataAbilities { npc.wrappedNPC, currentAbility, target); NpcAPI.EVENT_BUS.post(completeEvent); - // Clean up and roll cooldown + // Release rotation lock before cleanup + releaseLockedRotation(); + + // Clean up and roll cooldown (this sets currentAbility to null) onAbilityComplete(); - - // Clear AI pathfinding if ability is controlling movement - // But DON'T zero motion - let the ability set it if needed - if (currentAbility != null && currentAbility.isExecuting()) { - if (isAbilityControllingMovement()) { - // Clear navigator so AI doesn't fight with ability movement - npc.getNavigator().clearPathEntity(); - - // Only zero motion if lockMovement is true AND ability doesn't have its own movement - // This allows stationary abilities to freeze the NPC, while movement abilities can set their own motion - if (currentAbility.isLockMovement() && !currentAbility.hasAbilityMovement()) { - npc.motionX = 0; - npc.motionZ = 0; - } - } - - // Unlock rotation when leaving ACTIVE phase - if (rotationLocked && currentAbility.getPhase() != AbilityPhase.ACTIVE) { - releaseLockedRotation(); - } - } } /** @@ -787,8 +795,8 @@ public class DataAbilities { * Called by AI tasks to determine if they should try to path. *

* Rules: - * - WINDUP phase: Block AI if lockMovement is true - * - ACTIVE phase: Block AI if lockMovement OR hasAbilityMovement is true + * - WINDUP phase: Block AI if movement is locked during windup + * - ACTIVE phase: Block AI if movement is locked during active OR hasAbilityMovement is true * - DAZED phase: Always block AI (NPC is stunned) */ public boolean isAbilityControllingMovement() { @@ -800,15 +808,15 @@ public class DataAbilities { switch (phase) { case WINDUP: - // During windup, only block if lockMovement is set - // This allows NPC to keep chasing while winding up if lockMovement=false - return currentAbility.isLockMovement(); + // During windup, only block if movement is locked during windup + // This allows NPC to keep chasing while winding up if not locked + return currentAbility.isMovementLockedDuringWindup(); case ACTIVE: // During active phase, block if either: - // - lockMovement is true (stationary ability) + // - movement is locked during active (stationary ability) // - hasAbilityMovement is true (ability is moving the NPC itself) - return currentAbility.isLockMovement() || currentAbility.hasAbilityMovement(); + return currentAbility.isMovementLockedDuringActive() || currentAbility.hasAbilityMovement(); case DAZED: // During dazed phase, always block - NPC is stunned from interrupt @@ -821,7 +829,7 @@ public class DataAbilities { /** * Check if NPC's look direction should be locked to target. - * Only locks during ACTIVE phase when lockMovement is true. + * Only locks during ACTIVE phase when movement is locked during active. * This allows the NPC to freely track target during WINDUP and DAZED. */ public boolean shouldLockLookDirection() { @@ -834,7 +842,7 @@ public class DataAbilities { return false; } - return currentAbility.isLockMovement(); + return currentAbility.isMovementLockedDuringActive(); } /** @@ -854,7 +862,7 @@ public class DataAbilities { /** * Capture current rotation values to lock NPC's look direction. - * Called when entering ACTIVE phase with lockMovement enabled. + * Called when entering ACTIVE phase with movement locked during active. */ private void captureLockedRotation() { lockedYaw = npc.rotationYaw; diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index 831420473..11dbcbc85 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.Condition; +import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import noppes.npcs.controllers.data.Animation; @@ -58,7 +59,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi protected String name; protected boolean enabled; protected int weight; - protected boolean lockMovement; + protected LockMovementType lockMovement; protected boolean interruptible; // Timing @@ -111,7 +112,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi this.name = ability.getName() != null ? ability.getName() : ""; this.enabled = ability.isEnabled(); this.weight = ability.getWeight(); - this.lockMovement = ability.isLockMovement(); + this.lockMovement = ability.getLockMovement(); this.interruptible = ability.isInterruptible(); // Timing @@ -251,7 +252,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi addTextField(weightField); addLabel(new GuiNpcLabel(5, "ability.lockMove", col2LabelX, y + 5)); - GuiNpcButton lockMoveBtn = new GuiNpcButton(16, col2FieldX, y, 40, 20, new String[]{"gui.no", "gui.yes"}, lockMovement ? 1 : 0); + GuiNpcButton lockMoveBtn = new GuiNpcButton(16, col2FieldX, y, 60, 20, LockMovementType.getDisplayKeys(), lockMovement.ordinal()); lockMoveBtn.setHoverText("ability.hover.lockMove"); addButton(lockMoveBtn); @@ -644,7 +645,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi interruptible = ((GuiNpcButton) guibutton).getValue() == 1; initGui(); // Refresh to show/hide dazed ticks field } else if (id == 16) { - lockMovement = ((GuiNpcButton) guibutton).getValue() == 1; + lockMovement = LockMovementType.fromOrdinal(((GuiNpcButton) guibutton).getValue()); } else if (id == 17) { // Sync windup ticks with animation duration syncWindupWithAnimation(); @@ -920,7 +921,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi this.name = ability.getName() != null ? ability.getName() : ""; this.enabled = ability.isEnabled(); this.weight = ability.getWeight(); - this.lockMovement = ability.isLockMovement(); + this.lockMovement = ability.getLockMovement(); this.interruptible = ability.isInterruptible(); this.cooldownTicks = ability.getCooldownTicks(); diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index d4f75d883..fd148ea08 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1519,6 +1519,9 @@ ability.dazed=Dazed Ticks ability.interruptible=Interruptible ability.threshold=Threshold ability.lockMove=Lock Move +ability.lockMove.windup=Windup +ability.lockMove.active=Active +ability.lockMove.both=Both ability.telegraph=Telegraph ability.telegraphType=Telegraph Type From e73054a529e7d50f3d2fc75089651bb477b48795 Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 28 Jan 2026 16:31:26 -0500 Subject: [PATCH 304/358] Built In Animation System + Fixes to Defaults for Energy Projectile Abilities --- src/api | 2 +- .../controllers/data/ability/Ability.java | 56 ++++- .../data/ability/AbilityAnimation.java | 101 --------- .../data/ability/type/AbilityDisc.java | 14 +- .../data/ability/type/AbilityEnergyBeam.java | 42 ++-- .../data/ability/type/AbilityLaserShot.java | 30 ++- .../data/ability/type/AbilityOrb.java | 31 ++- .../data/ability/type/AbilitySweeper.java | 15 +- .../animation/AnimationsGetPacket.java | 1 + src/main/java/noppes/npcs/DataAbilities.java | 2 +- .../java/noppes/npcs/NoppesUtilServer.java | 11 +- .../gui/advanced/SubGuiAbilityConfig.java | 70 +++++-- .../gui/select/GuiAnimationSelection.java | 196 +++++++++++++----- .../noppes/npcs/constants/EnumScrollData.java | 4 +- .../npcs/controllers/AnimationController.java | 189 ++++++++++++++++- .../controllers/data/BuiltInAnimation.java | 94 +++++++++ .../animations}/Ability_Beam_Active.json | 0 .../animations}/Ability_Beam_Windup.json | 0 .../animations}/Ability_Orb_Windup.json | 0 .../assets/customnpcs/lang/en_US.lang | 2 + 20 files changed, 622 insertions(+), 238 deletions(-) delete mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java create mode 100644 src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java rename src/main/resources/{internal/animations/ABILITYBEAM => assets/customnpcs/animations}/Ability_Beam_Active.json (100%) rename src/main/resources/{internal/animations/ABILITYBEAM => assets/customnpcs/animations}/Ability_Beam_Windup.json (100%) rename src/main/resources/{internal/animations/ABILITYORB => assets/customnpcs/animations}/Ability_Orb_Windup.json (100%) diff --git a/src/api b/src/api index 2b21264ad..b8ba682e9 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 2b21264ada69ec5e800977a000e892bc82d7c61f +Subproject commit b8ba682e9dd4e542a5ccf0de231ab5e5ebdad448 diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index a82a8fc6e..08eed148b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -64,8 +64,12 @@ public abstract class Ability implements IAbility { protected String activeSound = ""; // Sound to play when active phase starts // Animations (global animation IDs, -1 = none) - protected int windUpAnimationId = -1; // Animation to play during wind up - protected int activeAnimationId = -1; // Animation to play during active phase + protected int windUpAnimationId = -1; // Animation to play during wind up (user animations) + protected int activeAnimationId = -1; // Animation to play during active phase (user animations) + + // Built-in animation names (empty = none, takes priority over IDs if set) + protected String windUpAnimationName = ""; // Built-in animation name for wind up + protected String activeAnimationName = ""; // Built-in animation name for active phase // Telegraph configuration protected boolean showTelegraph = true; @@ -669,6 +673,8 @@ public abstract class Ability implements IAbility { nbt.setString("activeSound", activeSound); nbt.setInteger("windUpAnimationId", windUpAnimationId); nbt.setInteger("activeAnimationId", activeAnimationId); + nbt.setString("windUpAnimationName", windUpAnimationName); + nbt.setString("activeAnimationName", activeAnimationName); nbt.setBoolean("showTelegraph", showTelegraph); nbt.setString("telegraphType", telegraphType.name()); nbt.setFloat("telegraphHeightOffset", telegraphHeightOffset); @@ -738,6 +744,8 @@ public abstract class Ability implements IAbility { windUpAnimationId = -1; } activeAnimationId = nbt.hasKey("activeAnimationId") ? nbt.getInteger("activeAnimationId") : -1; + windUpAnimationName = nbt.hasKey("windUpAnimationName") ? nbt.getString("windUpAnimationName") : ""; + activeAnimationName = nbt.hasKey("activeAnimationName") ? nbt.getString("activeAnimationName") : ""; showTelegraph = !nbt.hasKey("showTelegraph") || nbt.getBoolean("showTelegraph"); if (nbt.hasKey("telegraphType")) { try { @@ -959,23 +967,63 @@ public abstract class Ability implements IAbility { public Animation getWindUpAnimation() { if (AnimationController.Instance == null) return null; - return (Animation) AnimationController.Instance.get(windUpAnimationId); + // Built-in animation by name takes priority + if (windUpAnimationName != null && !windUpAnimationName.isEmpty()) { + return (Animation) AnimationController.Instance.get(windUpAnimationName); + } + // Fall back to user animation by ID + if (windUpAnimationId >= 0) { + return (Animation) AnimationController.Instance.get(windUpAnimationId); + } + return null; + } + + public int getWindUpAnimationId() { + return windUpAnimationId; } public void setWindUpAnimationId(int windUpAnimationId) { this.windUpAnimationId = windUpAnimationId; } + public String getWindUpAnimationName() { + return windUpAnimationName; + } + + public void setWindUpAnimationName(String windUpAnimationName) { + this.windUpAnimationName = windUpAnimationName != null ? windUpAnimationName : ""; + } + public Animation getActiveAnimation() { if (AnimationController.Instance == null) return null; - return (Animation) AnimationController.Instance.get(activeAnimationId); + // Built-in animation by name takes priority + if (activeAnimationName != null && !activeAnimationName.isEmpty()) { + return (Animation) AnimationController.Instance.get(activeAnimationName); + } + // Fall back to user animation by ID + if (activeAnimationId >= 0) { + return (Animation) AnimationController.Instance.get(activeAnimationId); + } + return null; + } + + public int getActiveAnimationId() { + return activeAnimationId; } public void setActiveAnimationId(int activeAnimationId) { this.activeAnimationId = activeAnimationId; } + public String getActiveAnimationName() { + return activeAnimationName; + } + + public void setActiveAnimationName(String activeAnimationName) { + this.activeAnimationName = activeAnimationName != null ? activeAnimationName : ""; + } + public boolean isShowTelegraph() { return showTelegraph; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java deleted file mode 100644 index 190be70d3..000000000 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityAnimation.java +++ /dev/null @@ -1,101 +0,0 @@ -package kamkeel.npcs.controllers.data.ability; - -import noppes.npcs.CustomNpcs; -import noppes.npcs.LogWriter; -import noppes.npcs.controllers.data.Animation; -import noppes.npcs.util.NBTJsonUtil; -import org.apache.commons.io.FileUtils; - -import java.io.File; -import java.io.InputStream; -import java.util.HashMap; - -import static kamkeel.npcs.controllers.data.ability.AbilityPhase.*; - -public enum AbilityAnimation { - ABILITYORB("Ability_Orb_Windup", WINDUP), - ABILITYBEAM("Ability_Beam_Windup", "Ability_Beam_Active"); - private final HashMap animationNames = new HashMap<>(); - - AbilityAnimation(String idle, String windUp, String active, String recovery) { - animationNames.put(IDLE, idle); - animationNames.put(WINDUP, windUp); - animationNames.put(ACTIVE, active); - animationNames.put(DAZED, recovery); - } - - AbilityAnimation(String idle, String windUp, String active) { - animationNames.put(IDLE, idle); - animationNames.put(WINDUP, windUp); - animationNames.put(ACTIVE, active); - } - - AbilityAnimation(String windUp, String active) { - animationNames.put(WINDUP, windUp); - animationNames.put(ACTIVE, active); - } - - AbilityAnimation(String anim, AbilityPhase phase) { - animationNames.put(phase, anim); - } - - public String fileName(AbilityPhase phase) { - return animationNames.get(phase); - } - - public File getFile(AbilityPhase phase) { - if (animationNames.get(phase).isEmpty() || animationNames.get(phase).equals("N/A")) - return null; - - return getFileFromName(animationNames.get(phase)); - } - - public Animation getAnimation(AbilityPhase phase) { - try { - File file = getFile(phase); - - if (file == null) { - return null; - } - - Animation animation = new Animation(); - animation.readFromNBT(NBTJsonUtil.LoadFile(file)); - - return animation; - } catch (Exception e) { - LogWriter.except(e); - } - - return null; - } - - public Animation idle() { - return getAnimation(IDLE); - } - - public Animation windUp() { - return getAnimation(WINDUP); - } - - public Animation active() { - return getAnimation(ACTIVE); - } - - public Animation dazed() { - return getAnimation(DAZED); - } - - private File getFileFromName(String fileName) { - try (InputStream stream = CustomNpcs.class.getResourceAsStream("/internal/animations/" + name() + "/" + fileName + ".json")) { - - File file = new File("temp"); - - FileUtils.copyInputStreamToFile(stream, file); - - return file; - } catch (Exception i) { - LogWriter.except(i); - } - return null; - } -} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index a68e38cd6..d85720b6e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -121,10 +121,13 @@ public class AbilityDisc extends Ability implements IAbilityDisc { if (discEntity != null && !discEntity.isDead) { discEntity.startMoving(target); } - discEntity = null; - // Disc entity manages itself - ability is done - signalCompletion(); + // If movement is locked during active phase, keep ability active until entity dies + // Otherwise signal completion immediately + if (!lockMovement.locksActive()) { + discEntity = null; + signalCompletion(); + } } @Override @@ -154,6 +157,11 @@ public class AbilityDisc extends Ability implements IAbilityDisc { @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + // If we're tracking the entity for movement lock, check if it's dead + if (discEntity != null && discEntity.isDead) { + discEntity = null; + signalCompletion(); + } } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index b8946306a..7ebd4bb7b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -3,7 +3,6 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityAnimation; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; @@ -13,13 +12,12 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityBeam; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; -import net.minecraft.nbt.NBTTagCompound;; +import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityEnergyBeam; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.api.ability.type.IAbilityEnergyBeam; -import noppes.npcs.controllers.data.Animation; import noppes.npcs.entity.EntityNPCInterface; /** @@ -71,9 +69,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { private float lightningRadius = 0.5f; // Anchor point for charging position - private AnchorPoint anchorPoint = AnchorPoint.FRONT; - - private final AbilityAnimation animation = AbilityAnimation.ABILITYBEAM; + private AnchorPoint anchorPoint = AnchorPoint.RIGHT_HAND; // Transient state for beam entity (used during windup charging) private transient EntityAbilityBeam beamEntity = null; @@ -89,6 +85,9 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; + // Default built-in animations + this.windUpAnimationName = "Ability_Beam_Windup"; + this.activeAnimationName = "Ability_Beam_Active"; } @Override @@ -123,10 +122,13 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { if (beamEntity != null && !beamEntity.isDead) { beamEntity.startFiring(target); } - beamEntity = null; - // Beam entity manages itself - ability is done - signalCompletion(); + // If movement is locked during active phase, keep ability active until entity dies + // Otherwise signal completion immediately + if (!lockMovement.locksActive()) { + beamEntity = null; + signalCompletion(); + } } @Override @@ -159,6 +161,11 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + // If we're tracking the entity for movement lock, check if it's dead + if (beamEntity != null && beamEntity.isDead) { + beamEntity = null; + signalCompletion(); + } } @Override @@ -313,23 +320,6 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { public float getLightningRadius() { return lightningRadius; } public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } - - @Override - public Animation getWindUpAnimation() { - if (windUpAnimationId == -1) - return animation.windUp(); - - return super.getWindUpAnimation(); - } - - @Override - public Animation getActiveAnimation() { - if (activeAnimationId == -1) - return animation.active(); - - return super.getActiveAnimation(); - } - public AnchorPoint getAnchorPointEnum() { return anchorPoint; } public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index da9f07780..a9cf74473 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -56,6 +56,9 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { private float lightningDensity = 0.15f; private float lightningRadius = 0.5f; + // Transient state for laser entity (used for movement locking) + private transient EntityAbilityLaser laserEntity = null; + public AbilityLaserShot() { this.typeId = "ability.cnpc.laser_shot"; this.name = "Laser Shot"; @@ -111,7 +114,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { double spawnY = npc.posY + npc.getEyeHeight() * 0.8; double spawnZ = npc.posZ; - EntityAbilityLaser laser = new EntityAbilityLaser( + laserEntity = new EntityAbilityLaser( world, npc, target, spawnX, spawnY, spawnZ, laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, @@ -123,15 +126,23 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { lightningEffect, lightningDensity, lightningRadius ); - world.spawnEntityInWorld(laser); + world.spawnEntityInWorld(laserEntity); - // Laser entity manages itself - ability is done - signalCompletion(); + // If movement is locked during active phase, keep ability active until entity dies + // Otherwise signal completion immediately + if (!lockMovement.locksActive()) { + laserEntity = null; + signalCompletion(); + } } @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // Laser entity manages itself + // If we're tracking the entity for movement lock, check if it's dead + if (laserEntity != null && laserEntity.isDead) { + laserEntity = null; + signalCompletion(); + } } @Override @@ -142,6 +153,15 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { } + @Override + public void cleanup() { + // Despawn laser entity if still alive + if (laserEntity != null && !laserEntity.isDead) { + laserEntity.setDead(); + } + laserEntity = null; + } + @Override public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("laserWidth", laserWidth); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 336f54e7c..d69a5c944 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -3,7 +3,6 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityAnimation; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; @@ -19,7 +18,6 @@ import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityOrb; import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.controllers.data.Animation; import noppes.npcs.api.ability.type.IAbilityOrb; import noppes.npcs.entity.EntityNPCInterface; @@ -70,13 +68,11 @@ public class AbilityOrb extends Ability implements IAbilityOrb { private int lightningFadeTime = 6; // Ticks before lightning fades out // Anchor point for charging position - private AnchorPoint anchorPoint = AnchorPoint.FRONT; + private AnchorPoint anchorPoint = AnchorPoint.RIGHT_HAND; // Transient state for orb entity (used during windup charging) private transient EntityAbilityOrb orbEntity = null; - private final AbilityAnimation animation = AbilityAnimation.ABILITYORB; - public AbilityOrb() { this.typeId = "ability.cnpc.orb"; this.name = "Orb"; @@ -88,6 +84,8 @@ public class AbilityOrb extends Ability implements IAbilityOrb { this.lockMovement = LockMovementType.WINDUP; this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; + // Default built-in animation + this.windUpAnimationName = "Ability_Orb_Windup"; } @Override @@ -122,10 +120,13 @@ public class AbilityOrb extends Ability implements IAbilityOrb { if (orbEntity != null && !orbEntity.isDead) { orbEntity.startMoving(target); } - orbEntity = null; - // Orb entity manages itself - ability is done - signalCompletion(); + // If movement is locked during active phase, keep ability active until entity dies + // Otherwise signal completion immediately + if (!lockMovement.locksActive()) { + orbEntity = null; + signalCompletion(); + } } @Override @@ -154,7 +155,11 @@ public class AbilityOrb extends Ability implements IAbilityOrb { @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // Nothing to do - entity manages itself + // If we're tracking the entity for movement lock, check if it's dead + if (orbEntity != null && orbEntity.isDead) { + orbEntity = null; + signalCompletion(); + } } @Override @@ -467,14 +472,6 @@ public class AbilityOrb extends Ability implements IAbilityOrb { this.anchorPoint = anchorPoint; } - @Override - public Animation getWindUpAnimation() { - if (windUpAnimationId != -1) - return super.getWindUpAnimation(); - - return animation.windUp(); - } - public int getAnchorPoint() { return anchorPoint.getId(); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index 2abbbbb48..78dae4fdc 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -91,8 +91,6 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { return; } - LogWriter.info("[Sweeper] onExecute called at NPC pos " + npc.posX + ", " + npc.posY + ", " + npc.posZ); - // Spawn the entity that handles BOTH visuals AND damage activeEntity = new EntityAbilitySweeper(world, npc, target, beamLength, beamWidth, beamHeight, @@ -102,14 +100,19 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { lockOnTarget); world.spawnEntityInWorld(activeEntity); - LogWriter.info("[Sweeper] Spawned sweeper entity"); - // Entity manages itself - completion signaled from onActiveTick when entity dies + // If movement is locked during active phase, keep ability active until entity dies + // Otherwise signal completion immediately + if (!lockMovement.locksActive()) { + activeEntity = null; + signalCompletion(); + } } @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // Entity handles everything - check if entity finished its rotations - if (activeEntity == null || activeEntity.isDead) { + // If we're tracking the entity for movement lock, check if it's dead + if (activeEntity != null && activeEntity.isDead) { + activeEntity = null; signalCompletion(); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/animation/AnimationsGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/animation/AnimationsGetPacket.java index a2bc51d0a..39b60a40f 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/animation/AnimationsGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/animation/AnimationsGetPacket.java @@ -44,5 +44,6 @@ public final class AnimationsGetPacket extends AbstractPacket { return; NoppesUtilServer.sendAnimationDataAll((EntityPlayerMP) player); + NoppesUtilServer.sendBuiltInAnimationData((EntityPlayerMP) player); } } diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 538829cef..526a3bdd7 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -412,7 +412,7 @@ public class DataAbilities { * @param animation The animation to play, or null if none */ public void playAbilityAnimation(Animation animation) { - if (animation == null || animation.id < 0) return; + if (animation == null) return; if (AnimationController.Instance == null) return; npc.display.animationData.setEnabled(true); diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index ffc778eb0..6a33cb2c0 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -163,7 +163,16 @@ public class NoppesUtilServer { for (Animation animation : AnimationController.getInstance().animations.values()) { map.put(animation.name, animation.id); } - sendScrollData(player, map, EnumScrollData.OPTIONAL); + sendScrollData(player, map, EnumScrollData.ANIMATIONS); + } + + public static void sendBuiltInAnimationData(EntityPlayerMP player) { + Map map = new HashMap(); + for (String name : AnimationController.getInstance().builtInAnimations.keySet()) { + // Use -1 as a marker for built-in animations (they have no ID) + map.put(AnimationController.getInstance().builtInAnimations.get(name).getName(), -1); + } + sendScrollData(player, map, EnumScrollData.BUILTIN_ANIMATIONS); } public static void sendTagDataAll(EntityPlayerMP player) { diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index 11dbcbc85..a4fef92ef 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -129,8 +129,11 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Effects this.windUpSound = ability.getWindUpSound(); this.activeSound = ability.getActiveSound(); - this.windUpAnimationId = ability.getWindUpAnimation() != null ? ability.getWindUpAnimation().getID() : -1; - this.activeAnimationId = ability.getActiveAnimation() != null ? ability.getActiveAnimation().getID() : -1; + // Animation: check name first (built-in), then ID (user) + this.windUpAnimationName = ability.getWindUpAnimationName(); + this.activeAnimationName = ability.getActiveAnimationName(); + this.windUpAnimationId = ability.getWindUpAnimationId(); + this.activeAnimationId = ability.getActiveAnimationId(); // Telegraph this.showTelegraph = ability.isShowTelegraph(); @@ -286,8 +289,8 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi windupField.setMinMaxDefault(0, 1000, 20); addTextField(windupField); - // Sync button - only show if windup animation is selected - if (windUpAnimationId >= 0) { + // Sync button - only show if windup animation is selected (either by name or ID) + if (windUpAnimationId >= 0 || (windUpAnimationName != null && !windUpAnimationName.isEmpty())) { GuiNpcButton syncBtn = new GuiNpcButton(17, guiLeft + 115, y, 40, 20, "gui.sync"); syncBtn.setHoverText("ability.hover.sync"); addButton(syncBtn); @@ -591,12 +594,16 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi } private String getAnimationName(int animId, boolean isWindUp) { - if (animId < 0) return StatCollector.translateToLocal("gui.none"); - String cachedName = isWindUp ? windUpAnimationName : activeAnimationName; - if (cachedName != null && !cachedName.isEmpty()) { - return truncateString(cachedName, 8); + String animName = isWindUp ? windUpAnimationName : activeAnimationName; + // Check if we have a built-in animation (by name) + if (animName != null && !animName.isEmpty()) { + return truncateString(animName, 8); } - return "ID: " + animId; + // Check if we have a user animation (by ID) + if (animId >= 0) { + return "ID: " + animId; + } + return StatCollector.translateToLocal("gui.none"); } // ═══════════════════════════════════════════════════════════════════════════ @@ -726,19 +733,19 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Animation buttons else if (id == 32) { editingAnimationId = 32; - setSubGui(new GuiAnimationSelection(windUpAnimationId)); + setSubGui(new GuiAnimationSelection(windUpAnimationId, windUpAnimationName)); } else if (id == 33) { editingAnimationId = 33; - setSubGui(new GuiAnimationSelection(activeAnimationId)); + setSubGui(new GuiAnimationSelection(activeAnimationId, activeAnimationName)); } // Animation clear buttons else if (id == 37) { windUpAnimationId = -1; - windUpAnimationName = null; + windUpAnimationName = ""; initGui(); } else if (id == 38) { activeAnimationId = -1; - activeAnimationName = null; + activeAnimationName = ""; initGui(); } @@ -824,11 +831,25 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi } else if (subgui instanceof GuiAnimationSelection) { GuiAnimationSelection animSelector = (GuiAnimationSelection) subgui; if (editingAnimationId == 32) { - windUpAnimationId = animSelector.selectedAnimationId; - windUpAnimationName = animSelector.getSelectedName(); + if (animSelector.isBuiltInSelected()) { + // Built-in animation selected - use name, clear ID + windUpAnimationName = animSelector.selectedBuiltInName; + windUpAnimationId = -1; + } else { + // User animation selected - use ID, clear name + windUpAnimationId = animSelector.selectedAnimationId; + windUpAnimationName = ""; + } } else if (editingAnimationId == 33) { - activeAnimationId = animSelector.selectedAnimationId; - activeAnimationName = animSelector.getSelectedName(); + if (animSelector.isBuiltInSelected()) { + // Built-in animation selected - use name, clear ID + activeAnimationName = animSelector.selectedBuiltInName; + activeAnimationId = -1; + } else { + // User animation selected - use ID, clear name + activeAnimationId = animSelector.selectedAnimationId; + activeAnimationName = ""; + } } editingAnimationId = 0; initGui(); @@ -896,6 +917,8 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi ability.setActiveSound(activeSound); ability.setWindUpAnimationId(windUpAnimationId); ability.setActiveAnimationId(activeAnimationId); + ability.setWindUpAnimationName(windUpAnimationName != null ? windUpAnimationName : ""); + ability.setActiveAnimationName(activeAnimationName != null ? activeAnimationName : ""); // Telegraph ability.setShowTelegraph(showTelegraph); @@ -948,11 +971,20 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi * Syncs windup ticks with the selected windup animation's total duration. */ private void syncWindupWithAnimation() { - if (windUpAnimationId < 0 || AnimationController.Instance == null) { + if (AnimationController.Instance == null) { return; } - Animation animation = (Animation) AnimationController.Instance.get(windUpAnimationId); + Animation animation = null; + // Check for built-in animation (by name) first + if (windUpAnimationName != null && !windUpAnimationName.isEmpty()) { + animation = (Animation) AnimationController.Instance.get(windUpAnimationName); + } + // Fall back to user animation (by ID) + else if (windUpAnimationId >= 0) { + animation = (Animation) AnimationController.Instance.get(windUpAnimationId); + } + if (animation == null || animation.frames.isEmpty()) { return; } diff --git a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java index cc3e06cca..2a1f04344 100644 --- a/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java +++ b/src/main/java/noppes/npcs/client/gui/select/GuiAnimationSelection.java @@ -19,20 +19,42 @@ import java.util.Vector; /** * Animation selection dialog for choosing global animations. - * Requests animation data from the server via packets. + * Toggle between Built-in animations (by name) and Custom animations (by ID). */ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScrollListener, IScrollData { private GuiCustomScroll scrollAnimations; - private HashMap animationData = new HashMap(); + // Built-in animations (name only, no ID) + private HashMap builtInData = new HashMap<>(); + // Custom/user animations (name -> ID) + private HashMap customData = new HashMap<>(); + + // View mode: true = built-in, false = custom + private boolean showingBuiltIn = true; + + // Initial values passed in private int initialAnimationId; - public int selectedAnimationId = -1; - private String selectedName = null; + private String initialAnimationName; + // Selected values to return + public int selectedAnimationId = -1; + public String selectedBuiltInName = ""; + + private String selectedCustomName = null; private String search = ""; + /** + * Constructor for selecting by ID (user animations). + */ public GuiAnimationSelection(int animationId) { + this(animationId, ""); + } + + /** + * Constructor for selecting by name (built-in) or ID (user). + */ + public GuiAnimationSelection(int animationId, String animationName) { drawDefaultBackground = false; title = ""; setBackground("menubg.png"); @@ -40,7 +62,12 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr ySize = 226; this.initialAnimationId = animationId; + this.initialAnimationName = animationName != null ? animationName : ""; this.selectedAnimationId = animationId; + this.selectedBuiltInName = this.initialAnimationName; + + // Start showing built-in if we have a built-in name, otherwise custom + this.showingBuiltIn = !this.initialAnimationName.isEmpty(); // Request animation data from server PacketClient.sendClient(new AnimationsGetPacket()); @@ -50,56 +77,87 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr public void initGui() { super.initGui(); - this.addLabel(new GuiNpcLabel(0, "menu.animations", guiLeft + 8, guiTop + 4)); + int y = guiTop + 4; - this.addButton(new GuiNpcButton(1, guiLeft + 4, guiTop + ySize - 35, 50, 20, "gui.clear")); - this.addButton(new GuiNpcButton(2, guiLeft + xSize - 56, guiTop + ySize - 35, 50, 20, "gui.done")); - this.addButton(new GuiNpcButton(3, guiLeft + xSize - 108, guiTop + ySize - 35, 50, 20, "gui.cancel")); + // Toggle button for switching views + String viewLabel = showingBuiltIn ? "gui.builtinAnimations" : "gui.customAnimations"; + this.addButton(new GuiNpcButton(10, guiLeft + 4, y, 212, 20, viewLabel)); + y += 24; + // Animation scroll list if (scrollAnimations == null) { scrollAnimations = new GuiCustomScroll(this, 0, 0); - scrollAnimations.setSize(212, 150); + scrollAnimations.setSize(212, 130); } scrollAnimations.setList(getSearchList()); - if (selectedName != null) { - scrollAnimations.setSelected(selectedName); - } - scrollAnimations.guiLeft = guiLeft + 4; - scrollAnimations.guiTop = guiTop + 14; - this.addScroll(scrollAnimations); - // Search textbox - moved up to avoid overlapping with buttons - addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 4, guiTop + 166, 212, 20, search)); + // Set selection based on current mode + if (showingBuiltIn && !selectedBuiltInName.isEmpty()) { + scrollAnimations.setSelected(selectedBuiltInName); + } else if (!showingBuiltIn && selectedCustomName != null) { + scrollAnimations.setSelected(selectedCustomName); + } + + scrollAnimations.guiLeft = guiLeft + 4; + scrollAnimations.guiTop = y; + this.addScroll(scrollAnimations); + y += 134; + + // Search textbox + addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 4, y, 212, 20, search)); + y += 24; + + // Buttons + this.addButton(new GuiNpcButton(1, guiLeft + 4, y, 50, 20, "gui.clear")); + this.addButton(new GuiNpcButton(3, guiLeft + xSize - 108, y, 50, 20, "gui.cancel")); + this.addButton(new GuiNpcButton(2, guiLeft + xSize - 56, y, 50, 20, "gui.done")); } @Override public void setData(Vector list, HashMap data, EnumScrollData type) { - this.animationData = data; + if (type == EnumScrollData.BUILTIN_ANIMATIONS) { + this.builtInData = data; - // Find the name for our initial animation ID - if (initialAnimationId >= 0) { - for (String name : data.keySet()) { - if (data.get(name) == initialAnimationId) { - selectedName = name; - break; + // Check if initial name matches a built-in animation + if (!initialAnimationName.isEmpty()) { + for (String name : builtInData.keySet()) { + if (name.equalsIgnoreCase(initialAnimationName)) { + selectedBuiltInName = name; + selectedAnimationId = -1; + selectedCustomName = null; + showingBuiltIn = true; + break; + } + } + } + } else if (type == EnumScrollData.ANIMATIONS) { + this.customData = data; + + // Find the name for our initial animation ID (if not using built-in) + if (selectedBuiltInName.isEmpty() && initialAnimationId >= 0) { + for (String name : data.keySet()) { + if (data.get(name) == initialAnimationId) { + selectedCustomName = name; + showingBuiltIn = false; + break; + } } } } if (scrollAnimations != null) { scrollAnimations.setList(getSearchList()); - if (selectedName != null) { - scrollAnimations.setSelected(selectedName); + if (showingBuiltIn && !selectedBuiltInName.isEmpty()) { + scrollAnimations.setSelected(selectedBuiltInName); + } else if (!showingBuiltIn && selectedCustomName != null) { + scrollAnimations.setSelected(selectedCustomName); } } } @Override public void setSelected(String selected) { - this.selectedName = selected; - if (scrollAnimations != null) { - scrollAnimations.setSelected(selected); - } + // Not used in this implementation } @Override @@ -110,18 +168,22 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr if (search.equals(getTextField(55).getText())) return; search = getTextField(55).getText().toLowerCase(); - scrollAnimations.resetScroll(); - scrollAnimations.setList(getSearchList()); + if (scrollAnimations != null) { + scrollAnimations.resetScroll(); + scrollAnimations.setList(getSearchList()); + } } } } private List getSearchList() { + HashMap sourceData = showingBuiltIn ? builtInData : customData; + if (search.isEmpty()) { - return new ArrayList(animationData.keySet()); + return new ArrayList<>(sourceData.keySet()); } - List list = new ArrayList(); - for (String name : animationData.keySet()) { + List list = new ArrayList<>(); + for (String name : sourceData.keySet()) { if (name.toLowerCase().contains(search)) list.add(name); } @@ -130,11 +192,20 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr @Override public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { - if (guiCustomScroll.id == 0) { - String selected = scrollAnimations.getSelected(); - if (selected != null && animationData.containsKey(selected)) { - selectedName = selected; - selectedAnimationId = animationData.get(selected); + String selected = scrollAnimations.getSelected(); + if (selected == null) return; + + if (showingBuiltIn) { + if (builtInData.containsKey(selected)) { + selectedBuiltInName = selected; + selectedAnimationId = -1; + selectedCustomName = null; + } + } else { + if (customData.containsKey(selected)) { + selectedCustomName = selected; + selectedAnimationId = customData.get(selected); + selectedBuiltInName = ""; } } initGui(); @@ -142,27 +213,40 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr @Override public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { - if (selectedAnimationId < 0) - return; - close(); + // Double-click to confirm selection + if (!selectedBuiltInName.isEmpty() || selectedAnimationId >= 0) { + close(); + } } @Override protected void actionPerformed(GuiButton guibutton) { int id = guibutton.id; - if (id == 1) { - // Clear selection - selectedName = null; + + // Toggle view button + if (id == 10) { + showingBuiltIn = !showingBuiltIn; + search = ""; + if (scrollAnimations != null) { + scrollAnimations.resetScroll(); + } + initGui(); + } + // Clear + else if (id == 1) { + selectedBuiltInName = ""; selectedAnimationId = -1; + selectedCustomName = null; close(); } - if (id == 2) { - // Done - keep selection + // Done + else if (id == 2) { close(); } - if (id == 3) { - // Cancel - restore initial value + // Cancel + else if (id == 3) { selectedAnimationId = initialAnimationId; + selectedBuiltInName = initialAnimationName; close(); } } @@ -171,6 +255,16 @@ public class GuiAnimationSelection extends SubGuiInterface implements ICustomScr * Get the name of the selected animation (for display purposes). */ public String getSelectedName() { - return selectedName; + if (!selectedBuiltInName.isEmpty()) { + return selectedBuiltInName; + } + return selectedCustomName; + } + + /** + * Check if a built-in animation was selected. + */ + public boolean isBuiltInSelected() { + return !selectedBuiltInName.isEmpty(); } } diff --git a/src/main/java/noppes/npcs/constants/EnumScrollData.java b/src/main/java/noppes/npcs/constants/EnumScrollData.java index abf55a84e..235f2719b 100644 --- a/src/main/java/noppes/npcs/constants/EnumScrollData.java +++ b/src/main/java/noppes/npcs/constants/EnumScrollData.java @@ -6,5 +6,7 @@ public enum EnumScrollData { MAGIC_CYCLES, DIALOG_GROUP, QUEST_GROUP, - ABILITY_TYPES + ABILITY_TYPES, + ANIMATIONS, + BUILTIN_ANIMATIONS } diff --git a/src/main/java/noppes/npcs/controllers/AnimationController.java b/src/main/java/noppes/npcs/controllers/AnimationController.java index e3caddce5..e5e11d666 100644 --- a/src/main/java/noppes/npcs/controllers/AnimationController.java +++ b/src/main/java/noppes/npcs/controllers/AnimationController.java @@ -8,23 +8,43 @@ import noppes.npcs.LogWriter; import noppes.npcs.api.handler.IAnimationHandler; import noppes.npcs.api.handler.data.IAnimation; import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.BuiltInAnimation; import noppes.npcs.util.NBTJsonUtil; import java.io.BufferedInputStream; +import java.io.BufferedReader; import java.io.DataInputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.Map; +import java.util.stream.Stream; import java.util.zip.GZIPInputStream; public class AnimationController implements IAnimationHandler { public HashMap animations; private HashMap bootOrder; + // Built-in animations loaded from assets folder (read-only, keyed by name) + public HashMap builtInAnimations = new HashMap<>(); + private static final String BUILTIN_ANIMATIONS_PATH = "/assets/customnpcs/animations"; + private static final String BUILTIN_ANIMATIONS_RESOURCE = "assets/customnpcs/animations"; + public static AnimationController Instance; private int lastUsedID = 0; @@ -42,12 +62,94 @@ public class AnimationController implements IAnimationHandler { public void load() { bootOrder = new HashMap<>(); animations = new HashMap<>(); + builtInAnimations = new HashMap<>(); LogWriter.info("Loading animations..."); + loadBuiltInAnimations(); readAnimationMap(); loadAnimations(); LogWriter.info("Done loading animations."); } + /** + * Load built-in animations from assets/customnpcs/animations/ folder. + * These animations are read-only and accessed by name only (no IDs). + * Scans the folder directly instead of using a manifest. + */ + private void loadBuiltInAnimations() { + try { + URL resourceUrl = CustomNpcs.class.getResource(BUILTIN_ANIMATIONS_PATH); + if (resourceUrl == null) { + LogWriter.info("Built-in animations folder not found: " + BUILTIN_ANIMATIONS_PATH); + return; + } + + URI uri = resourceUrl.toURI(); + Path animationsPath; + + if (uri.getScheme().equals("jar")) { + // Running from JAR - need to create a FileSystem to access it + FileSystem fileSystem = null; + try { + fileSystem = FileSystems.getFileSystem(uri); + } catch (Exception e) { + fileSystem = FileSystems.newFileSystem(uri, Collections.emptyMap()); + } + animationsPath = fileSystem.getPath(BUILTIN_ANIMATIONS_PATH); + } else { + // Running from file system (dev environment) + animationsPath = Paths.get(uri); + } + + // Scan the directory for .json files + try (Stream paths = Files.walk(animationsPath, 1)) { + paths.filter(path -> path.toString().endsWith(".json")) + .forEach(path -> { + String fileName = path.getFileName().toString(); + String animName = fileName.substring(0, fileName.length() - 5); // Remove .json + try { + loadBuiltInAnimation(animName); + } catch (Exception e) { + LogWriter.error("Error loading built-in animation: " + animName, e); + } + }); + } + + LogWriter.info("Loaded " + builtInAnimations.size() + " built-in animations."); + } catch (Exception e) { + LogWriter.error("Error scanning built-in animations folder", e); + } + } + + /** + * Load a single built-in animation by name from assets folder. + */ + private void loadBuiltInAnimation(String animName) throws Exception { + String resourcePath = BUILTIN_ANIMATIONS_PATH + "/" + animName + ".json"; + try (InputStream stream = CustomNpcs.class.getResourceAsStream(resourcePath)) { + if (stream == null) { + return; + } + + // Read JSON content + StringBuilder content = new StringBuilder(); + BufferedReader reader = new BufferedReader(new InputStreamReader(stream)); + String line; + while ((line = reader.readLine()) != null) { + content.append(line); + } + reader.close(); + + // Parse NBT from JSON + NBTTagCompound nbt = NBTJsonUtil.Convert(content.toString()); + + BuiltInAnimation animation = new BuiltInAnimation(animName); + animation.readFromNBT(nbt); + + // Store with lowercase key for case-insensitive lookup + builtInAnimations.put(animName.toLowerCase(), animation); + } + } + private void loadAnimations() { animations.clear(); @@ -110,6 +212,12 @@ public class AnimationController implements IAnimationHandler { } public IAnimation saveAnimation(IAnimation animation) { + // Reject saving built-in animations + if (animation instanceof BuiltInAnimation) { + LogWriter.info("Cannot save built-in animation: " + animation.getName()); + return animation; + } + if (animation.getID() < 0) { animation.setID(getUnusedId()); while (hasName(animation.getName())) @@ -157,6 +265,12 @@ public class AnimationController implements IAnimationHandler { public void delete(String name) { Animation delete = getAnimationFromName(name); if (delete != null) { + // Reject deleting built-in animations + if (delete instanceof BuiltInAnimation) { + LogWriter.info("Cannot delete built-in animation: " + name); + return; + } + Animation foundAnimation = this.animations.remove(delete.getID()); if (foundAnimation != null && foundAnimation.name != null) { File dir = this.getDir(); @@ -175,7 +289,8 @@ public class AnimationController implements IAnimationHandler { } public void delete(int id) { - if (!this.animations.containsKey(id)) + // Only user animations have valid IDs (>= 0) + if (id < 0 || !this.animations.containsKey(id)) return; Animation foundAnimation = this.animations.remove(id); @@ -199,20 +314,83 @@ public class AnimationController implements IAnimationHandler { } public IAnimation get(String name) { + // Check built-in animations first (by name) + BuiltInAnimation builtIn = builtInAnimations.get(name.toLowerCase()); + if (builtIn != null) { + return builtIn; + } + // Then check user animations return getAnimationFromName(name); } + /** + * Get a user animation by ID. + * Built-in animations have no IDs and cannot be retrieved this way. + */ public IAnimation get(int id) { + // Only user animations have valid IDs + if (id < 0) { + return null; + } return this.animations.get(id); } + /** + * Get a built-in animation by name. + * @param name The animation name (case-insensitive) + * @return The built-in animation, or null if not found + */ + public BuiltInAnimation getBuiltInAnimation(String name) { + return builtInAnimations.get(name.toLowerCase()); + } + public IAnimation[] getAnimations() { ArrayList animations = new ArrayList<>(this.animations.values()); return animations.toArray(new IAnimation[0]); } + /** + * Get all built-in animations. + */ + public IAnimation[] getBuiltInAnimations() { + return builtInAnimations.values().toArray(new IAnimation[0]); + } + + /** + * Get all animations (both built-in and user-created). + */ + public IAnimation[] getAllAnimations() { + ArrayList all = new ArrayList<>(); + all.addAll(builtInAnimations.values()); + all.addAll(animations.values()); + return all.toArray(new IAnimation[0]); + } + + /** + * Check if an animation name is a built-in animation. + * @param name The animation name to check + * @return true if this is a built-in animation + */ + public boolean isBuiltIn(String name) { + return builtInAnimations.containsKey(name.toLowerCase()); + } + + /** + * @deprecated Built-in animations no longer use IDs. Use isBuiltIn(String name) instead. + */ + @Deprecated + public boolean isBuiltIn(int id) { + return false; // Built-in animations have no IDs + } + public Animation getAnimationFromName(String animation) { - for (Map.Entry entryAnimation : AnimationController.getInstance().animations.entrySet()) { + // Check built-in animations first (by name) + BuiltInAnimation builtIn = builtInAnimations.get(animation.toLowerCase()); + if (builtIn != null) { + return builtIn; + } + // Then check user animations + for (Map.Entry entryAnimation : animations.entrySet()) { if (entryAnimation.getValue().name.equalsIgnoreCase(animation)) { return entryAnimation.getValue(); } @@ -220,6 +398,13 @@ public class AnimationController implements IAnimationHandler { return null; } + /** + * Get names of all built-in animations. + */ + public String[] getBuiltInAnimationNames() { + return builtInAnimations.keySet().toArray(new String[0]); + } + public String[] getNames() { String[] names = new String[animations.size()]; int i = 0; diff --git a/src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java b/src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java new file mode 100644 index 000000000..a30923eac --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/BuiltInAnimation.java @@ -0,0 +1,94 @@ +package noppes.npcs.controllers.data; + +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.api.handler.data.IAnimation; + +/** + * A read-only animation loaded from mod assets. + * Built-in animations are identified by name only, never by ID. + * They cannot be saved, deleted, or modified. + */ +public class BuiltInAnimation extends Animation { + + public BuiltInAnimation() { + super(); + this.id = -1; // No valid ID for built-in animations + } + + public BuiltInAnimation(String name) { + super(); + this.name = name; + this.id = -1; + } + + /** + * Built-in animations cannot be saved. + * @return this animation unchanged + */ + @Override + public IAnimation save() { + // Do nothing - built-in animations are read-only + return this; + } + + /** + * Built-in animations have no valid ID. + * @return always -1 + */ + @Override + public int getID() { + return -1; + } + + /** + * Built-in animations cannot have their ID changed. + */ + @Override + public void setID(int newID) { + // Ignore - built-in animations don't use IDs + } + + /** + * Check if this is a built-in animation. + * @return always true for BuiltInAnimation + */ + public boolean isBuiltIn() { + return true; + } + + /** + * Read animation data from NBT. + * Preserves the name set in constructor (from filename) rather than reading from NBT. + */ + @Override + public void readFromNBT(NBTTagCompound compound) { + // Preserve the name set in constructor - don't read from NBT + // (filename is the source of truth for built-in animations) + String preservedName = this.name; + + speed = compound.getFloat("Speed"); + smooth = compound.getByte("Smooth"); + loop = compound.getInteger("Loop"); + + frames.clear(); + net.minecraft.nbt.NBTTagList list = compound.getTagList("Frames", 10); + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound item = list.getCompoundTagAt(i); + Frame frame = new Frame(); + frame.parent = this; + frame.readFromNBT(item); + frames.add(frame); + } + + this.whileStanding = compound.getBoolean("WhileStanding"); + this.whileMoving = compound.getBoolean("WhileWalking"); + this.whileAttacking = compound.getBoolean("WhileAttacking"); + + this.currentFrame = compound.getInteger("CurrentFrame"); + this.currentFrameTime = compound.getInteger("CurrentFrameTime"); + + // Ensure name and ID are preserved + this.name = preservedName; + this.id = -1; + } +} diff --git a/src/main/resources/internal/animations/ABILITYBEAM/Ability_Beam_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json similarity index 100% rename from src/main/resources/internal/animations/ABILITYBEAM/Ability_Beam_Active.json rename to src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json diff --git a/src/main/resources/internal/animations/ABILITYBEAM/Ability_Beam_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Windup.json similarity index 100% rename from src/main/resources/internal/animations/ABILITYBEAM/Ability_Beam_Windup.json rename to src/main/resources/assets/customnpcs/animations/Ability_Beam_Windup.json diff --git a/src/main/resources/internal/animations/ABILITYORB/Ability_Orb_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Orb_Windup.json similarity index 100% rename from src/main/resources/internal/animations/ABILITYORB/Ability_Orb_Windup.json rename to src/main/resources/assets/customnpcs/animations/Ability_Orb_Windup.json diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index fd148ea08..8e3f6a606 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -120,6 +120,8 @@ gui.range=Range gui.partly=Partially gui.copy=Copy gui.clear=Clear +gui.builtinAnimations=Built-in Animations +gui.customAnimations=Custom Animations gui.done=Done gui.paste=Paste gui.test=Test From c339e5edbcfe57c80a85704cc94b2bd62d10b3c8 Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 28 Jan 2026 16:57:49 -0500 Subject: [PATCH 305/358] Fix Built In Animation --- .../data/ability/type/AbilityDisc.java | 12 +- .../data/ability/type/AbilityEnergyBeam.java | 12 +- .../data/ability/type/AbilityLaserShot.java | 12 +- .../data/ability/type/AbilityOrb.java | 12 +- .../data/ability/type/AbilitySweeper.java | 12 +- .../kamkeel/npcs/network/PacketHandler.java | 2 + .../npcs/network/enums/EnumRequestPacket.java | 1 + .../animation/BuiltInAnimationGetPacket.java | 72 +++++++++++ .../gui/global/GuiNPCManageAnimations.java | 118 ++++++++++++++---- .../npcs/controllers/AnimationController.java | 23 ++-- .../assets/customnpcs/lang/en_US.lang | 2 + 11 files changed, 205 insertions(+), 73 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index d85720b6e..72b97839d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -122,12 +122,8 @@ public class AbilityDisc extends Ability implements IAbilityDisc { discEntity.startMoving(target); } - // If movement is locked during active phase, keep ability active until entity dies - // Otherwise signal completion immediately - if (!lockMovement.locksActive()) { - discEntity = null; - signalCompletion(); - } + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class } @Override @@ -157,8 +153,8 @@ public class AbilityDisc extends Ability implements IAbilityDisc { @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // If we're tracking the entity for movement lock, check if it's dead - if (discEntity != null && discEntity.isDead) { + // Signal completion when entity dies + if (discEntity == null || discEntity.isDead) { discEntity = null; signalCompletion(); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 7ebd4bb7b..d4620c2bb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -123,12 +123,8 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { beamEntity.startFiring(target); } - // If movement is locked during active phase, keep ability active until entity dies - // Otherwise signal completion immediately - if (!lockMovement.locksActive()) { - beamEntity = null; - signalCompletion(); - } + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class } @Override @@ -161,8 +157,8 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // If we're tracking the entity for movement lock, check if it's dead - if (beamEntity != null && beamEntity.isDead) { + // Signal completion when entity dies + if (beamEntity == null || beamEntity.isDead) { beamEntity = null; signalCompletion(); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index a9cf74473..eeeebfa74 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -128,18 +128,14 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { world.spawnEntityInWorld(laserEntity); - // If movement is locked during active phase, keep ability active until entity dies - // Otherwise signal completion immediately - if (!lockMovement.locksActive()) { - laserEntity = null; - signalCompletion(); - } + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class } @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // If we're tracking the entity for movement lock, check if it's dead - if (laserEntity != null && laserEntity.isDead) { + // Signal completion when entity dies + if (laserEntity == null || laserEntity.isDead) { laserEntity = null; signalCompletion(); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index d69a5c944..badd4aec8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -121,12 +121,8 @@ public class AbilityOrb extends Ability implements IAbilityOrb { orbEntity.startMoving(target); } - // If movement is locked during active phase, keep ability active until entity dies - // Otherwise signal completion immediately - if (!lockMovement.locksActive()) { - orbEntity = null; - signalCompletion(); - } + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class } @Override @@ -155,8 +151,8 @@ public class AbilityOrb extends Ability implements IAbilityOrb { @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // If we're tracking the entity for movement lock, check if it's dead - if (orbEntity != null && orbEntity.isDead) { + // Signal completion when entity dies + if (orbEntity == null || orbEntity.isDead) { orbEntity = null; signalCompletion(); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index 78dae4fdc..d2dced201 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -100,18 +100,14 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { lockOnTarget); world.spawnEntityInWorld(activeEntity); - // If movement is locked during active phase, keep ability active until entity dies - // Otherwise signal completion immediately - if (!lockMovement.locksActive()) { - activeEntity = null; - signalCompletion(); - } + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class } @Override public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { - // If we're tracking the entity for movement lock, check if it's dead - if (activeEntity != null && activeEntity.isDead) { + // Signal completion when entity dies + if (activeEntity == null || activeEntity.isDead) { activeEntity = null; signalCompletion(); } diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index d09882cfc..e659fef74 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -96,6 +96,7 @@ import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationsGetPacket; +import kamkeel.npcs.network.packets.request.animation.BuiltInAnimationGetPacket; import kamkeel.npcs.network.packets.request.bank.BankGetPacket; import kamkeel.npcs.network.packets.request.bank.BankRemovePacket; import kamkeel.npcs.network.packets.request.bank.BankSavePacket; @@ -469,6 +470,7 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new AnimationGetPacket()); REQUEST_PACKET.registerPacket(new AnimationRemovePacket()); REQUEST_PACKET.registerPacket(new AnimationSavePacket()); + REQUEST_PACKET.registerPacket(new BuiltInAnimationGetPacket()); // Moving Path REQUEST_PACKET.registerPacket(new MovingPathSavePacket()); diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index 005da40a7..bdd566785 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -93,6 +93,7 @@ public enum EnumRequestPacket { AnimationGet, AnimationRemove, AnimationSave, + BuiltInAnimationGet, RemoteNpcsGet, RemoteTpToNpc, diff --git a/src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java new file mode 100644 index 000000000..e214689d8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/animation/BuiltInAnimationGetPacket.java @@ -0,0 +1,72 @@ +package kamkeel.npcs.network.packets.request.animation; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.BuiltInAnimation; + +import java.io.IOException; + +/** + * Packet to request a built-in animation by name. + * Built-in animations don't have IDs, so we fetch by name. + */ +public final class BuiltInAnimationGetPacket extends AbstractPacket { + public static String packetName = "Request|BuiltInAnimationGet"; + + private String animationName; + + public BuiltInAnimationGetPacket() { + } + + public BuiltInAnimationGetPacket(String animationName) { + this.animationName = animationName; + } + + @Override + public Enum getType() { + return EnumRequestPacket.BuiltInAnimationGet; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, animationName); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + String name = ByteBufUtils.readString(in); + BuiltInAnimation animation = AnimationController.getInstance().getBuiltInAnimation(name); + if (animation != null) { + NBTTagCompound compound = animation.writeToNBT(); + // Mark as built-in so client knows + compound.setBoolean("BuiltIn", true); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java index 7d1fa84be..0141e931a 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageAnimations.java @@ -5,6 +5,7 @@ import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationsGetPacket; +import kamkeel.npcs.network.packets.request.animation.BuiltInAnimationGetPacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiYesNo; import net.minecraft.client.gui.GuiYesNoCallback; @@ -37,13 +38,19 @@ import java.util.Vector; public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrollData, ICustomScrollListener, ITextfieldListener, IGuiData, ISubGuiListener, GuiYesNoCallback { private GuiCustomScroll scrollAnimations; - private HashMap data = new HashMap(); + private HashMap customData = new HashMap(); + private HashMap builtInData = new HashMap(); private Animation animation = new Animation(); public boolean playingAnimation = false; private long prevTick; private String selected = null; private String search = ""; + // Toggle between Custom (false) and Built-in (true) animations + private boolean showingBuiltIn = false; + // Track if current animation is built-in + private boolean currentIsBuiltIn = false; + public GuiNPCManageAnimations(EntityNPCInterface npc, boolean save) { super(npc); this.setSave(save); @@ -58,8 +65,15 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol public void initGui() { super.initGui(); - this.addButton(new GuiNpcButton(0, guiLeft + 368, guiTop + 8, 45, 20, "gui.add")); - this.addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 32, 45, 20, "gui.remove")); + // Add/Remove buttons - only show for custom animations + if (!showingBuiltIn) { + this.addButton(new GuiNpcButton(0, guiLeft + 368, guiTop + 8, 45, 20, "gui.add")); + this.addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 32, 45, 20, "gui.remove")); + } + + // Toggle button for Custom/Built-in - under Remove button + String toggleLabel = showingBuiltIn ? "gui.builtin" : "gui.custom"; + this.addButton(new GuiNpcButton(10, guiLeft + 368, guiTop + 56, 45, 20, toggleLabel)); if (scrollAnimations == null) { scrollAnimations = new GuiCustomScroll(this, 0, 0); @@ -68,13 +82,23 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol scrollAnimations.guiLeft = guiLeft + 220; scrollAnimations.guiTop = guiTop + 4; addScroll(scrollAnimations); + + // Search field addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 220, guiTop + 192, 143, 20, search)); - if (animation.id == -1) + // Update scroll list based on current view + scrollAnimations.setList(getSearchList()); + + if (animation.id == -1 && !currentIsBuiltIn) return; - addLabel(new GuiNpcLabel(10, "ID", guiLeft + 368, guiTop + 192)); - addLabel(new GuiNpcLabel(11, animation.id + "", guiLeft + 368, guiTop + 192 + 10)); + // ID label - only for custom animations + if (!currentIsBuiltIn) { + addLabel(new GuiNpcLabel(10, "ID", guiLeft + 368, guiTop + 192)); + addLabel(new GuiNpcLabel(11, animation.id + "", guiLeft + 368, guiTop + 192 + 10)); + } else { + addLabel(new GuiNpcLabel(10, "gui.builtin", guiLeft + 368, guiTop + 192)); + } AnimationData data = npc.display.animationData; if (!playingAnimation) { @@ -90,12 +114,13 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol } } - if (animation != null) { + // Edit button - only for custom animations + if (animation != null && !currentIsBuiltIn) { this.addButton(new GuiNpcButton(100, guiLeft + 10, guiTop + 192, 45, 20, "gui.edit")); } String animTexture = "customnpcs:textures/gui/animation.png"; - int playButtonOffsetX = 140; + int playButtonOffsetX = currentIsBuiltIn ? 80 : 140; if (animation != null && !animation.frames.isEmpty()) { if (!this.playingAnimation || data.animation.paused) {//Play this.addLabel(new GuiNpcLabel(90, data.animation.paused ? "animation.paused" : "animation.stopped", guiLeft + playButtonOffsetX - 15, guiTop + 198)); @@ -151,12 +176,17 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol } } + private HashMap getCurrentData() { + return showingBuiltIn ? builtInData : customData; + } + private List getSearchList() { + HashMap data = getCurrentData(); if (search.isEmpty()) { - return new ArrayList(this.data.keySet()); + return new ArrayList(data.keySet()); } List list = new ArrayList(); - for (String name : this.data.keySet()) { + for (String name : data.keySet()) { if (name.toLowerCase().contains(search)) list.add(name); } @@ -167,16 +197,31 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol protected void actionPerformed(GuiButton guibutton) { GuiNpcButton button = (GuiNpcButton) guibutton; - if (button.id == 0) { + // Toggle button + if (button.id == 10) { + showingBuiltIn = !showingBuiltIn; + // Clear selection when switching views + selected = null; + animation = new Animation(); + currentIsBuiltIn = false; + scrollAnimations.clear(); + initGui(); + return; + } + + // Add button - only for custom + if (button.id == 0 && !showingBuiltIn) { save(); String name = "New"; - while (data.containsKey(name)) + while (customData.containsKey(name)) name += "_"; Animation animation = new Animation(-1, name); PacketClient.sendClient(new AnimationSavePacket(animation.writeToNBT())); } - if (button.id == 1) { - if (data.containsKey(scrollAnimations.getSelected())) { + + // Remove button - only for custom + if (button.id == 1 && !showingBuiltIn) { + if (customData.containsKey(scrollAnimations.getSelected())) { GuiYesNo guiyesno = new GuiYesNo(this, scrollAnimations.getSelected(), StatCollector.translateToLocal("gui.delete"), 1); displayGuiScreen(guiyesno); } @@ -204,7 +249,8 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol data.animation.paused = false; } - if (guibutton.id == 100) { + // Edit button - only for custom + if (guibutton.id == 100 && !currentIsBuiltIn) { NoppesUtil.openGUI(player, new GuiNPCEditAnimation(this, this.animation, npc)); } @@ -215,6 +261,10 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol public void setGuiData(NBTTagCompound compound) { this.animation = new Animation(); animation.readFromNBT(compound); + + // Check if this is a built-in animation + currentIsBuiltIn = compound.hasKey("BuiltIn") && compound.getBoolean("BuiltIn"); + setSelected(animation.name); npc.display.animationData.setAnimation(this.animation); this.playingAnimation = false; @@ -225,7 +275,13 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol @Override public void setData(Vector list, HashMap data, EnumScrollData type) { String name = scrollAnimations.getSelected(); - this.data = data; + + if (type == EnumScrollData.ANIMATIONS) { + this.customData = data; + } else if (type == EnumScrollData.BUILTIN_ANIMATIONS) { + this.builtInData = data; + } + scrollAnimations.setList(getSearchList()); if (name != null) @@ -242,28 +298,40 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol public void customScrollClicked(int i, int j, int k, GuiCustomScroll guiCustomScroll) { if (guiCustomScroll.id == 0) { selected = scrollAnimations.getSelected(); - PacketClient.sendClient(new AnimationGetPacket(data.get(selected))); + HashMap data = getCurrentData(); + + if (showingBuiltIn) { + // Fetch built-in animation by name + PacketClient.sendClient(new BuiltInAnimationGetPacket(selected)); + } else { + // Fetch custom animation by ID + if (data.containsKey(selected)) { + PacketClient.sendClient(new AnimationGetPacket(data.get(selected))); + } + } } } public void save() { - if (selected != null && data.containsKey(selected) && animation != null) { + // Only save custom animations + if (selected != null && customData.containsKey(selected) && animation != null && !currentIsBuiltIn) { PacketClient.sendClient(new AnimationSavePacket(animation.writeToNBT())); } } @Override public void unFocused(GuiNpcTextField guiNpcTextField) { - if (animation.id == -1) + // Don't allow renaming built-in animations + if (animation.id == -1 || currentIsBuiltIn) return; if (guiNpcTextField.id == 0) { String name = guiNpcTextField.getText(); - if (!name.isEmpty() && !data.containsKey(name)) { + if (!name.isEmpty() && !customData.containsKey(name)) { String old = animation.name; - data.remove(animation.name); + customData.remove(animation.name); animation.name = name; - data.put(animation.name, animation.id); + customData.put(animation.name, animation.id); selected = name; scrollAnimations.replace(old, animation.name); } @@ -280,10 +348,12 @@ public class GuiNPCManageAnimations extends GuiModelInterface2 implements IScrol if (!result) return; if (id == 1) { - if (data.containsKey(scrollAnimations.getSelected())) { - PacketClient.sendClient(new AnimationRemovePacket(data.get(selected))); + // Only delete custom animations + if (customData.containsKey(scrollAnimations.getSelected()) && !showingBuiltIn) { + PacketClient.sendClient(new AnimationRemovePacket(customData.get(selected))); scrollAnimations.clear(); animation = new Animation(); + currentIsBuiltIn = false; initGui(); } } diff --git a/src/main/java/noppes/npcs/controllers/AnimationController.java b/src/main/java/noppes/npcs/controllers/AnimationController.java index e5e11d666..c6837b6c7 100644 --- a/src/main/java/noppes/npcs/controllers/AnimationController.java +++ b/src/main/java/noppes/npcs/controllers/AnimationController.java @@ -314,13 +314,18 @@ public class AnimationController implements IAnimationHandler { } public IAnimation get(String name) { - // Check built-in animations first (by name) + // Check user/custom animations first (allows overriding built-in) + for (Map.Entry entry : animations.entrySet()) { + if (entry.getValue().name.equalsIgnoreCase(name)) { + return entry.getValue(); + } + } + // Then check built-in animations BuiltInAnimation builtIn = builtInAnimations.get(name.toLowerCase()); if (builtIn != null) { return builtIn; } - // Then check user animations - return getAnimationFromName(name); + return null; } /** @@ -384,17 +389,17 @@ public class AnimationController implements IAnimationHandler { } public Animation getAnimationFromName(String animation) { - // Check built-in animations first (by name) - BuiltInAnimation builtIn = builtInAnimations.get(animation.toLowerCase()); - if (builtIn != null) { - return builtIn; - } - // Then check user animations + // Check user/custom animations first (allows overriding built-in) for (Map.Entry entryAnimation : animations.entrySet()) { if (entryAnimation.getValue().name.equalsIgnoreCase(animation)) { return entryAnimation.getValue(); } } + // Then check built-in animations + BuiltInAnimation builtIn = builtInAnimations.get(animation.toLowerCase()); + if (builtIn != null) { + return builtIn; + } return null; } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 8e3f6a606..de60eda25 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -120,6 +120,8 @@ gui.range=Range gui.partly=Partially gui.copy=Copy gui.clear=Clear +gui.builtin=Built-in +gui.custom=Custom gui.builtinAnimations=Built-in Animations gui.customAnimations=Custom Animations gui.done=Done From 67237e794fc7b39ce4da4d75554fca7b5e446a7c Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 28 Jan 2026 17:16:59 -0500 Subject: [PATCH 306/358] Fix Defaults / Syncing --- .../npcs/entity/EntityAbilityBeam.java | 8 +- .../npcs/entity/EntityAbilityDisc.java | 6 +- .../npcs/entity/EntityAbilityLaser.java | 3 +- .../kamkeel/npcs/entity/EntityAbilityOrb.java | 6 +- .../npcs/entity/EntityAbilityProjectile.java | 6 - .../gui/advanced/SubGuiAbilityConfig.java | 45 +++- .../animations/Ability_Beam_Active.json | 202 +++-------------- .../animations/Ability_Beam_Windup.json | 206 ++++++++++++------ .../assets/customnpcs/lang/en_US.lang | 2 + 9 files changed, 219 insertions(+), 265 deletions(-) diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 70aaf5254..60f7fd656 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -68,7 +68,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { private static final int DW_CHARGING = 20; // Debug logging - private static final boolean DEBUG_LOGGING = true; + private static final boolean DEBUG_LOGGING = false; public EntityAbilityBeam(World world) { super(world); @@ -560,12 +560,10 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { Vec3 currentPos = Vec3.createVectorHelper(prevHeadWorldX, prevHeadWorldY, prevHeadWorldZ); Vec3 nextPos = Vec3.createVectorHelper(headX, headY, headZ); - MovingObjectPosition blockHit = worldObj.rayTraceBlocks(currentPos, nextPos); + // Use full raytrace that doesn't stop at liquids and checks all blocks + MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false); if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { - if (DEBUG_LOGGING) { - LogWriter.info("[Beam] DEAD: Block collision at tick " + ticksExisted); - } hasHit = true; if (explosive) { posX = blockHit.hitVec.xCoord; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index b1f57f9c9..0c5b10ccb 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -413,11 +413,15 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { private void checkBlockCollision() { Vec3 currentPos = Vec3.createVectorHelper(posX, posY, posZ); Vec3 nextPos = Vec3.createVectorHelper(posX + motionX, posY + motionY, posZ + motionZ); - MovingObjectPosition blockHit = worldObj.rayTraceBlocks(currentPos, nextPos); + // Use full raytrace that doesn't stop at liquids and checks all blocks + MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false); if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { hasHit = true; if (explosive) { + posX = blockHit.hitVec.xCoord; + posY = blockHit.hitVec.yCoord; + posZ = blockHit.hitVec.zCoord; doExplosion(); } this.setDead(); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 7ad1f0d45..256a4faf7 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -163,7 +163,8 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { private void checkBlockCollision() { Vec3 start = Vec3.createVectorHelper(startX, startY, startZ); Vec3 end = Vec3.createVectorHelper(endX, endY, endZ); - MovingObjectPosition blockHit = worldObj.rayTraceBlocks(start, end); + // Use full raytrace that doesn't stop at liquids and checks all blocks + MovingObjectPosition blockHit = worldObj.func_147447_a(start, end, false, true, false); if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { // Laser stops at block diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index 18a0c8503..24ca69ef8 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -298,11 +298,15 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { private void checkBlockCollision() { Vec3 currentPos = Vec3.createVectorHelper(posX, posY, posZ); Vec3 nextPos = Vec3.createVectorHelper(posX + motionX, posY + motionY, posZ + motionZ); - MovingObjectPosition blockHit = worldObj.rayTraceBlocks(currentPos, nextPos); + // Use full raytrace that doesn't stop at liquids and checks all blocks + MovingObjectPosition blockHit = worldObj.func_147447_a(currentPos, nextPos, false, true, false); if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) { hasHit = true; if (explosive) { + posX = blockHit.hitVec.xCoord; + posY = blockHit.hitVec.yCoord; + posZ = blockHit.hitVec.zCoord; doExplosion(); } this.setDead(); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index 32c5796bd..5c418d640 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -215,9 +215,6 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA // Check lifespan using world time (survives chunk unload/reload) if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) { - if (!worldObj.isRemote) { - noppes.npcs.LogWriter.info("[Projectile:" + this.getClass().getSimpleName() + "] DEAD: Lifetime exceeded. worldTime=" + worldObj.getTotalWorldTime() + " deathTime=" + deathWorldTime); - } this.setDead(); return; } @@ -229,9 +226,6 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA } if (hasHit) { - if (!worldObj.isRemote) { - noppes.npcs.LogWriter.info("[Projectile:" + this.getClass().getSimpleName() + "] DEAD: hasHit=true at tick " + ticksExisted); - } this.setDead(); return; } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index a4fef92ef..684138891 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -66,6 +66,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi protected int cooldownTicks; protected int windUpTicks; protected int dazedTicks; + protected boolean syncWindupWithAnimation = true; // Sync windup ticks with animation duration // ═══════════════════════════════════════════════════════════════════════════ // CACHED VALUES - Target Tab @@ -282,16 +283,27 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi addLabel(new GuiNpcLabel(7, "ability.timing", col1LabelX, y)); y += 14; + // Check if windup animation is selected (either by name or ID) + boolean hasWindupAnimation = windUpAnimationId >= 0 || (windUpAnimationName != null && !windUpAnimationName.isEmpty()); + // Row 4: Windup Ticks + Cooldown Ticks addLabel(new GuiNpcLabel(10, "ability.windup", col1LabelX, y + 5)); - GuiNpcTextField windupField = new GuiNpcTextField(11, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(windUpTicks)); - windupField.setIntegersOnly(); - windupField.setMinMaxDefault(0, 1000, 20); - addTextField(windupField); - // Sync button - only show if windup animation is selected (either by name or ID) - if (windUpAnimationId >= 0 || (windUpAnimationName != null && !windUpAnimationName.isEmpty())) { - GuiNpcButton syncBtn = new GuiNpcButton(17, guiLeft + 115, y, 40, 20, "gui.sync"); + // Only show windup textbox if sync is off or no animation selected + if (!syncWindupWithAnimation || !hasWindupAnimation) { + GuiNpcTextField windupField = new GuiNpcTextField(11, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(windUpTicks)); + windupField.setIntegersOnly(); + windupField.setMinMaxDefault(0, 1000, 20); + addTextField(windupField); + } else { + // Show calculated value as label when synced + addLabel(new GuiNpcLabel(11, String.valueOf(windUpTicks), col1FieldX + 5, y + 5)); + } + + // Sync toggle button - only show if windup animation is selected + if (hasWindupAnimation) { + String syncLabel = syncWindupWithAnimation ? "ability.syncOn" : "ability.syncOff"; + GuiNpcButton syncBtn = new GuiNpcButton(17, guiLeft + 115, y, 50, 20, syncLabel); syncBtn.setHoverText("ability.hover.sync"); addButton(syncBtn); } @@ -654,8 +666,13 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi } else if (id == 16) { lockMovement = LockMovementType.fromOrdinal(((GuiNpcButton) guibutton).getValue()); } else if (id == 17) { - // Sync windup ticks with animation duration - syncWindupWithAnimation(); + // Toggle sync on/off + syncWindupWithAnimation = !syncWindupWithAnimation; + if (syncWindupWithAnimation) { + // Calculate windup ticks from animation when turning sync on + calculateWindupFromAnimation(); + } + initGui(); } // Target tab - Targeting mode @@ -896,6 +913,12 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi ability.setLockMovement(lockMovement); ability.setInterruptible(interruptible); + // If sync is enabled and we have a windup animation, recalculate before saving + boolean hasWindupAnimation = windUpAnimationId >= 0 || (windUpAnimationName != null && !windUpAnimationName.isEmpty()); + if (syncWindupWithAnimation && hasWindupAnimation) { + calculateWindupFromAnimation(); + } + // Timing ability.setCooldownTicks(cooldownTicks); ability.setWindUpTicks(windUpTicks); @@ -968,9 +991,9 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // ═══════════════════════════════════════════════════════════════════════════ /** - * Syncs windup ticks with the selected windup animation's total duration. + * Calculates windup ticks from the selected windup animation's total duration. */ - private void syncWindupWithAnimation() { + private void calculateWindupFromAnimation() { if (AnimationController.Instance == null) { return; } diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json index eb9516f38..466b9163c 100644 --- a/src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json +++ b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json @@ -1,5 +1,5 @@ { - "Loop": -1, + "Loop": 0, "Speed": 1.0f, "Frames": [ { @@ -7,87 +7,6 @@ "Comment": "", "Speed": 1.0f, "FrameParts": [ - { - "Pivot2": -5.0f, - "Part": "LEFT_ARM", - "Rotation1": 107.0f, - "Rotation0": -94.0f, - "Pivot0": -5.0f, - "Pivot1": 3.0f, - "Rotation2": -31.0f - }, - { - "Pivot2": 0.0f, - "Part": "RIGHT_ARM", - "Rotation1": 88.0f, - "Rotation0": -56.0f, - "Pivot0": 1.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -3.0f, - "Part": "HEAD", - "Rotation1": 12.0f, - "Rotation0": 18.0f, - "Pivot0": -1.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "RIGHT_LEG", - "Rotation1": 56.0f, - "Rotation0": 6.0f, - "Pivot0": -2.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -3.0f, - "Part": "BODY", - "Rotation1": 25.0f, - "Rotation0": 18.0f, - "Pivot0": -1.0f, - "Pivot1": 1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -5.0f, - "Part": "LEFT_LEG", - "Rotation1": -25.0f, - "Rotation0": 12.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "FULL_MODEL", - "Rotation1": 25.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - } - ], - "Smooth": 0b, - "Duration": 5 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 2.0f, - "FrameParts": [ - { - "Pivot2": -5.0f, - "Part": "LEFT_ARM", - "Rotation1": 12.0f, - "Rotation0": -75.0f, - "Pivot0": -2.0f, - "Pivot1": 3.0f, - "Rotation2": 50.0f - }, { "Pivot2": -5.0f, "Part": "RIGHT_ARM", @@ -97,6 +16,33 @@ "Pivot1": 2.0f, "Rotation2": 50.0f }, + { + "Pivot2": -6.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, { "Pivot2": -3.0f, "Part": "HEAD", @@ -115,42 +61,6 @@ "Pivot1": 0.0f, "Rotation2": 0.0f }, - { - "Pivot2": -3.0f, - "Part": "BODY", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": -1.0f, - "Pivot1": 1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -6.0f, - "Part": "LEFT_LEG", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "FULL_MODEL", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - } - ], - "Smooth": 0b, - "Duration": 5 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.75f, - "FrameParts": [ { "Pivot2": -5.0f, "Part": "LEFT_ARM", @@ -159,64 +69,10 @@ "Pivot0": -2.0f, "Pivot1": 3.0f, "Rotation2": 50.0f - }, - { - "Pivot2": -5.0f, - "Part": "RIGHT_ARM", - "Rotation1": -18.0f, - "Rotation0": -107.0f, - "Pivot0": -1.0f, - "Pivot1": 2.0f, - "Rotation2": 50.0f - }, - { - "Pivot2": -3.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": -1.0f, - "Pivot1": 1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -4.0f, - "Part": "RIGHT_LEG", - "Rotation1": 56.0f, - "Rotation0": 18.0f, - "Pivot0": -4.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -3.0f, - "Part": "BODY", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": -1.0f, - "Pivot1": 1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -6.0f, - "Part": "LEFT_LEG", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": -6.0f - }, - { - "Pivot2": 0.0f, - "Part": "FULL_MODEL", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f } ], "Smooth": 0b, - "Duration": 20 + "Duration": 30 } ], "CurrentFrameTime": 0, diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Beam_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Windup.json index 6c53e1eab..62e54167a 100644 --- a/src/main/resources/assets/customnpcs/animations/Ability_Beam_Windup.json +++ b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Windup.json @@ -7,33 +7,6 @@ "Comment": "", "Speed": 0.5f, "FrameParts": [ - { - "Pivot2": -5.0f, - "Part": "LEFT_ARM", - "Rotation1": 94.0f, - "Rotation0": -94.0f, - "Pivot0": -4.0f, - "Pivot1": 3.0f, - "Rotation2": -31.0f - }, - { - "Pivot2": 0.0f, - "Part": "RIGHT_ARM", - "Rotation1": 75.0f, - "Rotation0": -56.0f, - "Pivot0": 1.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -3.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 18.0f, - "Pivot0": -1.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -54,11 +27,20 @@ }, { "Pivot2": -5.0f, - "Part": "LEFT_LEG", - "Rotation1": -25.0f, - "Rotation0": 12.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 94.0f, + "Rotation0": -94.0f, + "Pivot0": -4.0f, + "Pivot1": 3.0f, + "Rotation2": -31.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, "Rotation2": -6.0f }, { @@ -69,6 +51,24 @@ "Pivot0": 0.0f, "Pivot1": 0.0f, "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 75.0f, + "Rotation0": -56.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f } ], "Smooth": 0b, @@ -77,35 +77,8 @@ { "ColorMarker": 16777215, "Comment": "", - "Speed": 0.5f, + "Speed": 0.25f, "FrameParts": [ - { - "Pivot2": -5.0f, - "Part": "LEFT_ARM", - "Rotation1": 107.0f, - "Rotation0": -94.0f, - "Pivot0": -5.0f, - "Pivot1": 3.0f, - "Rotation2": -31.0f - }, - { - "Pivot2": 0.0f, - "Part": "RIGHT_ARM", - "Rotation1": 88.0f, - "Rotation0": -56.0f, - "Pivot0": 1.0f, - "Pivot1": 0.0f, - "Rotation2": -18.0f - }, - { - "Pivot2": -3.0f, - "Part": "HEAD", - "Rotation1": 12.0f, - "Rotation0": 18.0f, - "Pivot0": -1.0f, - "Pivot1": 1.0f, - "Rotation2": -6.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -126,11 +99,20 @@ }, { "Pivot2": -5.0f, - "Part": "LEFT_LEG", - "Rotation1": -25.0f, - "Rotation0": 12.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 107.0f, + "Rotation0": -94.0f, + "Pivot0": -5.0f, + "Pivot1": 3.0f, + "Rotation2": -31.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 12.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, "Rotation2": -6.0f }, { @@ -141,14 +123,104 @@ "Pivot0": 0.0f, "Pivot1": 0.0f, "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 88.0f, + "Rotation0": -56.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f } ], "Smooth": 0b, - "Duration": 35 + "Duration": 30 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -4.0f, + "Part": "RIGHT_LEG", + "Rotation1": 56.0f, + "Rotation0": 18.0f, + "Pivot0": -4.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": 12.0f, + "Rotation0": -75.0f, + "Pivot0": -2.0f, + "Pivot1": 3.0f, + "Rotation2": 50.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": -18.0f, + "Rotation0": -107.0f, + "Pivot0": -1.0f, + "Pivot1": 2.0f, + "Rotation2": 50.0f + } + ], + "Smooth": 0b, + "Duration": 5 } ], "CurrentFrameTime": 0, - "CurrentFrame": 0, + "CurrentFrame": 1, "Smooth": 0b, "ID": 1, "WhileAttacking": 1b, diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index de60eda25..50c150624 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1519,6 +1519,8 @@ ability.maxRange=Max Range ability.timing=Timing (ticks) ability.cooldown=Cooldown ability.windup=Wind Up +ability.syncOn=Sync: On +ability.syncOff=Sync: Off ability.dazed=Dazed Ticks ability.interruptible=Interruptible ability.threshold=Threshold From 216a3dbbf8f088f1075c33f42c2764b322c6b431 Mon Sep 17 00:00:00 2001 From: Vidal Date: Wed, 28 Jan 2026 20:49:16 -0300 Subject: [PATCH 307/358] Added alpha to all abilities except beam --- .../client/renderer/RenderAbilityDisc.java | 2 +- .../data/ability/type/AbilityDisc.java | 7 +++- .../data/ability/type/AbilityEnergyBeam.java | 7 +++- .../data/ability/type/AbilityLaserShot.java | 7 +++- .../data/ability/type/AbilityOrb.java | 13 +++++- .../npcs/entity/EntityAbilityBeam.java | 16 ++++---- .../npcs/entity/EntityAbilityDisc.java | 12 +++--- .../npcs/entity/EntityAbilityLaser.java | 8 ++-- .../kamkeel/npcs/entity/EntityAbilityOrb.java | 12 +++--- .../npcs/entity/EntityAbilityProjectile.java | 14 +++++-- .../advanced/ability/SubGuiAbilityDisc.java | 40 ++++++++++++------- .../ability/SubGuiAbilityEnergyBeam.java | 31 +++++++++----- .../ability/SubGuiAbilityLaserShot.java | 27 ++++++++----- .../advanced/ability/SubGuiAbilityOrb.java | 39 +++++++++++------- 14 files changed, 154 insertions(+), 81 deletions(-) diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java index fdd80689a..20486d70e 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityDisc.java @@ -54,7 +54,7 @@ public class RenderAbilityDisc extends RenderAbilityProjectile { if (disc.isOuterColorEnabled()) { float outerScale = innerScale + disc.getOuterColorWidth(); GL11.glDepthMask(false); - renderFlatSquare(disc.getOuterColor(), 0.5f, radius * outerScale, thickness); + renderFlatSquare(disc.getOuterColor(), disc.getOuterColorAlpha(), radius * outerScale, thickness); GL11.glDepthMask(true); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 72b97839d..e783b3a42 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -62,6 +62,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { private int innerColor = 0xFFFFFF; private int outerColor = 0xFF8800; private float outerColorWidth = 0.4f; // Additive offset from inner size + private float outerColorAlpha = 0.5f; // Additive offset from inner size private boolean outerColorEnabled = true; private float rotationSpeed = 5.0f; @@ -135,7 +136,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { // Create disc in charging mode - follows NPC based on anchor point during windup discEntity = EntityAbilityDisc.createCharging( world, npc, target, - discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, boomerang, boomerangDelay, @@ -224,6 +225,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { nbt.setInteger("innerColor", innerColor); nbt.setInteger("outerColor", outerColor); nbt.setFloat("outerColorWidth", outerColorWidth); + nbt.setFloat("outerColorAlpha", outerColorAlpha); nbt.setBoolean("outerColorEnabled", outerColorEnabled); nbt.setFloat("rotationSpeed", rotationSpeed); nbt.setBoolean("lightningEffect", lightningEffect); @@ -256,6 +258,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF8800; this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; + this.outerColorAlpha = nbt.hasKey("outerColorAlpha") ? nbt.getFloat("outerColorAlpha") : 0.5f; this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 5.0f; this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); @@ -309,6 +312,8 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public void setOuterColor(int outerColor) { this.outerColor = outerColor; } public float getOuterColorWidth() { return outerColorWidth; } public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; } + public float getOuterColorAlpha() { return outerColorAlpha; } + public void setOuterColorAlpha(float outerColorAlpha) { this.outerColorAlpha = outerColorAlpha; } public boolean isOuterColorEnabled() { return outerColorEnabled; } public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } public float getRotationSpeed() { return rotationSpeed; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index d4620c2bb..7a81da548 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -60,6 +60,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { private int innerColor = 0xFFFFFF; private int outerColor = 0x00AAFF; private float outerColorWidth = 0.4f; // Additive offset from inner size + private float outerColorAlpha = 0.5f; private boolean outerColorEnabled = true; private float rotationSpeed = 6.0f; @@ -138,7 +139,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { // Create beam in charging mode - follows NPC based on anchor point during windup beamEntity = EntityAbilityBeam.createCharging( world, npc, target, - beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, explosive, explosionRadius, explosionDamageFalloff, @@ -226,6 +227,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { nbt.setInteger("innerColor", innerColor); nbt.setInteger("outerColor", outerColor); nbt.setFloat("outerColorWidth", outerColorWidth); + nbt.setFloat("outerColorAlpha", outerColorAlpha); nbt.setBoolean("outerColorEnabled", outerColorEnabled); nbt.setFloat("rotationSpeed", rotationSpeed); nbt.setBoolean("lightningEffect", lightningEffect); @@ -256,6 +258,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0x00AAFF; this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; + this.outerColorAlpha = nbt.hasKey("outerColorAlpha") ? nbt.getFloat("outerColorAlpha") : 0.5f; this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 6.0f; this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); @@ -305,6 +308,8 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { public void setOuterColor(int outerColor) { this.outerColor = outerColor; } public float getOuterColorWidth() { return outerColorWidth; } public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; } + public float getOuterColorAlpha() { return outerColorAlpha; } + public void setOuterColorAlpha(float outerColorAlpha) { this.outerColorAlpha = outerColorAlpha; } public boolean isOuterColorEnabled() { return outerColorEnabled; } public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } public float getRotationSpeed() { return rotationSpeed; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index eeeebfa74..7ead64c8e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -49,6 +49,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { private int innerColor = 0xFFFFFF; private int outerColor = 0xFF0000; private float outerColorWidth = 0.4f; // Additive offset from inner width + private float outerColorAlpha = 0.5f; private boolean outerColorEnabled = true; // Lightning effect properties @@ -117,7 +118,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { laserEntity = new EntityAbilityLaser( world, npc, target, spawnX, spawnY, spawnZ, - laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, + laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, damage, knockback, knockbackUp, expansionSpeed, lingerTicks, explosive, explosionRadius, explosionDamageFalloff, @@ -177,6 +178,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { nbt.setInteger("innerColor", innerColor); nbt.setInteger("outerColor", outerColor); nbt.setFloat("outerColorWidth", outerColorWidth); + nbt.setFloat("outerColorAlpha", outerColorAlpha); nbt.setBoolean("outerColorEnabled", outerColorEnabled); nbt.setBoolean("lightningEffect", lightningEffect); nbt.setFloat("lightningDensity", lightningDensity); @@ -202,6 +204,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF0000; this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; + this.outerColorAlpha = nbt.hasKey("outerColorAlpha") ? nbt.getFloat("outerColorAlpha") : 0.5f; this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; @@ -243,6 +246,8 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { public void setOuterColor(int outerColor) { this.outerColor = outerColor; } public float getOuterColorWidth() { return outerColorWidth; } public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; } + public float getOuterColorAlpha() { return outerColorAlpha; } + public void setOuterColorAlpha(float outerColorAlpha) { this.outerColorAlpha = outerColorAlpha; } public boolean isOuterColorEnabled() { return outerColorEnabled; } public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } public boolean hasLightningEffect() { return lightningEffect; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index badd4aec8..4ccbdcf12 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -58,6 +58,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { private int innerColor = 0xFFFFFF; // White core private int outerColor = 0x8888FF; // Light blue glow private float outerColorWidth = 0.4f; // Additive offset from inner size + private float outerColorAlpha = 0.5f; private boolean outerColorEnabled = true; // Whether to render outer glow private float rotationSpeed = 4.0f; // Degrees per tick @@ -134,7 +135,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { // Create orb in charging mode - follows NPC based on anchor point during windup orbEntity = EntityAbilityOrb.createCharging( world, npc, target, - orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, damage, knockback, knockbackUp, orbSpeed, homing, homingStrength, homingRange, explosive, explosionRadius, explosionDamageFalloff, @@ -220,6 +221,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { nbt.setInteger("innerColor", innerColor); nbt.setInteger("outerColor", outerColor); nbt.setFloat("outerColorWidth", outerColorWidth); + nbt.setFloat("outerColorAlpha", outerColorAlpha); nbt.setBoolean("outerColorEnabled", outerColorEnabled); nbt.setFloat("rotationSpeed", rotationSpeed); nbt.setBoolean("lightningEffect", lightningEffect); @@ -250,6 +252,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0x8888FF; this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; + this.outerColorAlpha = nbt.hasKey("outerColorAlpha") ? nbt.getFloat("outerColorAlpha") : 0.5f; this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 4.0f; this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); @@ -412,6 +415,14 @@ public class AbilityOrb extends Ability implements IAbilityOrb { this.outerColorWidth = outerColorWidth; } + public float getOuterColorAlpha() { + return outerColorAlpha; + } + + public void setOuterColorAlpha(float outerColorAlpha) { + this.outerColorAlpha = outerColorAlpha; + } + public boolean isOuterColorEnabled() { return outerColorEnabled; } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 60f7fd656..2e5926e67 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -104,14 +104,14 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, float beamWidth, float headSize, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime) { this(world, owner, target, x, y, z, - beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, @@ -124,7 +124,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, float beamWidth, float headSize, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean explosive, float explosionRadius, float explosionDamageFalloff, @@ -132,7 +132,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { float maxDistance, int maxLifetime, boolean lightningEffect, float lightningDensity, float lightningRadius) { this(world, owner, target, x, y, z, - beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, @@ -148,7 +148,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, float beamWidth, float headSize, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean explosive, float explosionRadius, float explosionDamageFalloff, @@ -160,7 +160,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { // Initialize base properties with lightning initProjectile(owner, target, x, y, z, - headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, @@ -221,7 +221,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { */ public static EntityAbilityBeam createCharging(World world, EntityNPCInterface owner, EntityLivingBase target, float beamWidth, float headSize, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean explosive, float explosionRadius, float explosionDamageFalloff, @@ -239,7 +239,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { EntityAbilityBeam beam = new EntityAbilityBeam( world, owner, target, spawnX, spawnY, spawnZ, - beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index 0c5b10ccb..f63014158 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -82,7 +82,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { public EntityAbilityDisc(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, float discRadius, float discThickness, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean boomerang, int boomerangDelay, @@ -90,7 +90,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime) { this(world, owner, target, x, y, z, - discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, boomerang, boomerangDelay, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, @@ -103,7 +103,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { public EntityAbilityDisc(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, float discRadius, float discThickness, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean boomerang, int boomerangDelay, @@ -115,7 +115,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { // Initialize base properties with lightning (use discRadius as size for base) initProjectile(owner, target, x, y, z, - discRadius, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + discRadius, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, @@ -161,7 +161,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { */ public static EntityAbilityDisc createCharging(World world, EntityNPCInterface owner, EntityLivingBase target, float discRadius, float discThickness, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean boomerang, int boomerangDelay, @@ -179,7 +179,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { EntityAbilityDisc disc = new EntityAbilityDisc( world, owner, target, spawnX, spawnY, spawnZ, - discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, boomerang, boomerangDelay, explosive, explosionRadius, explosionDamageFalloff, diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 256a4faf7..7a1f97d0f 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -50,14 +50,14 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { public EntityAbilityLaser(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, float laserWidth, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, + boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float damage, float knockback, float knockbackUp, float expansionSpeed, int lingerTicks, boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime) { this(world, owner, target, x, y, z, - laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, + laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, damage, knockback, knockbackUp, expansionSpeed, lingerTicks, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, @@ -70,7 +70,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { public EntityAbilityLaser(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, float laserWidth, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, + boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float damage, float knockback, float knockbackUp, float expansionSpeed, int lingerTicks, boolean explosive, float explosionRadius, float explosionDamageFalloff, @@ -81,7 +81,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { // Initialize base properties with lightning initProjectile(owner, target, x, y, z, - laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, 0.0f, // No rotation for laser + laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, 0.0f, // No rotation for laser damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index 24ca69ef8..04e59a240 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -71,14 +71,14 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { public EntityAbilityOrb(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, float orbSize, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime) { this(world, owner, target, x, y, z, - orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, explosive, explosionRadius, explosionDamageFalloff, @@ -93,7 +93,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { public EntityAbilityOrb(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, float orbSize, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean explosive, float explosionRadius, float explosionDamageFalloff, @@ -104,7 +104,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { // Initialize base properties with lightning initProjectile(owner, target, x, y, z, - orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + orbSize, innerColor, outerColor, outerColorEnabled, outerColorAlpha, outerColorWidth, rotationSpeed, damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, @@ -145,7 +145,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { */ public static EntityAbilityOrb createCharging(World world, EntityNPCInterface owner, EntityLivingBase target, float orbSize, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, float damage, float knockback, float knockbackUp, float speed, boolean homing, float homingStrength, float homingRange, boolean explosive, float explosionRadius, float explosionDamageFalloff, @@ -162,7 +162,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { EntityAbilityOrb orb = new EntityAbilityOrb( world, owner, target, spawnX, spawnY, spawnZ, - orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index 5c418d640..4b0f1e6a5 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -31,6 +31,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA protected int outerColor = 0x8888FF; protected boolean outerColorEnabled = true; protected float outerColorWidth = 0.4f; // Additive offset from inner size + protected float outerColorAlpha = 0.5f; protected float rotationSpeed = 4.0f; // ==================== COMBAT PROPERTIES ==================== @@ -98,13 +99,13 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA protected void initProjectile(EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, float size, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, float damage, float knockback, float knockbackUp, boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, float maxDistance, int maxLifetime) { initProjectile(owner, target, x, y, z, - size, innerColor, outerColor, outerColorEnabled, outerColorWidth, rotationSpeed, + size, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, stunDuration, slowDuration, slowLevel, @@ -118,7 +119,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA protected void initProjectile(EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, float size, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, float damage, float knockback, float knockbackUp, boolean explosive, float explosionRadius, float explosionDamageFalloff, int stunDuration, int slowDuration, int slowLevel, @@ -138,6 +139,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA this.outerColor = outerColor; this.outerColorEnabled = outerColorEnabled; this.outerColorWidth = outerColorWidth; + this.outerColorAlpha = outerColorAlpha; this.rotationSpeed = rotationSpeed; // Combat @@ -428,6 +430,10 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA return outerColorWidth; } + public float getOuterColorAlpha() { + return outerColorAlpha; + } + public float getRotationSpeed() { return rotationSpeed; } @@ -501,6 +507,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA this.innerColor = nbt.getInteger("InnerColor"); this.outerColor = nbt.getInteger("OuterColor"); this.outerColorEnabled = !nbt.hasKey("OuterColorEnabled") || nbt.getBoolean("OuterColorEnabled"); + this.outerColorAlpha = nbt.hasKey("OuterColorAlpha") ? nbt.getFloat("OuterColorAlpha") : 0.5f; this.outerColorWidth = nbt.hasKey("OuterColorWidth") ? nbt.getFloat("OuterColorWidth") : 0.4f; this.rotationSpeed = nbt.hasKey("RotationSpeed") ? nbt.getFloat("RotationSpeed") : 4.0f; @@ -552,6 +559,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA nbt.setInteger("OuterColor", outerColor); nbt.setBoolean("OuterColorEnabled", outerColorEnabled); nbt.setFloat("OuterColorWidth", outerColorWidth); + nbt.setFloat("OuterColorAlpha", outerColorAlpha); nbt.setFloat("RotationSpeed", rotationSpeed); nbt.setFloat("Damage", damage); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java index 00383853f..5faf662b0 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java @@ -155,15 +155,24 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { y += 24; - // Row 3: Rotation Speed - addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", labelX, y + 5)); - addTextField(createFloatField(204, fieldX, y, 50, disc.getRotationSpeed())); + // Row 3: Outer Color Alpha + addLabel(new GuiNpcLabel(204, "ability.outerAlpha", labelX, y + 5)); + GuiNpcTextField alphaField = createFloatField(204, fieldX, y, 55, disc.getOuterColorAlpha()); + alphaField.setEnabled(disc.isOuterColorEnabled()); + alphaField.setMinMaxDefaultFloat(0, 1, 0.5f); + addTextField(alphaField); y += 24; - // Row 4: Lightning Effect Enabled - addLabel(new GuiNpcLabel(205, "ability.lightning", labelX, y + 5)); - GuiNpcButton lightningBtn = new GuiNpcButton(205, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.hasLightningEffect() ? 1 : 0); + // Row 4: Rotation Speed + addLabel(new GuiNpcLabel(205, "ability.rotationSpeed", labelX, y + 5)); + addTextField(createFloatField(205, fieldX, y, 50, disc.getRotationSpeed())); + + y += 24; + + // Row 5: Lightning Effect Enabled + addLabel(new GuiNpcLabel(206, "ability.lightning", labelX, y + 5)); + GuiNpcButton lightningBtn = new GuiNpcButton(206, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.hasLightningEffect() ? 1 : 0); lightningBtn.setHoverText("ability.hover.lightning"); addButton(lightningBtn); @@ -171,14 +180,14 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { if (disc.hasLightningEffect()) { y += 24; - // Row 5: Density + Radius - addLabel(new GuiNpcLabel(206, "ability.lightningDensity", labelX, y + 5)); - GuiNpcTextField densityField = new GuiNpcTextField(206, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(disc.getLightningDensity())); + // Row 6: Density + Radius + addLabel(new GuiNpcLabel(207, "ability.lightningDensity", labelX, y + 5)); + GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(disc.getLightningDensity())); densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); addTextField(densityField); - addLabel(new GuiNpcLabel(207, "ability.lightningRadius", col2LabelX, y + 5)); - GuiNpcTextField radiusField = new GuiNpcTextField(207, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(disc.getLightningRadius())); + addLabel(new GuiNpcLabel(208, "ability.lightningRadius", col2LabelX, y + 5)); + GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(disc.getLightningRadius())); radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); addTextField(radiusField); } @@ -222,7 +231,7 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { disc.setOuterColorEnabled(value == 1); initGui(); break; - case 205: + case 206: disc.setLightningEffect(value == 1); initGui(); break; @@ -298,12 +307,15 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { disc.setOuterColorWidth(parseFloat(field, disc.getOuterColorWidth())); break; case 204: + disc.setOuterColorAlpha(parseFloat(field, disc.getOuterColorAlpha())); + break; + case 205: disc.setRotationSpeed(parseFloat(field, disc.getRotationSpeed())); break; - case 206: + case 207: disc.setLightningDensity(parseFloat(field, disc.getLightningDensity())); break; - case 207: + case 208: disc.setLightningRadius(parseFloat(field, disc.getLightningRadius())); break; } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java index d878c9381..49ab42e5d 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java @@ -152,14 +152,20 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { y += 24; // Row 3: Rotation Speed - addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", labelX, y + 5)); - addTextField(createFloatField(204, fieldX, y, 50, beam.getRotationSpeed())); + addLabel(new GuiNpcLabel(204, "ability.outerAlpha", labelX, y + 5)); + GuiNpcTextField alphaField = createFloatField(204, fieldX, y, 55, beam.getOuterColorAlpha()); + alphaField.setEnabled(beam.isOuterColorEnabled()); + alphaField.setMinMaxDefaultFloat(0, 1, 0.5f); + addTextField(alphaField); + + addLabel(new GuiNpcLabel(205, "ability.rotationSpeed", col2LabelX, y + 5)); + addTextField(createFloatField(205, col2FieldX, y, 50, beam.getRotationSpeed())); y += 24; // Row 4: Lightning Effect Enabled (only affects head) - addLabel(new GuiNpcLabel(205, "ability.lightning", labelX, y + 5)); - GuiNpcButton lightningBtn = new GuiNpcButton(205, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.hasLightningEffect() ? 1 : 0); + addLabel(new GuiNpcLabel(206, "ability.lightning", labelX, y + 5)); + GuiNpcButton lightningBtn = new GuiNpcButton(206, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.hasLightningEffect() ? 1 : 0); lightningBtn.setHoverText("ability.hover.lightning"); addButton(lightningBtn); @@ -168,13 +174,13 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { y += 24; // Row 5: Density + Radius - addLabel(new GuiNpcLabel(206, "ability.lightningDensity", labelX, y + 5)); - GuiNpcTextField densityField = new GuiNpcTextField(206, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(beam.getLightningDensity())); + addLabel(new GuiNpcLabel(207, "ability.lightningDensity", labelX, y + 5)); + GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(beam.getLightningDensity())); densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); addTextField(densityField); - addLabel(new GuiNpcLabel(207, "ability.lightningRadius", col2LabelX, y + 5)); - GuiNpcTextField radiusField = new GuiNpcTextField(207, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(beam.getLightningRadius())); + addLabel(new GuiNpcLabel(208, "ability.lightningRadius", col2LabelX, y + 5)); + GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(beam.getLightningRadius())); radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); addTextField(radiusField); } @@ -214,7 +220,7 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { beam.setOuterColorEnabled(value == 1); initGui(); break; - case 205: + case 206: beam.setLightningEffect(value == 1); initGui(); break; @@ -290,12 +296,15 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { beam.setOuterColorWidth(parseFloat(field, beam.getOuterColorWidth())); break; case 204: + beam.setOuterColorAlpha(parseFloat(field, beam.getOuterColorAlpha())); + break; + case 205: beam.setRotationSpeed(parseFloat(field, beam.getRotationSpeed())); break; - case 206: + case 207: beam.setLightningDensity(parseFloat(field, beam.getLightningDensity())); break; - case 207: + case 208: beam.setLightningRadius(parseFloat(field, beam.getLightningRadius())); break; } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java index 50e8fa92c..e5184a3bb 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java @@ -127,8 +127,14 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { y += 24; // Row 3: Lightning Effect Enabled - addLabel(new GuiNpcLabel(204, "ability.lightning", labelX, y + 5)); - GuiNpcButton lightningBtn = new GuiNpcButton(204, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, laser.hasLightningEffect() ? 1 : 0); + addLabel(new GuiNpcLabel(204, "ability.outerAlpha", labelX, y + 5)); + GuiNpcTextField alphaField = createFloatField(204, fieldX, y, 55, laser.getOuterColorAlpha()); + alphaField.setEnabled(laser.isOuterColorEnabled()); + alphaField.setMinMaxDefaultFloat(0, 1, 0.5f); + addTextField(alphaField); + + addLabel(new GuiNpcLabel(205, "ability.lightning", col2LabelX, y + 5)); + GuiNpcButton lightningBtn = new GuiNpcButton(205, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, laser.hasLightningEffect() ? 1 : 0); lightningBtn.setHoverText("ability.hover.lightning"); addButton(lightningBtn); @@ -137,13 +143,13 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { y += 24; // Row 4: Density + Radius - addLabel(new GuiNpcLabel(205, "ability.lightningDensity", labelX, y + 5)); - GuiNpcTextField densityField = new GuiNpcTextField(205, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(laser.getLightningDensity())); + addLabel(new GuiNpcLabel(206, "ability.lightningDensity", labelX, y + 5)); + GuiNpcTextField densityField = new GuiNpcTextField(206, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(laser.getLightningDensity())); densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); addTextField(densityField); - addLabel(new GuiNpcLabel(206, "ability.lightningRadius", col2LabelX, y + 5)); - GuiNpcTextField radiusField = new GuiNpcTextField(206, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(laser.getLightningRadius())); + addLabel(new GuiNpcLabel(207, "ability.lightningRadius", col2LabelX, y + 5)); + GuiNpcTextField radiusField = new GuiNpcTextField(207, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(laser.getLightningRadius())); radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); addTextField(radiusField); } @@ -179,7 +185,7 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { laser.setOuterColorEnabled(value == 1); initGui(); break; - case 204: + case 205: laser.setLightningEffect(value == 1); initGui(); break; @@ -215,10 +221,13 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { case 203: laser.setOuterColorWidth(parseFloat(field, laser.getOuterColorWidth())); break; - case 205: - laser.setLightningDensity(parseFloat(field, laser.getLightningDensity())); + case 204: + laser.setOuterColorAlpha(parseFloat(field, laser.getOuterColorAlpha())); break; case 206: + laser.setLightningDensity(parseFloat(field, laser.getLightningDensity())); + break; + case 207: laser.setLightningRadius(parseFloat(field, laser.getLightningRadius())); break; } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java index 40cff112f..7cc0f9f2f 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -119,20 +119,26 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { innerColorBtn.setTextColor(orb.getInnerColor() & 0xFFFFFF); addButton(innerColorBtn); - addLabel(new GuiNpcLabel(204, "ability.rotationSpeed", col2LabelX, y + 5)); - addTextField(createFloatField(204, col2FieldX, y, 55, orb.getRotationSpeed())); + addLabel(new GuiNpcLabel(205, "ability.rotationSpeed", col2LabelX, y + 5)); + addTextField(createFloatField(205, col2FieldX, y, 55, orb.getRotationSpeed())); y += 24; - // Row 2: Outer Glow Enabled + // Row 3: Outer Glow Enabled addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isOuterColorEnabled() ? 1 : 0)); // Only show Outer Color and Width if Outer Glow is enabled if (orb.isOuterColorEnabled()) { + addLabel(new GuiNpcLabel(204, "ability.outerAlpha", col2LabelX, y + 5)); + GuiNpcTextField alphaField = createFloatField(204, col2FieldX, y, 55, orb.getOuterColorAlpha()); + alphaField.setEnabled(orb.isOuterColorEnabled()); + alphaField.setMinMaxDefaultFloat(0, 1, 0.5f); + addTextField(alphaField); + y += 24; - // Row 3: Outer Color + Outer Width + // Row 4: Outer Color + Outer Width addLabel(new GuiNpcLabel(201, "ability.outerColor", labelX, y + 5)); String outerHex = String.format("%06X", orb.getOuterColor() & 0xFFFFFF); GuiNpcButton outerColorBtn = new GuiNpcButton(201, fieldX, y, 55, 20, outerHex); @@ -145,9 +151,9 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { y += 24; - // Row 4: Lightning Effect Enabled - addLabel(new GuiNpcLabel(205, "ability.lightning", labelX, y + 5)); - GuiNpcButton lightningBtn = new GuiNpcButton(205, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.hasLightningEffect() ? 1 : 0); + // Row 5: Lightning Effect Enabled + addLabel(new GuiNpcLabel(206, "ability.lightning", labelX, y + 5)); + GuiNpcButton lightningBtn = new GuiNpcButton(206, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.hasLightningEffect() ? 1 : 0); lightningBtn.setHoverText("ability.hover.lightning"); addButton(lightningBtn); @@ -155,14 +161,14 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { if (orb.hasLightningEffect()) { y += 24; - // Row 5: Density + Radius - addLabel(new GuiNpcLabel(206, "ability.lightningDensity", labelX, y + 5)); - GuiNpcTextField densityField = new GuiNpcTextField(206, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(orb.getLightningDensity())); + // Row 61: Density + Radius + addLabel(new GuiNpcLabel(207, "ability.lightningDensity", labelX, y + 5)); + GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(orb.getLightningDensity())); densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); addTextField(densityField); - addLabel(new GuiNpcLabel(207, "ability.lightningRadius", col2LabelX, y + 5)); - GuiNpcTextField radiusField = new GuiNpcTextField(207, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(orb.getLightningRadius())); + addLabel(new GuiNpcLabel(208, "ability.lightningRadius", col2LabelX, y + 5)); + GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(orb.getLightningRadius())); radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); addTextField(radiusField); } @@ -202,7 +208,7 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { orb.setOuterColorEnabled(value == 1); initGui(); break; - case 205: + case 206: orb.setLightningEffect(value == 1); initGui(); break; @@ -272,12 +278,15 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { orb.setOuterColorWidth(parseFloat(field, orb.getOuterColorWidth())); break; case 204: + orb.setOuterColorAlpha(parseFloat(field, orb.getOuterColorAlpha())); + break; + case 205: orb.setRotationSpeed(parseFloat(field, orb.getRotationSpeed())); break; - case 206: + case 207: orb.setLightningDensity(parseFloat(field, orb.getLightningDensity())); break; - case 207: + case 208: orb.setLightningRadius(parseFloat(field, orb.getLightningRadius())); break; } From 55658a795651cc5e767c346e58f918e7b19ffa7a Mon Sep 17 00:00:00 2001 From: Kam Date: Wed, 28 Jan 2026 20:43:32 -0500 Subject: [PATCH 308/358] Ability Preview GUI --- .../renderer/RenderAbilityProjectile.java | 5 + .../client/renderer/RenderAbilitySweeper.java | 4 + .../client/renderer/TelegraphRenderer.java | 79 ++++ .../controllers/data/ability/Ability.java | 89 ++++ .../data/ability/type/AbilityCharge.java | 63 +++ .../data/ability/type/AbilityDash.java | 63 +++ .../data/ability/type/AbilityDisc.java | 19 + .../data/ability/type/AbilityEnergyBeam.java | 19 + .../data/ability/type/AbilityLaserShot.java | 25 ++ .../data/ability/type/AbilityOrb.java | 19 + .../data/ability/type/AbilitySlam.java | 94 +++++ .../npcs/entity/EntityAbilityBeam.java | 138 +++++++ .../npcs/entity/EntityAbilityDisc.java | 108 +++++ .../npcs/entity/EntityAbilityLaser.java | 52 +++ .../kamkeel/npcs/entity/EntityAbilityOrb.java | 109 +++++ .../npcs/entity/EntityAbilityProjectile.java | 76 +++- .../kamkeel/npcs/network/PacketHandler.java | 8 + .../npcs/network/enums/EnumRequestPacket.java | 4 + .../ability/SavedAbilitiesGetPacket.java | 51 +++ .../ability/SavedAbilityGetPacket.java | 68 +++ .../ability/SavedAbilityRemovePacket.java | 69 ++++ .../ability/SavedAbilitySavePacket.java | 76 ++++ .../noppes/npcs/CustomNpcsPermissions.java | 1 + .../java/noppes/npcs/NoppesUtilServer.java | 12 + .../java/noppes/npcs/client/ClientProxy.java | 15 + .../client/gui/advanced/GuiNPCAbilities.java | 7 + .../advanced/SubGuiAbilityDeleteConfirm.java | 22 +- .../gui/advanced/SubGuiAbilityLoad.java | 111 +++-- .../advanced/SubGuiAbilitySaveConfirm.java | 32 +- .../gui/global/GuiNpcManageAbilities.java | 342 +++++++++++---- .../gui/mainmenu/GuiNPCGlobalMainMenu.java | 11 +- .../gui/util/AbilityPreviewExecutor.java | 368 +++++++++++++++++ .../client/gui/util/GuiAbilityInterface.java | 391 ++++++++++++++++++ .../client/gui/util/GuiModelInterface2.java | 4 +- .../noppes/npcs/constants/EnumScrollData.java | 1 + .../assets/customnpcs/lang/en_US.lang | 3 +- .../assets/customnpcs/textures/gui/icons.png | Bin 5249 -> 11934 bytes 37 files changed, 2396 insertions(+), 162 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitiesGetPacket.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityGetPacket.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityRemovePacket.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitySavePacket.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java create mode 100644 src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java index 4180783bf..cce9bbe58 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java @@ -2,6 +2,7 @@ package kamkeel.npcs.client.renderer; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; +import net.minecraft.client.renderer.OpenGlHelper; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.entity.Render; import net.minecraft.entity.Entity; @@ -26,6 +27,7 @@ public abstract class RenderAbilityProjectile extends Render { /** * Setup GL state for translucent, self-illuminated rendering. + * Forces full brightness so world lighting does not affect projectiles. */ protected void setupRenderState() { GL11.glPushMatrix(); @@ -37,6 +39,9 @@ public abstract class RenderAbilityProjectile extends Render { GL11.glAlphaFunc(GL11.GL_GREATER, 0.003921569F); GL11.glDisable(GL11.GL_LIGHTING); GL11.glDisable(GL11.GL_TEXTURE_2D); + + // Force full brightness - prevents world lighting from darkening projectiles + OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240.0f, 240.0f); } /** diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java index 0a24f1773..10705d762 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilitySweeper.java @@ -3,6 +3,7 @@ package kamkeel.npcs.client.renderer; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.entity.EntityAbilitySweeper; +import net.minecraft.client.renderer.OpenGlHelper; import net.minecraft.client.renderer.Tessellator; import net.minecraft.client.renderer.entity.Render; import net.minecraft.entity.Entity; @@ -52,6 +53,9 @@ public class RenderAbilitySweeper extends Render { GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); GL11.glDisable(GL11.GL_LIGHTING); GL11.glShadeModel(GL11.GL_SMOOTH); + + // Force full brightness - prevents world lighting from darkening sweeper + OpenGlHelper.setLightmapTextureCoords(OpenGlHelper.lightmapTexUnit, 240.0f, 240.0f); } private void restoreRenderState() { diff --git a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java index 5f4e815b7..3b5e49e2d 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java +++ b/src/main/java/kamkeel/npcs/client/renderer/TelegraphRenderer.java @@ -257,4 +257,83 @@ public class TelegraphRenderer { tessellator.draw(); } + + // ==================== GUI PREVIEW RENDERING ==================== + + /** + * Render a telegraph in GUI space for ability preview. + * This is used by the Manage Abilities GUI to show telegraph shapes. + * + * @param instance The telegraph instance to render + * @param centerX X center position in GUI space + * @param centerY Y center position in GUI space + * @param centerZ Z center position in GUI space + * @param scale Scale factor for the telegraph + * @param partialTicks Partial ticks for interpolation + */ + public void renderTelegraphInGUI(TelegraphInstance instance, + double centerX, double centerY, double centerZ, + float scale, float partialTicks) { + if (instance == null) return; + + Telegraph telegraph = instance.getTelegraph(); + if (telegraph == null || telegraph.getType() == TelegraphType.NONE) { + return; + } + + // Get animated color + int color = instance.getAnimatedColor(partialTicks); + float alpha = ((color >> 24) & 0xFF) / 255.0f; + float red = ((color >> 16) & 0xFF) / 255.0f; + float green = ((color >> 8) & 0xFF) / 255.0f; + float blue = (color & 0xFF) / 255.0f; + + // Setup GL state + GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); + + GL11.glDisable(GL11.GL_TEXTURE_2D); + GL11.glDisable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_BLEND); + GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA); + GL11.glDisable(GL11.GL_LIGHTING); + GL11.glDepthMask(false); + + GL11.glTranslated(centerX, centerY, centerZ); + GL11.glScalef(scale, scale, scale); + + // Rotate for directional shapes + GL11.glRotatef(-instance.getInterpolatedYaw(partialTicks), 0, 1, 0); + + // Render based on type + switch (telegraph.getType()) { + case CIRCLE: + renderCircle(telegraph.getRadius(), red, green, blue, alpha); + break; + case RING: + renderRing(telegraph.getRadius(), telegraph.getInnerRadius(), red, green, blue, alpha); + break; + case LINE: + renderLine(telegraph.getLength(), telegraph.getWidth(), red, green, blue, alpha); + break; + case CONE: + renderCone(telegraph.getLength(), telegraph.getAngle(), red, green, blue, alpha); + break; + case POINT: + renderPoint(red, green, blue, alpha); + break; + default: + break; + } + + // Restore GL state + GL11.glDepthMask(true); + GL11.glEnable(GL11.GL_LIGHTING); + GL11.glDisable(GL11.GL_BLEND); + GL11.glEnable(GL11.GL_CULL_FACE); + GL11.glEnable(GL11.GL_TEXTURE_2D); + + GL11.glPopAttrib(); + GL11.glPopMatrix(); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 08eed148b..eb59edf7c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -26,6 +26,8 @@ import noppes.npcs.scripted.event.AbilityEvent; import java.util.ArrayList; import java.util.List; +import net.minecraft.entity.Entity; + public abstract class Ability implements IAbility { // ═══════════════════════════════════════════════════════════════════ @@ -636,6 +638,93 @@ public abstract class Ability implements IAbility { // Override in subclasses to clean up spawned entities, etc. } + // ═══════════════════════════════════════════════════════════════════ + // CLIENT-SIDE PREVIEW METHODS + // ═══════════════════════════════════════════════════════════════════ + + /** + * Get total active phase duration for preview (in ticks). + * Override in subclasses to provide accurate preview duration. + * + * @return Duration of active phase in ticks (default: 40 = 2 seconds) + */ + public int getPreviewActiveDuration() { + return 40; + } + + /** + * Called during preview windup tick (client-side only). + * Override for visual updates during windup preview. + * Do NOT apply damage or world effects here. + * + * @param npc The preview NPC + * @param tick Current tick within windup phase + */ + @SideOnly(Side.CLIENT) + public void onPreviewWindUpTick(EntityNPCInterface npc, int tick) { + // Default: no-op. Override in subclasses for visual effects. + } + + /** + * Called during preview active tick (client-side only). + * Override for visual updates during active preview. + * Do NOT apply damage or world effects here. + * + * @param npc The preview NPC + * @param tick Current tick within active phase + */ + @SideOnly(Side.CLIENT) + public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { + // Default: no-op. Override in subclasses for visual effects. + } + + /** + * Create a preview entity for GUI display (client-side only). + * The entity should be in preview mode and not apply damage. + * Override in abilities that spawn entities (Beam, Orb, Disc, etc.) + * + * @param npc The preview NPC + * @return Preview entity, or null if ability doesn't spawn entities + */ + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + return null; // Override in entity-spawning abilities + } + + /** + * Whether to spawn preview entity during WINDUP phase (true) or ACTIVE phase (false). + * Most abilities (Orb, Beam, Disc) spawn during windup for charging effect. + * Laser spawns at active phase since it has no charging state. + */ + public boolean spawnPreviewDuringWindup() { + return true; // Default: spawn during windup for charging + } + + // ── Preview target (set by executor for abilities that need targeting) ── + + protected transient EntityLivingBase previewTarget; + + /** + * Set the fake target entity for preview mode. + * Called by AbilityPreviewExecutor before starting preview. + */ + public void setPreviewTarget(EntityLivingBase target) { + this.previewTarget = target; + } + + public EntityLivingBase getPreviewTarget() { + return previewTarget; + } + + /** + * Called once when transitioning from WINDUP to ACTIVE in preview mode. + * Override in movement-based abilities to initiate movement. + */ + @SideOnly(Side.CLIENT) + public void onPreviewExecute(EntityNPCInterface npc) { + // Default: no-op. Override in movement abilities. + } + // ═══════════════════════════════════════════════════════════════════ // CONDITION CHECKING // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 1f6f31250..25941e0a5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -288,6 +288,69 @@ public class AbilityCharge extends Ability implements IAbilityCharge { return instance; } + // ==================== PREVIEW MODE ==================== + + private transient double previewDirX, previewDirZ; + private transient double previewStartX, previewStartZ; + private transient boolean previewCharging = false; + + @Override + @SideOnly(Side.CLIENT) + public void onPreviewExecute(EntityNPCInterface npc) { + previewStartX = npc.posX; + previewStartZ = npc.posZ; + previewCharging = false; + + // Calculate direction toward fake target + if (previewTarget != null) { + double dx = previewTarget.posX - npc.posX; + double dz = previewTarget.posZ - npc.posZ; + double len = Math.sqrt(dx * dx + dz * dz); + if (len > 0) { + previewDirX = dx / len; + previewDirZ = dz / len; + } else { + float yaw = (float) Math.toRadians(npc.rotationYaw); + previewDirX = -Math.sin(yaw); + previewDirZ = Math.cos(yaw); + } + } else { + float yaw = (float) Math.toRadians(npc.rotationYaw); + previewDirX = -Math.sin(yaw); + previewDirZ = Math.cos(yaw); + } + previewCharging = true; + } + + @Override + @SideOnly(Side.CLIENT) + public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { + if (!previewCharging) return; + + // Check distance traveled + double distTraveled = Math.sqrt( + Math.pow(npc.posX - previewStartX, 2) + + Math.pow(npc.posZ - previewStartZ, 2) + ); + + if (distTraveled >= maxRange) { + previewCharging = false; + return; + } + + npc.prevPosX = npc.posX; + npc.prevPosY = npc.posY; + npc.prevPosZ = npc.posZ; + + npc.posX += previewDirX * chargeSpeed; + npc.posZ += previewDirZ * chargeSpeed; + } + + @Override + public int getPreviewActiveDuration() { + return (int) Math.ceil(maxRange / chargeSpeed) + 5; + } + @Override public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("chargeSpeed", chargeSpeed); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 2c03de4c2..054176a7b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -224,6 +224,69 @@ public class AbilityDash extends Ability implements IAbilityDash { return !npc.worldObj.getCollidingBoundingBoxes(npc, npc.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty(); } + // ==================== PREVIEW MODE ==================== + + private transient double previewDirX, previewDirZ; + private transient double previewStartX, previewStartZ; + private transient boolean previewDashing = false; + + @Override + @SideOnly(Side.CLIENT) + public void onPreviewExecute(EntityNPCInterface npc) { + previewStartX = npc.posX; + previewStartZ = npc.posZ; + previewDashing = false; + + // Pick a random direction based on mode + DashDirection[] directions = dashMode == DashMode.AGGRESSIVE + ? AGGRESSIVE_DIRECTIONS + : DEFENSIVE_DIRECTIONS; + DashDirection dir = directions[RANDOM.nextInt(directions.length)]; + + float baseYaw; + if (previewTarget != null) { + double dx = previewTarget.posX - npc.posX; + double dz = previewTarget.posZ - npc.posZ; + baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); + } else { + baseYaw = npc.rotationYaw; + } + + float dashYaw = baseYaw + dir.getAngleOffset(); + float yawRad = (float) Math.toRadians(dashYaw); + previewDirX = -Math.sin(yawRad); + previewDirZ = Math.cos(yawRad); + previewDashing = true; + } + + @Override + @SideOnly(Side.CLIENT) + public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { + if (!previewDashing) return; + + double distTraveled = Math.sqrt( + Math.pow(npc.posX - previewStartX, 2) + + Math.pow(npc.posZ - previewStartZ, 2) + ); + + if (distTraveled >= dashDistance) { + previewDashing = false; + return; + } + + npc.prevPosX = npc.posX; + npc.prevPosY = npc.posY; + npc.prevPosZ = npc.posZ; + + npc.posX += previewDirX * dashSpeed; + npc.posZ += previewDirZ * dashSpeed; + } + + @Override + public int getPreviewActiveDuration() { + return (int) Math.ceil(dashDistance / dashSpeed) + 5; + } + @Override public float getTelegraphRadius() { return 0; // No telegraph for dash diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index e783b3a42..e5c15e010 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -332,4 +332,23 @@ public class AbilityDisc extends Ability implements IAbilityDisc { @Override public void setAnchorPoint(int point) { this.anchorPoint = AnchorPoint.fromId(point); } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + return EntityAbilityDisc.createPreview( + npc.worldObj, npc, + discRadius, discThickness, innerColor, outerColor, + outerColorEnabled, outerColorWidth, rotationSpeed, + lightningEffect, lightningDensity, lightningRadius, + anchorPoint, windUpTicks + ); + } + + @Override + public int getPreviewActiveDuration() { + return maxLifetime > 0 ? Math.min(maxLifetime, 100) : 100; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 7a81da548..c2b452339 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -329,4 +329,23 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { @Override public void setAnchorPoint(int point) { this.anchorPoint = AnchorPoint.fromId(point); } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + return EntityAbilityBeam.createPreview( + npc.worldObj, npc, + beamWidth, headSize, innerColor, outerColor, + outerColorEnabled, outerColorWidth, rotationSpeed, + lightningEffect, lightningDensity, lightningRadius, + anchorPoint, windUpTicks, 1.0f + ); + } + + @Override + public int getPreviewActiveDuration() { + return maxLifetime > 0 ? Math.min(maxLifetime, 100) : 100; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index 7ead64c8e..c1a94e657 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -7,6 +7,7 @@ import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityLaser; +import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; @@ -256,4 +257,28 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } public float getLightningRadius() { return lightningRadius; } public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + return EntityAbilityLaser.createPreview( + npc.worldObj, npc, + laserWidth, innerColor, outerColor, + outerColorEnabled, outerColorWidth, + expansionSpeed, maxDistance, + lightningEffect, lightningDensity, lightningRadius + ); + } + + @Override + public int getPreviewActiveDuration() { + return maxLifetime > 0 ? Math.min(maxLifetime, 60) : 60; + } + + @Override + public boolean spawnPreviewDuringWindup() { + return false; // Laser has no charging, spawns at active phase + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 4ccbdcf12..13c57478a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -487,4 +487,23 @@ public class AbilityOrb extends Ability implements IAbilityOrb { public void setAnchorPoint(int point) { this.anchorPoint = AnchorPoint.fromId(point); } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + return EntityAbilityOrb.createPreview( + npc.worldObj, npc, + orbSize, innerColor, outerColor, + outerColorEnabled, outerColorWidth, rotationSpeed, + lightningEffect, lightningDensity, lightningRadius, lightningFadeTime, + anchorPoint, windUpTicks + ); + } + + @Override + public int getPreviewActiveDuration() { + return maxLifetime > 0 ? Math.min(maxLifetime, 100) : 100; + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index b9da81dc5..a46100c96 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -361,6 +361,100 @@ public class AbilitySlam extends Ability implements IAbilitySlam { airTicks = 0; } + // ==================== PREVIEW MODE ==================== + + private transient double previewVelX, previewVelY, previewVelZ; + private transient boolean previewLaunched = false; + private transient double previewGroundY; + + @Override + @SideOnly(Side.CLIENT) + public void onPreviewExecute(EntityNPCInterface npc) { + previewLaunched = false; + + double tx, ty, tz; + if (previewTarget != null && targetingMode == TargetingMode.AOE_TARGET) { + tx = previewTarget.posX; + ty = previewTarget.posY; + tz = previewTarget.posZ; + } else { + tx = npc.posX; + ty = npc.posY; + tz = npc.posZ; + } + + previewGroundY = npc.posY; + + double dx = tx - npc.posX; + double dz = tz - npc.posZ; + double horizontalDist = Math.sqrt(dx * dx + dz * dz); + + if (horizontalDist < 0.5) { + previewVelX = 0; + previewVelZ = 0; + previewVelY = 0.8 * leapSpeed; + } else { + if (horizontalDist > maxRange) { + double scale = maxRange / horizontalDist; + dx *= scale; + dz *= scale; + horizontalDist = maxRange; + } + + int flightTicks = (int) Math.max(15, Math.min(horizontalDist * 1.5, 30)); + double drag = 0.91; + double dragPowN = Math.pow(drag, flightTicks); + double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN) * leapSpeed; + + double arcHeight = Math.max(1.0, leapHeight) * leapSpeed; + double peakTicks = flightTicks * 0.4; + double vy = (arcHeight * 2.0 / peakTicks) + (0.08 * peakTicks * 0.5); + vy = Math.max(vy, 0.6 * leapSpeed); + + double dirX = dx / horizontalDist; + double dirZ = dz / horizontalDist; + previewVelX = dirX * vHorizontal; + previewVelZ = dirZ * vHorizontal; + previewVelY = vy; + } + previewLaunched = true; + } + + @Override + @SideOnly(Side.CLIENT) + public void onPreviewActiveTick(EntityNPCInterface npc, int tick) { + if (!previewLaunched) return; + + // Apply velocity + npc.prevPosX = npc.posX; + npc.prevPosY = npc.posY; + npc.prevPosZ = npc.posZ; + + npc.posX += previewVelX; + npc.posY += previewVelY; + npc.posZ += previewVelZ; + + // Gravity and drag + previewVelY -= 0.08; + previewVelY *= 0.98; + previewVelX *= 0.91; + previewVelZ *= 0.91; + + // Ground clamp - stop falling below starting Y + if (npc.posY < previewGroundY && previewVelY < 0) { + npc.posY = previewGroundY; + previewVelY = 0; + previewVelX = 0; + previewVelZ = 0; + previewLaunched = false; + } + } + + @Override + public int getPreviewActiveDuration() { + return 60; + } + @Override public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { // Check if telegraph should be shown diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 2e5926e67..c841ee0f9 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -83,8 +83,12 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { /** * Check if beam is in charging state (synced via data watcher). + * In preview mode, uses local field since data watcher isn't synced. */ public boolean isCharging() { + if (previewMode) { + return this.charging; + } return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; } @@ -264,6 +268,103 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { return beam; } + /** + * Create a beam in preview mode for GUI display. + * Follows anchor point and animations like in the real game. + * Can be fired when transitioning to active phase. + */ + public static EntityAbilityBeam createPreview(World world, EntityNPCInterface owner, + float beamWidth, float headSize, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean lightningEffect, float lightningDensity, float lightningRadius, + AnchorPoint anchorPoint, int chargeDuration, float chargeOffsetDistance) { + EntityAbilityBeam beam = new EntityAbilityBeam(world); + beam.setPreviewMode(true); + beam.setPreviewOwner(owner); + + // Set visual properties + beam.beamWidth = beamWidth; + beam.headSize = headSize; + beam.size = headSize; + beam.innerColor = innerColor; + beam.outerColor = outerColor; + beam.outerColorEnabled = outerColorEnabled; + beam.outerColorWidth = outerColorWidth; + beam.rotationSpeed = rotationSpeed; + beam.lightningEffect = lightningEffect; + beam.lightningDensity = lightningDensity; + beam.lightningRadius = lightningRadius; + + // Set charging state + beam.setCharging(true); + beam.chargeDuration = chargeDuration; + beam.chargeTick = 0; + beam.chargeOffsetDistance = chargeOffsetDistance; + beam.anchorPoint = anchorPoint; + + // Initial position at anchor point + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint, chargeOffsetDistance); + beam.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + beam.prevPosX = pos.xCoord; + beam.prevPosY = pos.yCoord; + beam.prevPosZ = pos.zCoord; + beam.startX = pos.xCoord; + beam.startY = pos.yCoord; + beam.startZ = pos.zCoord; + + // Initialize head offsets + beam.headOffsetX = 0; + beam.headOffsetY = 0; + beam.headOffsetZ = 0; + beam.prevHeadOffsetX = 0; + beam.prevHeadOffsetY = 0; + beam.prevHeadOffsetZ = 0; + + // Attach to owner for anchor following + beam.attachedToOwner = true; + beam.renderTailOrb = true; + + // Clear motion while charging + beam.motionX = 0; + beam.motionY = 0; + beam.motionZ = 0; + + return beam; + } + + /** + * Start preview firing (simulates firing toward a point in front of NPC). + */ + public void startPreviewFiring() { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Sync prev position to prevent visual jump on first frame + prevPosX = posX; + prevPosY = posY; + prevPosZ = posZ; + + // Fire forward based on owner facing direction + Entity owner = getOwner(); + if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = 0; // Fire horizontally + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } else { + motionX = speed; + motionY = 0; + motionZ = 0; + } + } + /** * Start the beam firing (exit charging mode). * Called by ability when windup ends. @@ -361,6 +462,12 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { ageTrailPoints(); } + // In preview mode, run movement on client (no server) + if (previewMode) { + updatePreviewMovement(); + return; + } + if (worldObj.isRemote) { // Client: interpolate entity position handleClientInterpolation(); @@ -510,6 +617,37 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { } } + /** + * Update movement in preview mode (client-side only, no damage). + */ + private void updatePreviewMovement() { + // Move head offset + headOffsetX += motionX; + headOffsetY += motionY; + headOffsetZ += motionZ; + + // Update origin if attached to owner + if (attachedToOwner) { + Entity owner = getOwner(); + if (owner != null) { + startX = owner.posX; + startY = owner.posY + owner.getEyeHeight() * 0.7; + startZ = owner.posZ; + } + } + + // Calculate world position of head + double headWorldX = startX + headOffsetX; + double headWorldY = startY + headOffsetY; + double headWorldZ = startZ + headOffsetZ; + + // Update entity position to head + this.setPosition(headWorldX, headWorldY, headWorldZ); + + // Add trail point + addTrailPoint(); + } + private void updateHoming() { if (!homing) return; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index f63014158..e3896a839 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -61,8 +61,12 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { /** * Check if disc is in charging state (synced via data watcher). + * In preview mode, uses local field since data watcher isn't synced. */ public boolean isCharging() { + if (previewMode) { + return this.charging; + } return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; } @@ -209,6 +213,94 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { return disc; } + /** + * Create a disc in preview mode for GUI display. + * Follows anchor point and animations like in the real game. + * Can be fired when transitioning to active phase. + */ + public static EntityAbilityDisc createPreview(World world, EntityNPCInterface owner, + float discRadius, float discThickness, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean lightningEffect, float lightningDensity, float lightningRadius, + AnchorPoint anchorPoint, int chargeDuration) { + EntityAbilityDisc disc = new EntityAbilityDisc(world); + disc.setPreviewMode(true); + disc.setPreviewOwner(owner); + + // Set visual properties + disc.innerColor = innerColor; + disc.outerColor = outerColor; + disc.outerColorEnabled = outerColorEnabled; + disc.outerColorWidth = outerColorWidth; + disc.rotationSpeed = rotationSpeed; + disc.lightningEffect = lightningEffect; + disc.lightningDensity = lightningDensity; + disc.lightningRadius = lightningRadius; + + // Set charging state + disc.setCharging(true); + disc.chargeDuration = chargeDuration; + disc.chargeTick = 0; + disc.anchorPoint = anchorPoint; + + // Store target size and start at 0 for grow effect + disc.targetDiscRadius = discRadius; + disc.targetDiscThickness = discThickness; + disc.discRadius = 0.01f; + disc.discThickness = 0.01f; + disc.size = 0.01f; + + // Initial position at anchor point + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint); + disc.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + disc.prevPosX = pos.xCoord; + disc.prevPosY = pos.yCoord; + disc.prevPosZ = pos.zCoord; + disc.startX = pos.xCoord; + disc.startY = pos.yCoord; + disc.startZ = pos.zCoord; + + // Clear motion while charging + disc.motionX = 0; + disc.motionY = 0; + disc.motionZ = 0; + + return disc; + } + + /** + * Start preview firing (simulates firing toward a point in front of NPC). + */ + public void startPreviewFiring() { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Sync prev position to prevent visual jump on first frame + prevPosX = posX; + prevPosY = posY; + prevPosZ = posZ; + + // Fire forward based on owner facing direction + Entity owner = getOwner(); + if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = 0; // Fire horizontally + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } else { + motionX = speed; + motionY = 0; + motionZ = 0; + } + } + /** * Start the disc moving (exit charging mode). * Called by ability when windup ends. @@ -261,6 +353,12 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { return; } + // In preview mode, run movement on client (no server) + if (previewMode) { + updatePreviewMovement(); + return; + } + if (worldObj.isRemote) { handleClientInterpolation(); } else { @@ -332,6 +430,16 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { return distTraveled >= maxDistance; } + /** + * Update movement in preview mode (client-side only, no damage). + */ + private void updatePreviewMovement() { + // Simple movement - just apply motion + this.posX += motionX; + this.posY += motionY; + this.posZ += motionZ; + } + private void updateHoming() { if (!homing) return; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 7a1f97d0f..ec208e2cf 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -347,4 +347,56 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { nbt.setDouble("EndY", endY); nbt.setDouble("EndZ", endZ); } + + /** + * Create a laser in preview mode for GUI display. + * Laser doesn't have charging state - spawns at active phase and fires immediately. + */ + public static EntityAbilityLaser createPreview(World world, EntityNPCInterface owner, + float laserWidth, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, + float expansionSpeed, float maxDistance, + boolean lightningEffect, float lightningDensity, float lightningRadius) { + EntityAbilityLaser laser = new EntityAbilityLaser(world); + laser.setPreviewMode(true); + laser.setPreviewOwner(owner); + + // Set visual properties + laser.laserWidth = laserWidth; + laser.size = laserWidth; + laser.innerColor = innerColor; + laser.outerColor = outerColor; + laser.outerColorEnabled = outerColorEnabled; + laser.outerColorWidth = outerColorWidth; + laser.expansionSpeed = expansionSpeed; + laser.maxDistance = Math.min(maxDistance, 5.0f); // Limit for GUI preview + laser.lightningEffect = lightningEffect; + laser.lightningDensity = lightningDensity; + laser.lightningRadius = lightningRadius; + + // Position at chest height + double x = owner.posX; + double y = owner.posY + owner.height * 0.7; + double z = owner.posZ; + laser.setPosition(x, y, z); + laser.prevPosX = x; + laser.prevPosY = y; + laser.prevPosZ = z; + laser.startX = x; + laser.startY = y; + laser.startZ = z; + + // Fire in owner's facing direction + float yaw = (float) Math.toRadians(owner.rotationYaw); + laser.dirX = -Math.sin(yaw); + laser.dirY = 0; + laser.dirZ = Math.cos(yaw); + + // Initialize end point (same as start, will expand) + laser.endX = x; + laser.endY = y; + laser.endZ = z; + + return laser; + } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index 04e59a240..bad2bc955 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -50,8 +50,12 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { /** * Check if orb is in charging state (synced via data watcher). + * In preview mode, uses local field since data watcher isn't synced. */ public boolean isCharging() { + if (previewMode) { + return this.charging; + } return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; } @@ -188,6 +192,95 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { return orb; } + /** + * Create an orb in preview mode for GUI display. + * Follows anchor point and animations like in the real game. + * Can be fired when transitioning to active phase. + */ + public static EntityAbilityOrb createPreview(World world, EntityNPCInterface owner, + float orbSize, int innerColor, int outerColor, + boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, + boolean lightningEffect, float lightningDensity, float lightningRadius, int lightningFadeTime, + AnchorPoint anchorPoint, int chargeDuration) { + EntityAbilityOrb orb = new EntityAbilityOrb(world); + orb.setPreviewMode(true); + orb.setPreviewOwner(owner); + + // Set visual properties + orb.innerColor = innerColor; + orb.outerColor = outerColor; + orb.outerColorEnabled = outerColorEnabled; + orb.outerColorWidth = outerColorWidth; + orb.rotationSpeed = rotationSpeed; + orb.lightningEffect = lightningEffect; + orb.lightningDensity = lightningDensity; + orb.lightningRadius = lightningRadius; + orb.lightningFadeTime = lightningFadeTime; + + // Set charging state (like createCharging does) + orb.setCharging(true); + orb.chargeDuration = chargeDuration; + orb.chargeTick = 0; + orb.anchorPoint = anchorPoint; + + // Store target size and start at 0 for grow effect + orb.targetSize = orbSize; + orb.size = 0.01f; + orb.renderCurrentSize = 0.01f; + orb.prevRenderSize = 0.01f; + + // Initial position at anchor point + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint); + orb.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + orb.prevPosX = pos.xCoord; + orb.prevPosY = pos.yCoord; + orb.prevPosZ = pos.zCoord; + orb.startX = pos.xCoord; + orb.startY = pos.yCoord; + orb.startZ = pos.zCoord; + + // Clear motion while charging + orb.motionX = 0; + orb.motionY = 0; + orb.motionZ = 0; + + return orb; + } + + /** + * Start preview firing (simulates firing toward a point in front of NPC). + */ + public void startPreviewFiring() { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Sync prev position to prevent visual jump on first frame + prevPosX = posX; + prevPosY = posY; + prevPosZ = posZ; + + // Fire forward based on owner facing direction + Entity owner = getOwner(); + if (owner != null) { + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(0); // Fire horizontally + motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; + motionY = -Math.sin(pitch) * speed; + motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + } else { + // Default: fire forward (positive X in model space) + motionX = speed; + motionY = 0; + motionZ = 0; + } + } + /** * Start the orb moving (exit charging mode). * Called by ability when windup ends. @@ -232,6 +325,12 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { return; } + // In preview mode, run movement on client (no server) + if (previewMode) { + updatePreviewMovement(); + return; + } + if (worldObj.isRemote) { handleClientInterpolation(); } else { @@ -254,6 +353,16 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { } } + /** + * Update movement in preview mode (client-side only, no damage). + */ + private void updatePreviewMovement() { + // Simple movement - just apply motion + this.posX += motionX; + this.posY += motionY; + this.posZ += motionZ; + } + private void updateHoming() { if (!homing) return; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index 4b0f1e6a5..149ffd329 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -71,6 +71,8 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA // ==================== STATE ==================== protected boolean hasHit = false; + protected boolean previewMode = false; // Client-side preview mode (no damage/effects) + protected EntityNPCInterface previewOwner = null; // Direct reference for GUI preview (no world lookup) // ==================== ROTATION INTERPOLATION ==================== public float prevRotationValX, prevRotationValY, prevRotationValZ; @@ -202,7 +204,10 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA this.prevRotationValZ = this.rotationValZ; this.prevRenderSize = this.renderCurrentSize; - super.onUpdate(); + // Skip super.onUpdate() in preview mode to avoid world checks + if (!previewMode) { + super.onUpdate(); + } // Update rotation updateRotation(); @@ -210,26 +215,29 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA // Lerp render size toward actual size this.renderCurrentSize = this.renderCurrentSize + (this.size - this.renderCurrentSize) * 0.15f; - // Set death time on first tick if not already set (handles chunk load/unload) - if (deathWorldTime < 0 && worldObj != null) { - deathWorldTime = worldObj.getTotalWorldTime() + maxLifetime; - } + // Skip lifetime/distance checks in preview mode + if (!previewMode) { + // Set death time on first tick if not already set (handles chunk load/unload) + if (deathWorldTime < 0 && worldObj != null) { + deathWorldTime = worldObj.getTotalWorldTime() + maxLifetime; + } - // Check lifespan using world time (survives chunk unload/reload) - if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) { - this.setDead(); - return; - } + // Check lifespan using world time (survives chunk unload/reload) + if (deathWorldTime > 0 && worldObj.getTotalWorldTime() >= deathWorldTime) { + this.setDead(); + return; + } - // Check max distance (subclass can override if needed) - if (checkMaxDistance()) { - this.setDead(); - return; - } + // Check max distance (subclass can override if needed) + if (checkMaxDistance()) { + this.setDead(); + return; + } - if (hasHit) { - this.setDead(); - return; + if (hasHit) { + this.setDead(); + return; + } } // Subclass-specific update @@ -310,13 +318,32 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA } } + // ==================== PREVIEW MODE ==================== + + /** + * Set preview mode for GUI display. + * In preview mode, no damage or world effects are applied. + */ + public void setPreviewMode(boolean preview) { + this.previewMode = preview; + } + + /** + * Check if entity is in preview mode. + */ + public boolean isPreviewMode() { + return previewMode; + } + // ==================== DAMAGE & EFFECTS ==================== protected void applyDamage(EntityLivingBase target) { + if (previewMode) return; // Skip damage in preview mode applyDamage(target, this.damage, this.knockback); } protected void applyDamage(EntityLivingBase target, float dmg, float kb) { + if (previewMode) return; // Skip damage in preview mode Entity owner = getOwner(); EntityNPCInterface npc = (owner instanceof EntityNPCInterface) ? (EntityNPCInterface) owner : null; @@ -353,6 +380,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA } protected void doExplosion() { + if (previewMode) return; // Skip explosion in preview mode worldObj.playSoundEffect(posX, posY, posZ, "random.explode", 1.0f, 1.0f); Entity owner = getOwner(); @@ -384,10 +412,22 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA // ==================== ENTITY HELPERS ==================== protected Entity getOwner() { + // In preview mode, use direct reference (no world lookup) + if (previewMode && previewOwner != null) { + return previewOwner; + } if (ownerEntityId == -1) return null; return worldObj.getEntityByID(ownerEntityId); } + /** + * Set the preview owner for GUI preview mode. + * This allows anchor point calculations without world entity lookup. + */ + public void setPreviewOwner(EntityNPCInterface owner) { + this.previewOwner = owner; + } + protected Entity getTarget() { if (targetEntityId == -1) return null; return worldObj.getEntityByID(targetEntityId); diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index e659fef74..68c021559 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -92,6 +92,10 @@ import kamkeel.npcs.network.packets.request.TraderMarketSavePacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcSavePacket; +import kamkeel.npcs.network.packets.request.ability.SavedAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.SavedAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.SavedAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.SavedAbilitySavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket; @@ -501,6 +505,10 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new AbilitiesGetAllPacket()); REQUEST_PACKET.registerPacket(new AbilitiesNpcGetPacket()); REQUEST_PACKET.registerPacket(new AbilitiesNpcSavePacket()); + REQUEST_PACKET.registerPacket(new SavedAbilitiesGetPacket()); + REQUEST_PACKET.registerPacket(new SavedAbilityGetPacket()); + REQUEST_PACKET.registerPacket(new SavedAbilityRemovePacket()); + REQUEST_PACKET.registerPacket(new SavedAbilitySavePacket()); // Other Packets REQUEST_PACKET.registerPacket(new IsGuiOpenInform()); diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index bdd566785..d4968bc5c 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -198,6 +198,10 @@ public enum EnumRequestPacket { NpcAbilitiesGet, NpcAbilitiesSave, AbilitiesGetAll, + SavedAbilitiesGet, + SavedAbilityGet, + SavedAbilityRemove, + SavedAbilitySave, Hammer, RecipeScript, diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitiesGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitiesGetPacket.java new file mode 100644 index 000000000..0e8b547a3 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitiesGetPacket.java @@ -0,0 +1,51 @@ +package kamkeel.npcs.network.packets.request.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import noppes.npcs.NoppesUtilServer; + +import java.io.IOException; + +/** + * Request packet to get list of all saved ability preset names. + */ +public final class SavedAbilitiesGetPacket extends AbstractPacket { + public static String packetName = "Request|SavedAbilitiesGet"; + + public SavedAbilitiesGetPacket() { + } + + @Override + public Enum getType() { + return EnumRequestPacket.SavedAbilitiesGet; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + NoppesUtilServer.sendSavedAbilitiesData((EntityPlayerMP) player); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityGetPacket.java new file mode 100644 index 000000000..7bb2f1826 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityGetPacket.java @@ -0,0 +1,68 @@ +package kamkeel.npcs.network.packets.request.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; + +import java.io.IOException; + +/** + * Request packet to get a specific saved ability by name. + */ +public final class SavedAbilityGetPacket extends AbstractPacket { + public static String packetName = "Request|SavedAbilityGet"; + + private String abilityName; + + public SavedAbilityGetPacket() { + } + + public SavedAbilityGetPacket(String abilityName) { + this.abilityName = abilityName; + } + + @Override + public Enum getType() { + return EnumRequestPacket.SavedAbilityGet; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, abilityName); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + String name = ByteBufUtils.readString(in); + Ability ability = AbilityController.Instance.getSavedAbility(name); + if (ability != null) { + NBTTagCompound compound = ability.writeNBT(); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); + } + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityRemovePacket.java new file mode 100644 index 000000000..3fe4f5a0c --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityRemovePacket.java @@ -0,0 +1,69 @@ +package kamkeel.npcs.network.packets.request.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.NoppesUtilServer; + +import java.io.IOException; + +/** + * Request packet to remove a saved ability by name. + */ +public final class SavedAbilityRemovePacket extends AbstractPacket { + public static String packetName = "Request|SavedAbilityRemove"; + + private String abilityName; + + public SavedAbilityRemovePacket() { + } + + public SavedAbilityRemovePacket(String abilityName) { + this.abilityName = abilityName; + } + + @Override + public Enum getType() { + return EnumRequestPacket.SavedAbilityRemove; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @Override + public CustomNpcsPermissions.Permission getPermission() { + return CustomNpcsPermissions.GLOBAL_ABILITY; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeString(out, abilityName); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + String name = ByteBufUtils.readString(in); + AbilityController.Instance.deleteAbility(name); + NoppesUtilServer.sendSavedAbilitiesData((EntityPlayerMP) player); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitySavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitySavePacket.java new file mode 100644 index 000000000..2cf2fa489 --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitySavePacket.java @@ -0,0 +1,76 @@ +package kamkeel.npcs.network.packets.request.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.PacketUtil; +import kamkeel.npcs.network.enums.EnumItemPacketType; +import kamkeel.npcs.network.enums.EnumRequestPacket; +import kamkeel.npcs.network.packets.data.large.GuiDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.CustomNpcsPermissions; +import noppes.npcs.NoppesUtilServer; + +import java.io.IOException; + +/** + * Request packet to save an ability preset. + */ +public final class SavedAbilitySavePacket extends AbstractPacket { + public static String packetName = "Request|SavedAbilitySave"; + + private NBTTagCompound abilityNBT; + + public SavedAbilitySavePacket() { + } + + public SavedAbilitySavePacket(NBTTagCompound abilityNBT) { + this.abilityNBT = abilityNBT; + } + + @Override + public Enum getType() { + return EnumRequestPacket.SavedAbilitySave; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.REQUEST_PACKET; + } + + @Override + public CustomNpcsPermissions.Permission getPermission() { + return CustomNpcsPermissions.GLOBAL_ABILITY; + } + + @SideOnly(Side.CLIENT) + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeNBT(out, abilityNBT); + } + + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + if (!(player instanceof EntityPlayerMP)) + return; + + if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) + return; + + NBTTagCompound compound = ByteBufUtils.readNBT(in); + Ability ability = AbilityController.Instance.fromNBT(compound); + if (ability != null) { + AbilityController.Instance.saveAbility(ability); + NoppesUtilServer.sendSavedAbilitiesData((EntityPlayerMP) player); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, ability.writeNBT()); + } + } +} diff --git a/src/main/java/noppes/npcs/CustomNpcsPermissions.java b/src/main/java/noppes/npcs/CustomNpcsPermissions.java index 0d758ad41..5e7c2924e 100644 --- a/src/main/java/noppes/npcs/CustomNpcsPermissions.java +++ b/src/main/java/noppes/npcs/CustomNpcsPermissions.java @@ -66,6 +66,7 @@ public class CustomNpcsPermissions { public static final Permission GLOBAL_NATURALSPAWN = new Permission("customnpcs.global.naturalspawn"); public static final Permission GLOBAL_TAG = new Permission("customnpcs.global.tag"); public static final Permission GLOBAL_ANIMATION = new Permission("customnpcs.global.animation"); + public static final Permission GLOBAL_ABILITY = new Permission("customnpcs.global.ability"); public static final Permission GLOBAL_MAGIC = new Permission("customnpcs.global.magic"); public static final Permission GLOBAL_EFFECT = new Permission("customnpcs.global.effect"); diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index 6a33cb2c0..ae97f70b4 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -785,6 +785,18 @@ public class NoppesUtilServer { sendScrollData(player, map, EnumScrollData.ABILITY_TYPES); } + public static void sendSavedAbilitiesData(EntityPlayerMP player) { + HashMap map = new HashMap(); + kamkeel.npcs.controllers.data.ability.AbilityController controller = kamkeel.npcs.controllers.data.ability.AbilityController.Instance; + if (controller != null) { + int index = 0; + for (String name : controller.getSavedAbilityNames()) { + map.put(name, index++); + } + } + sendScrollData(player, map, EnumScrollData.ABILITIES); + } + public static DialogOption setNpcDialog(int slot, int dialogId, EntityPlayer player) throws IOException { return setNpcDialog(slot, dialogId, getEditingNpc(player)); } diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 9a0044191..01c226812 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -87,6 +87,7 @@ import noppes.npcs.client.gui.global.GuiNPCManageAnimations; import noppes.npcs.client.gui.global.GuiNPCManageBanks; import noppes.npcs.client.gui.global.GuiNPCManageDialogs; import noppes.npcs.client.gui.global.GuiNPCManageEffects; +import noppes.npcs.client.gui.global.GuiNpcManageAbilities; import noppes.npcs.client.gui.global.GuiNPCManageFactions; import noppes.npcs.client.gui.global.GuiNPCManageLinked; import noppes.npcs.client.gui.global.GuiNPCManageQuest; @@ -497,6 +498,20 @@ public class ClientProxy extends CommonProxy { else if (gui == EnumGuiType.ManageEffects) return new GuiNPCManageEffects(npc); + else if (gui == EnumGuiType.ManageAbilities) { + EntityCustomNpc abilityNpc; + if (npc != null) { + abilityNpc = new EntityCustomNpc(npc.worldObj); + abilityNpc.copyDataFrom(npc, true); + abilityNpc.display.showName = 1; + abilityNpc.display.showBossBar = 0; + } else { + abilityNpc = new EntityCustomNpc(Minecraft.getMinecraft().theWorld); + abilityNpc.display.texture = "customnpcs:textures/entity/humanmale/AnimationBody.png"; + } + return new GuiNpcManageAbilities(abilityNpc); + } + else if (gui == EnumGuiType.MainMenuGlobal) return new GuiNPCGlobalMainMenu(npc); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java index 9abcebbe7..750fe282e 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -281,6 +281,13 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @Override public void setGuiData(NBTTagCompound compound) { + // Only process NPC ability data, not saved ability preset data + // SubGuiAbilityLoad also implements IGuiData and may receive responses + // that get forwarded here - ignore those by checking for expected keys + if (!compound.hasKey("Abilities")) { + return; + } + abilitiesEnabled = compound.getBoolean("AbilitiesEnabled"); // Support old globalCooldown for backwards compatibility diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java index ee2561b4e..3760666b4 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.advanced; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.ability.SavedAbilityRemovePacket; import net.minecraft.client.gui.GuiButton; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -8,15 +9,20 @@ import noppes.npcs.client.gui.util.SubGuiInterface; /** * Confirmation dialog for deleting an ability preset. + * Uses packet-based communication for proper client-server architecture. */ public class SubGuiAbilityDeleteConfirm extends SubGuiInterface { private final String abilityName; - private final SubGuiAbilityLoad loadGui; + private final Runnable onDeleted; - public SubGuiAbilityDeleteConfirm(String abilityName, SubGuiAbilityLoad loadGui) { + public SubGuiAbilityDeleteConfirm(String abilityName) { + this(abilityName, null); + } + + public SubGuiAbilityDeleteConfirm(String abilityName, Runnable onDeleted) { this.abilityName = abilityName; - this.loadGui = loadGui; + this.onDeleted = onDeleted; setBackground("menubg.png"); xSize = 200; @@ -44,9 +50,11 @@ public class SubGuiAbilityDeleteConfirm extends SubGuiInterface { int id = guibutton.id; if (id == 0) { - // Delete the ability - AbilityController.Instance.deleteAbility(abilityName); - loadGui.onAbilityDeleted(abilityName); + // Delete the ability via packet + PacketClient.sendClient(new SavedAbilityRemovePacket(abilityName)); + if (onDeleted != null) { + onDeleted.run(); + } close(); } else if (id == 1) { close(); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java index c48683070..1e9133d9d 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java @@ -2,28 +2,39 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.ability.SavedAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.SavedAbilityGetPacket; import net.minecraft.client.gui.GuiButton; +import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.client.gui.util.GuiCustomScroll; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.client.gui.util.IScrollData; import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.constants.EnumScrollData; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import java.util.Set; +import java.util.Vector; /** * Dialog for loading a saved ability preset. - * Can be opened from SubGuiAbilityConfig (to load into editing ability) - * or from GuiNPCAbilities (to add ability directly to NPC). + * Uses packet-based communication for proper client-server architecture. */ -public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollListener { +public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollListener, IScrollData, IGuiData { private final SubGuiAbilityConfig parentConfig; private final GuiNPCAbilities parentAbilities; private String selectedName = null; - private List abilityNames = new ArrayList<>(); + private HashMap abilityData = new HashMap<>(); + private GuiCustomScroll scroll; + + // Track if we're waiting for a load + private boolean waitingForLoad = false; public SubGuiAbilityLoad(SubGuiAbilityConfig parentConfig) { this.parentConfig = parentConfig; @@ -32,6 +43,9 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL setBackground("menubg.png"); xSize = 220; ySize = 180; + + // Request ability list from server + PacketClient.sendClient(new SavedAbilitiesGetPacket()); } public SubGuiAbilityLoad(GuiNPCAbilities parentAbilities) { @@ -41,28 +55,29 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL setBackground("menubg.png"); xSize = 220; ySize = 180; + + // Request ability list from server + PacketClient.sendClient(new SavedAbilitiesGetPacket()); } @Override public void initGui() { super.initGui(); - // Load ability names - Set names = AbilityController.Instance.getSavedAbilityNames(); - abilityNames = new ArrayList<>(names); - int y = guiTop + 5; addLabel(new GuiNpcLabel(0, "ability.load.select", guiLeft + 10, y)); y += 14; // Scroll list of saved abilities - GuiCustomScroll scroll = new GuiCustomScroll(this, 0); - scroll.setSize(200, 110); + if (scroll == null) { + scroll = new GuiCustomScroll(this, 0); + scroll.setSize(200, 110); + } scroll.guiLeft = guiLeft + 10; scroll.guiTop = y; - scroll.setUnsortedList(abilityNames); - if (selectedName != null && abilityNames.contains(selectedName)) { + scroll.setList(getFilteredList()); + if (selectedName != null && abilityData.containsKey(selectedName)) { scroll.setSelected(selectedName); } addScroll(scroll); @@ -70,13 +85,15 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL y += 115; // Buttons - addButton(new GuiNpcButton(0, guiLeft + 10, y, 60, 20, "gui.load")); - addButton(new GuiNpcButton(1, guiLeft + 80, y, 60, 20, "gui.delete")); - addButton(new GuiNpcButton(2, guiLeft + 150, y, 60, 20, "gui.cancel")); + addButton(new GuiNpcButton(0, guiLeft + 10, y, 95, 20, "gui.load")); + addButton(new GuiNpcButton(2, guiLeft + 115, y, 95, 20, "gui.cancel")); - // Disable load/delete if nothing selected + // Disable load if nothing selected getButton(0).setEnabled(selectedName != null && !selectedName.isEmpty()); - getButton(1).setEnabled(selectedName != null && !selectedName.isEmpty()); + } + + private List getFilteredList() { + return new ArrayList<>(abilityData.keySet()); } @Override @@ -84,19 +101,9 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL int id = guibutton.id; if (id == 0 && selectedName != null) { - // Load the selected ability - Ability loaded = AbilityController.Instance.getSavedAbility(selectedName); - if (loaded != null) { - if (parentConfig != null) { - parentConfig.loadAbility(loaded); - } else if (parentAbilities != null) { - parentAbilities.loadAbility(loaded); - } - } - close(); - } else if (id == 1 && selectedName != null) { - // Delete confirmation - setSubGui(new SubGuiAbilityDeleteConfirm(selectedName, this)); + // Request ability data from server + waitingForLoad = true; + PacketClient.sendClient(new SavedAbilityGetPacket(selectedName)); } else if (id == 2) { close(); } @@ -113,8 +120,34 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL @Override public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { if (scroll.id == 0 && selection != null && !selection.isEmpty()) { - // Double-click to load immediately - Ability loaded = AbilityController.Instance.getSavedAbility(selection); + // Double-click to load - request from server + selectedName = selection; + waitingForLoad = true; + PacketClient.sendClient(new SavedAbilityGetPacket(selection)); + } + } + + @Override + public void setData(Vector list, HashMap data, EnumScrollData type) { + if (type == EnumScrollData.ABILITIES) { + String name = scroll != null ? scroll.getSelected() : null; + this.abilityData = data; + if (scroll != null) { + scroll.setList(getFilteredList()); + if (name != null && abilityData.containsKey(name)) { + scroll.setSelected(name); + } + } + initGui(); + } + } + + @Override + public void setGuiData(NBTTagCompound compound) { + // Received ability data from server + if (waitingForLoad) { + waitingForLoad = false; + Ability loaded = AbilityController.Instance.fromNBT(compound); if (loaded != null) { if (parentConfig != null) { parentConfig.loadAbility(loaded); @@ -126,13 +159,11 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL } } - /** - * Called after an ability is deleted to refresh the list. - */ - public void onAbilityDeleted(String name) { - if (name.equals(selectedName)) { - selectedName = null; + @Override + public void setSelected(String selected) { + this.selectedName = selected; + if (scroll != null) { + scroll.setSelected(selected); } - initGui(); } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java index 3be13f0be..760f3eb9b 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java @@ -1,23 +1,30 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; -import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.ability.SavedAbilitySavePacket; import net.minecraft.client.gui.GuiButton; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.util.SubGuiInterface; /** * Confirmation dialog for saving an ability preset. + * Uses packet-based communication for proper client-server architecture. */ public class SubGuiAbilitySaveConfirm extends SubGuiInterface { private final Ability ability; - private final boolean exists; + private final IAbilityConfigCallback callback; public SubGuiAbilitySaveConfirm(Ability ability) { + this(ability, null); + } + + public SubGuiAbilitySaveConfirm(Ability ability, IAbilityConfigCallback callback) { this.ability = ability; - this.exists = AbilityController.Instance.hasSavedAbility(ability.getName()); + this.callback = callback; setBackground("menubg.png"); xSize = 200; @@ -30,15 +37,9 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface { int y = guiTop + 10; - if (exists) { - addLabel(new GuiNpcLabel(0, "ability.save.overwrite", guiLeft + 10, y)); - y += 12; - addLabel(new GuiNpcLabel(1, "'" + ability.getName() + "'?", guiLeft + 10, y)); - } else { - addLabel(new GuiNpcLabel(0, "ability.save.confirm", guiLeft + 10, y)); - y += 12; - addLabel(new GuiNpcLabel(1, "'" + ability.getName() + "'?", guiLeft + 10, y)); - } + addLabel(new GuiNpcLabel(0, "ability.save.confirm", guiLeft + 10, y)); + y += 12; + addLabel(new GuiNpcLabel(1, "'" + ability.getName() + "'?", guiLeft + 10, y)); y += 30; @@ -51,8 +52,11 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface { int id = guibutton.id; if (id == 0) { - // Save the ability - AbilityController.Instance.saveAbility(ability); + // Save the ability via packet + PacketClient.sendClient(new SavedAbilitySavePacket(ability.writeNBT())); + if (callback != null) { + callback.onAbilitySaved(ability); + } close(); } else if (id == 1) { close(); diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java index 22d0f3103..cb5220d54 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -1,122 +1,266 @@ package noppes.npcs.client.gui.global; +import kamkeel.npcs.client.renderer.TelegraphRenderer; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.network.PacketClient; +import kamkeel.npcs.network.packets.request.ability.SavedAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.SavedAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.SavedAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.SavedAbilitySavePacket; import net.minecraft.client.gui.GuiButton; -import noppes.npcs.client.gui.advanced.SubGuiAbilityDeleteConfirm; +import net.minecraft.client.gui.GuiYesNo; +import net.minecraft.client.gui.GuiYesNoCallback; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.NoppesUtil; +import noppes.npcs.client.gui.util.AbilityPreviewExecutor; +import noppes.npcs.client.gui.util.GuiAbilityInterface; import noppes.npcs.client.gui.util.GuiCustomScroll; -import noppes.npcs.client.gui.util.GuiNPCInterface2; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.GuiTexturedButton; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.util.ICustomScrollListener; +import noppes.npcs.client.gui.util.IGuiData; +import noppes.npcs.client.gui.util.IScrollData; import noppes.npcs.client.gui.util.ISubGuiListener; +import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.client.gui.util.SubGuiInterface; +import noppes.npcs.constants.EnumScrollData; +import noppes.npcs.controllers.data.AnimationData; import noppes.npcs.entity.EntityNPCInterface; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; -import java.util.Set; +import java.util.Vector; /** - * Global GUI for managing saved ability presets. - * Allows viewing, editing, and deleting saved abilities. + * Global GUI for managing saved ability presets with 3D preview. + * + * Features: + * - Left side: 3D preview of NPC with ability effects + * - Right side: Ability list with search + * - Play/Pause/Stop buttons to preview abilities */ -public class GuiNpcManageAbilities extends GuiNPCInterface2 implements ICustomScrollListener, ISubGuiListener, IAbilityConfigCallback { +public class GuiNpcManageAbilities extends GuiAbilityInterface + implements ICustomScrollListener, ISubGuiListener, IAbilityConfigCallback, + ITextfieldListener, GuiYesNoCallback, IScrollData, IGuiData { + // ==================== DATA ==================== private GuiCustomScroll scroll; - private String selectedName = null; - private List abilityNames = new ArrayList<>(); + private HashMap abilityData = new HashMap<>(); + private String selected = null; private String search = ""; + private Ability selectedAbility = null; + + // ==================== PREVIEW ==================== + private AbilityPreviewExecutor previewExecutor; + private long prevTick = 0; public GuiNpcManageAbilities(EntityNPCInterface npc) { super(npc); - loadAbilityNames(); - } - private void loadAbilityNames() { - Set names = AbilityController.Instance.getSavedAbilityNames(); - abilityNames = new ArrayList<>(names); + // Layout offset + this.xOffset = -148 + 110; + this.yOffset = -170 + 167; + + // Initialize preview executor + previewExecutor = new AbilityPreviewExecutor(); + previewExecutor.setParentGui(this); + + // Request ability data from server + PacketClient.sendClient(new SavedAbilitiesGetPacket()); + + // Enable animation data + AnimationData data = npc.display.animationData; + data.setEnabled(true); } @Override public void initGui() { super.initGui(); - int y = guiTop + 10; + // Edit button (above Remove) + addButton(new GuiNpcButton(100, guiLeft + 368, guiTop + 8, 45, 20, "gui.edit")); + getButton(100).setEnabled(selected != null && !selected.isEmpty() && selectedAbility != null); - // Title label - addLabel(new GuiNpcLabel(0, "ability.manager.title", guiLeft + 10, y)); - y += 20; + // Remove button + addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove")); + getButton(1).setEnabled(selected != null && !selected.isEmpty() && abilityData.containsKey(selected)); - // Scroll list of saved abilities + // Scroll list of saved abilities (right side) if (scroll == null) { scroll = new GuiCustomScroll(this, 0); + scroll.setSize(143, 185); } - scroll.setSize(xSize - 90, 160); - scroll.guiLeft = guiLeft + 10; - scroll.guiTop = y; - scroll.setUnsortedList(getFilteredList()); - if (selectedName != null && abilityNames.contains(selectedName)) { - scroll.setSelected(selectedName); - } + scroll.guiLeft = guiLeft + 220; + scroll.guiTop = guiTop + 4; addScroll(scroll); + // Update scroll list + scroll.setList(getSearchList()); + if (selected != null && abilityData.containsKey(selected)) { + scroll.setSelected(selected); + } + // Search bar - addTextField(new GuiNpcTextField(1, this, fontRendererObj, guiLeft + 10, y + 163, xSize - 90, 18, search)); + addTextField(new GuiNpcTextField(55, this, fontRendererObj, guiLeft + 220, guiTop + 192, 143, 20, search)); - // Right side buttons - int btnX = guiLeft + xSize - 70; - addButton(new GuiNpcButton(1, btnX, y, 60, 20, "gui.edit")); - addButton(new GuiNpcButton(2, btnX, y + 25, 60, 20, "gui.delete")); - addButton(new GuiNpcButton(3, btnX, y + 60, 60, 20, "selectServer.refresh")); + // Show ability info only if one is selected + if (selectedAbility == null || selected == null) { + return; + } - // Enable/disable buttons based on selection - getButton(1).setEnabled(selectedName != null && !selectedName.isEmpty()); - getButton(2).setEnabled(selectedName != null && !selectedName.isEmpty()); + // ID/Name label (below search bar) + addLabel(new GuiNpcLabel(10, "ID: " + selectedAbility.getId(), guiLeft + 220, guiTop + 215)); + + // Play/Pause/Stop buttons (below model preview area) + String animTexture = "customnpcs:textures/gui/animation.png"; + int playButtonOffsetX = 60; + + boolean isPlaying = previewExecutor.isPlaying() && !previewExecutor.isPaused(); + boolean isPaused = previewExecutor.isPaused(); + boolean isActive = previewExecutor.isActive(); + + if (!isPlaying || isPaused) { + // Show Play button + String statusKey = isPaused ? "animation.paused" : "animation.stopped"; + addLabel(new GuiNpcLabel(90, statusKey, guiLeft + playButtonOffsetX, guiTop + 198)); + addButton(new GuiTexturedButton(91, "", guiLeft + playButtonOffsetX + 70, guiTop + 192, 11, 20, animTexture, 18, 71)); + } else { + // Show Pause button + addLabel(new GuiNpcLabel(90, "animation.playing", guiLeft + playButtonOffsetX, guiTop + 198)); + addButton(new GuiTexturedButton(92, "", guiLeft + playButtonOffsetX + 70, guiTop + 192, 14, 20, animTexture, 0, 71)); + } + if (isActive) { + // Show Stop button + addButton(new GuiTexturedButton(93, "", guiLeft + playButtonOffsetX + 90, guiTop + 192, 14, 20, animTexture, 33, 71)); + } } - private List getFilteredList() { + private List getSearchList() { if (search.isEmpty()) { - return new ArrayList<>(abilityNames); + return new ArrayList<>(abilityData.keySet()); } - List filtered = new ArrayList<>(); - for (String name : abilityNames) { + List list = new ArrayList<>(); + for (String name : abilityData.keySet()) { if (name.toLowerCase().contains(search.toLowerCase())) { - filtered.add(name); + list.add(name); } } - return filtered; + return list; } @Override - public void buttonEvent(GuiButton guibutton) { + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + // IMPORTANT: Set NPC rotation BEFORE ticking entities + // This ensures anchor point calculations use the correct facing direction + npc.prevRenderYawOffset = npc.renderYawOffset = NPC_FACING_YAW; + npc.prevRotationYaw = npc.rotationYaw = NPC_FACING_YAW; + npc.prevRotationYawHead = npc.rotationYawHead = NPC_FACING_YAW; + + // Tick preview executor (entities will use the rotation set above) + tickPreview(); + + // Draw base (includes 3D preview via parent class) + super.drawScreen(mouseX, mouseY, partialTicks); + + // Draw status text + if (previewExecutor.isActive()) { + String status = previewExecutor.getStatusString(); + fontRendererObj.drawString(status, guiLeft + 12, guiTop + 175, 0xFFFFFF); + } + } + + /** + * Tick the preview executor and animations. + */ + private void tickPreview() { + if (previewExecutor.isPlaying() && !previewExecutor.isPaused()) { + long time = mc.theWorld != null ? mc.theWorld.getTotalWorldTime() : System.currentTimeMillis() / 50; + if (time != prevTick) { + // Advance animation + npc.display.animationData.increaseTime(); + + // Tick preview + previewExecutor.tick(); + + // Sync telegraph to GUI for rendering + setPreviewTelegraph(previewExecutor.getTelegraph()); + + prevTick = time; + + // Check if preview finished + if (!previewExecutor.isActive()) { + initGui(); + } + } + } + } + + @Override + protected void actionPerformed(GuiButton guibutton) { + super.actionPerformed(guibutton); + int id = guibutton.id; - if (id == 1 && selectedName != null) { + if (id == 1 && selected != null && abilityData.containsKey(selected)) { + // Remove - show confirmation + GuiYesNo guiyesno = new GuiYesNo(this, selected, StatCollector.translateToLocal("gui.delete"), 1); + displayGuiScreen(guiyesno); + } else if (id == 100 && selectedAbility != null) { // Edit - Ability ability = AbilityController.Instance.getSavedAbility(selectedName); - if (ability != null) { - setSubGui(ability.createConfigGui(this)); + previewExecutor.stop(); + setSubGui(selectedAbility.createConfigGui(this)); + } else if (id == 91) { + // Play button + if (selectedAbility != null) { + if (previewExecutor.isPaused()) { + previewExecutor.play(); + } else { + previewExecutor.startPreview(selectedAbility, npc); + } + initGui(); } - } else if (id == 2 && selectedName != null) { - // Delete - setSubGui(new SubGuiAbilityDeleteConfirm(selectedName, null)); - } else if (id == 3) { - // Refresh - AbilityController.Instance.load(); - loadAbilityNames(); - selectedName = null; + } else if (id == 92) { + // Pause button + previewExecutor.pause(); initGui(); + } else if (id == 93) { + // Stop button + previewExecutor.stop(); + initGui(); + } + } + + @Override + public void keyTyped(char c, int i) { + super.keyTyped(c, i); + if (getTextField(55) != null && getTextField(55).isFocused()) { + if (!search.equals(getTextField(55).getText())) { + search = getTextField(55).getText(); + scroll.resetScroll(); + scroll.setList(getSearchList()); + } } } @Override public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { if (scroll.id == 0) { - selectedName = scroll.getSelected(); - initGui(); + String newSelection = scroll.getSelected(); + if (newSelection != null && !newSelection.equals(selected)) { + // Stop any playing preview + previewExecutor.stop(); + + selected = newSelection; + // Request ability data from server + PacketClient.sendClient(new SavedAbilityGetPacket(selected)); + } } } @@ -124,48 +268,90 @@ public class GuiNpcManageAbilities extends GuiNPCInterface2 implements ICustomSc public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { if (scroll.id == 0 && selection != null && !selection.isEmpty()) { // Double-click to edit - Ability ability = AbilityController.Instance.getSavedAbility(selection); - if (ability != null) { - setSubGui(ability.createConfigGui(this)); + if (selectedAbility != null) { + previewExecutor.stop(); + setSubGui(selectedAbility.createConfigGui(this)); } } } @Override - public void keyTyped(char c, int i) { - super.keyTyped(c, i); - if (getTextField(1) != null && getTextField(1).isFocused()) { - if (!search.equals(getTextField(1).getText())) { - search = getTextField(1).getText(); - scroll.setUnsortedList(getFilteredList()); - scroll.resetScroll(); + public void setData(Vector list, HashMap data, EnumScrollData type) { + if (type == EnumScrollData.ABILITIES) { + String name = scroll != null ? scroll.getSelected() : null; + this.abilityData = data; + if (scroll != null) { + scroll.setList(getSearchList()); + if (name != null && abilityData.containsKey(name)) { + scroll.setSelected(name); + } } + initGui(); + } + } + + @Override + public void setGuiData(NBTTagCompound compound) { + // Received ability data from server + selectedAbility = AbilityController.Instance.fromNBT(compound); + if (selectedAbility != null) { + selected = selectedAbility.getName(); + if (scroll != null) { + scroll.setSelected(selected); + } + } + previewExecutor.stop(); + initGui(); + } + + @Override + public void setSelected(String selected) { + this.selected = selected; + if (scroll != null) { + scroll.setSelected(selected); } } @Override public void subGuiClosed(SubGuiInterface subgui) { - // Refresh list after any sub gui closes - loadAbilityNames(); - if (subgui instanceof SubGuiAbilityDeleteConfirm) { - // After delete, clear selection if it was deleted - if (selectedName != null && !abilityNames.contains(selectedName)) { - selectedName = null; - } - } initGui(); } + @Override + public void confirmClicked(boolean result, int id) { + NoppesUtil.openGUI(player, this); + if (!result) return; + + if (id == 1 && selected != null && abilityData.containsKey(selected)) { + // Delete ability via packet + PacketClient.sendClient(new SavedAbilityRemovePacket(selected)); + scroll.clear(); + selected = null; + selectedAbility = null; + initGui(); + } + } + @Override public void onAbilitySaved(Ability ability) { - // Save the edited ability - AbilityController.Instance.saveAbility(ability); - loadAbilityNames(); - initGui(); + // Save the edited ability via packet + PacketClient.sendClient(new SavedAbilitySavePacket(ability.writeNBT())); + } + + @Override + public void unFocused(GuiNpcTextField textfield) { + // Search field handling is done in keyTyped } @Override public void save() { - // Nothing to save - abilities are saved individually + // Nothing to save - abilities are saved individually via packets + } + + @Override + public void onGuiClosed() { + super.onGuiClosed(); + // Stop preview when GUI closes + previewExecutor.stop(); } } diff --git a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java index bca447b1f..b055cbf43 100644 --- a/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java +++ b/src/main/java/noppes/npcs/client/gui/mainmenu/GuiNPCGlobalMainMenu.java @@ -39,6 +39,7 @@ public class GuiNPCGlobalMainMenu extends GuiNPCInterface2 { private GuiNpcSquareButton effectsButton; private GuiNpcSquareButton magicsButton; private GuiNpcSquareButton abilitiesButton; + private GuiNpcSquareButton auctionsButton; public GuiNPCGlobalMainMenu(EntityNPCInterface npc) { super(npc, 5); @@ -96,7 +97,10 @@ public class GuiNPCGlobalMainMenu extends GuiNPCInterface2 { magicsButton.setIconPos(24, 24, 48, 56).setIconTexture(specialIcons); registerButton(abilitiesButton = new GuiNpcSquareButton(21, 0, 0, 20, "global.abilities", 0xFF333333)); - abilitiesButton.setIconPos(24, 24, 72, 56).setIconTexture(specialIcons); // Placeholder icon position + abilitiesButton.setIconPos(24, 24, 72, 56).setIconTexture(specialIcons); + + registerButton(auctionsButton = new GuiNpcSquareButton(22, 0, 0, 20, "global.auction", 0xFF333333)); + auctionsButton.setIconPos(24, 24, 96, 56).setIconTexture(specialIcons); // Layout composite buttons optimally. layoutButtons(); @@ -240,7 +244,10 @@ public class GuiNPCGlobalMainMenu extends GuiNPCInterface2 { NoppesUtil.requestOpenGUI(EnumGuiType.ManageEffects); } if (id == 21) { - NoppesUtil.openGUI(player, new noppes.npcs.client.gui.global.GuiNpcManageAbilities(npc)); + NoppesUtil.requestOpenGUI(EnumGuiType.ManageAbilities); + } + if (id == 22) { + // NoppesUtil.requestOpenGUI(EnumGuiType.ManageAuction); } } diff --git a/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java new file mode 100644 index 000000000..3dce761d5 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/AbilityPreviewExecutor.java @@ -0,0 +1,368 @@ +package noppes.npcs.client.gui.util; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.entity.EntityAbilityProjectile; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.world.World; +import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.AnimationData; +import noppes.npcs.entity.EntityCustomNpc; +import noppes.npcs.entity.EntityNPCInterface; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +/** + * Client-side executor for previewing abilities in the GUI. + * + * Lifecycle: + * 1. WINDUP phase - plays windup animation, entities spawn and charge + * 2. ACTIVE phase - plays active animation, entities fire and move + * 3. EXTENDED phase - continues for +100 ticks after animations to see results + * 4. IDLE - preview complete + * + * Features: + * - Invisible fake target NPC placed in front of the preview NPC for targeting + * - NPC movement tracking for abilities that move the NPC (e.g. slam) + */ +@SideOnly(Side.CLIENT) +public class AbilityPreviewExecutor { + + // ==================== PREVIEW STATE ==================== + private Ability previewAbility; + private EntityNPCInterface previewNpc; + private AbilityPhase phase = AbilityPhase.IDLE; + private int currentTick = 0; + private boolean playing = false; + private boolean paused = false; + + // ==================== TIMING ==================== + private static final int EXTENDED_DURATION = 100; + private int activeDuration = 40; + + // ==================== FAKE TARGET ==================== + /** Invisible fake target NPC for ability targeting (placed 5 blocks in front of preview NPC) */ + private EntityNPCInterface fakeTarget; + private static final double TARGET_DISTANCE = 5.0; + + // ==================== NPC START POSITION ==================== + /** Saved NPC position for restoring after movement abilities */ + private double npcStartX, npcStartY, npcStartZ; + + // ==================== PREVIEW CONTENT ==================== + private TelegraphInstance previewTelegraph; + private List previewEntities = new ArrayList<>(); + + // ==================== CALLBACKS ==================== + private GuiAbilityInterface parentGui; + + public AbilityPreviewExecutor() { + } + + public void setParentGui(GuiAbilityInterface gui) { + this.parentGui = gui; + } + + /** + * Start previewing an ability. + */ + public void startPreview(Ability ability, EntityNPCInterface npc) { + stop(); + + this.previewAbility = ability; + this.previewNpc = npc; + this.phase = AbilityPhase.WINDUP; + this.currentTick = 0; + this.playing = true; + this.paused = false; + this.activeDuration = ability.getPreviewActiveDuration(); + + // Save NPC start position for restoring later + this.npcStartX = npc.posX; + this.npcStartY = npc.posY; + this.npcStartZ = npc.posZ; + + // Create invisible fake target NPC for targeting + createFakeTarget(npc); + + // Set preview target on the ability + ability.setPreviewTarget(fakeTarget); + + // Start tracking NPC movement + if (parentGui != null) { + parentGui.startTrackingMovement(); + } + + // Create telegraph for preview + previewTelegraph = ability.createTelegraph(npc, fakeTarget); + + // Start windup animation + Animation windUpAnim = ability.getWindUpAnimation(); + if (windUpAnim != null) { + AnimationData data = npc.display.animationData; + data.setEnabled(true); + data.setAnimation(windUpAnim); + data.animation.paused = false; + } + + // Spawn preview entity on first tick if ability spawns during windup + if (ability.spawnPreviewDuringWindup()) { + spawnPreviewEntity(); + } + } + + /** + * Create an invisible fake target NPC placed in front of the preview NPC. + * This entity is NOT rendered - it only exists for ability targeting logic. + */ + private void createFakeTarget(EntityNPCInterface npc) { + if (npc.worldObj == null) return; + + try { + fakeTarget = new EntityCustomNpc(npc.worldObj); + // Position target in front of NPC based on its facing direction + float facingYaw = GuiAbilityInterface.NPC_FACING_YAW; + double yawRad = Math.toRadians(facingYaw); + double targetX = npc.posX - Math.sin(yawRad) * TARGET_DISTANCE; + double targetY = npc.posY; + double targetZ = npc.posZ + Math.cos(yawRad) * TARGET_DISTANCE; + fakeTarget.setPosition(targetX, targetY, targetZ); + fakeTarget.prevPosX = targetX; + fakeTarget.prevPosY = targetY; + fakeTarget.prevPosZ = targetZ; + } catch (Exception e) { + fakeTarget = null; + } + } + + /** + * Get the fake target entity for abilities that need a target. + */ + public EntityLivingBase getFakeTarget() { + return fakeTarget; + } + + public void play() { + if (previewAbility == null || phase == AbilityPhase.IDLE) { + return; + } + playing = true; + paused = false; + + if (previewNpc != null) { + AnimationData data = previewNpc.display.animationData; + if (data.animation != null) { + data.animation.paused = false; + } + } + } + + public void pause() { + paused = true; + + if (previewNpc != null) { + AnimationData data = previewNpc.display.animationData; + if (data.animation != null) { + data.animation.paused = true; + } + } + } + + public void stop() { + playing = false; + paused = false; + phase = AbilityPhase.IDLE; + currentTick = 0; + + previewTelegraph = null; + + if (parentGui != null) { + for (Entity entity : previewEntities) { + parentGui.removePreviewEntity(entity); + } + parentGui.stopTrackingMovement(); + parentGui.setPreviewTelegraph(null); + } + previewEntities.clear(); + + // Reset NPC position to start position (undo movement from Slam/Charge/Dash) + if (previewNpc != null && npcStartX != 0 && npcStartY != 0 && npcStartZ != 0) { + previewNpc.posX = npcStartX; + previewNpc.posY = npcStartY; + previewNpc.posZ = npcStartZ; + previewNpc.prevPosX = npcStartX; + previewNpc.prevPosY = npcStartY; + previewNpc.prevPosZ = npcStartZ; + } + + // Remove fake target + fakeTarget = null; + + if (previewNpc != null) { + AnimationData data = previewNpc.display.animationData; + data.setAnimation(new Animation()); + data.animation.paused = false; + } + } + + /** + * Tick the preview forward. + */ + public void tick() { + if (!playing || paused || previewAbility == null || previewNpc == null) { + return; + } + + currentTick++; + + if (previewTelegraph != null) { + previewTelegraph.tick(null); + } + + tickEntities(); + + switch (phase) { + case WINDUP: + tickWindup(); + break; + case ACTIVE: + tickActive(); + break; + case IDLE: + tickExtended(); + break; + default: + break; + } + } + + private void tickWindup() { + previewAbility.onPreviewWindUpTick(previewNpc, currentTick); + + if (currentTick >= previewAbility.getWindUpTicks()) { + transitionToActive(); + } + } + + private void tickActive() { + previewAbility.onPreviewActiveTick(previewNpc, currentTick); + + if (currentTick == 1 && !previewAbility.spawnPreviewDuringWindup()) { + spawnPreviewEntity(); + fireEntities(); + } + + if (currentTick >= activeDuration) { + transitionToExtended(); + } + } + + private void tickExtended() { + if (currentTick >= EXTENDED_DURATION) { + playing = false; + } + } + + private void transitionToActive() { + phase = AbilityPhase.ACTIVE; + currentTick = 0; + + previewTelegraph = null; + + // Notify ability of execution (for movement abilities like Slam/Charge/Dash) + previewAbility.onPreviewExecute(previewNpc); + + fireEntities(); + + Animation activeAnim = previewAbility.getActiveAnimation(); + if (activeAnim != null && previewNpc != null) { + AnimationData data = previewNpc.display.animationData; + data.setAnimation(activeAnim); + data.animation.paused = false; + } + } + + private void transitionToExtended() { + phase = AbilityPhase.IDLE; + currentTick = 0; + + if (previewNpc != null) { + AnimationData data = previewNpc.display.animationData; + data.setAnimation(new Animation()); + } + } + + private void spawnPreviewEntity() { + Entity entity = previewAbility.createPreviewEntity(previewNpc); + if (entity != null) { + previewEntities.add(entity); + if (parentGui != null) { + parentGui.addPreviewEntity(entity); + } + } + } + + private void fireEntities() { + for (Entity entity : previewEntities) { + if (entity instanceof EntityAbilityOrb) { + ((EntityAbilityOrb) entity).startPreviewFiring(); + } else if (entity instanceof EntityAbilityBeam) { + ((EntityAbilityBeam) entity).startPreviewFiring(); + } else if (entity instanceof EntityAbilityDisc) { + ((EntityAbilityDisc) entity).startPreviewFiring(); + } + } + } + + private void tickEntities() { + Iterator iter = previewEntities.iterator(); + while (iter.hasNext()) { + Entity entity = iter.next(); + if (entity == null || entity.isDead) { + if (parentGui != null) { + parentGui.removePreviewEntity(entity); + } + iter.remove(); + continue; + } + entity.onUpdate(); + } + } + + // ==================== GETTERS ==================== + + public boolean isPlaying() { return playing; } + public boolean isPaused() { return paused; } + public boolean isActive() { return playing || paused; } + public AbilityPhase getPhase() { return phase; } + public int getCurrentTick() { return currentTick; } + public TelegraphInstance getTelegraph() { return previewTelegraph; } + public List getPreviewEntities() { return previewEntities; } + public Ability getPreviewAbility() { return previewAbility; } + public EntityNPCInterface getPreviewNpc() { return previewNpc; } + + public String getStatusString() { + if (!playing && !paused) { + return "Stopped"; + } else if (paused) { + return "Paused"; + } else if (phase == AbilityPhase.WINDUP) { + return "Windup: " + currentTick + "/" + (previewAbility != null ? previewAbility.getWindUpTicks() : 0); + } else if (phase == AbilityPhase.ACTIVE) { + return "Active: " + currentTick + "/" + activeDuration; + } else if (phase == AbilityPhase.IDLE && playing) { + return "Extended: " + currentTick + "/" + EXTENDED_DURATION; + } + return ""; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java new file mode 100644 index 000000000..c9c44ff93 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java @@ -0,0 +1,391 @@ +package noppes.npcs.client.gui.util; + +import kamkeel.npcs.client.renderer.TelegraphRenderer; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import net.minecraft.client.gui.GuiButton; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.client.gui.ScaledResolution; +import net.minecraft.client.renderer.OpenGlHelper; +import net.minecraft.client.renderer.RenderHelper; +import net.minecraft.client.renderer.entity.RenderManager; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import noppes.npcs.client.ClientEventHandler; +import noppes.npcs.client.EntityUtil; +import noppes.npcs.entity.EntityCustomNpc; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.entity.data.ModelData; +import org.lwjgl.input.Mouse; +import org.lwjgl.opengl.GL11; +import org.lwjgl.opengl.GL12; + +import java.util.ArrayList; +import java.util.List; + +/** + * Base GUI class for ability preview with fixed camera. + * + * Key concepts: + * - NPC positioned on left side of preview area + * - NPC faces a fixed direction (NPC_FACING_YAW) + * - Viewing direction can be rotated (rotates entire 3D space) + * - Fixed zoom level + * - Preview entities rendered in same coordinate space as NPC + * - NPC position changes (e.g. slam jump) tracked and rendered + */ +public class GuiAbilityInterface extends GuiNPCInterface2 { + + // ==================== FIXED CAMERA SETTINGS ==================== + /** Fixed camera zoom level (higher = more zoomed in) */ + protected static float CAMERA_ZOOM = 45.0f; + /** NPC facing direction - affects anchor points and visual facing */ + protected static float NPC_FACING_YAW = 160.0f; + /** Camera pitch (slight downward angle) */ + protected static float CAMERA_PITCH = 10.0f; + + // ==================== VIEWING ROTATION ==================== + /** Viewing direction rotation (rotates entire 3D space) */ + protected static float viewRotation = 0.0f; + + private GuiNpcButton btnLeft, btnRight; + + // ==================== NPC STATE ==================== + public ModelData playerdata; + public EntityNPCInterface npc; + + /** NPC starting position (saved when preview starts, used to track movement) */ + protected double npcStartX, npcStartY, npcStartZ; + /** Whether we're tracking NPC movement */ + protected boolean trackingNpcMovement = false; + + /** Entities to render in preview space */ + protected List previewEntities = new ArrayList<>(); + + /** Current telegraph to render in preview */ + protected TelegraphInstance previewTelegraph; + + // ==================== LAYOUT ==================== + /** Preview area bounds */ + protected int previewX, previewY, previewWidth, previewHeight; + /** NPC render position on screen */ + protected int npcScreenX, npcScreenY; + + public int xOffset = 0; + public int yOffset = 0; + + public GuiAbilityInterface(EntityNPCInterface npc) { + super(npc); + this.npc = npc; + this.playerdata = ((EntityCustomNpc) npc).modelData; + this.drawDefaultBackground = false; + + CAMERA_ZOOM = 30f; + NPC_FACING_YAW = 310f; + CAMERA_PITCH = 5f; + } + + @Override + public void initGui() { + super.initGui(); + + // Calculate preview area (left side of GUI) + previewX = guiLeft + 10; + previewY = guiTop + 10; + previewWidth = 200; + previewHeight = 180; + + // NPC position: left side of preview, slightly below center + npcScreenX = previewX + (int)(previewWidth * 0.33f) + xOffset; + npcScreenY = previewY + (int)(previewHeight * 0.85f) + yOffset; + + // Viewing rotation buttons (below preview area) + int btnY = guiTop + 192; + addButton(btnLeft = new GuiNpcButton(680, guiLeft + 10, btnY, 20, 20, "<")); + addButton(btnRight = new GuiNpcButton(681, guiLeft + 32, btnY, 20, 20, ">")); + } + + @Override + public void drawScreen(int mouseX, int mouseY, float partialTicks) { + // Handle held rotation buttons + if (Mouse.isButtonDown(0)) { + if (btnLeft != null && btnLeft.mousePressed(mc, mouseX, mouseY)) { + viewRotation += partialTicks * 2; + } else if (btnRight != null && btnRight.mousePressed(mc, mouseX, mouseY)) { + viewRotation -= partialTicks * 2; + } + } + + // Draw background and UI elements + super.drawScreen(mouseX, mouseY, partialTicks); + + // Draw the 3D preview + drawPreview(mouseX, mouseY, partialTicks); + } + + /** + * Draw the 3D preview with NPC and entities. + */ + protected void drawPreview(int mouseX, int mouseY, float partialTicks) { + GL11.glColor4f(1, 1, 1, 1); + + // Enable scissor to clip rendering to preview area + GL11.glEnable(GL11.GL_SCISSOR_TEST); + setScissorClip(previewX, previewY, previewWidth, previewHeight); + + // Get the entity to render (could be transformed player model) + EntityLivingBase renderEntity = playerdata.getEntity(npc); + if (renderEntity == null) { + renderEntity = this.npc; + } + + // Sync NPC properties to render entity + EntityUtil.Copy(npc, renderEntity); + + // Set NPC facing direction (affects anchor points and visual facing) + npc.prevRenderYawOffset = npc.renderYawOffset = NPC_FACING_YAW; + npc.prevRotationYaw = npc.rotationYaw = NPC_FACING_YAW; + npc.prevRotationYawHead = npc.rotationYawHead = NPC_FACING_YAW; + + // Also set on render entity + renderEntity.prevRenderYawOffset = renderEntity.renderYawOffset = NPC_FACING_YAW; + renderEntity.prevRotationYaw = renderEntity.rotationYaw = NPC_FACING_YAW; + renderEntity.prevRotationYawHead = renderEntity.rotationYawHead = NPC_FACING_YAW; + renderEntity.rotationPitch = 0; + + // Calculate NPC position delta for movement rendering (e.g. slam jump) + // Interpolate with partial ticks for smooth movement between frames + double npcDeltaX = 0, npcDeltaY = 0, npcDeltaZ = 0; + if (trackingNpcMovement) { + double interpX = npc.prevPosX + (npc.posX - npc.prevPosX) * partialTicks; + double interpY = npc.prevPosY + (npc.posY - npc.prevPosY) * partialTicks; + double interpZ = npc.prevPosZ + (npc.posZ - npc.prevPosZ) * partialTicks; + npcDeltaX = interpX - npcStartX; + npcDeltaY = interpY - npcStartY; + npcDeltaZ = interpZ - npcStartZ; + } + + // ==================== BEGIN 3D RENDERING ==================== + GL11.glEnable(GL11.GL_COLOR_MATERIAL); + GL11.glPushMatrix(); + + // Move to NPC render position on screen + GL11.glTranslatef(npcScreenX, npcScreenY, 50F); + + // Apply zoom (scale) + GL11.glScalef(-CAMERA_ZOOM, CAMERA_ZOOM, CAMERA_ZOOM); + + // Flip for screen coordinates (Y is inverted) + GL11.glRotatef(180F, 0.0F, 0.0F, 1.0F); + + // Apply slight camera pitch (looking down at scene) + GL11.glRotatef(CAMERA_PITCH, 1.0F, 0.0F, 0.0F); + + // Apply VIEWING rotation (rotates entire space, not just NPC) + GL11.glRotatef(viewRotation, 0.0F, 1.0F, 0.0F); + + // Setup lighting (standard item lighting for consistent colors) + GL11.glRotatef(135F, 0.0F, 1.0F, 0.0F); + RenderHelper.enableStandardItemLighting(); + GL11.glRotatef(-135F, 0.0F, 1.0F, 0.0F); + + // Adjust for entity offset + GL11.glTranslatef(0.0F, renderEntity.yOffset, 0.0F); + + RenderManager.instance.playerViewY = 180F; + ClientEventHandler.renderingEntityInGUI = true; + + // Render the NPC at its position delta (for movement like slam jumps) + try { + RenderManager.instance.renderEntityWithPosYaw(renderEntity, npcDeltaX, npcDeltaY, npcDeltaZ, 0.0F, partialTicks); + } catch (Exception e) { + playerdata.setEntityClass(null); + } + + // Render preview entities at their positions relative to NPC start position + renderPreviewEntities(partialTicks); + + // Render telegraph in preview space + renderPreviewTelegraph(partialTicks); + + ClientEventHandler.renderingEntityInGUI = false; + + GL11.glPopMatrix(); + + // Cleanup GL state + RenderHelper.disableStandardItemLighting(); + GL11.glDisable(GL12.GL_RESCALE_NORMAL); + OpenGlHelper.setActiveTexture(OpenGlHelper.lightmapTexUnit); + GL11.glDisable(GL11.GL_TEXTURE_2D); + OpenGlHelper.setActiveTexture(OpenGlHelper.defaultTexUnit); + + // Disable scissor + GL11.glDisable(GL11.GL_SCISSOR_TEST); + + // Clear depth buffer so UI renders on top + GL11.glClear(GL11.GL_DEPTH_BUFFER_BIT); + } + + /** + * Render preview entities in the same coordinate space as the NPC. + * Called WITHIN the 3D matrix transformation. + */ + protected void renderPreviewEntities(float partialTicks) { + // Save current GL state that NPC render may have changed + boolean lightingWasEnabled = GL11.glIsEnabled(GL11.GL_LIGHTING); + + // Use the start position as the reference point for entity offsets + double refX = trackingNpcMovement ? npcStartX : npc.posX; + double refY = trackingNpcMovement ? npcStartY : npc.posY; + double refZ = trackingNpcMovement ? npcStartZ : npc.posZ; + + for (Entity entity : previewEntities) { + if (entity == null || entity.isDead) continue; + + // Calculate entity position relative to reference (NPC start position) + double offsetX = entity.prevPosX + (entity.posX - entity.prevPosX) * partialTicks - refX; + double offsetY = entity.prevPosY + (entity.posY - entity.prevPosY) * partialTicks - refY; + double offsetZ = entity.prevPosZ + (entity.posZ - entity.prevPosZ) * partialTicks - refZ; + + // The NPC model was translated by npc.yOffset, so we need to subtract it + offsetY -= npc.yOffset; + + // Reset GL state before each entity to ensure colors render correctly + GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + GL11.glDisable(GL11.GL_LIGHTING); + + try { + RenderManager.instance.renderEntityWithPosYaw(entity, offsetX, offsetY, offsetZ, entity.rotationYaw, partialTicks); + } catch (Exception e) { + // Ignore rendering errors + } + } + + // Restore lighting state for any subsequent rendering + if (lightingWasEnabled) { + GL11.glEnable(GL11.GL_LIGHTING); + } + GL11.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); + } + + /** + * Render the telegraph in the 3D preview space. + * Called within the GL matrix transformation. + */ + protected void renderPreviewTelegraph(float partialTicks) { + if (previewTelegraph == null) return; + if (TelegraphRenderer.Instance == null) return; + + // Telegraph position is in world coords - convert to preview space + double refX = trackingNpcMovement ? npcStartX : npc.posX; + double refY = trackingNpcMovement ? npcStartY : npc.posY; + double refZ = trackingNpcMovement ? npcStartZ : npc.posZ; + + double offsetX = previewTelegraph.getInterpolatedX(partialTicks) - refX; + double offsetY = previewTelegraph.getInterpolatedY(partialTicks) - refY; + double offsetZ = previewTelegraph.getInterpolatedZ(partialTicks) - refZ; + + // Adjust for NPC yOffset (same as entities) + offsetY -= npc.yOffset; + + TelegraphRenderer.Instance.renderTelegraphInGUI(previewTelegraph, offsetX, offsetY, offsetZ, 1.0f, partialTicks); + } + + public void setPreviewTelegraph(TelegraphInstance telegraph) { + this.previewTelegraph = telegraph; + } + + /** + * Set scissor clip for preview area. + */ + protected void setScissorClip(int x, int y, int width, int height) { + ScaledResolution sr = new ScaledResolution(mc, mc.displayWidth, mc.displayHeight); + int scale = sr.getScaleFactor(); + int scaledY = mc.displayHeight - (y + height) * scale; + GL11.glScissor(x * scale, scaledY, width * scale, height * scale); + } + + // ==================== MOVEMENT TRACKING ==================== + + /** + * Start tracking NPC movement from current position. + */ + public void startTrackingMovement() { + npcStartX = npc.posX; + npcStartY = npc.posY; + npcStartZ = npc.posZ; + trackingNpcMovement = true; + } + + /** + * Stop tracking NPC movement. + */ + public void stopTrackingMovement() { + trackingNpcMovement = false; + } + + // ==================== PREVIEW ENTITY MANAGEMENT ==================== + + public void addPreviewEntity(Entity entity) { + if (entity != null && !previewEntities.contains(entity)) { + previewEntities.add(entity); + } + } + + public void removePreviewEntity(Entity entity) { + previewEntities.remove(entity); + } + + public void clearPreviewEntities() { + previewEntities.clear(); + } + + public List getPreviewEntities() { + return previewEntities; + } + + public float getNpcFacingYaw() { + return NPC_FACING_YAW; + } + + // ==================== BACKGROUND ==================== + + @Override + protected void drawBackground() { + super.drawBackground(); + + drawGradientRect(previewX, previewY, previewX + previewWidth, previewY + previewHeight, + 0xc0101010, 0xd0101010); + GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F); + } + + // ==================== STANDARD OVERRIDES ==================== + + @Override + public boolean doesGuiPauseGame() { + return false; + } + + @Override + public void keyTyped(char par1, int par2) { + super.keyTyped(par1, par2); + if (par2 == 1) { + close(); + } + } + + public void close() { + this.mc.displayGuiScreen((GuiScreen) null); + this.mc.setIngameFocus(); + } + + @Override + public void save() { + } + + @Override + protected void actionPerformed(GuiButton btn) { + if (btn.id == 670) { + close(); + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiModelInterface2.java b/src/main/java/noppes/npcs/client/gui/util/GuiModelInterface2.java index 8a1d015d0..cef818647 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiModelInterface2.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiModelInterface2.java @@ -18,11 +18,11 @@ import org.lwjgl.opengl.GL12; public class GuiModelInterface2 extends GuiNPCInterface2 { public ModelData playerdata; - private static float rotation = 0; + protected static float rotation = 0; private GuiNpcButton left, right, zoom, unzoom; - private static float zoomed = 60; + protected static float zoomed = 60; public int xOffset = 0; public int yOffset = 0; diff --git a/src/main/java/noppes/npcs/constants/EnumScrollData.java b/src/main/java/noppes/npcs/constants/EnumScrollData.java index 235f2719b..baf994b4e 100644 --- a/src/main/java/noppes/npcs/constants/EnumScrollData.java +++ b/src/main/java/noppes/npcs/constants/EnumScrollData.java @@ -7,6 +7,7 @@ public enum EnumScrollData { DIALOG_GROUP, QUEST_GROUP, ABILITY_TYPES, + ABILITIES, ANIMATIONS, BUILTIN_ANIMATIONS } diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index 50c150624..fef02e070 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -689,6 +689,8 @@ global.naturalspawn=Spawns global.linked=Linked global.customeffects=Effects global.magic=Magic +global.abilities=Abilities +global.auction=Auction bank.slot=Slot bank.unlock=Unlock @@ -1714,7 +1716,6 @@ ability.delete.confirm=Delete ability preset gui.load=Load # Global Abilities Manager -global.abilities=Abilities ability.manager.title=Ability Manager # Explosive Hover Text diff --git a/src/main/resources/assets/customnpcs/textures/gui/icons.png b/src/main/resources/assets/customnpcs/textures/gui/icons.png index 86bf094c4c9681be9d316b9dced36f8b635d2a81..6f2993cd1eeadf33e09f0723f3f0746d1ff045c6 100644 GIT binary patch literal 11934 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 GM*I(C=F0Z~ literal 5249 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 Date: Thu, 29 Jan 2026 14:19:57 -0500 Subject: [PATCH 309/358] Refactor to Cleaner Class Setup --- .../data/ability/data/EnergyColorData.java | 50 ++++ .../data/ability/data/EnergyCombatData.java | 51 ++++ .../data/ability/data/EnergyHomingData.java | 41 +++ .../data/ability/data/EnergyLifespanData.java | 33 +++ .../ability/data/EnergyLightningData.java | 42 +++ .../data/ability/type/AbilityDisc.java | 232 ++++++---------- .../data/ability/type/AbilityEnergyBeam.java | 246 ++++++----------- .../data/ability/type/AbilityLaserShot.java | 187 +++++-------- .../data/ability/type/AbilityOrb.java | 260 ++++++------------ .../data/ability/type/AbilitySlam.java | 5 +- .../data/ability/type/AbilitySweeper.java | 34 +-- .../npcs/entity/EntityAbilityBeam.java | 236 +++++----------- .../npcs/entity/EntityAbilityDisc.java | 204 +++++--------- .../npcs/entity/EntityAbilityLaser.java | 115 +++----- .../kamkeel/npcs/entity/EntityAbilityOrb.java | 208 +++++--------- .../npcs/entity/EntityAbilityProjectile.java | 124 ++++----- .../npcs/entity/EntityAbilitySweeper.java | 12 +- .../npcs/entity/EntityNPCInterface.java | 7 + 18 files changed, 874 insertions(+), 1213 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyColorData.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyColorData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyColorData.java new file mode 100644 index 000000000..4bd841742 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyColorData.java @@ -0,0 +1,50 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Groups visual color properties shared by all energy abilities. + * Used as a parameter object for entity constructors and ability configuration. + */ +public class EnergyColorData { + public int innerColor = 0xFFFFFF; + public int outerColor = 0x8888FF; + public boolean outerColorEnabled = true; + public float outerColorWidth = 0.4f; + public float outerColorAlpha = 0.5f; + public float rotationSpeed = 4.0f; + + public EnergyColorData() {} + + public EnergyColorData(int innerColor, int outerColor, boolean outerColorEnabled, + float outerColorWidth, float outerColorAlpha, float rotationSpeed) { + this.innerColor = innerColor; + this.outerColor = outerColor; + this.outerColorEnabled = outerColorEnabled; + this.outerColorWidth = outerColorWidth; + this.outerColorAlpha = outerColorAlpha; + this.rotationSpeed = rotationSpeed; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setInteger("innerColor", innerColor); + nbt.setInteger("outerColor", outerColor); + nbt.setBoolean("outerColorEnabled", outerColorEnabled); + nbt.setFloat("outerColorWidth", outerColorWidth); + nbt.setFloat("outerColorAlpha", outerColorAlpha); + nbt.setFloat("rotationSpeed", rotationSpeed); + } + + public void readNBT(NBTTagCompound nbt) { + innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; + outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0x8888FF; + outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); + outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; + outerColorAlpha = nbt.hasKey("outerColorAlpha") ? nbt.getFloat("outerColorAlpha") : 0.5f; + rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 4.0f; + } + + public EnergyColorData copy() { + return new EnergyColorData(innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java new file mode 100644 index 000000000..fa09645a6 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyCombatData.java @@ -0,0 +1,51 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Groups combat properties shared by energy projectile abilities. + * Used as a parameter object for entity constructors and ability configuration. + */ +public class EnergyCombatData { + public float damage = 7.0f; + public float knockback = 1.0f; + public float knockbackUp = 0.1f; + public boolean explosive = false; + public float explosionRadius = 3.0f; + public float explosionDamageFalloff = 0.5f; + + public EnergyCombatData() {} + + public EnergyCombatData(float damage, float knockback, float knockbackUp, + boolean explosive, float explosionRadius, float explosionDamageFalloff) { + this.damage = damage; + this.knockback = knockback; + this.knockbackUp = knockbackUp; + this.explosive = explosive; + this.explosionRadius = explosionRadius; + this.explosionDamageFalloff = explosionDamageFalloff; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setFloat("damage", damage); + nbt.setFloat("knockback", knockback); + nbt.setFloat("knockbackUp", knockbackUp); + nbt.setBoolean("explosive", explosive); + nbt.setFloat("explosionRadius", explosionRadius); + nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff); + } + + public void readNBT(NBTTagCompound nbt) { + damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 7.0f; + knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.0f; + knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.1f; + explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); + explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 3.0f; + explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f; + } + + public EnergyCombatData copy() { + return new EnergyCombatData(damage, knockback, knockbackUp, + explosive, explosionRadius, explosionDamageFalloff); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java new file mode 100644 index 000000000..5b90bafce --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyHomingData.java @@ -0,0 +1,41 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Groups homing/movement properties for energy projectile abilities. + * Used by Orb, Disc, and Beam (not Laser or Sweeper). + */ +public class EnergyHomingData { + public float speed = 0.5f; + public boolean homing = true; + public float homingStrength = 0.15f; + public float homingRange = 20.0f; + + public EnergyHomingData() {} + + public EnergyHomingData(float speed, boolean homing, float homingStrength, float homingRange) { + this.speed = speed; + this.homing = homing; + this.homingStrength = homingStrength; + this.homingRange = homingRange; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setFloat("speed", speed); + nbt.setBoolean("homing", homing); + nbt.setFloat("homingStrength", homingStrength); + nbt.setFloat("homingRange", homingRange); + } + + public void readNBT(NBTTagCompound nbt) { + speed = nbt.hasKey("speed") ? nbt.getFloat("speed") : 0.5f; + homing = !nbt.hasKey("homing") || nbt.getBoolean("homing"); + homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.15f; + homingRange = nbt.hasKey("homingRange") ? nbt.getFloat("homingRange") : 20.0f; + } + + public EnergyHomingData copy() { + return new EnergyHomingData(speed, homing, homingStrength, homingRange); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java new file mode 100644 index 000000000..f8f0122e8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLifespanData.java @@ -0,0 +1,33 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Groups lifespan/range properties for energy projectile abilities. + * Used as a parameter object for entity constructors and ability configuration. + */ +public class EnergyLifespanData { + public float maxDistance = 30.0f; + public int maxLifetime = 200; + + public EnergyLifespanData() {} + + public EnergyLifespanData(float maxDistance, int maxLifetime) { + this.maxDistance = maxDistance; + this.maxLifetime = maxLifetime; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setFloat("maxDistance", maxDistance); + nbt.setInteger("maxLifetime", maxLifetime); + } + + public void readNBT(NBTTagCompound nbt) { + maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 30.0f; + maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200; + } + + public EnergyLifespanData copy() { + return new EnergyLifespanData(maxDistance, maxLifetime); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java new file mode 100644 index 000000000..30d4ea6d8 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyLightningData.java @@ -0,0 +1,42 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Groups lightning visual effect properties for energy abilities. + * Used as a parameter object for entity constructors and ability configuration. + */ +public class EnergyLightningData { + public boolean lightningEffect = false; + public float lightningDensity = 0.15f; + public float lightningRadius = 0.5f; + public int lightningFadeTime = 6; + + public EnergyLightningData() {} + + public EnergyLightningData(boolean lightningEffect, float lightningDensity, + float lightningRadius, int lightningFadeTime) { + this.lightningEffect = lightningEffect; + this.lightningDensity = lightningDensity; + this.lightningRadius = lightningRadius; + this.lightningFadeTime = lightningFadeTime; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setBoolean("lightningEffect", lightningEffect); + nbt.setFloat("lightningDensity", lightningDensity); + nbt.setFloat("lightningRadius", lightningRadius); + nbt.setInteger("lightningFadeTime", lightningFadeTime); + } + + public void readNBT(NBTTagCompound nbt) { + lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); + lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; + lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; + lightningFadeTime = nbt.hasKey("lightningFadeTime") ? nbt.getInteger("lightningFadeTime") : 6; + } + + public EnergyLightningData copy() { + return new EnergyLightningData(lightningEffect, lightningDensity, lightningRadius, lightningFadeTime); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index e5c15e010..006f6f128 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -6,14 +6,21 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; +import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDisc; @@ -27,53 +34,24 @@ import noppes.npcs.entity.EntityNPCInterface; */ public class AbilityDisc extends Ability implements IAbilityDisc { - // Movement properties - private float speed = 0.6f; + // Disc geometry private float discRadius = 1.0f; private float discThickness = 0.2f; - private float maxDistance = 35.0f; - private int maxLifetime = 200; - - // Combat properties - private float damage = 8.0f; - private float knockback = 1.2f; - private float knockbackUp = 0.15f; - - // Homing properties - private boolean homing = true; - private float homingStrength = 0.12f; - private float homingRange = 18.0f; // Boomerang properties private boolean boomerang = false; private int boomerangDelay = 40; - // Explosion properties - private boolean explosive = false; - private float explosionRadius = 3.0f; - private float explosionDamageFalloff = 0.5f; - - // Effect properties - private int stunDuration = 0; - private int slowDuration = 0; - private int slowLevel = 0; - - // Visual properties - private int innerColor = 0xFFFFFF; - private int outerColor = 0xFF8800; - private float outerColorWidth = 0.4f; // Additive offset from inner size - private float outerColorAlpha = 0.5f; // Additive offset from inner size - private boolean outerColorEnabled = true; - private float rotationSpeed = 5.0f; - - // Lightning effect properties - private boolean lightningEffect = false; - private float lightningDensity = 0.15f; - private float lightningRadius = 0.5f; - // Anchor point for charging position private AnchorPoint anchorPoint = AnchorPoint.FRONT; + // Energy data classes + private EnergyColorData colorData = new EnergyColorData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f); + private EnergyCombatData combatData = new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f); + private EnergyHomingData homingData = new EnergyHomingData(0.6f, true, 0.12f, 18.0f); + private EnergyLightningData lightningData = new EnergyLightningData(); + private EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200); + // Transient state for disc entity (used during windup charging) private transient EntityAbilityDisc discEntity = null; @@ -134,20 +112,16 @@ public class AbilityDisc extends Ability implements IAbilityDisc { // Spawn disc in charging mode on first tick of windup if (tick == 1) { // Create disc in charging mode - follows NPC based on anchor point during windup - discEntity = EntityAbilityDisc.createCharging( + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(npc, anchorPoint); + discEntity = new EntityAbilityDisc( world, npc, target, - discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, - damage, knockback, knockbackUp, - speed, homing, homingStrength, homingRange, - boomerang, boomerangDelay, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime, - lightningEffect, lightningDensity, lightningRadius, - anchorPoint, // Anchor point for charging position - windUpTicks // Charge duration = windup duration - ); + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, + discRadius, discThickness, + colorData, combatData, homingData, lightningData, lifespanData, + boomerang, boomerangDelay); + discEntity.setupCharging(anchorPoint, windUpTicks); + discEntity.setEffects(this.effects); world.spawnEntityInWorld(discEntity); } } @@ -203,127 +177,83 @@ public class AbilityDisc extends Ability implements IAbilityDisc { @Override public void writeTypeNBT(NBTTagCompound nbt) { - nbt.setFloat("speed", speed); nbt.setFloat("discRadius", discRadius); nbt.setFloat("discThickness", discThickness); - nbt.setFloat("maxDistance", maxDistance); - nbt.setInteger("maxLifetime", maxLifetime); - nbt.setFloat("damage", damage); - nbt.setFloat("knockback", knockback); - nbt.setFloat("knockbackUp", knockbackUp); - nbt.setBoolean("homing", homing); - nbt.setFloat("homingStrength", homingStrength); - nbt.setFloat("homingRange", homingRange); nbt.setBoolean("boomerang", boomerang); nbt.setInteger("boomerangDelay", boomerangDelay); - nbt.setBoolean("explosive", explosive); - nbt.setFloat("explosionRadius", explosionRadius); - nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff); - nbt.setInteger("stunDuration", stunDuration); - nbt.setInteger("slowDuration", slowDuration); - nbt.setInteger("slowLevel", slowLevel); - nbt.setInteger("innerColor", innerColor); - nbt.setInteger("outerColor", outerColor); - nbt.setFloat("outerColorWidth", outerColorWidth); - nbt.setFloat("outerColorAlpha", outerColorAlpha); - nbt.setBoolean("outerColorEnabled", outerColorEnabled); - nbt.setFloat("rotationSpeed", rotationSpeed); - nbt.setBoolean("lightningEffect", lightningEffect); - nbt.setFloat("lightningDensity", lightningDensity); - nbt.setFloat("lightningRadius", lightningRadius); nbt.setInteger("anchorPoint", anchorPoint.getId()); + colorData.writeNBT(nbt); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lightningData.writeNBT(nbt); + lifespanData.writeNBT(nbt); } @Override public void readTypeNBT(NBTTagCompound nbt) { - this.speed = nbt.hasKey("speed") ? nbt.getFloat("speed") : 0.6f; this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f; this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; - this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 35.0f; - this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f; - this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.2f; - this.knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.15f; - this.homing = !nbt.hasKey("homing") || nbt.getBoolean("homing"); - this.homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.12f; - this.homingRange = nbt.hasKey("homingRange") ? nbt.getFloat("homingRange") : 18.0f; this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang"); this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40; - this.explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); - this.explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 3.0f; - this.explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; - this.slowDuration = nbt.hasKey("slowDuration") ? nbt.getInteger("slowDuration") : 0; - this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; - this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; - this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF8800; - this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; - this.outerColorAlpha = nbt.hasKey("outerColorAlpha") ? nbt.getFloat("outerColorAlpha") : 0.5f; - this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); - this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 5.0f; - this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); - this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; - this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromId(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; + colorData.readNBT(nbt); + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lightningData.readNBT(nbt); + lifespanData.readNBT(nbt); } // Getters & Setters - public float getSpeed() { return speed; } - public void setSpeed(float speed) { this.speed = speed; } + public float getSpeed() { return homingData.speed; } + public void setSpeed(float speed) { homingData.speed = speed; } public float getDiscRadius() { return discRadius; } public void setDiscRadius(float discRadius) { this.discRadius = discRadius; } public float getDiscThickness() { return discThickness; } public void setDiscThickness(float discThickness) { this.discThickness = discThickness; } - public float getMaxDistance() { return maxDistance; } - public void setMaxDistance(float maxDistance) { this.maxDistance = maxDistance; } - public int getMaxLifetime() { return maxLifetime; } - public void setMaxLifetime(int maxLifetime) { this.maxLifetime = maxLifetime; } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } - public float getKnockbackUp() { return knockbackUp; } - public void setKnockbackUp(float knockbackUp) { this.knockbackUp = knockbackUp; } - public boolean isHoming() { return homing; } - public void setHoming(boolean homing) { this.homing = homing; } - public float getHomingStrength() { return homingStrength; } - public void setHomingStrength(float homingStrength) { this.homingStrength = homingStrength; } - public float getHomingRange() { return homingRange; } - public void setHomingRange(float homingRange) { this.homingRange = homingRange; } + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; } + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int maxLifetime) { lifespanData.maxLifetime = maxLifetime; } + public float getDamage() { return combatData.damage; } + public void setDamage(float damage) { combatData.damage = damage; } + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float knockback) { combatData.knockback = knockback; } + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float knockbackUp) { combatData.knockbackUp = knockbackUp; } + public boolean isHoming() { return homingData.homing; } + public void setHoming(boolean homing) { homingData.homing = homing; } + public float getHomingStrength() { return homingData.homingStrength; } + public void setHomingStrength(float homingStrength) { homingData.homingStrength = homingStrength; } + public float getHomingRange() { return homingData.homingRange; } + public void setHomingRange(float homingRange) { homingData.homingRange = homingRange; } public boolean isBoomerang() { return boomerang; } public void setBoomerang(boolean boomerang) { this.boomerang = boomerang; } public int getBoomerangDelay() { return boomerangDelay; } public void setBoomerangDelay(int boomerangDelay) { this.boomerangDelay = boomerangDelay; } - public boolean isExplosive() { return explosive; } - public void setExplosive(boolean explosive) { this.explosive = explosive; } - public float getExplosionRadius() { return explosionRadius; } - public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; } - public float getExplosionDamageFalloff() { return explosionDamageFalloff; } - public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.explosionDamageFalloff = explosionDamageFalloff; } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } - public int getSlowDuration() { return slowDuration; } - public void setSlowDuration(int slowDuration) { this.slowDuration = slowDuration; } - public int getSlowLevel() { return slowLevel; } - public void setSlowLevel(int slowLevel) { this.slowLevel = slowLevel; } - public int getInnerColor() { return innerColor; } - public void setInnerColor(int innerColor) { this.innerColor = innerColor; } - public int getOuterColor() { return outerColor; } - public void setOuterColor(int outerColor) { this.outerColor = outerColor; } - public float getOuterColorWidth() { return outerColorWidth; } - public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; } - public float getOuterColorAlpha() { return outerColorAlpha; } - public void setOuterColorAlpha(float outerColorAlpha) { this.outerColorAlpha = outerColorAlpha; } - public boolean isOuterColorEnabled() { return outerColorEnabled; } - public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } - public float getRotationSpeed() { return rotationSpeed; } - public void setRotationSpeed(float rotationSpeed) { this.rotationSpeed = rotationSpeed; } - public boolean hasLightningEffect() { return lightningEffect; } - public void setLightningEffect(boolean lightningEffect) { this.lightningEffect = lightningEffect; } - public float getLightningDensity() { return lightningDensity; } - public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } - public float getLightningRadius() { return lightningRadius; } - public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean explosive) { combatData.explosive = explosive; } + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float explosionRadius) { combatData.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { combatData.explosionDamageFalloff = explosionDamageFalloff; } + public int getInnerColor() { return colorData.innerColor; } + public void setInnerColor(int innerColor) { colorData.innerColor = innerColor; } + public int getOuterColor() { return colorData.outerColor; } + public void setOuterColor(int outerColor) { colorData.outerColor = outerColor; } + public float getOuterColorWidth() { return colorData.outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { colorData.outerColorWidth = outerColorWidth; } + public float getOuterColorAlpha() { return colorData.outerColorAlpha; } + public void setOuterColorAlpha(float outerColorAlpha) { colorData.outerColorAlpha = outerColorAlpha; } + public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; } + public float getRotationSpeed() { return colorData.rotationSpeed; } + public void setRotationSpeed(float rotationSpeed) { colorData.rotationSpeed = rotationSpeed; } + public boolean hasLightningEffect() { return lightningData.lightningEffect; } + public void setLightningEffect(boolean lightningEffect) { lightningData.lightningEffect = lightningEffect; } + public float getLightningDensity() { return lightningData.lightningDensity; } + public void setLightningDensity(float lightningDensity) { lightningData.lightningDensity = lightningDensity; } + public float getLightningRadius() { return lightningData.lightningRadius; } + public void setLightningRadius(float lightningRadius) { lightningData.lightningRadius = lightningRadius; } public AnchorPoint getAnchorPointEnum() { return anchorPoint; } public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } @@ -338,17 +268,13 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public Entity createPreviewEntity(EntityNPCInterface npc) { if (npc == null || npc.worldObj == null) return null; - return EntityAbilityDisc.createPreview( - npc.worldObj, npc, - discRadius, discThickness, innerColor, outerColor, - outerColorEnabled, outerColorWidth, rotationSpeed, - lightningEffect, lightningDensity, lightningRadius, - anchorPoint, windUpTicks - ); + EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj); + disc.setupPreview(npc, discRadius, discThickness, colorData, lightningData, anchorPoint, windUpTicks); + return disc; } @Override public int getPreviewActiveDuration() { - return maxLifetime > 0 ? Math.min(maxLifetime, 100) : 100; + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index c2b452339..049c4c281 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -6,13 +6,20 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityEnergyBeam; @@ -27,47 +34,16 @@ import noppes.npcs.entity.EntityNPCInterface; */ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { - // Movement properties - private float speed = 0.4f; - private float maxDistance = 25.0f; - private int maxLifetime = 200; - - // Shape properties + // Shape properties (standalone) private float beamWidth = 0.4f; private float headSize = 0.6f; - // Combat properties - private float damage = 10.0f; - private float knockback = 1.5f; - private float knockbackUp = 0.2f; - - // Homing properties - private boolean homing = true; - private float homingStrength = 0.1f; - private float homingRange = 15.0f; - - // Explosion properties - private boolean explosive = false; - private float explosionRadius = 4.0f; - private float explosionDamageFalloff = 0.5f; - - // Effect properties - private int stunDuration = 0; - private int slowDuration = 0; - private int slowLevel = 0; - - // Visual properties - private int innerColor = 0xFFFFFF; - private int outerColor = 0x00AAFF; - private float outerColorWidth = 0.4f; // Additive offset from inner size - private float outerColorAlpha = 0.5f; - private boolean outerColorEnabled = true; - private float rotationSpeed = 6.0f; - - // Lightning effect properties (only on head) - private boolean lightningEffect = false; - private float lightningDensity = 0.15f; - private float lightningRadius = 0.5f; + // Data classes + private EnergyColorData colorData = new EnergyColorData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f); + private EnergyCombatData combatData = new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f); + private EnergyHomingData homingData = new EnergyHomingData(0.4f, true, 0.1f, 15.0f); + private EnergyLightningData lightningData = new EnergyLightningData(); + private EnergyLifespanData lifespanData = new EnergyLifespanData(25.0f, 200); // Anchor point for charging position private AnchorPoint anchorPoint = AnchorPoint.RIGHT_HAND; @@ -137,21 +113,16 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { float offsetDist = 1.0f; // Create beam in charging mode - follows NPC based on anchor point during windup - beamEntity = EntityAbilityBeam.createCharging( + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(npc, anchorPoint, offsetDist); + beamEntity = new EntityAbilityBeam( world, npc, target, - beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, - damage, knockback, knockbackUp, - speed, homing, homingStrength, homingRange, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime, - lightningEffect, lightningDensity, lightningRadius, - lockMovement.locksActive(), // Anchored mode during firing - windUpTicks, // Charge duration = windup duration - offsetDist, // Offset distance (used by FRONT anchor) - anchorPoint // Anchor point for charging position - ); + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, + beamWidth, headSize, + colorData, combatData, homingData, lightningData, lifespanData, + lockMovement.locksActive()); + beamEntity.setupCharging(anchorPoint, windUpTicks, offsetDist); + beamEntity.setEffects(this.effects); world.spawnEntityInWorld(beamEntity); } } @@ -207,120 +178,87 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { @Override public void writeTypeNBT(NBTTagCompound nbt) { - nbt.setFloat("speed", speed); - nbt.setFloat("maxDistance", maxDistance); - nbt.setInteger("maxLifetime", maxLifetime); nbt.setFloat("beamWidth", beamWidth); nbt.setFloat("headSize", headSize); - nbt.setFloat("damage", damage); - nbt.setFloat("knockback", knockback); - nbt.setFloat("knockbackUp", knockbackUp); - nbt.setBoolean("homing", homing); - nbt.setFloat("homingStrength", homingStrength); - nbt.setFloat("homingRange", homingRange); - nbt.setBoolean("explosive", explosive); - nbt.setFloat("explosionRadius", explosionRadius); - nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff); - nbt.setInteger("stunDuration", stunDuration); - nbt.setInteger("slowDuration", slowDuration); - nbt.setInteger("slowLevel", slowLevel); - nbt.setInteger("innerColor", innerColor); - nbt.setInteger("outerColor", outerColor); - nbt.setFloat("outerColorWidth", outerColorWidth); - nbt.setFloat("outerColorAlpha", outerColorAlpha); - nbt.setBoolean("outerColorEnabled", outerColorEnabled); - nbt.setFloat("rotationSpeed", rotationSpeed); - nbt.setBoolean("lightningEffect", lightningEffect); - nbt.setFloat("lightningDensity", lightningDensity); - nbt.setFloat("lightningRadius", lightningRadius); nbt.setInteger("anchorPoint", anchorPoint.getId()); + colorData.writeNBT(nbt); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lightningData.writeNBT(nbt); + lifespanData.writeNBT(nbt); } @Override public void readTypeNBT(NBTTagCompound nbt) { - this.speed = nbt.hasKey("speed") ? nbt.getFloat("speed") : 0.4f; - this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 25.0f; - this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200; this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f; this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 10.0f; - this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.5f; - this.knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.2f; - this.homing = !nbt.hasKey("homing") || nbt.getBoolean("homing"); - this.homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.1f; - this.homingRange = nbt.hasKey("homingRange") ? nbt.getFloat("homingRange") : 15.0f; - this.explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); - this.explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 4.0f; - this.explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; - this.slowDuration = nbt.hasKey("slowDuration") ? nbt.getInteger("slowDuration") : 0; - this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; - this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; - this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0x00AAFF; - this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; - this.outerColorAlpha = nbt.hasKey("outerColorAlpha") ? nbt.getFloat("outerColorAlpha") : 0.5f; - this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); - this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 6.0f; - this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); - this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; - this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromId(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; + colorData.readNBT(nbt); + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lightningData.readNBT(nbt); + lifespanData.readNBT(nbt); } - // Getters & Setters - public float getSpeed() { return speed; } - public void setSpeed(float speed) { this.speed = speed; } - public float getMaxDistance() { return maxDistance; } - public void setMaxDistance(float maxDistance) { this.maxDistance = maxDistance; } - public int getMaxLifetime() { return maxLifetime; } - public void setMaxLifetime(int maxLifetime) { this.maxLifetime = maxLifetime; } + // Getters & Setters - Standalone fields public float getBeamWidth() { return beamWidth; } public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; } public float getHeadSize() { return headSize; } public void setHeadSize(float headSize) { this.headSize = headSize; } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } - public float getKnockbackUp() { return knockbackUp; } - public void setKnockbackUp(float knockbackUp) { this.knockbackUp = knockbackUp; } - public boolean isHoming() { return homing; } - public void setHoming(boolean homing) { this.homing = homing; } - public float getHomingStrength() { return homingStrength; } - public void setHomingStrength(float homingStrength) { this.homingStrength = homingStrength; } - public float getHomingRange() { return homingRange; } - public void setHomingRange(float homingRange) { this.homingRange = homingRange; } - public boolean isExplosive() { return explosive; } - public void setExplosive(boolean explosive) { this.explosive = explosive; } - public float getExplosionRadius() { return explosionRadius; } - public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; } - public float getExplosionDamageFalloff() { return explosionDamageFalloff; } - public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.explosionDamageFalloff = explosionDamageFalloff; } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } - public int getSlowDuration() { return slowDuration; } - public void setSlowDuration(int slowDuration) { this.slowDuration = slowDuration; } - public int getSlowLevel() { return slowLevel; } - public void setSlowLevel(int slowLevel) { this.slowLevel = slowLevel; } - public int getInnerColor() { return innerColor; } - public void setInnerColor(int innerColor) { this.innerColor = innerColor; } - public int getOuterColor() { return outerColor; } - public void setOuterColor(int outerColor) { this.outerColor = outerColor; } - public float getOuterColorWidth() { return outerColorWidth; } - public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; } - public float getOuterColorAlpha() { return outerColorAlpha; } - public void setOuterColorAlpha(float outerColorAlpha) { this.outerColorAlpha = outerColorAlpha; } - public boolean isOuterColorEnabled() { return outerColorEnabled; } - public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } - public float getRotationSpeed() { return rotationSpeed; } - public void setRotationSpeed(float rotationSpeed) { this.rotationSpeed = rotationSpeed; } - public boolean hasLightningEffect() { return lightningEffect; } - public void setLightningEffect(boolean lightningEffect) { this.lightningEffect = lightningEffect; } - public float getLightningDensity() { return lightningDensity; } - public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } - public float getLightningRadius() { return lightningRadius; } - public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } + // Getters & Setters - Color data + public int getInnerColor() { return colorData.innerColor; } + public void setInnerColor(int innerColor) { this.colorData.innerColor = innerColor; } + public int getOuterColor() { return colorData.outerColor; } + public void setOuterColor(int outerColor) { this.colorData.outerColor = outerColor; } + public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { this.colorData.outerColorEnabled = outerColorEnabled; } + public float getOuterColorWidth() { return colorData.outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { this.colorData.outerColorWidth = outerColorWidth; } + public float getOuterColorAlpha() { return colorData.outerColorAlpha; } + public void setOuterColorAlpha(float outerColorAlpha) { this.colorData.outerColorAlpha = outerColorAlpha; } + public float getRotationSpeed() { return colorData.rotationSpeed; } + public void setRotationSpeed(float rotationSpeed) { this.colorData.rotationSpeed = rotationSpeed; } + + // Getters & Setters - Combat data + public float getDamage() { return combatData.damage; } + public void setDamage(float damage) { this.combatData.damage = damage; } + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float knockback) { this.combatData.knockback = knockback; } + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; } + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; } + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; } + + // Getters & Setters - Homing data + public float getSpeed() { return homingData.speed; } + public void setSpeed(float speed) { this.homingData.speed = speed; } + public boolean isHoming() { return homingData.homing; } + public void setHoming(boolean homing) { this.homingData.homing = homing; } + public float getHomingStrength() { return homingData.homingStrength; } + public void setHomingStrength(float homingStrength) { this.homingData.homingStrength = homingStrength; } + public float getHomingRange() { return homingData.homingRange; } + public void setHomingRange(float homingRange) { this.homingData.homingRange = homingRange; } + + // Getters & Setters - Lightning data + public boolean hasLightningEffect() { return lightningData.lightningEffect; } + public void setLightningEffect(boolean lightningEffect) { this.lightningData.lightningEffect = lightningEffect; } + public float getLightningDensity() { return lightningData.lightningDensity; } + public void setLightningDensity(float lightningDensity) { this.lightningData.lightningDensity = lightningDensity; } + public float getLightningRadius() { return lightningData.lightningRadius; } + public void setLightningRadius(float lightningRadius) { this.lightningData.lightningRadius = lightningRadius; } + + // Getters & Setters - Lifespan data + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; } + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; } + + // Getters & Setters - Anchor point public AnchorPoint getAnchorPointEnum() { return anchorPoint; } public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } @@ -335,17 +273,13 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { public Entity createPreviewEntity(EntityNPCInterface npc) { if (npc == null || npc.worldObj == null) return null; - return EntityAbilityBeam.createPreview( - npc.worldObj, npc, - beamWidth, headSize, innerColor, outerColor, - outerColorEnabled, outerColorWidth, rotationSpeed, - lightningEffect, lightningDensity, lightningRadius, - anchorPoint, windUpTicks, 1.0f - ); + EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj); + beam.setupPreview(npc, beamWidth, headSize, colorData, lightningData, anchorPoint, windUpTicks, 1.0f); + return beam; } @Override public int getPreviewActiveDuration() { - return maxLifetime > 0 ? Math.min(maxLifetime, 100) : 100; + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index c1a94e657..3a25b9aa4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -5,6 +5,10 @@ import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityLaser; import net.minecraft.entity.Entity; @@ -28,35 +32,12 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { private float laserWidth = 0.3f; private float expansionSpeed = 3.0f; private int lingerTicks = 8; - private float maxDistance = 40.0f; - private int maxLifetime = 100; - // Combat properties - private float damage = 6.0f; - private float knockback = 0.5f; - private float knockbackUp = 0.05f; - - // Explosion properties - private boolean explosive = false; - private float explosionRadius = 2.0f; - private float explosionDamageFalloff = 0.5f; - - // Effect properties - private int stunDuration = 0; - private int slowDuration = 0; - private int slowLevel = 0; - - // Visual properties - private int innerColor = 0xFFFFFF; - private int outerColor = 0xFF0000; - private float outerColorWidth = 0.4f; // Additive offset from inner width - private float outerColorAlpha = 0.5f; - private boolean outerColorEnabled = true; - - // Lightning effect properties - private boolean lightningEffect = false; - private float lightningDensity = 0.15f; - private float lightningRadius = 0.5f; + // Data classes + private EnergyColorData colorData = new EnergyColorData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f); + private EnergyCombatData combatData = new EnergyCombatData(6.0f, 0.5f, 0.05f, false, 2.0f, 0.5f); + private EnergyLightningData lightningData = new EnergyLightningData(); + private EnergyLifespanData lifespanData = new EnergyLifespanData(40.0f, 100); // Transient state for laser entity (used for movement locking) private transient EntityAbilityLaser laserEntity = null; @@ -97,7 +78,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { @Override public float getTelegraphLength() { - return maxDistance; + return lifespanData.maxDistance; } @Override @@ -119,15 +100,12 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { laserEntity = new EntityAbilityLaser( world, npc, target, spawnX, spawnY, spawnZ, - laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, - damage, knockback, knockbackUp, - expansionSpeed, lingerTicks, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime, - lightningEffect, lightningDensity, lightningRadius + laserWidth, + colorData, combatData, lightningData, lifespanData, + expansionSpeed, lingerTicks ); + laserEntity.setEffects(this.effects); world.spawnEntityInWorld(laserEntity); // Ability stays active until entity dies (prevents firing another while projectile is alive) @@ -165,25 +143,10 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { nbt.setFloat("laserWidth", laserWidth); nbt.setFloat("expansionSpeed", expansionSpeed); nbt.setInteger("lingerTicks", lingerTicks); - nbt.setFloat("maxDistance", maxDistance); - nbt.setInteger("maxLifetime", maxLifetime); - nbt.setFloat("damage", damage); - nbt.setFloat("knockback", knockback); - nbt.setFloat("knockbackUp", knockbackUp); - nbt.setBoolean("explosive", explosive); - nbt.setFloat("explosionRadius", explosionRadius); - nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff); - nbt.setInteger("stunDuration", stunDuration); - nbt.setInteger("slowDuration", slowDuration); - nbt.setInteger("slowLevel", slowLevel); - nbt.setInteger("innerColor", innerColor); - nbt.setInteger("outerColor", outerColor); - nbt.setFloat("outerColorWidth", outerColorWidth); - nbt.setFloat("outerColorAlpha", outerColorAlpha); - nbt.setBoolean("outerColorEnabled", outerColorEnabled); - nbt.setBoolean("lightningEffect", lightningEffect); - nbt.setFloat("lightningDensity", lightningDensity); - nbt.setFloat("lightningRadius", lightningRadius); + colorData.writeNBT(nbt); + combatData.writeNBT(nbt); + lightningData.writeNBT(nbt); + lifespanData.writeNBT(nbt); } @Override @@ -191,90 +154,72 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { this.laserWidth = nbt.hasKey("laserWidth") ? nbt.getFloat("laserWidth") : 0.3f; this.expansionSpeed = nbt.hasKey("expansionSpeed") ? nbt.getFloat("expansionSpeed") : 3.0f; this.lingerTicks = nbt.hasKey("lingerTicks") ? nbt.getInteger("lingerTicks") : 8; - this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 40.0f; - this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 100; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 6.0f; - this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 0.5f; - this.knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.05f; - this.explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); - this.explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 2.0f; - this.explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; - this.slowDuration = nbt.hasKey("slowDuration") ? nbt.getInteger("slowDuration") : 0; - this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; - this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; - this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF0000; - this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; - this.outerColorAlpha = nbt.hasKey("outerColorAlpha") ? nbt.getFloat("outerColorAlpha") : 0.5f; - this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); - this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); - this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; - this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; + colorData.readNBT(nbt); + combatData.readNBT(nbt); + lightningData.readNBT(nbt); + lifespanData.readNBT(nbt); } - // Getters & Setters + // Getters & Setters - Standalone fields public float getLaserWidth() { return laserWidth; } public void setLaserWidth(float laserWidth) { this.laserWidth = laserWidth; } public float getExpansionSpeed() { return expansionSpeed; } public void setExpansionSpeed(float expansionSpeed) { this.expansionSpeed = expansionSpeed; } public int getLingerTicks() { return lingerTicks; } public void setLingerTicks(int lingerTicks) { this.lingerTicks = lingerTicks; } - public float getMaxDistance() { return maxDistance; } - public void setMaxDistance(float maxDistance) { this.maxDistance = maxDistance; } - public int getMaxLifetime() { return maxLifetime; } - public void setMaxLifetime(int maxLifetime) { this.maxLifetime = maxLifetime; } - public float getDamage() { return damage; } - public void setDamage(float damage) { this.damage = damage; } - public float getKnockback() { return knockback; } - public void setKnockback(float knockback) { this.knockback = knockback; } - public float getKnockbackUp() { return knockbackUp; } - public void setKnockbackUp(float knockbackUp) { this.knockbackUp = knockbackUp; } - public boolean isExplosive() { return explosive; } - public void setExplosive(boolean explosive) { this.explosive = explosive; } - public float getExplosionRadius() { return explosionRadius; } - public void setExplosionRadius(float explosionRadius) { this.explosionRadius = explosionRadius; } - public float getExplosionDamageFalloff() { return explosionDamageFalloff; } - public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.explosionDamageFalloff = explosionDamageFalloff; } - public int getStunDuration() { return stunDuration; } - public void setStunDuration(int stunDuration) { this.stunDuration = stunDuration; } - public int getSlowDuration() { return slowDuration; } - public void setSlowDuration(int slowDuration) { this.slowDuration = slowDuration; } - public int getSlowLevel() { return slowLevel; } - public void setSlowLevel(int slowLevel) { this.slowLevel = slowLevel; } - public int getInnerColor() { return innerColor; } - public void setInnerColor(int innerColor) { this.innerColor = innerColor; } - public int getOuterColor() { return outerColor; } - public void setOuterColor(int outerColor) { this.outerColor = outerColor; } - public float getOuterColorWidth() { return outerColorWidth; } - public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; } - public float getOuterColorAlpha() { return outerColorAlpha; } - public void setOuterColorAlpha(float outerColorAlpha) { this.outerColorAlpha = outerColorAlpha; } - public boolean isOuterColorEnabled() { return outerColorEnabled; } - public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } - public boolean hasLightningEffect() { return lightningEffect; } - public void setLightningEffect(boolean lightningEffect) { this.lightningEffect = lightningEffect; } - public float getLightningDensity() { return lightningDensity; } - public void setLightningDensity(float lightningDensity) { this.lightningDensity = lightningDensity; } - public float getLightningRadius() { return lightningRadius; } - public void setLightningRadius(float lightningRadius) { this.lightningRadius = lightningRadius; } + + // Getters & Setters - Lifespan data + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; } + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; } + + // Getters & Setters - Combat data + public float getDamage() { return combatData.damage; } + public void setDamage(float damage) { this.combatData.damage = damage; } + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float knockback) { this.combatData.knockback = knockback; } + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; } + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; } + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; } + // Getters & Setters - Color data + public int getInnerColor() { return colorData.innerColor; } + public void setInnerColor(int innerColor) { this.colorData.innerColor = innerColor; } + public int getOuterColor() { return colorData.outerColor; } + public void setOuterColor(int outerColor) { this.colorData.outerColor = outerColor; } + public float getOuterColorWidth() { return colorData.outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { this.colorData.outerColorWidth = outerColorWidth; } + public float getOuterColorAlpha() { return colorData.outerColorAlpha; } + public void setOuterColorAlpha(float outerColorAlpha) { this.colorData.outerColorAlpha = outerColorAlpha; } + public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { this.colorData.outerColorEnabled = outerColorEnabled; } + + // Getters & Setters - Lightning data + public boolean hasLightningEffect() { return lightningData.lightningEffect; } + public void setLightningEffect(boolean lightningEffect) { this.lightningData.lightningEffect = lightningEffect; } + public float getLightningDensity() { return lightningData.lightningDensity; } + public void setLightningDensity(float lightningDensity) { this.lightningData.lightningDensity = lightningDensity; } + public float getLightningRadius() { return lightningData.lightningRadius; } + public void setLightningRadius(float lightningRadius) { this.lightningData.lightningRadius = lightningRadius; } @Override @SideOnly(Side.CLIENT) public Entity createPreviewEntity(EntityNPCInterface npc) { if (npc == null || npc.worldObj == null) return null; - return EntityAbilityLaser.createPreview( - npc.worldObj, npc, - laserWidth, innerColor, outerColor, - outerColorEnabled, outerColorWidth, - expansionSpeed, maxDistance, - lightningEffect, lightningDensity, lightningRadius - ); + EntityAbilityLaser laser = new EntityAbilityLaser(npc.worldObj); + laser.setupPreview(npc, laserWidth, colorData, lightningData, expansionSpeed, lifespanData.maxDistance); + return laser; } @Override public int getPreviewActiveDuration() { - return maxLifetime > 0 ? Math.min(maxLifetime, 60) : 60; + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 60) : 60; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 13c57478a..6c7e8c5b6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -6,14 +6,21 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; +import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityOrb; @@ -28,49 +35,19 @@ import noppes.npcs.entity.EntityNPCInterface; */ public class AbilityOrb extends Ability implements IAbilityOrb { - // Movement properties - private float orbSpeed = 0.5f; + // Ability-specific properties private float orbSize = 1.0f; - private float maxDistance = 30.0f; // Max distance before orb dies - private int maxLifetime = 200; // Ticks the orb lives - - // Combat properties - private float damage = 7.0f; - private float knockback = 1.0f; - private float knockbackUp = 0.1f; - - // Homing properties - private boolean homing = true; - private float homingStrength = 0.15f; - private float homingRange = 20.0f; - - // Explosion properties - private boolean explosive = false; - private float explosionRadius = 3.0f; - private float explosionDamageFalloff = 0.5f; - - // Effect properties - private int stunDuration = 0; - private int slowDuration = 0; - private int slowLevel = 0; - - // Visual properties - private int innerColor = 0xFFFFFF; // White core - private int outerColor = 0x8888FF; // Light blue glow - private float outerColorWidth = 0.4f; // Additive offset from inner size - private float outerColorAlpha = 0.5f; - private boolean outerColorEnabled = true; // Whether to render outer glow - private float rotationSpeed = 4.0f; // Degrees per tick - - // Lightning effect properties - private boolean lightningEffect = false; - private float lightningDensity = 0.15f; // Bolts spawned per tick (0.15 = ~15% chance per frame) - private float lightningRadius = 0.5f; // Max distance lightning arcs - scales with orbSize - private int lightningFadeTime = 6; // Ticks before lightning fades out // Anchor point for charging position private AnchorPoint anchorPoint = AnchorPoint.RIGHT_HAND; + // Data classes for energy properties + public final EnergyColorData colorData = new EnergyColorData(); + public final EnergyCombatData combatData = new EnergyCombatData(); + public final EnergyHomingData homingData = new EnergyHomingData(); + public final EnergyLightningData lightningData = new EnergyLightningData(); + public final EnergyLifespanData lifespanData = new EnergyLifespanData(); + // Transient state for orb entity (used during windup charging) private transient EntityAbilityOrb orbEntity = null; @@ -133,19 +110,14 @@ public class AbilityOrb extends Ability implements IAbilityOrb { // Spawn orb in charging mode on first tick of windup if (tick == 1) { // Create orb in charging mode - follows NPC based on anchor point during windup - orbEntity = EntityAbilityOrb.createCharging( + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(npc, anchorPoint); + orbEntity = new EntityAbilityOrb( world, npc, target, - orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, - damage, knockback, knockbackUp, - orbSpeed, homing, homingStrength, homingRange, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime, - lightningEffect, lightningDensity, lightningRadius, lightningFadeTime, - anchorPoint, // Anchor point for charging position - windUpTicks // Charge duration = windup duration - ); + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize, + colorData, combatData, homingData, lightningData, lifespanData); + orbEntity.setupCharging(anchorPoint, windUpTicks); + orbEntity.setEffects(this.effects); world.spawnEntityInWorld(orbEntity); } } @@ -202,73 +174,39 @@ public class AbilityOrb extends Ability implements IAbilityOrb { @Override public void writeTypeNBT(NBTTagCompound nbt) { - nbt.setFloat("orbSpeed", orbSpeed); nbt.setFloat("orbSize", orbSize); - nbt.setFloat("maxDistance", maxDistance); - nbt.setInteger("maxLifetime", maxLifetime); - nbt.setFloat("damage", damage); - nbt.setFloat("knockback", knockback); - nbt.setFloat("knockbackUp", knockbackUp); - nbt.setBoolean("homing", homing); - nbt.setFloat("homingStrength", homingStrength); - nbt.setFloat("homingRange", homingRange); - nbt.setBoolean("explosive", explosive); - nbt.setFloat("explosionRadius", explosionRadius); - nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff); - nbt.setInteger("stunDuration", stunDuration); - nbt.setInteger("slowDuration", slowDuration); - nbt.setInteger("slowLevel", slowLevel); - nbt.setInteger("innerColor", innerColor); - nbt.setInteger("outerColor", outerColor); - nbt.setFloat("outerColorWidth", outerColorWidth); - nbt.setFloat("outerColorAlpha", outerColorAlpha); - nbt.setBoolean("outerColorEnabled", outerColorEnabled); - nbt.setFloat("rotationSpeed", rotationSpeed); - nbt.setBoolean("lightningEffect", lightningEffect); - nbt.setFloat("lightningDensity", lightningDensity); - nbt.setFloat("lightningRadius", lightningRadius); - nbt.setInteger("lightningFadeTime", lightningFadeTime); nbt.setInteger("anchorPoint", anchorPoint.getId()); + colorData.writeNBT(nbt); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lightningData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + // Backward compat: old key was "orbSpeed" + nbt.setFloat("orbSpeed", homingData.speed); } @Override public void readTypeNBT(NBTTagCompound nbt) { - this.orbSpeed = nbt.hasKey("orbSpeed") ? nbt.getFloat("orbSpeed") : 0.5f; this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f; - this.maxDistance = nbt.hasKey("maxDistance") ? nbt.getFloat("maxDistance") : 30.0f; - this.maxLifetime = nbt.hasKey("maxLifetime") ? nbt.getInteger("maxLifetime") : 200; - this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 7.0f; - this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.0f; - this.knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.1f; - this.homing = !nbt.hasKey("homing") || nbt.getBoolean("homing"); - this.homingStrength = nbt.hasKey("homingStrength") ? nbt.getFloat("homingStrength") : 0.15f; - this.homingRange = nbt.hasKey("homingRange") ? nbt.getFloat("homingRange") : 20.0f; - this.explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive"); - this.explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 3.0f; - this.explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f; - this.stunDuration = nbt.hasKey("stunDuration") ? nbt.getInteger("stunDuration") : 0; - this.slowDuration = nbt.hasKey("slowDuration") ? nbt.getInteger("slowDuration") : 0; - this.slowLevel = nbt.hasKey("slowLevel") ? nbt.getInteger("slowLevel") : 0; - this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFFFFFF; - this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0x8888FF; - this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 0.4f; - this.outerColorAlpha = nbt.hasKey("outerColorAlpha") ? nbt.getFloat("outerColorAlpha") : 0.5f; - this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); - this.rotationSpeed = nbt.hasKey("rotationSpeed") ? nbt.getFloat("rotationSpeed") : 4.0f; - this.lightningEffect = nbt.hasKey("lightningEffect") && nbt.getBoolean("lightningEffect"); - this.lightningDensity = nbt.hasKey("lightningDensity") ? nbt.getFloat("lightningDensity") : 0.15f; - this.lightningRadius = nbt.hasKey("lightningRadius") ? nbt.getFloat("lightningRadius") : 0.5f; - this.lightningFadeTime = nbt.hasKey("lightningFadeTime") ? nbt.getInteger("lightningFadeTime") : 6; this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromId(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; + colorData.readNBT(nbt); + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lightningData.readNBT(nbt); + lifespanData.readNBT(nbt); + // Backward compat: old key was "orbSpeed" + if (nbt.hasKey("orbSpeed")) { + homingData.speed = nbt.getFloat("orbSpeed"); + } } // Getters & Setters public float getOrbSpeed() { - return orbSpeed; + return homingData.speed; } public void setOrbSpeed(float orbSpeed) { - this.orbSpeed = orbSpeed; + homingData.speed = orbSpeed; } public float getOrbSize() { @@ -280,195 +218,171 @@ public class AbilityOrb extends Ability implements IAbilityOrb { } public float getMaxDistance() { - return maxDistance; + return lifespanData.maxDistance; } public void setMaxDistance(float maxDistance) { - this.maxDistance = maxDistance; + lifespanData.maxDistance = maxDistance; } public int getMaxLifetime() { - return maxLifetime; + return lifespanData.maxLifetime; } public void setMaxLifetime(int maxLifetime) { - this.maxLifetime = maxLifetime; + lifespanData.maxLifetime = maxLifetime; } public float getDamage() { - return damage; + return combatData.damage; } public void setDamage(float damage) { - this.damage = damage; + combatData.damage = damage; } public float getKnockback() { - return knockback; + return combatData.knockback; } public void setKnockback(float knockback) { - this.knockback = knockback; + combatData.knockback = knockback; } public float getKnockbackUp() { - return knockbackUp; + return combatData.knockbackUp; } public void setKnockbackUp(float knockbackUp) { - this.knockbackUp = knockbackUp; + combatData.knockbackUp = knockbackUp; } public boolean isHoming() { - return homing; + return homingData.homing; } public void setHoming(boolean homing) { - this.homing = homing; + homingData.homing = homing; } public float getHomingStrength() { - return homingStrength; + return homingData.homingStrength; } public void setHomingStrength(float homingStrength) { - this.homingStrength = homingStrength; + homingData.homingStrength = homingStrength; } public float getHomingRange() { - return homingRange; + return homingData.homingRange; } public void setHomingRange(float homingRange) { - this.homingRange = homingRange; + homingData.homingRange = homingRange; } public boolean isExplosive() { - return explosive; + return combatData.explosive; } public void setExplosive(boolean explosive) { - this.explosive = explosive; + combatData.explosive = explosive; } public float getExplosionRadius() { - return explosionRadius; + return combatData.explosionRadius; } public void setExplosionRadius(float explosionRadius) { - this.explosionRadius = explosionRadius; + combatData.explosionRadius = explosionRadius; } public float getExplosionDamageFalloff() { - return explosionDamageFalloff; + return combatData.explosionDamageFalloff; } public void setExplosionDamageFalloff(float explosionDamageFalloff) { - this.explosionDamageFalloff = explosionDamageFalloff; - } - - public int getStunDuration() { - return stunDuration; - } - - public void setStunDuration(int stunDuration) { - this.stunDuration = stunDuration; - } - - public int getSlowDuration() { - return slowDuration; - } - - public void setSlowDuration(int slowDuration) { - this.slowDuration = slowDuration; - } - - public int getSlowLevel() { - return slowLevel; - } - - public void setSlowLevel(int slowLevel) { - this.slowLevel = slowLevel; + combatData.explosionDamageFalloff = explosionDamageFalloff; } public int getInnerColor() { - return innerColor; + return colorData.innerColor; } public void setInnerColor(int innerColor) { - this.innerColor = innerColor; + colorData.innerColor = innerColor; } public int getOuterColor() { - return outerColor; + return colorData.outerColor; } public void setOuterColor(int outerColor) { - this.outerColor = outerColor; + colorData.outerColor = outerColor; } public float getOuterColorWidth() { - return outerColorWidth; + return colorData.outerColorWidth; } public void setOuterColorWidth(float outerColorWidth) { - this.outerColorWidth = outerColorWidth; + colorData.outerColorWidth = outerColorWidth; } public float getOuterColorAlpha() { - return outerColorAlpha; + return colorData.outerColorAlpha; } public void setOuterColorAlpha(float outerColorAlpha) { - this.outerColorAlpha = outerColorAlpha; + colorData.outerColorAlpha = outerColorAlpha; } public boolean isOuterColorEnabled() { - return outerColorEnabled; + return colorData.outerColorEnabled; } public void setOuterColorEnabled(boolean outerColorEnabled) { - this.outerColorEnabled = outerColorEnabled; + colorData.outerColorEnabled = outerColorEnabled; } public float getRotationSpeed() { - return rotationSpeed; + return colorData.rotationSpeed; } public void setRotationSpeed(float rotationSpeed) { - this.rotationSpeed = rotationSpeed; + colorData.rotationSpeed = rotationSpeed; } public boolean hasLightningEffect() { - return lightningEffect; + return lightningData.lightningEffect; } public void setLightningEffect(boolean lightningEffect) { - this.lightningEffect = lightningEffect; + lightningData.lightningEffect = lightningEffect; } public float getLightningDensity() { - return lightningDensity; + return lightningData.lightningDensity; } public void setLightningDensity(float lightningDensity) { - this.lightningDensity = lightningDensity; + lightningData.lightningDensity = lightningDensity; } public float getLightningRadius() { - return lightningRadius; + return lightningData.lightningRadius; } public void setLightningRadius(float lightningRadius) { - this.lightningRadius = lightningRadius; + lightningData.lightningRadius = lightningRadius; } public int getLightningFadeTime() { - return lightningFadeTime; + return lightningData.lightningFadeTime; } public void setLightningFadeTime(int lightningFadeTime) { - this.lightningFadeTime = lightningFadeTime; + lightningData.lightningFadeTime = lightningFadeTime; } public AnchorPoint getAnchorPointEnum() { @@ -493,17 +407,13 @@ public class AbilityOrb extends Ability implements IAbilityOrb { public Entity createPreviewEntity(EntityNPCInterface npc) { if (npc == null || npc.worldObj == null) return null; - return EntityAbilityOrb.createPreview( - npc.worldObj, npc, - orbSize, innerColor, outerColor, - outerColorEnabled, outerColorWidth, rotationSpeed, - lightningEffect, lightningDensity, lightningRadius, lightningFadeTime, - anchorPoint, windUpTicks - ); + EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj); + orb.setupPreview(npc, orbSize, colorData, lightningData, anchorPoint, windUpTicks); + return orb; } @Override public int getPreviewActiveDuration() { - return maxLifetime > 0 ? Math.min(maxLifetime, 100) : 100; + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index a46100c96..46faef9c9 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -58,7 +58,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam { this.maxRange = 15.0f; this.telegraphType = TelegraphType.CIRCLE; this.windUpSound = "mob.irongolem.throw"; - this.activeSound = "random.explode"; + this.activeSound = ""; } @Override @@ -278,6 +278,9 @@ public class AbilitySlam extends Ability implements IAbilitySlam { if (world.isRemote) return; + // Play slam impact sound on landing + world.playSoundAtEntity(npc, "random.explode", 1.0f, 1.0f); + // Find all entities in radius @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABBExcludingEntity( diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index d2dced201..976d9e44e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; @@ -41,10 +42,7 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { private boolean lockOnTarget = false; // Visual properties - private int innerColor = 0xFF6600; - private int outerColor = 0xFF0000; - private float outerColorWidth = 1.8f; - private boolean outerColorEnabled = true; + private EnergyColorData colorData = new EnergyColorData(0xFF6600, 0xFF0000, true, 1.8f, 0.5f, 0.0f); // Runtime state (transient) private transient EntityAbilitySweeper activeEntity = null; @@ -94,7 +92,7 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { // Spawn the entity that handles BOTH visuals AND damage activeEntity = new EntityAbilitySweeper(world, npc, target, beamLength, beamWidth, beamHeight, - innerColor, outerColor, outerColorEnabled, outerColorWidth, + colorData, sweepSpeed, numberOfRotations, damage, damageInterval, piercing, lockOnTarget); @@ -163,10 +161,7 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { nbt.setFloat("sweepSpeed", sweepSpeed); nbt.setInteger("numberOfRotations", numberOfRotations); nbt.setBoolean("lockOnTarget", lockOnTarget); - nbt.setInteger("innerColor", innerColor); - nbt.setInteger("outerColor", outerColor); - nbt.setFloat("outerColorWidth", outerColorWidth); - nbt.setBoolean("outerColorEnabled", outerColorEnabled); + colorData.writeNBT(nbt); } @Override @@ -180,10 +175,7 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { this.sweepSpeed = nbt.hasKey("sweepSpeed") ? nbt.getFloat("sweepSpeed") : 3.0f; this.numberOfRotations = nbt.hasKey("numberOfRotations") ? nbt.getInteger("numberOfRotations") : 2; this.lockOnTarget = nbt.hasKey("lockOnTarget") && nbt.getBoolean("lockOnTarget"); - this.innerColor = nbt.hasKey("innerColor") ? nbt.getInteger("innerColor") : 0xFF6600; - this.outerColor = nbt.hasKey("outerColor") ? nbt.getInteger("outerColor") : 0xFF0000; - this.outerColorWidth = nbt.hasKey("outerColorWidth") ? nbt.getFloat("outerColorWidth") : 1.8f; - this.outerColorEnabled = !nbt.hasKey("outerColorEnabled") || nbt.getBoolean("outerColorEnabled"); + colorData.readNBT(nbt); } // Getters & Setters @@ -205,12 +197,12 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { public void setNumberOfRotations(int numberOfRotations) { this.numberOfRotations = numberOfRotations; } public boolean isLockOnTarget() { return lockOnTarget; } public void setLockOnTarget(boolean lockOnTarget) { this.lockOnTarget = lockOnTarget; } - public int getInnerColor() { return innerColor; } - public void setInnerColor(int innerColor) { this.innerColor = innerColor; } - public int getOuterColor() { return outerColor; } - public void setOuterColor(int outerColor) { this.outerColor = outerColor; } - public float getOuterColorWidth() { return outerColorWidth; } - public void setOuterColorWidth(float outerColorWidth) { this.outerColorWidth = outerColorWidth; } - public boolean isOuterColorEnabled() { return outerColorEnabled; } - public void setOuterColorEnabled(boolean outerColorEnabled) { this.outerColorEnabled = outerColorEnabled; } + public int getInnerColor() { return colorData.innerColor; } + public void setInnerColor(int innerColor) { colorData.innerColor = innerColor; } + public int getOuterColor() { return colorData.outerColor; } + public void setOuterColor(int outerColor) { colorData.outerColor = outerColor; } + public float getOuterColorWidth() { return colorData.outerColorWidth; } + public void setOuterColorWidth(float outerColorWidth) { colorData.outerColorWidth = outerColorWidth; } + public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } + public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index c841ee0f9..ca600b84f 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -1,6 +1,11 @@ package kamkeel.npcs.entity; import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; @@ -103,79 +108,27 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { } /** - * Full constructor with all parameters (no lightning). - */ - public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, - double x, double y, double z, - float beamWidth, float headSize, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, - float damage, float knockback, float knockbackUp, - float speed, boolean homing, float homingStrength, float homingRange, - boolean explosive, float explosionRadius, float explosionDamageFalloff, - int stunDuration, int slowDuration, int slowLevel, - float maxDistance, int maxLifetime) { - this(world, owner, target, x, y, z, - beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, - damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, - false, 0.15f, 0.5f); - } - - /** - * Full constructor with all parameters including lightning. - */ - public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, - double x, double y, double z, - float beamWidth, float headSize, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, - float damage, float knockback, float knockbackUp, - float speed, boolean homing, float homingStrength, float homingRange, - boolean explosive, float explosionRadius, float explosionDamageFalloff, - int stunDuration, int slowDuration, int slowLevel, - float maxDistance, int maxLifetime, - boolean lightningEffect, float lightningDensity, float lightningRadius) { - this(world, owner, target, x, y, z, - beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, - damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, - lightningEffect, lightningDensity, lightningRadius, - true); // Default to anchored mode - } - - /** - * Full constructor with all parameters including lightning and anchored mode. + * Full constructor with all parameters using data classes. * @param anchoredMode If true, origin follows owner and tail orb is rendered. * If false, beam is free-moving with trailing length (no tail orb). */ public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, - float beamWidth, float headSize, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, - float damage, float knockback, float knockbackUp, - float speed, boolean homing, float homingStrength, float homingRange, - boolean explosive, float explosionRadius, float explosionDamageFalloff, - int stunDuration, int slowDuration, int slowLevel, - float maxDistance, int maxLifetime, - boolean lightningEffect, float lightningDensity, float lightningRadius, + float beamWidth, float headSize, + EnergyColorData color, EnergyCombatData combat, + EnergyHomingData homing, EnergyLightningData lightning, + EnergyLifespanData lifespan, boolean anchoredMode) { super(world); - // Initialize base properties with lightning - initProjectile(owner, target, x, y, z, - headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, - damage, knockback, knockbackUp, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime, - lightningEffect, lightningDensity, lightningRadius, 6); + // Initialize base properties via parent + initProjectile(owner, target, x, y, z, headSize, color, combat, lightning, lifespan); - // Beam-specific properties - this.speed = speed; - this.homing = homing; - this.homingStrength = homingStrength; - this.homingRange = homingRange; + // Beam-specific properties from homing data + this.speed = homing.speed; + this.homing = homing.homing; + this.homingStrength = homing.homingStrength; + this.homingRange = homing.homingRange; this.beamWidth = beamWidth; this.headSize = headSize; @@ -195,13 +148,13 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { trailPoints.add(Vec3.createVectorHelper(0, 0, 0)); if (DEBUG_LOGGING && !world.isRemote) { - LogWriter.info("[Beam] Created at origin " + x + ", " + y + ", " + z + " maxDist=" + maxDistance); + LogWriter.info("[Beam] Created at origin " + x + ", " + y + ", " + z + " maxDist=" + lifespan.maxDistance); } // Calculate initial velocity toward target if (target != null) { double dx = target.posX - x; - double dy = (target.posY + target.height * 0.5) - y; + double dy = (target.posY + target.getEyeHeight()) - y; double dz = target.posZ - z; double len = Math.sqrt(dx * dx + dy * dy + dz * dz); if (len > 0) { @@ -218,118 +171,69 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { } } - /** - * Create a beam in charging mode (for windup phase). - * The beam will grow from 0 to headSize over chargeDuration ticks. - * Position follows the owner based on anchor point. - */ - public static EntityAbilityBeam createCharging(World world, EntityNPCInterface owner, EntityLivingBase target, - float beamWidth, float headSize, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, - float damage, float knockback, float knockbackUp, - float speed, boolean homing, float homingStrength, float homingRange, - boolean explosive, float explosionRadius, float explosionDamageFalloff, - int stunDuration, int slowDuration, int slowLevel, - float maxDistance, int maxLifetime, - boolean lightningEffect, float lightningDensity, float lightningRadius, - boolean anchoredMode, int chargeDuration, float chargeOffsetDistance, - AnchorPoint anchorPoint) { - // Calculate initial position based on anchor point - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint, chargeOffsetDistance); - double spawnX = spawnPos.xCoord; - double spawnY = spawnPos.yCoord; - double spawnZ = spawnPos.zCoord; - - EntityAbilityBeam beam = new EntityAbilityBeam( - world, owner, target, - spawnX, spawnY, spawnZ, - beamWidth, headSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, - damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, - lightningEffect, lightningDensity, lightningRadius, - anchoredMode); - - // Set charging state (uses data watcher for client sync) - beam.setCharging(true); - beam.chargeDuration = chargeDuration; - beam.chargeTick = 0; - beam.chargeOffsetDistance = chargeOffsetDistance; - beam.anchorPoint = anchorPoint; - - // Non-anchored beams have fading trail (comet effect) - beam.fadeTrail = !anchoredMode; - - // Clear motion while charging - beam.motionX = 0; - beam.motionY = 0; - beam.motionZ = 0; - - return beam; + public void setupCharging(AnchorPoint anchorPoint, int chargeDuration, float chargeOffsetDistance) { + setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.chargeOffsetDistance = chargeOffsetDistance; + this.anchorPoint = anchorPoint; + this.fadeTrail = !attachedToOwner; + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; } - /** - * Create a beam in preview mode for GUI display. - * Follows anchor point and animations like in the real game. - * Can be fired when transitioning to active phase. - */ - public static EntityAbilityBeam createPreview(World world, EntityNPCInterface owner, - float beamWidth, float headSize, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, - boolean lightningEffect, float lightningDensity, float lightningRadius, - AnchorPoint anchorPoint, int chargeDuration, float chargeOffsetDistance) { - EntityAbilityBeam beam = new EntityAbilityBeam(world); - beam.setPreviewMode(true); - beam.setPreviewOwner(owner); + public void setupPreview(EntityNPCInterface owner, float beamWidth, float headSize, EnergyColorData color, EnergyLightningData lightning, AnchorPoint anchorPoint, int chargeDuration, float chargeOffsetDistance) { + this.setPreviewMode(true); + this.setPreviewOwner(owner); // Set visual properties - beam.beamWidth = beamWidth; - beam.headSize = headSize; - beam.size = headSize; - beam.innerColor = innerColor; - beam.outerColor = outerColor; - beam.outerColorEnabled = outerColorEnabled; - beam.outerColorWidth = outerColorWidth; - beam.rotationSpeed = rotationSpeed; - beam.lightningEffect = lightningEffect; - beam.lightningDensity = lightningDensity; - beam.lightningRadius = lightningRadius; + this.beamWidth = beamWidth; + this.headSize = headSize; + this.size = headSize; + this.innerColor = color.innerColor; + this.outerColor = color.outerColor; + this.outerColorEnabled = color.outerColorEnabled; + this.outerColorWidth = color.outerColorWidth; + this.outerColorAlpha = color.outerColorAlpha; + this.rotationSpeed = color.rotationSpeed; + this.lightningEffect = lightning.lightningEffect; + this.lightningDensity = lightning.lightningDensity; + this.lightningRadius = lightning.lightningRadius; // Set charging state - beam.setCharging(true); - beam.chargeDuration = chargeDuration; - beam.chargeTick = 0; - beam.chargeOffsetDistance = chargeOffsetDistance; - beam.anchorPoint = anchorPoint; + this.setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.chargeOffsetDistance = chargeOffsetDistance; + this.anchorPoint = anchorPoint; // Initial position at anchor point Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint, chargeOffsetDistance); - beam.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); - beam.prevPosX = pos.xCoord; - beam.prevPosY = pos.yCoord; - beam.prevPosZ = pos.zCoord; - beam.startX = pos.xCoord; - beam.startY = pos.yCoord; - beam.startZ = pos.zCoord; + this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + this.prevPosX = pos.xCoord; + this.prevPosY = pos.yCoord; + this.prevPosZ = pos.zCoord; + this.startX = pos.xCoord; + this.startY = pos.yCoord; + this.startZ = pos.zCoord; // Initialize head offsets - beam.headOffsetX = 0; - beam.headOffsetY = 0; - beam.headOffsetZ = 0; - beam.prevHeadOffsetX = 0; - beam.prevHeadOffsetY = 0; - beam.prevHeadOffsetZ = 0; + this.headOffsetX = 0; + this.headOffsetY = 0; + this.headOffsetZ = 0; + this.prevHeadOffsetX = 0; + this.prevHeadOffsetY = 0; + this.prevHeadOffsetZ = 0; // Attach to owner for anchor following - beam.attachedToOwner = true; - beam.renderTailOrb = true; + this.attachedToOwner = true; + this.renderTailOrb = true; - // Clear motion while charging - beam.motionX = 0; - beam.motionY = 0; - beam.motionZ = 0; - - return beam; + // Clear motion + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; } /** @@ -406,7 +310,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { if (target != null) { double dx = target.posX - startX; - double dy = (target.posY + target.height * 0.5) - startY; + double dy = (target.posY + target.getEyeHeight()) - startY; double dz = target.posZ - startZ; double len = Math.sqrt(dx * dx + dy * dy + dz * dz); if (len > 0) { @@ -660,7 +564,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { double headWorldZ = startZ + headOffsetZ; double dx = target.posX - headWorldX; - double dy = (target.posY + target.height * 0.5) - headWorldY; + double dy = (target.posY + target.getEyeHeight()) - headWorldY; double dz = target.posZ - headWorldZ; double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index e3896a839..20a43a98a 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -1,6 +1,11 @@ package kamkeel.npcs.entity; import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; @@ -81,56 +86,25 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { } /** - * Full constructor with all parameters (no lightning). + * Full constructor with all parameters using data classes. */ public EntityAbilityDisc(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, - float discRadius, float discThickness, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, - float damage, float knockback, float knockbackUp, - float speed, boolean homing, float homingStrength, float homingRange, - boolean boomerang, int boomerangDelay, - boolean explosive, float explosionRadius, float explosionDamageFalloff, - int stunDuration, int slowDuration, int slowLevel, - float maxDistance, int maxLifetime) { - this(world, owner, target, x, y, z, - discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, - damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, - boomerang, boomerangDelay, explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, - false, 0.15f, 0.5f); - } - - /** - * Full constructor with all parameters including lightning. - */ - public EntityAbilityDisc(World world, EntityNPCInterface owner, EntityLivingBase target, - double x, double y, double z, - float discRadius, float discThickness, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, - float damage, float knockback, float knockbackUp, - float speed, boolean homing, float homingStrength, float homingRange, - boolean boomerang, int boomerangDelay, - boolean explosive, float explosionRadius, float explosionDamageFalloff, - int stunDuration, int slowDuration, int slowLevel, - float maxDistance, int maxLifetime, - boolean lightningEffect, float lightningDensity, float lightningRadius) { + float discRadius, float discThickness, + EnergyColorData color, EnergyCombatData combat, + EnergyHomingData homing, EnergyLightningData lightning, + EnergyLifespanData lifespan, + boolean boomerang, int boomerangDelay) { super(world); - // Initialize base properties with lightning (use discRadius as size for base) - initProjectile(owner, target, x, y, z, - discRadius, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, - damage, knockback, knockbackUp, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime, - lightningEffect, lightningDensity, lightningRadius, 6); + // Initialize base properties + initProjectile(owner, target, x, y, z, discRadius, color, combat, lightning, lifespan); - // Disc-specific properties - this.speed = speed; - this.homing = homing; - this.homingStrength = homingStrength; - this.homingRange = homingRange; + // Disc-specific properties from homing data + this.speed = homing.speed; + this.homing = homing.homing; + this.homingStrength = homing.homingStrength; + this.homingRange = homing.homingRange; this.boomerang = boomerang; this.boomerangDelay = boomerangDelay; @@ -141,7 +115,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { // Calculate initial velocity toward target if (target != null) { double dx = target.posX - x; - double dy = (target.posY + target.height * 0.5) - y; + double dy = (target.posY + target.getEyeHeight()) - y; double dz = target.posZ - z; double len = Math.sqrt(dx * dx + dy * dy + dz * dz); if (len > 0) { @@ -159,113 +133,73 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { } /** - * Create a disc in charging mode (for windup phase). + * Setup this disc in charging mode (for windup phase). * The disc will grow from 0 to discRadius over chargeDuration ticks. * Position follows the owner based on anchor point. */ - public static EntityAbilityDisc createCharging(World world, EntityNPCInterface owner, EntityLivingBase target, - float discRadius, float discThickness, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, - float damage, float knockback, float knockbackUp, - float speed, boolean homing, float homingStrength, float homingRange, - boolean boomerang, int boomerangDelay, - boolean explosive, float explosionRadius, float explosionDamageFalloff, - int stunDuration, int slowDuration, int slowLevel, - float maxDistance, int maxLifetime, - boolean lightningEffect, float lightningDensity, float lightningRadius, - AnchorPoint anchorPoint, int chargeDuration) { - // Calculate initial position based on anchor point - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint); - double spawnX = spawnPos.xCoord; - double spawnY = spawnPos.yCoord; - double spawnZ = spawnPos.zCoord; - - EntityAbilityDisc disc = new EntityAbilityDisc( - world, owner, target, - spawnX, spawnY, spawnZ, - discRadius, discThickness, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, - damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, - boomerang, boomerangDelay, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, - lightningEffect, lightningDensity, lightningRadius); - - // Set charging state (uses data watcher for client sync) - disc.setCharging(true); - disc.chargeDuration = chargeDuration; - disc.chargeTick = 0; - disc.anchorPoint = anchorPoint; - - // Store target size and start at 0 for grow effect - disc.targetDiscRadius = disc.discRadius; - disc.targetDiscThickness = disc.discThickness; - disc.discRadius = 0.01f; // Start very small - disc.discThickness = 0.01f; - disc.size = 0.01f; // Base size used for rendering - disc.renderCurrentSize = 0.01f; - disc.prevRenderSize = 0.01f; - - // Clear motion while charging - disc.motionX = 0; - disc.motionY = 0; - disc.motionZ = 0; - - return disc; + public void setupCharging(AnchorPoint anchorPoint, int chargeDuration) { + setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.anchorPoint = anchorPoint; + this.targetDiscRadius = this.discRadius; + this.targetDiscThickness = this.discThickness; + this.discRadius = 0.01f; + this.discThickness = 0.01f; + this.size = 0.01f; + this.renderCurrentSize = 0.01f; + this.prevRenderSize = 0.01f; + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; } /** - * Create a disc in preview mode for GUI display. + * Setup this disc in preview mode for GUI display. * Follows anchor point and animations like in the real game. * Can be fired when transitioning to active phase. */ - public static EntityAbilityDisc createPreview(World world, EntityNPCInterface owner, - float discRadius, float discThickness, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, - boolean lightningEffect, float lightningDensity, float lightningRadius, - AnchorPoint anchorPoint, int chargeDuration) { - EntityAbilityDisc disc = new EntityAbilityDisc(world); - disc.setPreviewMode(true); - disc.setPreviewOwner(owner); + public void setupPreview(EntityNPCInterface owner, float discRadius, float discThickness, EnergyColorData color, EnergyLightningData lightning, AnchorPoint anchorPoint, int chargeDuration) { + this.setPreviewMode(true); + this.setPreviewOwner(owner); // Set visual properties - disc.innerColor = innerColor; - disc.outerColor = outerColor; - disc.outerColorEnabled = outerColorEnabled; - disc.outerColorWidth = outerColorWidth; - disc.rotationSpeed = rotationSpeed; - disc.lightningEffect = lightningEffect; - disc.lightningDensity = lightningDensity; - disc.lightningRadius = lightningRadius; + this.innerColor = color.innerColor; + this.outerColor = color.outerColor; + this.outerColorEnabled = color.outerColorEnabled; + this.outerColorWidth = color.outerColorWidth; + this.rotationSpeed = color.rotationSpeed; + this.lightningEffect = lightning.lightningEffect; + this.lightningDensity = lightning.lightningDensity; + this.lightningRadius = lightning.lightningRadius; // Set charging state - disc.setCharging(true); - disc.chargeDuration = chargeDuration; - disc.chargeTick = 0; - disc.anchorPoint = anchorPoint; + this.setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.anchorPoint = anchorPoint; // Store target size and start at 0 for grow effect - disc.targetDiscRadius = discRadius; - disc.targetDiscThickness = discThickness; - disc.discRadius = 0.01f; - disc.discThickness = 0.01f; - disc.size = 0.01f; + this.targetDiscRadius = discRadius; + this.targetDiscThickness = discThickness; + this.discRadius = 0.01f; + this.discThickness = 0.01f; + this.size = 0.01f; // Initial position at anchor point Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint); - disc.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); - disc.prevPosX = pos.xCoord; - disc.prevPosY = pos.yCoord; - disc.prevPosZ = pos.zCoord; - disc.startX = pos.xCoord; - disc.startY = pos.yCoord; - disc.startZ = pos.zCoord; + this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + this.prevPosX = pos.xCoord; + this.prevPosY = pos.yCoord; + this.prevPosZ = pos.zCoord; + this.startX = pos.xCoord; + this.startY = pos.yCoord; + this.startZ = pos.zCoord; - // Clear motion while charging - disc.motionX = 0; - disc.motionY = 0; - disc.motionZ = 0; - - return disc; + // Clear motion + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; } /** @@ -320,7 +254,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { if (target != null) { double dx = target.posX - posX; - double dy = (target.posY + target.height * 0.5) - posY; + double dy = (target.posY + target.getEyeHeight()) - posY; double dz = target.posZ - posZ; double len = Math.sqrt(dx * dx + dy * dy + dz * dz); if (len > 0) { @@ -447,7 +381,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { if (target == null || !target.isEntityAlive()) return; double dx = target.posX - posX; - double dy = (target.posY + target.height * 0.5) - posY; + double dy = (target.posY + target.getEyeHeight()) - posY; double dz = target.posZ - posZ; double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index ec208e2cf..c45a6ee9e 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -1,5 +1,9 @@ package kamkeel.npcs.entity; +import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -45,48 +49,19 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { } /** - * Full constructor with all parameters (no lightning). + * Full constructor with all parameters using data classes. */ public EntityAbilityLaser(World world, EntityNPCInterface owner, EntityLivingBase target, double x, double y, double z, - float laserWidth, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, - float damage, float knockback, float knockbackUp, - float expansionSpeed, int lingerTicks, - boolean explosive, float explosionRadius, float explosionDamageFalloff, - int stunDuration, int slowDuration, int slowLevel, - float maxDistance, int maxLifetime) { - this(world, owner, target, x, y, z, - laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, - damage, knockback, knockbackUp, expansionSpeed, lingerTicks, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, - false, 0.15f, 0.5f); - } - - /** - * Full constructor with all parameters including lightning. - */ - public EntityAbilityLaser(World world, EntityNPCInterface owner, EntityLivingBase target, - double x, double y, double z, - float laserWidth, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, - float damage, float knockback, float knockbackUp, - float expansionSpeed, int lingerTicks, - boolean explosive, float explosionRadius, float explosionDamageFalloff, - int stunDuration, int slowDuration, int slowLevel, - float maxDistance, int maxLifetime, - boolean lightningEffect, float lightningDensity, float lightningRadius) { + float laserWidth, + EnergyColorData color, EnergyCombatData combat, + EnergyLightningData lightning, EnergyLifespanData lifespan, + float expansionSpeed, int lingerTicks) { super(world); - // Initialize base properties with lightning - initProjectile(owner, target, x, y, z, - laserWidth, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, 0.0f, // No rotation for laser - damage, knockback, knockbackUp, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime, - lightningEffect, lightningDensity, lightningRadius, 6); + // Initialize base properties (laser doesn't rotate) + initProjectile(owner, target, x, y, z, laserWidth, color, combat, lightning, lifespan); + this.rotationSpeed = 0.0f; // Laser-specific properties this.laserWidth = laserWidth; @@ -96,7 +71,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { // Calculate direction toward target or forward if (target != null) { double dx = target.posX - x; - double dy = (target.posY + target.height * 0.5) - y; + double dy = (target.posY + target.getEyeHeight()) - y; double dz = target.posZ - z; double len = Math.sqrt(dx * dx + dy * dy + dz * dz); if (len > 0) { @@ -349,54 +324,48 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { } /** - * Create a laser in preview mode for GUI display. + * Setup this laser in preview mode for GUI display. * Laser doesn't have charging state - spawns at active phase and fires immediately. */ - public static EntityAbilityLaser createPreview(World world, EntityNPCInterface owner, - float laserWidth, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, - float expansionSpeed, float maxDistance, - boolean lightningEffect, float lightningDensity, float lightningRadius) { - EntityAbilityLaser laser = new EntityAbilityLaser(world); - laser.setPreviewMode(true); - laser.setPreviewOwner(owner); + public void setupPreview(EntityNPCInterface owner, float laserWidth, EnergyColorData color, + EnergyLightningData lightning, float expansionSpeed, float maxDistance) { + this.setPreviewMode(true); + this.setPreviewOwner(owner); // Set visual properties - laser.laserWidth = laserWidth; - laser.size = laserWidth; - laser.innerColor = innerColor; - laser.outerColor = outerColor; - laser.outerColorEnabled = outerColorEnabled; - laser.outerColorWidth = outerColorWidth; - laser.expansionSpeed = expansionSpeed; - laser.maxDistance = Math.min(maxDistance, 5.0f); // Limit for GUI preview - laser.lightningEffect = lightningEffect; - laser.lightningDensity = lightningDensity; - laser.lightningRadius = lightningRadius; + this.laserWidth = laserWidth; + this.size = laserWidth; + this.innerColor = color.innerColor; + this.outerColor = color.outerColor; + this.outerColorEnabled = color.outerColorEnabled; + this.outerColorWidth = color.outerColorWidth; + this.expansionSpeed = expansionSpeed; + this.maxDistance = Math.min(maxDistance, 5.0f); // Limit for GUI preview + this.lightningEffect = lightning.lightningEffect; + this.lightningDensity = lightning.lightningDensity; + this.lightningRadius = lightning.lightningRadius; // Position at chest height double x = owner.posX; double y = owner.posY + owner.height * 0.7; double z = owner.posZ; - laser.setPosition(x, y, z); - laser.prevPosX = x; - laser.prevPosY = y; - laser.prevPosZ = z; - laser.startX = x; - laser.startY = y; - laser.startZ = z; + this.setPosition(x, y, z); + this.prevPosX = x; + this.prevPosY = y; + this.prevPosZ = z; + this.startX = x; + this.startY = y; + this.startZ = z; // Fire in owner's facing direction float yaw = (float) Math.toRadians(owner.rotationYaw); - laser.dirX = -Math.sin(yaw); - laser.dirY = 0; - laser.dirZ = Math.cos(yaw); + this.dirX = -Math.sin(yaw); + this.dirY = 0; + this.dirZ = Math.cos(yaw); // Initialize end point (same as start, will expand) - laser.endX = x; - laser.endY = y; - laser.endZ = z; - - return laser; + this.endX = x; + this.endY = y; + this.endZ = z; } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index bad2bc955..2473730c3 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -1,6 +1,11 @@ package kamkeel.npcs.entity; import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; @@ -70,181 +75,110 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { } /** - * Full constructor with all parameters. + * Full constructor using data classes for grouped parameters. */ public EntityAbilityOrb(World world, EntityNPCInterface owner, EntityLivingBase target, - double x, double y, double z, - float orbSize, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, - float damage, float knockback, float knockbackUp, - float speed, boolean homing, float homingStrength, float homingRange, - boolean explosive, float explosionRadius, float explosionDamageFalloff, - int stunDuration, int slowDuration, int slowLevel, - float maxDistance, int maxLifetime) { - this(world, owner, target, x, y, z, - orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, - damage, knockback, knockbackUp, - speed, homing, homingStrength, homingRange, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime, - false, 0.15f, 0.5f, 6); // Default: no lightning, low density, small radius, 6 tick fade - } - - /** - * Full constructor with lightning effect support. - */ - public EntityAbilityOrb(World world, EntityNPCInterface owner, EntityLivingBase target, - double x, double y, double z, - float orbSize, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, - float damage, float knockback, float knockbackUp, - float speed, boolean homing, float homingStrength, float homingRange, - boolean explosive, float explosionRadius, float explosionDamageFalloff, - int stunDuration, int slowDuration, int slowLevel, - float maxDistance, int maxLifetime, - boolean lightningEffect, float lightningDensity, float lightningRadius, int lightningFadeTime) { + double x, double y, double z, float orbSize, + EnergyColorData color, EnergyCombatData combat, + EnergyHomingData homing, EnergyLightningData lightning, + EnergyLifespanData lifespan) { super(world); - // Initialize base properties with lightning - initProjectile(owner, target, x, y, z, - orbSize, innerColor, outerColor, outerColorEnabled, outerColorAlpha, outerColorWidth, rotationSpeed, - damage, knockback, knockbackUp, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime, - lightningEffect, lightningDensity, lightningRadius, lightningFadeTime); + // Initialize base properties + initProjectile(owner, target, x, y, z, orbSize, color, combat, lightning, lifespan); // Orb-specific properties - this.speed = speed; - this.homing = homing; - this.homingStrength = homingStrength; - this.homingRange = homingRange; + this.speed = homing.speed; + this.homing = homing.homing; + this.homingStrength = homing.homingStrength; + this.homingRange = homing.homingRange; // Calculate initial velocity toward target if (target != null) { double dx = target.posX - x; - double dy = (target.posY + target.height * 0.5) - y; + double dy = (target.posY + target.getEyeHeight()) - y; double dz = target.posZ - z; double len = Math.sqrt(dx * dx + dy * dy + dz * dz); if (len > 0) { - this.motionX = (dx / len) * speed; - this.motionY = (dy / len) * speed; - this.motionZ = (dz / len) * speed; + this.motionX = (dx / len) * homing.speed; + this.motionY = (dy / len) * homing.speed; + this.motionZ = (dz / len) * homing.speed; } } else { // Fire in NPC's facing direction float yaw = (float) Math.toRadians(owner.rotationYaw); float pitch = (float) Math.toRadians(owner.rotationPitch); - this.motionX = -Math.sin(yaw) * Math.cos(pitch) * speed; - this.motionY = -Math.sin(pitch) * speed; - this.motionZ = Math.cos(yaw) * Math.cos(pitch) * speed; + this.motionX = -Math.sin(yaw) * Math.cos(pitch) * homing.speed; + this.motionY = -Math.sin(pitch) * homing.speed; + this.motionZ = Math.cos(yaw) * Math.cos(pitch) * homing.speed; } } /** - * Create an orb in charging mode (for windup phase). + * Setup this orb in charging mode (for windup phase). * The orb will grow from 0 to orbSize over chargeDuration ticks. * Position follows the owner based on anchor point. */ - public static EntityAbilityOrb createCharging(World world, EntityNPCInterface owner, EntityLivingBase target, - float orbSize, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, - float damage, float knockback, float knockbackUp, - float speed, boolean homing, float homingStrength, float homingRange, - boolean explosive, float explosionRadius, float explosionDamageFalloff, - int stunDuration, int slowDuration, int slowLevel, - float maxDistance, int maxLifetime, - boolean lightningEffect, float lightningDensity, float lightningRadius, int lightningFadeTime, - AnchorPoint anchorPoint, int chargeDuration) { - // Calculate initial position based on anchor point - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint); - double spawnX = spawnPos.xCoord; - double spawnY = spawnPos.yCoord; - double spawnZ = spawnPos.zCoord; - - EntityAbilityOrb orb = new EntityAbilityOrb( - world, owner, target, - spawnX, spawnY, spawnZ, - orbSize, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, - damage, knockback, knockbackUp, speed, homing, homingStrength, homingRange, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, maxDistance, maxLifetime, - lightningEffect, lightningDensity, lightningRadius, lightningFadeTime); - - // Set charging state (uses data watcher for client sync) - orb.setCharging(true); - orb.chargeDuration = chargeDuration; - orb.chargeTick = 0; - orb.anchorPoint = anchorPoint; - - // Store target size and start at 0 for grow effect - orb.targetSize = orb.size; - orb.size = 0.01f; // Start very small - orb.renderCurrentSize = 0.01f; - orb.prevRenderSize = 0.01f; - - // Clear motion while charging - orb.motionX = 0; - orb.motionY = 0; - orb.motionZ = 0; - - return orb; + public void setupCharging(AnchorPoint anchorPoint, int chargeDuration) { + setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.anchorPoint = anchorPoint; + this.targetSize = this.size; + this.size = 0.01f; + this.renderCurrentSize = 0.01f; + this.prevRenderSize = 0.01f; + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; } /** - * Create an orb in preview mode for GUI display. + * Setup this orb in preview mode for GUI display. * Follows anchor point and animations like in the real game. * Can be fired when transitioning to active phase. */ - public static EntityAbilityOrb createPreview(World world, EntityNPCInterface owner, - float orbSize, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float rotationSpeed, - boolean lightningEffect, float lightningDensity, float lightningRadius, int lightningFadeTime, - AnchorPoint anchorPoint, int chargeDuration) { - EntityAbilityOrb orb = new EntityAbilityOrb(world); - orb.setPreviewMode(true); - orb.setPreviewOwner(owner); + public void setupPreview(EntityNPCInterface owner, float orbSize, EnergyColorData color, EnergyLightningData lightning, AnchorPoint anchorPoint, int chargeDuration) { + this.setPreviewMode(true); + this.setPreviewOwner(owner); // Set visual properties - orb.innerColor = innerColor; - orb.outerColor = outerColor; - orb.outerColorEnabled = outerColorEnabled; - orb.outerColorWidth = outerColorWidth; - orb.rotationSpeed = rotationSpeed; - orb.lightningEffect = lightningEffect; - orb.lightningDensity = lightningDensity; - orb.lightningRadius = lightningRadius; - orb.lightningFadeTime = lightningFadeTime; + this.innerColor = color.innerColor; + this.outerColor = color.outerColor; + this.outerColorEnabled = color.outerColorEnabled; + this.outerColorWidth = color.outerColorWidth; + this.rotationSpeed = color.rotationSpeed; + this.lightningEffect = lightning.lightningEffect; + this.lightningDensity = lightning.lightningDensity; + this.lightningRadius = lightning.lightningRadius; + this.lightningFadeTime = lightning.lightningFadeTime; - // Set charging state (like createCharging does) - orb.setCharging(true); - orb.chargeDuration = chargeDuration; - orb.chargeTick = 0; - orb.anchorPoint = anchorPoint; + // Set charging state + this.setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.anchorPoint = anchorPoint; // Store target size and start at 0 for grow effect - orb.targetSize = orbSize; - orb.size = 0.01f; - orb.renderCurrentSize = 0.01f; - orb.prevRenderSize = 0.01f; + this.targetSize = orbSize; + this.size = 0.01f; + this.renderCurrentSize = 0.01f; + this.prevRenderSize = 0.01f; // Initial position at anchor point Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint); - orb.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); - orb.prevPosX = pos.xCoord; - orb.prevPosY = pos.yCoord; - orb.prevPosZ = pos.zCoord; - orb.startX = pos.xCoord; - orb.startY = pos.yCoord; - orb.startZ = pos.zCoord; + this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + this.prevPosX = pos.xCoord; + this.prevPosY = pos.yCoord; + this.prevPosZ = pos.zCoord; + this.startX = pos.xCoord; + this.startY = pos.yCoord; + this.startZ = pos.zCoord; - // Clear motion while charging - orb.motionX = 0; - orb.motionY = 0; - orb.motionZ = 0; - - return orb; + // Clear motion + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; } /** @@ -300,7 +234,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { if (target != null) { double dx = target.posX - posX; - double dy = (target.posY + target.height * 0.5) - posY; + double dy = (target.posY + target.getEyeHeight()) - posY; double dz = target.posZ - posZ; double len = Math.sqrt(dx * dx + dy * dy + dz * dz); if (len > 0) { @@ -370,7 +304,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { if (target == null || !target.isEntityAlive()) return; double dx = target.posX - posX; - double dy = (target.posY + target.height * 0.5) - posY; + double dy = (target.posY + target.getEyeHeight()) - posY; double dz = target.posZ - posZ; double dist = Math.sqrt(dx * dx + dy * dy + dz * dz); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index 149ffd329..5508ecd94 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -4,17 +4,22 @@ import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.potion.Potion; -import net.minecraft.potion.PotionEffect; +import net.minecraft.nbt.NBTTagList; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.NpcDamageSource; import noppes.npcs.entity.EntityNPCInterface; +import java.util.ArrayList; import java.util.List; /** @@ -40,9 +45,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA protected float knockbackUp = 0.1f; // ==================== EFFECT PROPERTIES ==================== - protected int stunDuration = 0; - protected int slowDuration = 0; - protected int slowLevel = 0; + protected List effects = new ArrayList<>(); // ==================== EXPLOSION PROPERTIES ==================== protected boolean explosive = false; @@ -96,37 +99,12 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA } /** - * Initialize common properties. Call from subclass constructors. + * Initialize common properties using data classes. Call from subclass constructors. */ protected void initProjectile(EntityNPCInterface owner, EntityLivingBase target, - double x, double y, double z, - float size, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, - float damage, float knockback, float knockbackUp, - boolean explosive, float explosionRadius, float explosionDamageFalloff, - int stunDuration, int slowDuration, int slowLevel, - float maxDistance, int maxLifetime) { - initProjectile(owner, target, x, y, z, - size, innerColor, outerColor, outerColorEnabled, outerColorWidth, outerColorAlpha, rotationSpeed, - damage, knockback, knockbackUp, - explosive, explosionRadius, explosionDamageFalloff, - stunDuration, slowDuration, slowLevel, - maxDistance, maxLifetime, - false, 0.15f, 0.5f, 6); // Default: no lightning, low density, small radius, 6 tick fade - } - - /** - * Initialize common properties with lightning effect support. - */ - protected void initProjectile(EntityNPCInterface owner, EntityLivingBase target, - double x, double y, double z, - float size, int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, float outerColorAlpha, float rotationSpeed, - float damage, float knockback, float knockbackUp, - boolean explosive, float explosionRadius, float explosionDamageFalloff, - int stunDuration, int slowDuration, int slowLevel, - float maxDistance, int maxLifetime, - boolean lightningEffect, float lightningDensity, float lightningRadius, int lightningFadeTime) { + double x, double y, double z, float size, + EnergyColorData color, EnergyCombatData combat, + EnergyLightningData lightning, EnergyLifespanData lifespan) { this.setPosition(x, y, z); this.startX = x; this.startY = y; @@ -137,38 +115,33 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA // Visual this.size = size; - this.innerColor = innerColor; - this.outerColor = outerColor; - this.outerColorEnabled = outerColorEnabled; - this.outerColorWidth = outerColorWidth; - this.outerColorAlpha = outerColorAlpha; - this.rotationSpeed = rotationSpeed; + this.innerColor = color.innerColor; + this.outerColor = color.outerColor; + this.outerColorEnabled = color.outerColorEnabled; + this.outerColorWidth = color.outerColorWidth; + this.outerColorAlpha = color.outerColorAlpha; + this.rotationSpeed = color.rotationSpeed; // Combat - this.damage = damage; - this.knockback = knockback; - this.knockbackUp = knockbackUp; + this.damage = combat.damage; + this.knockback = combat.knockback; + this.knockbackUp = combat.knockbackUp; // Explosion - this.explosive = explosive; - this.explosionRadius = explosionRadius; - this.explosionDamageFalloff = explosionDamageFalloff; - - // Effects - this.stunDuration = stunDuration; - this.slowDuration = slowDuration; - this.slowLevel = slowLevel; + this.explosive = combat.explosive; + this.explosionRadius = combat.explosionRadius; + this.explosionDamageFalloff = combat.explosionDamageFalloff; // Lifespan - this.maxDistance = maxDistance; - this.maxLifetime = maxLifetime; + this.maxDistance = lifespan.maxDistance; + this.maxLifetime = lifespan.maxLifetime; // deathWorldTime will be set on first tick when world is available // Lightning - this.lightningEffect = lightningEffect; - this.lightningDensity = lightningDensity; - this.lightningRadius = lightningRadius; - this.lightningFadeTime = lightningFadeTime; + this.lightningEffect = lightning.lightningEffect; + this.lightningDensity = lightning.lightningDensity; + this.lightningRadius = lightning.lightningRadius; + this.lightningFadeTime = lightning.lightningFadeTime; // Initialize render size this.renderCurrentSize = size; @@ -370,15 +343,18 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA } protected void applyEffects(EntityLivingBase target) { - if (stunDuration > 0) { - target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, stunDuration, 10)); - target.addPotionEffect(new PotionEffect(Potion.weakness.id, stunDuration, 2)); - } - if (slowDuration > 0 && slowLevel > 0) { - target.addPotionEffect(new PotionEffect(Potion.moveSlowdown.id, slowDuration, slowLevel)); + for (AbilityEffect effect : effects) { + effect.apply(target); } } + /** + * Set the effects list from the ability's configured effects. + */ + public void setEffects(List effects) { + this.effects = effects != null ? effects : new ArrayList<>(); + } + protected void doExplosion() { if (previewMode) return; // Skip explosion in preview mode worldObj.playSoundEffect(posX, posY, posZ, "random.explode", 1.0f, 1.0f); @@ -559,9 +535,14 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA this.explosionRadius = nbt.getFloat("ExplosionRadius"); this.explosionDamageFalloff = nbt.getFloat("ExplosionDamageFalloff"); - this.stunDuration = nbt.getInteger("StunDuration"); - this.slowDuration = nbt.getInteger("SlowDuration"); - this.slowLevel = nbt.getInteger("SlowLevel"); + // Read effects list + this.effects.clear(); + if (nbt.hasKey("Effects")) { + NBTTagList effectsList = nbt.getTagList("Effects", 10); // 10 = compound tag + for (int i = 0; i < effectsList.tagCount(); i++) { + effects.add(AbilityEffect.fromNBT(effectsList.getCompoundTagAt(i))); + } + } this.maxDistance = nbt.getFloat("MaxDistance"); this.maxLifetime = nbt.getInteger("MaxLifetime"); @@ -610,9 +591,14 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA nbt.setFloat("ExplosionRadius", explosionRadius); nbt.setFloat("ExplosionDamageFalloff", explosionDamageFalloff); - nbt.setInteger("StunDuration", stunDuration); - nbt.setInteger("SlowDuration", slowDuration); - nbt.setInteger("SlowLevel", slowLevel); + // Write effects list + if (!effects.isEmpty()) { + NBTTagList effectsList = new NBTTagList(); + for (AbilityEffect effect : effects) { + effectsList.appendTag(effect.writeNBT()); + } + nbt.setTag("Effects", effectsList); + } nbt.setFloat("MaxDistance", maxDistance); nbt.setLong("DeathWorldTime", deathWorldTime); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java index 6ec4f8b38..ffb5f558a 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java @@ -4,6 +4,7 @@ import cpw.mods.fml.common.registry.IEntityAdditionalSpawnData; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import io.netty.buffer.ByteBuf; +import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -71,8 +72,7 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa public EntityAbilitySweeper(World world, EntityNPCInterface owner, EntityLivingBase target, float beamLength, float beamWidth, float beamHeight, - int innerColor, int outerColor, - boolean outerColorEnabled, float outerColorWidth, + EnergyColorData colorData, float sweepSpeed, int numberOfRotations, float damage, int damageInterval, boolean piercing, boolean lockOnTarget) { @@ -83,10 +83,10 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa this.beamLength = beamLength; this.beamWidth = beamWidth; this.beamHeight = beamHeight; - this.innerColor = innerColor; - this.outerColor = outerColor; - this.outerColorEnabled = outerColorEnabled; - this.outerColorWidth = outerColorWidth; + this.innerColor = colorData.innerColor; + this.outerColor = colorData.outerColor; + this.outerColorEnabled = colorData.outerColorEnabled; + this.outerColorWidth = colorData.outerColorWidth; this.sweepSpeed = sweepSpeed; this.numberOfRotations = numberOfRotations; this.damage = damage; diff --git a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java index ae292d59d..16389f74d 100644 --- a/src/main/java/noppes/npcs/entity/EntityNPCInterface.java +++ b/src/main/java/noppes/npcs/entity/EntityNPCInterface.java @@ -1934,6 +1934,13 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti if (NPCMountUtil.isFlyingMountWithFlightEnabled(this)) { return; } + // Skip fall damage during abilities that control NPC movement (e.g., Slam leap) + if (this.abilities != null && this.abilities.isExecutingAbility() + && this.abilities.getCurrentAbility() != null + && this.abilities.getCurrentAbility().hasAbilityMovement()) { + this.fallDistance = 0; + return; + } if (!this.stats.noFallDamage) super.fall(distance); } From 5350a9882ba8bb5925117c6dac1c09c8911cf997 Mon Sep 17 00:00:00 2001 From: Kam Date: Thu, 29 Jan 2026 14:20:12 -0500 Subject: [PATCH 310/358] Update api --- src/api | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api b/src/api index b8ba682e9..06c9daf81 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit b8ba682e9dd4e542a5ccf0de231ab5e5ebdad448 +Subproject commit 06c9daf81be8df0138a992ca6bcabc05e37189cb From babe58dc34c49aba160d1b81eaf55b0f47c23705 Mon Sep 17 00:00:00 2001 From: Vidal Date: Thu, 29 Jan 2026 17:08:58 -0300 Subject: [PATCH 311/358] Added builtin animations to disc, laser and slam --- .../data/ability/type/AbilityDisc.java | 7 +- .../data/ability/type/AbilityLaserShot.java | 3 + .../data/ability/type/AbilitySlam.java | 3 + .../animations/Ability_Disc_Active.json | 77 ++ .../animations/Ability_Disc_Windup.json | 896 ++++++++++++++++++ .../animations/Ability_Laser_Active.json | 77 ++ .../animations/Ability_Laser_Windup.json | 329 +++++++ .../animations/Ability_Slam_Active.json | 84 ++ .../animations/Ability_Slam_Windup.json | 147 +++ 9 files changed, 1621 insertions(+), 2 deletions(-) create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_Disc_Active.json create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_Disc_Windup.json create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_Laser_Active.json create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_Laser_Windup.json create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_Slam_Active.json create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_Slam_Windup.json diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 006f6f128..b80dcbdef 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -43,7 +43,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { private int boomerangDelay = 40; // Anchor point for charging position - private AnchorPoint anchorPoint = AnchorPoint.FRONT; + private AnchorPoint anchorPoint = AnchorPoint.RIGHT_HAND; // Energy data classes private EnergyColorData colorData = new EnergyColorData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f); @@ -62,10 +62,13 @@ public class AbilityDisc extends Ability implements IAbilityDisc { this.maxRange = 30.0f; this.minRange = 5.0f; this.cooldownTicks = 0; - this.windUpTicks = 20; + this.windUpTicks = 60; this.lockMovement = LockMovementType.WINDUP; this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; + // Default built-in animations + this.windUpAnimationName = "Ability_Disc_Windup"; + this.activeAnimationName = "Ability_Disc_Active"; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index 3a25b9aa4..3b45894b4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -53,6 +53,9 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { this.lockMovement = LockMovementType.WINDUP; this.telegraphType = TelegraphType.LINE; this.showTelegraph = true; + // Default built-in animations + this.windUpAnimationName = "Ability_Laser_Windup"; + this.activeAnimationName = "Ability_Laser_Active"; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 46faef9c9..2dae47d05 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -59,6 +59,9 @@ public class AbilitySlam extends Ability implements IAbilitySlam { this.telegraphType = TelegraphType.CIRCLE; this.windUpSound = "mob.irongolem.throw"; this.activeSound = ""; + // Default built-in animations + this.windUpAnimationName = "Ability_Slam_Windup"; + this.activeAnimationName = "Ability_Slam_Active"; } @Override diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Disc_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Disc_Active.json new file mode 100644 index 000000000..0e221560e --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Disc_Active.json @@ -0,0 +1,77 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 12.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": -44.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 4.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_ARM", + "Rotation1": 31.0f, + "Rotation0": 44.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": -82.0f + }, + { + "Pivot2": -5.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 6.0f + } + ], + "Smooth": 0b, + "Duration": 10 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 7, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Disc_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Disc_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Disc_Windup.json new file mode 100644 index 000000000..035e51f7f --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Disc_Windup.json @@ -0,0 +1,896 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.8f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 0.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + } + ], + "Smooth": 0b, + "Duration": 12 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.2f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 25.0f, + "Pivot0": -1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -2.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + } + ], + "Smooth": 0b, + "Duration": 6 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.25f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 110.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": -2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -12.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + } + ], + "Smooth": 0b, + "Duration": 6 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.4f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 120.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + } + ], + "Smooth": 0b, + "Duration": 6 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -18.0f, + "Pivot0": 3.0f, + "Pivot1": -1.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": 0.0f, + "Pivot1": 8.0f, + "Rotation2": 37.0f + } + ], + "Smooth": 0b, + "Duration": 4 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": -2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -18.0f, + "Pivot0": 3.0f, + "Pivot1": -1.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": 1.0f, + "Part": "LEFT_ARM", + "Rotation1": -25.0f, + "Rotation0": -94.0f, + "Pivot0": 0.0f, + "Pivot1": 8.0f, + "Rotation2": 37.0f + } + ], + "Smooth": 0b, + "Duration": 4 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.6f, + "FrameParts": [ + { + "Pivot2": -2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 0.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 0.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 25.0f, + "Pivot0": -1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -2.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 110.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": -2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -12.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.2f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 120.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 6.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 7.0f, + "Rotation2": 37.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.25f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -18.0f, + "Pivot0": 3.0f, + "Pivot1": -1.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": 3.0f, + "Part": "LEFT_ARM", + "Rotation1": -18.0f, + "Rotation0": -94.0f, + "Pivot0": 0.0f, + "Pivot1": 8.0f, + "Rotation2": 37.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.5f, + "FrameParts": [ + { + "Pivot2": -2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -18.0f, + "Pivot0": 3.0f, + "Pivot1": -1.0f, + "Rotation2": 180.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": 1.0f, + "Part": "LEFT_ARM", + "Rotation1": -25.0f, + "Rotation0": -94.0f, + "Pivot0": 0.0f, + "Pivot1": 8.0f, + "Rotation2": 37.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.5f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 12.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": -44.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 4.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_ARM", + "Rotation1": 31.0f, + "Rotation0": 44.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": -82.0f + }, + { + "Pivot2": -5.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 6.0f + } + ], + "Smooth": 0b, + "Duration": 4 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 6, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Disc_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Laser_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Laser_Active.json new file mode 100644 index 000000000..5cb4fc3be --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Laser_Active.json @@ -0,0 +1,77 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 5.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": -195.0f, + "Pivot0": -1.0f, + "Pivot1": -1.0f, + "Rotation2": -56.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": -1.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 5.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -37.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 3, + "Smooth": 0b, + "ID": 10, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Laser_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Laser_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Laser_Windup.json new file mode 100644 index 000000000..5b7dc3e16 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Laser_Windup.json @@ -0,0 +1,329 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 50.0f, + "Rotation0": -82.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 50.0f, + "Rotation0": -82.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 2.0f, + "Rotation0": -88.0f, + "Pivot0": 1.0f, + "Pivot1": 2.0f, + "Rotation2": 86.0f + }, + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 50.0f, + "Rotation0": -82.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -92.0f, + "Pivot0": 1.0f, + "Pivot1": 2.0f, + "Rotation2": 94.0f + }, + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 50.0f, + "Rotation0": -82.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": -2.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 2.0f, + "Rotation2": 80.0f + }, + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 5.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": -195.0f, + "Pivot0": -1.0f, + "Pivot1": -1.0f, + "Rotation2": -56.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": -1.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 5.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -37.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 3, + "Smooth": 0b, + "ID": 8, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Laser_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Slam_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Slam_Active.json new file mode 100644 index 000000000..90e16e948 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Slam_Active.json @@ -0,0 +1,84 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": -208.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 12.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -208.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 107.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 5, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Slam_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Slam_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Slam_Windup.json new file mode 100644 index 000000000..b35cef0cf --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Slam_Windup.json @@ -0,0 +1,147 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.25f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 0.0f, + "Pivot1": 6.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 6.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 6.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 6.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 82.0f, + "Pivot0": 0.0f, + "Pivot1": 7.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 27 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": -82.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -88.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 4, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Slam_Windup", + "WhileStanding": 1b +} From da33baf971a21fae4d5679cf90c32e3e516f6e18 Mon Sep 17 00:00:00 2001 From: Vidal Date: Thu, 29 Jan 2026 20:34:10 -0300 Subject: [PATCH 312/358] Added EnergyAnchorData, a wrapper for anchor points which also stores 3 flat offset values --- .../data/ability/data/EnergyAnchorData.java | 54 +++++++++++++++++++ .../data/ability/type/AbilityDisc.java | 35 ++++++------ .../data/ability/type/AbilityEnergyBeam.java | 34 ++++++------ .../data/ability/type/AbilityOrb.java | 45 ++++++++-------- .../npcs/entity/EntityAbilityBeam.java | 26 +++++---- .../npcs/entity/EntityAbilityDisc.java | 26 +++++---- .../kamkeel/npcs/entity/EntityAbilityOrb.java | 25 ++++----- .../kamkeel/npcs/util/AnchorPointHelper.java | 39 ++++++++++---- 8 files changed, 175 insertions(+), 109 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java new file mode 100644 index 000000000..39c7db9e7 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java @@ -0,0 +1,54 @@ +package kamkeel.npcs.controllers.data.ability.data; + +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import net.minecraft.nbt.NBTTagCompound; + +public class EnergyAnchorData { + public AnchorPoint anchorPoint = AnchorPoint.FRONT; + public float anchorOffsetX = 0; + public float anchorOffsetY = 0; + public float anchorOffsetZ = 0; + + public EnergyAnchorData() { + } + + public EnergyAnchorData(AnchorPoint anchorPoint) { + this.anchorPoint = anchorPoint; + } + + public EnergyAnchorData(AnchorPoint anchorPoint, float anchorOffsetX, float anchorOffsetY, float anchorOffsetZ) { + this.anchorPoint = anchorPoint; + this.anchorOffsetX = anchorOffsetX; + this.anchorOffsetY = anchorOffsetY; + this.anchorOffsetZ = anchorOffsetZ; + } + + public EnergyAnchorData(int anchorPoint) { + this.anchorPoint = AnchorPoint.fromId(anchorPoint); + } + + public EnergyAnchorData(int anchorPoint, float anchorOffsetX, float anchorOffsetY, float anchorOffsetZ) { + this.anchorPoint = AnchorPoint.fromId(anchorPoint); + this.anchorOffsetX = anchorOffsetX; + this.anchorOffsetY = anchorOffsetY; + this.anchorOffsetZ = anchorOffsetZ; + } + + public void writeNBT(NBTTagCompound nbt) { + nbt.setInteger("anchorPoint", anchorPoint.getId()); + nbt.setFloat("anchorOffsetX", anchorOffsetX); + nbt.setFloat("anchorOffsetY", anchorOffsetY); + nbt.setFloat("anchorOffsetZ", anchorOffsetZ); + } + + public void readNBT(NBTTagCompound nbt) { + this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromId(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; + this.anchorOffsetX = nbt.hasKey("anchorOffsetX") ? nbt.getFloat("anchorOffsetX") : 0; + this.anchorOffsetY = nbt.hasKey("anchorOffsetY") ? nbt.getFloat("anchorOffsetY") : 0; + this.anchorOffsetZ = nbt.hasKey("anchorOffsetZ") ? nbt.getFloat("anchorOffsetZ") : 0; + } + + public EnergyAnchorData copy() { + return new EnergyAnchorData(anchorPoint, anchorOffsetX, anchorOffsetY, anchorOffsetZ); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index b80dcbdef..3ef2e164d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -6,11 +6,7 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; -import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; -import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData; -import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; -import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; +import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; @@ -19,7 +15,6 @@ import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.DamageSource; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -42,15 +37,13 @@ public class AbilityDisc extends Ability implements IAbilityDisc { private boolean boomerang = false; private int boomerangDelay = 40; - // Anchor point for charging position - private AnchorPoint anchorPoint = AnchorPoint.RIGHT_HAND; - // Energy data classes private EnergyColorData colorData = new EnergyColorData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f); private EnergyCombatData combatData = new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f); private EnergyHomingData homingData = new EnergyHomingData(0.6f, true, 0.12f, 18.0f); private EnergyLightningData lightningData = new EnergyLightningData(); private EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200); + private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); // Transient state for disc entity (used during windup charging) private transient EntityAbilityDisc discEntity = null; @@ -115,14 +108,14 @@ public class AbilityDisc extends Ability implements IAbilityDisc { // Spawn disc in charging mode on first tick of windup if (tick == 1) { // Create disc in charging mode - follows NPC based on anchor point during windup - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(npc, anchorPoint); + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(npc, anchorData); discEntity = new EntityAbilityDisc( world, npc, target, spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, discRadius, discThickness, colorData, combatData, homingData, lightningData, lifespanData, boomerang, boomerangDelay); - discEntity.setupCharging(anchorPoint, windUpTicks); + discEntity.setupCharging(anchorData, windUpTicks); discEntity.setEffects(this.effects); world.spawnEntityInWorld(discEntity); @@ -184,7 +177,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { nbt.setFloat("discThickness", discThickness); nbt.setBoolean("boomerang", boomerang); nbt.setInteger("boomerangDelay", boomerangDelay); - nbt.setInteger("anchorPoint", anchorPoint.getId()); + anchorData.writeNBT(nbt); colorData.writeNBT(nbt); combatData.writeNBT(nbt); homingData.writeNBT(nbt); @@ -198,7 +191,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang"); this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40; - this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromId(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; + anchorData.readNBT(nbt); colorData.readNBT(nbt); combatData.readNBT(nbt); homingData.readNBT(nbt); @@ -257,14 +250,20 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public void setLightningDensity(float lightningDensity) { lightningData.lightningDensity = lightningDensity; } public float getLightningRadius() { return lightningData.lightningRadius; } public void setLightningRadius(float lightningRadius) { lightningData.lightningRadius = lightningRadius; } - public AnchorPoint getAnchorPointEnum() { return anchorPoint; } - public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } + public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; } + public float getAnchorOffsetX() { return anchorData.anchorOffsetX; } + public float getAnchorOffsetY() { return anchorData.anchorOffsetY; } + public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; } + public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; } + public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; } + public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } @Override - public int getAnchorPoint() { return anchorPoint.getId(); } + public int getAnchorPoint() { return anchorData.anchorPoint.getId(); } @Override - public void setAnchorPoint(int point) { this.anchorPoint = AnchorPoint.fromId(point); } + public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromId(point); } @Override @SideOnly(Side.CLIENT) @@ -272,7 +271,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { if (npc == null || npc.worldObj == null) return null; EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj); - disc.setupPreview(npc, discRadius, discThickness, colorData, lightningData, anchorPoint, windUpTicks); + disc.setupPreview(npc, discRadius, discThickness, colorData, lightningData, anchorData, windUpTicks); return disc; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 049c4c281..53377161a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -6,11 +6,7 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; -import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; -import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData; -import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; -import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; @@ -44,9 +40,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { private EnergyHomingData homingData = new EnergyHomingData(0.4f, true, 0.1f, 15.0f); private EnergyLightningData lightningData = new EnergyLightningData(); private EnergyLifespanData lifespanData = new EnergyLifespanData(25.0f, 200); - - // Anchor point for charging position - private AnchorPoint anchorPoint = AnchorPoint.RIGHT_HAND; + private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); // Transient state for beam entity (used during windup charging) private transient EntityAbilityBeam beamEntity = null; @@ -113,14 +107,14 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { float offsetDist = 1.0f; // Create beam in charging mode - follows NPC based on anchor point during windup - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(npc, anchorPoint, offsetDist); + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(npc, anchorData, offsetDist); beamEntity = new EntityAbilityBeam( world, npc, target, spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, beamWidth, headSize, colorData, combatData, homingData, lightningData, lifespanData, lockMovement.locksActive()); - beamEntity.setupCharging(anchorPoint, windUpTicks, offsetDist); + beamEntity.setupCharging(anchorData, windUpTicks, offsetDist); beamEntity.setEffects(this.effects); world.spawnEntityInWorld(beamEntity); @@ -180,7 +174,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("beamWidth", beamWidth); nbt.setFloat("headSize", headSize); - nbt.setInteger("anchorPoint", anchorPoint.getId()); + anchorData.writeNBT(nbt); colorData.writeNBT(nbt); combatData.writeNBT(nbt); homingData.writeNBT(nbt); @@ -192,7 +186,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { public void readTypeNBT(NBTTagCompound nbt) { this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f; this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f; - this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromId(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; + anchorData.readNBT(nbt); colorData.readNBT(nbt); combatData.readNBT(nbt); homingData.readNBT(nbt); @@ -259,14 +253,20 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; } // Getters & Setters - Anchor point - public AnchorPoint getAnchorPointEnum() { return anchorPoint; } - public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorPoint = anchorPoint; } + public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; } + public float getAnchorOffsetX() { return anchorData.anchorOffsetX; } + public float getAnchorOffsetY() { return anchorData.anchorOffsetY; } + public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; } + public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; } + public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; } + public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } @Override - public int getAnchorPoint() { return anchorPoint.getId(); } + public int getAnchorPoint() { return anchorData.anchorPoint.getId(); } @Override - public void setAnchorPoint(int point) { this.anchorPoint = AnchorPoint.fromId(point); } + public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromId(point); } @Override @SideOnly(Side.CLIENT) @@ -274,7 +274,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { if (npc == null || npc.worldObj == null) return null; EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj); - beam.setupPreview(npc, beamWidth, headSize, colorData, lightningData, anchorPoint, windUpTicks, 1.0f); + beam.setupPreview(npc, beamWidth, headSize, colorData, lightningData, anchorData, windUpTicks, 1.0f); return beam; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 6c7e8c5b6..014a88f40 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -6,11 +6,7 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; -import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; -import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData; -import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; -import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; +import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; @@ -19,7 +15,6 @@ import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; -import net.minecraft.util.DamageSource; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -38,15 +33,13 @@ public class AbilityOrb extends Ability implements IAbilityOrb { // Ability-specific properties private float orbSize = 1.0f; - // Anchor point for charging position - private AnchorPoint anchorPoint = AnchorPoint.RIGHT_HAND; - // Data classes for energy properties public final EnergyColorData colorData = new EnergyColorData(); public final EnergyCombatData combatData = new EnergyCombatData(); public final EnergyHomingData homingData = new EnergyHomingData(); public final EnergyLightningData lightningData = new EnergyLightningData(); public final EnergyLifespanData lifespanData = new EnergyLifespanData(); + private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); // Transient state for orb entity (used during windup charging) private transient EntityAbilityOrb orbEntity = null; @@ -110,12 +103,12 @@ public class AbilityOrb extends Ability implements IAbilityOrb { // Spawn orb in charging mode on first tick of windup if (tick == 1) { // Create orb in charging mode - follows NPC based on anchor point during windup - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(npc, anchorPoint); + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(npc, anchorData); orbEntity = new EntityAbilityOrb( world, npc, target, spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize, colorData, combatData, homingData, lightningData, lifespanData); - orbEntity.setupCharging(anchorPoint, windUpTicks); + orbEntity.setupCharging(anchorData, windUpTicks); orbEntity.setEffects(this.effects); world.spawnEntityInWorld(orbEntity); @@ -175,7 +168,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { @Override public void writeTypeNBT(NBTTagCompound nbt) { nbt.setFloat("orbSize", orbSize); - nbt.setInteger("anchorPoint", anchorPoint.getId()); + anchorData.writeNBT(nbt); colorData.writeNBT(nbt); combatData.writeNBT(nbt); homingData.writeNBT(nbt); @@ -188,7 +181,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { @Override public void readTypeNBT(NBTTagCompound nbt) { this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f; - this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromId(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; + anchorData.readNBT(nbt); colorData.readNBT(nbt); combatData.readNBT(nbt); homingData.readNBT(nbt); @@ -385,21 +378,29 @@ public class AbilityOrb extends Ability implements IAbilityOrb { lightningData.lightningFadeTime = lightningFadeTime; } - public AnchorPoint getAnchorPointEnum() { - return anchorPoint; - } + public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; } - public void setAnchorPointEnum(AnchorPoint anchorPoint) { - this.anchorPoint = anchorPoint; - } + public float getAnchorOffsetX() { return anchorData.anchorOffsetX; } + + public float getAnchorOffsetY() { return anchorData.anchorOffsetY; } + + public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; } + + public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; } + + public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; } + + public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; } + + public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } public int getAnchorPoint() { - return anchorPoint.getId(); + return anchorData.anchorPoint.getId(); } @Override public void setAnchorPoint(int point) { - this.anchorPoint = AnchorPoint.fromId(point); + this.anchorData.anchorPoint = AnchorPoint.fromId(point); } @Override @@ -408,7 +409,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { if (npc == null || npc.worldObj == null) return null; EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj); - orb.setupPreview(npc, orbSize, colorData, lightningData, anchorPoint, windUpTicks); + orb.setupPreview(npc, orbSize, colorData, lightningData, anchorData, windUpTicks); return orb; } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index ca600b84f..75fa6ef7d 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -1,11 +1,7 @@ package kamkeel.npcs.entity; import kamkeel.npcs.controllers.data.ability.AnchorPoint; -import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; -import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; -import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData; -import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; -import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; +import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; @@ -62,7 +58,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { private int chargeDuration = 40; private int chargeTick = 0; private float chargeOffsetDistance = 1.0f; - private AnchorPoint anchorPoint = AnchorPoint.FRONT; + private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT); // Trail fading for non-anchored beams (comet effect) private boolean fadeTrail = false; @@ -171,19 +167,19 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { } } - public void setupCharging(AnchorPoint anchorPoint, int chargeDuration, float chargeOffsetDistance) { + public void setupCharging(EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) { setCharging(true); this.chargeDuration = chargeDuration; this.chargeTick = 0; this.chargeOffsetDistance = chargeOffsetDistance; - this.anchorPoint = anchorPoint; + this.anchorData = anchor; this.fadeTrail = !attachedToOwner; this.motionX = 0; this.motionY = 0; this.motionZ = 0; } - public void setupPreview(EntityNPCInterface owner, float beamWidth, float headSize, EnergyColorData color, EnergyLightningData lightning, AnchorPoint anchorPoint, int chargeDuration, float chargeOffsetDistance) { + public void setupPreview(EntityNPCInterface owner, float beamWidth, float headSize, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) { this.setPreviewMode(true); this.setPreviewOwner(owner); @@ -206,10 +202,10 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { this.chargeDuration = chargeDuration; this.chargeTick = 0; this.chargeOffsetDistance = chargeOffsetDistance; - this.anchorPoint = anchorPoint; + this.anchorData = anchor; // Initial position at anchor point - Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint, chargeOffsetDistance); + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData, chargeOffsetDistance); this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); this.prevPosX = pos.xCoord; this.prevPosY = pos.yCoord; @@ -450,7 +446,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { // Calculate position based on anchor point Vec3 pos; if (owner instanceof EntityLivingBase) { - pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorPoint, chargeOffsetDistance); + pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData, chargeOffsetDistance); } else { // Fallback for non-living entities (shouldn't happen normally) float yaw = (float) Math.toRadians(owner.rotationYaw); @@ -772,10 +768,11 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; this.chargeOffsetDistance = nbt.hasKey("ChargeOffsetDistance") ? nbt.getFloat("ChargeOffsetDistance") : 1.0f; - this.anchorPoint = nbt.hasKey("AnchorPoint") ? AnchorPoint.fromId(nbt.getInteger("AnchorPoint")) : AnchorPoint.FRONT; this.fadeTrail = nbt.hasKey("FadeTrail") && nbt.getBoolean("FadeTrail"); this.trailFadeTime = nbt.hasKey("TrailFadeTime") ? nbt.getInteger("TrailFadeTime") : 20; + this.anchorData.readNBT(nbt); + // Read trail points (relative to origin) trailPoints.clear(); if (nbt.hasKey("Trail")) { @@ -808,10 +805,11 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { nbt.setInteger("ChargeDuration", chargeDuration); nbt.setInteger("ChargeTick", chargeTick); nbt.setFloat("ChargeOffsetDistance", chargeOffsetDistance); - nbt.setInteger("AnchorPoint", anchorPoint.getId()); nbt.setBoolean("FadeTrail", fadeTrail); nbt.setInteger("TrailFadeTime", trailFadeTime); + this.anchorData.writeNBT(nbt); + // Write trail points NBTTagList trailList = new NBTTagList(); for (Vec3 point : trailPoints) { diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index 20a43a98a..f6003df80 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -1,11 +1,7 @@ package kamkeel.npcs.entity; import kamkeel.npcs.controllers.data.ability.AnchorPoint; -import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; -import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; -import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData; -import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; -import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; +import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; @@ -46,9 +42,9 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { private boolean charging = false; private int chargeDuration = 40; private int chargeTick = 0; - private AnchorPoint anchorPoint = AnchorPoint.FRONT; private float targetDiscRadius = 1.0f; // Full radius to grow to during charging private float targetDiscThickness = 0.2f; // Full thickness to grow to during charging + private EnergyAnchorData anchorData = new EnergyAnchorData(); // Data watcher index for charging state (synced to clients) private static final int DW_CHARGING = 20; @@ -137,11 +133,11 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { * The disc will grow from 0 to discRadius over chargeDuration ticks. * Position follows the owner based on anchor point. */ - public void setupCharging(AnchorPoint anchorPoint, int chargeDuration) { + public void setupCharging(EnergyAnchorData anchor, int chargeDuration) { setCharging(true); this.chargeDuration = chargeDuration; this.chargeTick = 0; - this.anchorPoint = anchorPoint; + this.anchorData = anchor; this.targetDiscRadius = this.discRadius; this.targetDiscThickness = this.discThickness; this.discRadius = 0.01f; @@ -159,7 +155,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { * Follows anchor point and animations like in the real game. * Can be fired when transitioning to active phase. */ - public void setupPreview(EntityNPCInterface owner, float discRadius, float discThickness, EnergyColorData color, EnergyLightningData lightning, AnchorPoint anchorPoint, int chargeDuration) { + public void setupPreview(EntityNPCInterface owner, float discRadius, float discThickness, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) { this.setPreviewMode(true); this.setPreviewOwner(owner); @@ -177,7 +173,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { this.setCharging(true); this.chargeDuration = chargeDuration; this.chargeTick = 0; - this.anchorPoint = anchorPoint; + this.anchorData = anchor; // Store target size and start at 0 for grow effect this.targetDiscRadius = discRadius; @@ -187,7 +183,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { this.size = 0.01f; // Initial position at anchor point - Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint); + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData); this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); this.prevPosX = pos.xCoord; this.prevPosY = pos.yCoord; @@ -518,7 +514,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { // Calculate position based on anchor point if (owner instanceof EntityLivingBase) { - Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorPoint); + Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); setPosition(pos.xCoord, pos.yCoord, pos.zCoord); } @@ -589,9 +585,9 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; - this.anchorPoint = nbt.hasKey("AnchorPoint") ? AnchorPoint.fromId(nbt.getInteger("AnchorPoint")) : AnchorPoint.FRONT; this.targetDiscRadius = nbt.hasKey("TargetDiscRadius") ? nbt.getFloat("TargetDiscRadius") : this.discRadius; this.targetDiscThickness = nbt.hasKey("TargetDiscThickness") ? nbt.getFloat("TargetDiscThickness") : this.discThickness; + this.anchorData.readNBT(nbt); } @Override @@ -609,8 +605,10 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { nbt.setBoolean("Charging", isCharging()); nbt.setInteger("ChargeDuration", chargeDuration); nbt.setInteger("ChargeTick", chargeTick); - nbt.setInteger("AnchorPoint", anchorPoint.getId()); nbt.setFloat("TargetDiscRadius", targetDiscRadius); nbt.setFloat("TargetDiscThickness", targetDiscThickness); + + // Anchor data + this.anchorData.writeNBT(nbt); } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index 2473730c3..eff147448 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -1,11 +1,7 @@ package kamkeel.npcs.entity; import kamkeel.npcs.controllers.data.ability.AnchorPoint; -import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; -import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; -import kamkeel.npcs.controllers.data.ability.data.EnergyHomingData; -import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; -import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; +import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; @@ -36,7 +32,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { private boolean charging = false; private int chargeDuration = 40; private int chargeTick = 0; - private AnchorPoint anchorPoint = AnchorPoint.FRONT; + private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT); private float targetSize = 1.0f; // Full size to grow to during charging // Data watcher index for charging state (synced to clients) @@ -119,11 +115,11 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { * The orb will grow from 0 to orbSize over chargeDuration ticks. * Position follows the owner based on anchor point. */ - public void setupCharging(AnchorPoint anchorPoint, int chargeDuration) { + public void setupCharging(EnergyAnchorData anchor, int chargeDuration) { setCharging(true); this.chargeDuration = chargeDuration; this.chargeTick = 0; - this.anchorPoint = anchorPoint; + this.anchorData = anchor; this.targetSize = this.size; this.size = 0.01f; this.renderCurrentSize = 0.01f; @@ -138,7 +134,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { * Follows anchor point and animations like in the real game. * Can be fired when transitioning to active phase. */ - public void setupPreview(EntityNPCInterface owner, float orbSize, EnergyColorData color, EnergyLightningData lightning, AnchorPoint anchorPoint, int chargeDuration) { + public void setupPreview(EntityNPCInterface owner, float orbSize, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) { this.setPreviewMode(true); this.setPreviewOwner(owner); @@ -157,7 +153,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { this.setCharging(true); this.chargeDuration = chargeDuration; this.chargeTick = 0; - this.anchorPoint = anchorPoint; + this.anchorData = anchor; // Store target size and start at 0 for grow effect this.targetSize = orbSize; @@ -166,7 +162,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { this.prevRenderSize = 0.01f; // Initial position at anchor point - Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorPoint); + Vec3 pos = AnchorPointHelper.calculateAnchorPosition(owner, anchorData); this.setPosition(pos.xCoord, pos.yCoord, pos.zCoord); this.prevPosX = pos.xCoord; this.prevPosY = pos.yCoord; @@ -400,7 +396,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { // Calculate position based on anchor point if (owner instanceof EntityLivingBase) { - Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorPoint); + Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); setPosition(pos.xCoord, pos.yCoord, pos.zCoord); } } @@ -460,8 +456,9 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; - this.anchorPoint = nbt.hasKey("AnchorPoint") ? AnchorPoint.fromId(nbt.getInteger("AnchorPoint")) : AnchorPoint.FRONT; this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size; + // Anchor data + this.anchorData.readNBT(nbt); } @Override @@ -474,7 +471,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { nbt.setBoolean("Charging", isCharging()); nbt.setInteger("ChargeDuration", chargeDuration); nbt.setInteger("ChargeTick", chargeTick); - nbt.setInteger("AnchorPoint", anchorPoint.getId()); + anchorData.writeNBT(nbt); nbt.setFloat("TargetSize", targetSize); } } diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java index 45e4f3eab..d9d584b64 100644 --- a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -1,6 +1,7 @@ package kamkeel.npcs.util; import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.data.EnergyAnchorData; import net.minecraft.entity.EntityLivingBase; import net.minecraft.util.Vec3; import noppes.npcs.constants.EnumAnimationPart; @@ -48,17 +49,23 @@ public class AnchorPointHelper { /** * Calculate world position for an anchor point on an entity. */ - public static Vec3 calculateAnchorPosition(EntityLivingBase entity, AnchorPoint anchor) { - return calculateAnchorPosition(entity, anchor, DEFAULT_FRONT_DISTANCE); + public static Vec3 calculateAnchorPosition(EntityLivingBase entity, EnergyAnchorData anchorData) { + return calculateAnchorPosition(entity, anchorData, DEFAULT_FRONT_DISTANCE); } /** * Calculate world position for an anchor point with custom front offset. */ - public static Vec3 calculateAnchorPosition(EntityLivingBase entity, AnchorPoint anchor, float frontOffset) { + public static Vec3 calculateAnchorPosition(EntityLivingBase entity, EnergyAnchorData anchorData, float frontOffset) { float scale = getModelScale(entity); float height = entity.height; + AnchorPoint anchor = anchorData.anchorPoint; + + float anchorX = anchorData.anchorOffsetX; + float anchorY = anchorData.anchorOffsetY; + float anchorZ = anchorData.anchorOffsetZ; + double x = entity.posX; double y = entity.posY; double z = entity.posZ; @@ -78,10 +85,10 @@ public class AnchorPointHelper { break; case RIGHT_HAND: - return calculateHandPosition(entity, true, scale); + return calculateHandPosition(entity, anchorData, true, scale); case LEFT_HAND: - return calculateHandPosition(entity, false, scale); + return calculateHandPosition(entity, anchorData, false, scale); case ABOVE_HEAD: y += height * ABOVE_HEAD_HEIGHT; @@ -92,6 +99,10 @@ public class AnchorPointHelper { break; } + x += anchorX * scale; + y += anchorY * scale; + z += anchorZ * scale; + return Vec3.createVectorHelper(x, y, z); } @@ -124,22 +135,22 @@ public class AnchorPointHelper { /** * Calculate hand position using animation data if available. */ - private static Vec3 calculateHandPosition(EntityLivingBase entity, boolean rightHand, float scale) { + private static Vec3 calculateHandPosition(EntityLivingBase entity, EnergyAnchorData anchor, boolean rightHand, float scale) { // Try to get animated position first - Vec3 animatedPos = getAnimatedHandPosition(entity, rightHand, scale); + Vec3 animatedPos = getAnimatedHandPosition(entity, anchor, rightHand, scale); if (animatedPos != null) { return animatedPos; } // Fallback to static position - return calculateFallbackHandPosition(entity, rightHand, scale); + return calculateFallbackHandPosition(entity, anchor, rightHand, scale); } /** * Calculate hand position from animation data. * Works for both NPCs and Players. */ - private static Vec3 getAnimatedHandPosition(EntityLivingBase entity, boolean rightHand, float scale) { + private static Vec3 getAnimatedHandPosition(EntityLivingBase entity, EnergyAnchorData anchor, boolean rightHand, float scale) { AnimationData animData = AnimationData.getData(entity); if (animData == null || !animData.isActive() || animData.animation == null) { return null; @@ -235,6 +246,10 @@ public class AnchorPointHelper { double worldY = entity.posY + shoulderHeight - blockY; double worldZ = entity.posZ + worldOffsetZ; + worldX += anchor.anchorOffsetX * scale; + worldY += anchor.anchorOffsetY * scale; + worldZ += anchor.anchorOffsetZ * scale; + return Vec3.createVectorHelper(worldX, worldY, worldZ); } @@ -277,7 +292,7 @@ public class AnchorPointHelper { * Calculate fallback hand position when no animation is active. * Represents arms hanging at sides in default idle pose. */ - private static Vec3 calculateFallbackHandPosition(EntityLivingBase entity, boolean rightHand, float scale) { + private static Vec3 calculateFallbackHandPosition(EntityLivingBase entity, EnergyAnchorData anchor, boolean rightHand, float scale) { float bodyYaw = (float) Math.toRadians(entity.renderYawOffset); // Get arm scale for EntityCustomNpc @@ -302,6 +317,10 @@ public class AnchorPointHelper { double y = entity.posY + entity.height * FALLBACK_ARM_HEIGHT + armLengthOffset; double z = entity.posZ + lateralZ + forwardZ; + x += anchor.anchorOffsetX * scale; + y += anchor.anchorOffsetY * scale; + z += anchor.anchorOffsetZ * scale; + return Vec3.createVectorHelper(x, y, z); } From 601b0d721ed297e53778477a5488ab337993b720 Mon Sep 17 00:00:00 2001 From: Vidal Date: Fri, 30 Jan 2026 00:33:32 -0300 Subject: [PATCH 313/358] Added an active animation for orb and updated windup and IDs --- .../data/ability/type/AbilityOrb.java | 1 + .../animations/Ability_Beam_Active.json | 64 +-- .../animations/Ability_Disc_Active.json | 28 +- .../animations/Ability_Disc_Windup.json | 482 +++++++++--------- .../animations/Ability_Laser_Windup.json | 112 ++-- .../animations/Ability_Orb_Active.json | 77 +++ .../animations/Ability_Orb_Windup.json | 143 ++---- .../animations/Ability_Slam_Active.json | 60 +-- .../animations/Ability_Slam_Windup.json | 94 ++-- 9 files changed, 538 insertions(+), 523 deletions(-) create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_Orb_Active.json diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 014a88f40..78936adbf 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -57,6 +57,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { this.showTelegraph = true; // Default built-in animation this.windUpAnimationName = "Ability_Orb_Windup"; + this.activeAnimationName = "Ability_Orb_Active"; } @Override diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json index 466b9163c..e285bc679 100644 --- a/src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json +++ b/src/main/resources/assets/customnpcs/animations/Ability_Beam_Active.json @@ -9,13 +9,22 @@ "FrameParts": [ { "Pivot2": -5.0f, - "Part": "RIGHT_ARM", - "Rotation1": -18.0f, - "Rotation0": -107.0f, - "Pivot0": -1.0f, - "Pivot1": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 12.0f, + "Rotation0": -75.0f, + "Pivot0": -2.0f, + "Pivot1": 3.0f, "Rotation2": 50.0f }, + { + "Pivot2": -4.0f, + "Part": "RIGHT_LEG", + "Rotation1": 56.0f, + "Rotation0": 18.0f, + "Pivot0": -4.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, { "Pivot2": -6.0f, "Part": "LEFT_LEG", @@ -25,6 +34,15 @@ "Pivot1": 0.0f, "Rotation2": -6.0f }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, { "Pivot2": -3.0f, "Part": "BODY", @@ -34,6 +52,15 @@ "Pivot1": 1.0f, "Rotation2": 0.0f }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": -18.0f, + "Rotation0": -107.0f, + "Pivot0": -1.0f, + "Pivot1": 2.0f, + "Rotation2": 50.0f + }, { "Pivot2": 0.0f, "Part": "FULL_MODEL", @@ -42,33 +69,6 @@ "Pivot0": 0.0f, "Pivot1": 0.0f, "Rotation2": 0.0f - }, - { - "Pivot2": -3.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": -1.0f, - "Pivot1": 1.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -4.0f, - "Part": "RIGHT_LEG", - "Rotation1": 56.0f, - "Rotation0": 18.0f, - "Pivot0": -4.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -5.0f, - "Part": "LEFT_ARM", - "Rotation1": 12.0f, - "Rotation0": -75.0f, - "Pivot0": -2.0f, - "Pivot1": 3.0f, - "Rotation2": 50.0f } ], "Smooth": 0b, diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Disc_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Disc_Active.json index 0e221560e..13fe07485 100644 --- a/src/main/resources/assets/customnpcs/animations/Ability_Disc_Active.json +++ b/src/main/resources/assets/customnpcs/animations/Ability_Disc_Active.json @@ -9,12 +9,12 @@ "FrameParts": [ { "Pivot2": -5.0f, - "Part": "BODY", + "Part": "HEAD", "Rotation1": 0.0f, "Rotation0": 25.0f, "Pivot0": 2.0f, "Pivot1": 3.0f, - "Rotation2": 12.0f + "Rotation2": 6.0f }, { "Pivot2": 0.0f, @@ -25,15 +25,6 @@ "Pivot1": 1.0f, "Rotation2": 31.0f }, - { - "Pivot2": -7.0f, - "Part": "RIGHT_ARM", - "Rotation1": -44.0f, - "Rotation0": -90.0f, - "Pivot0": 1.0f, - "Pivot1": 4.0f, - "Rotation2": 90.0f - }, { "Pivot2": -4.0f, "Part": "LEFT_LEG", @@ -43,6 +34,15 @@ "Pivot1": 0.0f, "Rotation2": 0.0f }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": -44.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 4.0f, + "Rotation2": 90.0f + }, { "Pivot2": -2.0f, "Part": "LEFT_ARM", @@ -54,12 +54,12 @@ }, { "Pivot2": -5.0f, - "Part": "HEAD", + "Part": "BODY", "Rotation1": 0.0f, "Rotation0": 25.0f, "Pivot0": 2.0f, "Pivot1": 3.0f, - "Rotation2": 6.0f + "Rotation2": 12.0f } ], "Smooth": 0b, @@ -69,7 +69,7 @@ "CurrentFrameTime": 0, "CurrentFrame": 0, "Smooth": 0b, - "ID": 7, + "ID": 9, "WhileAttacking": 1b, "WhileWalking": 1b, "Name": "Ability_Disc_Active", diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Disc_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Disc_Windup.json index 035e51f7f..a8231ff50 100644 --- a/src/main/resources/assets/customnpcs/animations/Ability_Disc_Windup.json +++ b/src/main/resources/assets/customnpcs/animations/Ability_Disc_Windup.json @@ -9,22 +9,13 @@ "FrameParts": [ { "Pivot2": 0.0f, - "Part": "BODY", + "Part": "HEAD", "Rotation1": 0.0f, "Rotation0": 0.0f, "Pivot0": 2.0f, "Pivot1": 2.0f, "Rotation2": 18.0f }, - { - "Pivot2": -1.0f, - "Part": "RIGHT_ARM", - "Rotation1": 90.0f, - "Rotation0": 0.0f, - "Pivot0": 1.0f, - "Pivot1": -2.0f, - "Rotation2": 180.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -44,13 +35,13 @@ "Rotation2": 0.0f }, { - "Pivot2": 0.0f, - "Part": "HEAD", - "Rotation1": 0.0f, + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, "Rotation0": 0.0f, - "Pivot0": 2.0f, - "Pivot1": 2.0f, - "Rotation2": 18.0f + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f }, { "Pivot2": 3.0f, @@ -60,6 +51,15 @@ "Pivot0": -1.0f, "Pivot1": 7.0f, "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f } ], "Smooth": 0b, @@ -72,22 +72,13 @@ "FrameParts": [ { "Pivot2": 0.0f, - "Part": "BODY", + "Part": "HEAD", "Rotation1": 0.0f, "Rotation0": 0.0f, "Pivot0": 0.0f, "Pivot1": 1.0f, "Rotation2": 6.0f }, - { - "Pivot2": -1.0f, - "Part": "RIGHT_ARM", - "Rotation1": 90.0f, - "Rotation0": 25.0f, - "Pivot0": -1.0f, - "Pivot1": -2.0f, - "Rotation2": 180.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -107,13 +98,13 @@ "Rotation2": 0.0f }, { - "Pivot2": 0.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": 6.0f + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 25.0f, + "Pivot0": -1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f }, { "Pivot2": 3.0f, @@ -123,6 +114,15 @@ "Pivot0": -2.0f, "Pivot1": 7.0f, "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f } ], "Smooth": 0b, @@ -135,22 +135,13 @@ "FrameParts": [ { "Pivot2": 2.0f, - "Part": "BODY", + "Part": "HEAD", "Rotation1": 0.0f, "Rotation0": -12.0f, "Pivot0": 0.0f, "Pivot1": 2.0f, "Rotation2": 6.0f }, - { - "Pivot2": 1.0f, - "Part": "RIGHT_ARM", - "Rotation1": 110.0f, - "Rotation0": -90.0f, - "Pivot0": -1.0f, - "Pivot1": -2.0f, - "Rotation2": 90.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -170,13 +161,13 @@ "Rotation2": 0.0f }, { - "Pivot2": 2.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": -12.0f, - "Pivot0": 0.0f, - "Pivot1": 2.0f, - "Rotation2": 6.0f + "Pivot2": 1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 110.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": -2.0f, + "Rotation2": 90.0f }, { "Pivot2": 4.0f, @@ -186,6 +177,15 @@ "Pivot0": -1.0f, "Pivot1": 7.0f, "Rotation2": 37.0f + }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 6.0f } ], "Smooth": 0b, @@ -198,22 +198,13 @@ "FrameParts": [ { "Pivot2": 2.0f, - "Part": "BODY", + "Part": "HEAD", "Rotation1": 0.0f, "Rotation0": -12.0f, "Pivot0": 2.0f, "Pivot1": 2.0f, "Rotation2": 18.0f }, - { - "Pivot2": 2.0f, - "Part": "RIGHT_ARM", - "Rotation1": 120.0f, - "Rotation0": -90.0f, - "Pivot0": 1.0f, - "Pivot1": -2.0f, - "Rotation2": 90.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -234,12 +225,12 @@ }, { "Pivot2": 2.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": -12.0f, - "Pivot0": 2.0f, - "Pivot1": 2.0f, - "Rotation2": 18.0f + "Part": "RIGHT_ARM", + "Rotation1": 120.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 90.0f }, { "Pivot2": 4.0f, @@ -249,6 +240,15 @@ "Pivot0": -1.0f, "Pivot1": 7.0f, "Rotation2": 37.0f + }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f } ], "Smooth": 0b, @@ -261,22 +261,13 @@ "FrameParts": [ { "Pivot2": 0.0f, - "Part": "BODY", + "Part": "HEAD", "Rotation1": 0.0f, "Rotation0": 0.0f, "Pivot0": 4.0f, "Pivot1": 3.0f, "Rotation2": 31.0f }, - { - "Pivot2": -1.0f, - "Part": "RIGHT_ARM", - "Rotation1": 90.0f, - "Rotation0": -18.0f, - "Pivot0": 3.0f, - "Pivot1": -1.0f, - "Rotation2": 180.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -296,13 +287,13 @@ "Rotation2": 0.0f }, { - "Pivot2": 0.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 4.0f, - "Pivot1": 3.0f, - "Rotation2": 31.0f + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -18.0f, + "Pivot0": 3.0f, + "Pivot1": -1.0f, + "Rotation2": 180.0f }, { "Pivot2": 3.0f, @@ -312,6 +303,15 @@ "Pivot0": 0.0f, "Pivot1": 8.0f, "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f } ], "Smooth": 0b, @@ -324,22 +324,13 @@ "FrameParts": [ { "Pivot2": -2.0f, - "Part": "BODY", + "Part": "HEAD", "Rotation1": 0.0f, "Rotation0": 12.0f, "Pivot0": 4.0f, "Pivot1": 3.0f, "Rotation2": 31.0f }, - { - "Pivot2": -3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 90.0f, - "Rotation0": -18.0f, - "Pivot0": 3.0f, - "Pivot1": -1.0f, - "Rotation2": 180.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -359,13 +350,13 @@ "Rotation2": 0.0f }, { - "Pivot2": -2.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 12.0f, - "Pivot0": 4.0f, - "Pivot1": 3.0f, - "Rotation2": 31.0f + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -18.0f, + "Pivot0": 3.0f, + "Pivot1": -1.0f, + "Rotation2": 180.0f }, { "Pivot2": 1.0f, @@ -375,6 +366,15 @@ "Pivot0": 0.0f, "Pivot1": 8.0f, "Rotation2": 37.0f + }, + { + "Pivot2": -2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f } ], "Smooth": 0b, @@ -387,22 +387,13 @@ "FrameParts": [ { "Pivot2": -2.0f, - "Part": "BODY", + "Part": "HEAD", "Rotation1": 0.0f, "Rotation0": 12.0f, "Pivot0": 2.0f, "Pivot1": 2.0f, "Rotation2": 18.0f }, - { - "Pivot2": -2.0f, - "Part": "RIGHT_ARM", - "Rotation1": 90.0f, - "Rotation0": 0.0f, - "Pivot0": 1.0f, - "Pivot1": -2.0f, - "Rotation2": 180.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -423,12 +414,12 @@ }, { "Pivot2": -2.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 12.0f, - "Pivot0": 2.0f, - "Pivot1": 2.0f, - "Rotation2": 18.0f + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": 0.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f }, { "Pivot2": 2.0f, @@ -438,6 +429,15 @@ "Pivot0": -1.0f, "Pivot1": 7.0f, "Rotation2": 37.0f + }, + { + "Pivot2": -2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f } ], "Smooth": 0b, @@ -450,22 +450,13 @@ "FrameParts": [ { "Pivot2": 0.0f, - "Part": "BODY", + "Part": "HEAD", "Rotation1": 0.0f, "Rotation0": 0.0f, "Pivot0": 2.0f, "Pivot1": 2.0f, "Rotation2": 18.0f }, - { - "Pivot2": -1.0f, - "Part": "RIGHT_ARM", - "Rotation1": 90.0f, - "Rotation0": 0.0f, - "Pivot0": 1.0f, - "Pivot1": -2.0f, - "Rotation2": 180.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -485,13 +476,13 @@ "Rotation2": 0.0f }, { - "Pivot2": 0.0f, - "Part": "HEAD", - "Rotation1": 0.0f, + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, "Rotation0": 0.0f, - "Pivot0": 2.0f, - "Pivot1": 2.0f, - "Rotation2": 18.0f + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 180.0f }, { "Pivot2": 3.0f, @@ -501,6 +492,15 @@ "Pivot0": -1.0f, "Pivot1": 7.0f, "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f } ], "Smooth": 0b, @@ -513,13 +513,31 @@ "FrameParts": [ { "Pivot2": 0.0f, - "Part": "BODY", + "Part": "HEAD", "Rotation1": 0.0f, "Rotation0": 0.0f, "Pivot0": 0.0f, "Pivot1": 1.0f, "Rotation2": 6.0f }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 31.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, { "Pivot2": -1.0f, "Part": "RIGHT_ARM", @@ -529,33 +547,6 @@ "Pivot1": -2.0f, "Rotation2": 180.0f }, - { - "Pivot2": 0.0f, - "Part": "RIGHT_LEG", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": -2.0f, - "Pivot1": 0.0f, - "Rotation2": 31.0f - }, - { - "Pivot2": -3.0f, - "Part": "LEFT_LEG", - "Rotation1": -31.0f, - "Rotation0": 12.0f, - "Pivot0": 1.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": 0.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": 6.0f - }, { "Pivot2": 3.0f, "Part": "LEFT_ARM", @@ -564,6 +555,15 @@ "Pivot0": -2.0f, "Pivot1": 7.0f, "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f } ], "Smooth": 0b, @@ -576,22 +576,13 @@ "FrameParts": [ { "Pivot2": 2.0f, - "Part": "BODY", + "Part": "HEAD", "Rotation1": 0.0f, "Rotation0": -12.0f, "Pivot0": 0.0f, "Pivot1": 2.0f, "Rotation2": 6.0f }, - { - "Pivot2": 1.0f, - "Part": "RIGHT_ARM", - "Rotation1": 110.0f, - "Rotation0": -90.0f, - "Pivot0": -1.0f, - "Pivot1": -2.0f, - "Rotation2": 90.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -611,13 +602,13 @@ "Rotation2": 0.0f }, { - "Pivot2": 2.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": -12.0f, - "Pivot0": 0.0f, - "Pivot1": 2.0f, - "Rotation2": 6.0f + "Pivot2": 1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 110.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": -2.0f, + "Rotation2": 90.0f }, { "Pivot2": 4.0f, @@ -627,6 +618,15 @@ "Pivot0": -1.0f, "Pivot1": 7.0f, "Rotation2": 37.0f + }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 6.0f } ], "Smooth": 0b, @@ -639,22 +639,13 @@ "FrameParts": [ { "Pivot2": 2.0f, - "Part": "BODY", + "Part": "HEAD", "Rotation1": 0.0f, "Rotation0": -12.0f, "Pivot0": 2.0f, "Pivot1": 2.0f, "Rotation2": 18.0f }, - { - "Pivot2": 2.0f, - "Part": "RIGHT_ARM", - "Rotation1": 120.0f, - "Rotation0": -90.0f, - "Pivot0": 1.0f, - "Pivot1": -2.0f, - "Rotation2": 90.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -675,12 +666,12 @@ }, { "Pivot2": 2.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": -12.0f, - "Pivot0": 2.0f, - "Pivot1": 2.0f, - "Rotation2": 18.0f + "Part": "RIGHT_ARM", + "Rotation1": 120.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": -2.0f, + "Rotation2": 90.0f }, { "Pivot2": 4.0f, @@ -690,6 +681,15 @@ "Pivot0": -1.0f, "Pivot1": 7.0f, "Rotation2": 37.0f + }, + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": 2.0f, + "Pivot1": 2.0f, + "Rotation2": 18.0f } ], "Smooth": 0b, @@ -702,22 +702,13 @@ "FrameParts": [ { "Pivot2": 0.0f, - "Part": "BODY", + "Part": "HEAD", "Rotation1": 0.0f, "Rotation0": 0.0f, "Pivot0": 4.0f, "Pivot1": 3.0f, "Rotation2": 31.0f }, - { - "Pivot2": -1.0f, - "Part": "RIGHT_ARM", - "Rotation1": 90.0f, - "Rotation0": -18.0f, - "Pivot0": 3.0f, - "Pivot1": -1.0f, - "Rotation2": 180.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -737,13 +728,13 @@ "Rotation2": 0.0f }, { - "Pivot2": 0.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 0.0f, - "Pivot0": 4.0f, - "Pivot1": 3.0f, - "Rotation2": 31.0f + "Pivot2": -1.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -18.0f, + "Pivot0": 3.0f, + "Pivot1": -1.0f, + "Rotation2": 180.0f }, { "Pivot2": 3.0f, @@ -753,6 +744,15 @@ "Pivot0": 0.0f, "Pivot1": 8.0f, "Rotation2": 37.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f } ], "Smooth": 0b, @@ -765,22 +765,13 @@ "FrameParts": [ { "Pivot2": -2.0f, - "Part": "BODY", + "Part": "HEAD", "Rotation1": 0.0f, "Rotation0": 12.0f, "Pivot0": 4.0f, "Pivot1": 3.0f, "Rotation2": 31.0f }, - { - "Pivot2": -3.0f, - "Part": "RIGHT_ARM", - "Rotation1": 90.0f, - "Rotation0": -18.0f, - "Pivot0": 3.0f, - "Pivot1": -1.0f, - "Rotation2": 180.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -800,13 +791,13 @@ "Rotation2": 0.0f }, { - "Pivot2": -2.0f, - "Part": "HEAD", - "Rotation1": 0.0f, - "Rotation0": 12.0f, - "Pivot0": 4.0f, - "Pivot1": 3.0f, - "Rotation2": 31.0f + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -18.0f, + "Pivot0": 3.0f, + "Pivot1": -1.0f, + "Rotation2": 180.0f }, { "Pivot2": 1.0f, @@ -816,6 +807,15 @@ "Pivot0": 0.0f, "Pivot1": 8.0f, "Rotation2": 37.0f + }, + { + "Pivot2": -2.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 4.0f, + "Pivot1": 3.0f, + "Rotation2": 31.0f } ], "Smooth": 0b, @@ -828,12 +828,12 @@ "FrameParts": [ { "Pivot2": -5.0f, - "Part": "BODY", + "Part": "HEAD", "Rotation1": 0.0f, "Rotation0": 25.0f, "Pivot0": 2.0f, "Pivot1": 3.0f, - "Rotation2": 12.0f + "Rotation2": 6.0f }, { "Pivot2": 0.0f, @@ -844,15 +844,6 @@ "Pivot1": 1.0f, "Rotation2": 31.0f }, - { - "Pivot2": -7.0f, - "Part": "RIGHT_ARM", - "Rotation1": -44.0f, - "Rotation0": -90.0f, - "Pivot0": 1.0f, - "Pivot1": 4.0f, - "Rotation2": 90.0f - }, { "Pivot2": -4.0f, "Part": "LEFT_LEG", @@ -862,6 +853,15 @@ "Pivot1": 0.0f, "Rotation2": 0.0f }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": -44.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 4.0f, + "Rotation2": 90.0f + }, { "Pivot2": -2.0f, "Part": "LEFT_ARM", @@ -873,12 +873,12 @@ }, { "Pivot2": -5.0f, - "Part": "HEAD", + "Part": "BODY", "Rotation1": 0.0f, "Rotation0": 25.0f, "Pivot0": 2.0f, "Pivot1": 3.0f, - "Rotation2": 6.0f + "Rotation2": 12.0f } ], "Smooth": 0b, @@ -888,7 +888,7 @@ "CurrentFrameTime": 0, "CurrentFrame": 0, "Smooth": 0b, - "ID": 6, + "ID": 11, "WhileAttacking": 1b, "WhileWalking": 1b, "Name": "Ability_Disc_Windup", diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Laser_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Laser_Windup.json index 5b7dc3e16..c445faca9 100644 --- a/src/main/resources/assets/customnpcs/animations/Ability_Laser_Windup.json +++ b/src/main/resources/assets/customnpcs/animations/Ability_Laser_Windup.json @@ -7,6 +7,15 @@ "Comment": "", "Speed": 3.0f, "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, { "Pivot2": 0.0f, "Part": "LEFT_LEG", @@ -16,15 +25,6 @@ "Pivot1": 0.0f, "Rotation2": -18.0f }, - { - "Pivot2": 3.0f, - "Part": "BODY", - "Rotation1": 0.0f, - "Rotation0": -12.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": 0.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -54,9 +54,9 @@ }, { "Pivot2": 3.0f, - "Part": "HEAD", + "Part": "BODY", "Rotation1": 0.0f, - "Rotation0": 0.0f, + "Rotation0": -12.0f, "Pivot0": 0.0f, "Pivot1": 1.0f, "Rotation2": 0.0f @@ -70,6 +70,15 @@ "Comment": "", "Speed": 3.0f, "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, { "Pivot2": 0.0f, "Part": "LEFT_LEG", @@ -79,15 +88,6 @@ "Pivot1": 0.0f, "Rotation2": -18.0f }, - { - "Pivot2": 3.0f, - "Part": "BODY", - "Rotation1": 0.0f, - "Rotation0": -12.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": 0.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -117,9 +117,9 @@ }, { "Pivot2": 3.0f, - "Part": "HEAD", + "Part": "BODY", "Rotation1": 0.0f, - "Rotation0": 0.0f, + "Rotation0": -12.0f, "Pivot0": 0.0f, "Pivot1": 1.0f, "Rotation2": 0.0f @@ -133,6 +133,15 @@ "Comment": "", "Speed": 3.0f, "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, { "Pivot2": 0.0f, "Part": "LEFT_LEG", @@ -142,15 +151,6 @@ "Pivot1": 0.0f, "Rotation2": -18.0f }, - { - "Pivot2": 3.0f, - "Part": "BODY", - "Rotation1": 0.0f, - "Rotation0": -12.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": 0.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -180,9 +180,9 @@ }, { "Pivot2": 3.0f, - "Part": "HEAD", + "Part": "BODY", "Rotation1": 0.0f, - "Rotation0": 0.0f, + "Rotation0": -12.0f, "Pivot0": 0.0f, "Pivot1": 1.0f, "Rotation2": 0.0f @@ -196,6 +196,15 @@ "Comment": "", "Speed": 3.0f, "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, { "Pivot2": 0.0f, "Part": "LEFT_LEG", @@ -205,15 +214,6 @@ "Pivot1": 0.0f, "Rotation2": -18.0f }, - { - "Pivot2": 3.0f, - "Part": "BODY", - "Rotation1": 0.0f, - "Rotation0": -12.0f, - "Pivot0": 0.0f, - "Pivot1": 1.0f, - "Rotation2": 0.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -243,9 +243,9 @@ }, { "Pivot2": 3.0f, - "Part": "HEAD", + "Part": "BODY", "Rotation1": 0.0f, - "Rotation0": 0.0f, + "Rotation0": -12.0f, "Pivot0": 0.0f, "Pivot1": 1.0f, "Rotation2": 0.0f @@ -259,6 +259,15 @@ "Comment": "", "Speed": 3.0f, "FrameParts": [ + { + "Pivot2": 5.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -37.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, { "Pivot2": 0.0f, "Part": "LEFT_LEG", @@ -268,15 +277,6 @@ "Pivot1": 0.0f, "Rotation2": -18.0f }, - { - "Pivot2": 5.0f, - "Part": "BODY", - "Rotation1": 0.0f, - "Rotation0": -25.0f, - "Pivot0": 0.0f, - "Pivot1": 2.0f, - "Rotation2": 0.0f - }, { "Pivot2": 0.0f, "Part": "RIGHT_LEG", @@ -306,9 +306,9 @@ }, { "Pivot2": 5.0f, - "Part": "HEAD", + "Part": "BODY", "Rotation1": 0.0f, - "Rotation0": -37.0f, + "Rotation0": -25.0f, "Pivot0": 0.0f, "Pivot1": 2.0f, "Rotation2": 0.0f @@ -321,7 +321,7 @@ "CurrentFrameTime": 0, "CurrentFrame": 3, "Smooth": 0b, - "ID": 8, + "ID": 12, "WhileAttacking": 1b, "WhileWalking": 1b, "Name": "Ability_Laser_Windup", diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Orb_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Orb_Active.json new file mode 100644 index 000000000..994e2190c --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Orb_Active.json @@ -0,0 +1,77 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": -37.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_LEG", + "Rotation1": -75.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -50.0f, + "Rotation0": 50.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": -56.0f + }, + { + "Pivot2": -6.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": -44.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 30 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 13, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Orb_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Orb_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Orb_Windup.json index 77f21f941..1be8ef71c 100644 --- a/src/main/resources/assets/customnpcs/animations/Ability_Orb_Windup.json +++ b/src/main/resources/assets/customnpcs/animations/Ability_Orb_Windup.json @@ -7,11 +7,11 @@ "Comment": "", "FrameParts": [ { - "Pivot2": 0.0f, - "Part": "BODY", - "Rotation1": 31.0f, - "Rotation0": 0.0f, - "Pivot0": 0.0f, + "Pivot2": -3.0f, + "Part": "LEFT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": 1.0f, "Pivot1": 0.0f, "Rotation2": 0.0f }, @@ -24,15 +24,6 @@ "Pivot1": 0.0f, "Rotation2": 0.0f }, - { - "Pivot2": 5.0f, - "Part": "RIGHT_ARM", - "Rotation1": 37.0f, - "Rotation0": -101.0f, - "Pivot0": 2.0f, - "Pivot1": 1.0f, - "Rotation2": -25.0f - }, { "Pivot2": 0.0f, "Part": "LEFT_ARM", @@ -43,31 +34,49 @@ "Rotation2": -90.0f }, { - "Pivot2": -3.0f, - "Part": "LEFT_LEG", - "Rotation1": -18.0f, - "Rotation0": 12.0f, - "Pivot0": 1.0f, + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 70.0f, + "Rotation0": -125.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -50.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 31.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, "Pivot1": 0.0f, "Rotation2": 0.0f } ], - "Duration": 20 + "Duration": 26 }, { "ColorMarker": 16777215, "Comment": "", - "Speed": 1.0f, + "Speed": 2.0f, "FrameParts": [ { "Pivot2": 0.0f, - "Part": "BODY", - "Rotation1": -44.0f, - "Rotation0": 12.0f, + "Part": "HEAD", + "Rotation1": -37.0f, + "Rotation0": 25.0f, "Pivot0": 0.0f, "Pivot1": 0.0f, "Rotation2": 0.0f }, + { + "Pivot2": 4.0f, + "Part": "LEFT_LEG", + "Rotation1": -75.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, { "Pivot2": -3.0f, "Part": "RIGHT_LEG", @@ -78,13 +87,13 @@ "Rotation2": 0.0f }, { - "Pivot2": 0.0f, - "Part": "HEAD", - "Rotation1": -37.0f, - "Rotation0": 25.0f, - "Pivot0": 0.0f, + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -50.0f, + "Rotation0": 50.0f, + "Pivot0": -3.0f, "Pivot1": 0.0f, - "Rotation2": 0.0f + "Rotation2": -56.0f }, { "Pivot2": -6.0f, @@ -95,33 +104,6 @@ "Pivot1": 0.0f, "Rotation2": 0.0f }, - { - "Pivot2": 4.0f, - "Part": "LEFT_ARM", - "Rotation1": -50.0f, - "Rotation0": 50.0f, - "Pivot0": -3.0f, - "Pivot1": 0.0f, - "Rotation2": -56.0f - }, - { - "Pivot2": 4.0f, - "Part": "LEFT_LEG", - "Rotation1": -75.0f, - "Rotation0": 12.0f, - "Pivot0": -1.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - } - ], - "Smooth": 0b, - "Duration": 5 - }, - { - "ColorMarker": 16777215, - "Comment": "", - "Speed": 0.5f, - "FrameParts": [ { "Pivot2": 0.0f, "Part": "BODY", @@ -130,55 +112,10 @@ "Pivot0": 0.0f, "Pivot1": 0.0f, "Rotation2": 0.0f - }, - { - "Pivot2": -3.0f, - "Part": "RIGHT_LEG", - "Rotation1": -18.0f, - "Rotation0": 12.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": 0.0f, - "Part": "HEAD", - "Rotation1": -37.0f, - "Rotation0": 25.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": -6.0f, - "Part": "RIGHT_ARM", - "Rotation1": 0.0f, - "Rotation0": -90.0f, - "Pivot0": 2.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": 4.0f, - "Part": "LEFT_ARM", - "Rotation1": -50.0f, - "Rotation0": 50.0f, - "Pivot0": -3.0f, - "Pivot1": 0.0f, - "Rotation2": -56.0f - }, - { - "Pivot2": 4.0f, - "Part": "LEFT_LEG", - "Rotation1": -75.0f, - "Rotation0": 12.0f, - "Pivot0": -1.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f } ], "Smooth": 0b, - "Duration": 5 + "Duration": 4 } ], "CurrentFrameTime": 0, diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Slam_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Slam_Active.json index 90e16e948..da3e8d61f 100644 --- a/src/main/resources/assets/customnpcs/animations/Ability_Slam_Active.json +++ b/src/main/resources/assets/customnpcs/animations/Ability_Slam_Active.json @@ -6,6 +6,15 @@ "ColorMarker": 16777215, "Comment": "", "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, { "Pivot2": 0.0f, "Part": "HEAD", @@ -15,33 +24,6 @@ "Pivot1": 0.0f, "Rotation2": 0.0f }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": 0.0f, - "Rotation0": -208.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": 12.0f - }, - { - "Pivot2": 0.0f, - "Part": "BODY", - "Rotation1": 0.0f, - "Rotation0": -6.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": 0.0f, - "Part": "RIGHT_ARM", - "Rotation1": 0.0f, - "Rotation0": -208.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": -12.0f - }, { "Pivot2": -2.0f, "Part": "RIGHT_LEG", @@ -62,9 +44,27 @@ }, { "Pivot2": 0.0f, - "Part": "FULL_MODEL", + "Part": "LEFT_ARM", "Rotation1": 0.0f, - "Rotation0": 0.0f, + "Rotation0": -208.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 12.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -208.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -6.0f, "Pivot0": 0.0f, "Pivot1": 0.0f, "Rotation2": 0.0f @@ -76,7 +76,7 @@ "CurrentFrameTime": 0, "CurrentFrame": 0, "Smooth": 0b, - "ID": 5, + "ID": 14, "WhileAttacking": 1b, "WhileWalking": 1b, "Name": "Ability_Slam_Active", diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Slam_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Slam_Windup.json index b35cef0cf..0c22ac017 100644 --- a/src/main/resources/assets/customnpcs/animations/Ability_Slam_Windup.json +++ b/src/main/resources/assets/customnpcs/animations/Ability_Slam_Windup.json @@ -17,12 +17,21 @@ "Rotation2": 0.0f }, { - "Pivot2": 2.0f, - "Part": "BODY", + "Pivot2": 4.0f, + "Part": "RIGHT_LEG", "Rotation1": 0.0f, - "Rotation0": 12.0f, + "Rotation0": 82.0f, "Pivot0": 0.0f, - "Pivot1": 6.0f, + "Pivot1": 7.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, "Rotation2": 0.0f }, { @@ -44,21 +53,12 @@ "Rotation2": 6.0f }, { - "Pivot2": 4.0f, - "Part": "RIGHT_LEG", + "Pivot2": 2.0f, + "Part": "BODY", "Rotation1": 0.0f, - "Rotation0": 82.0f, + "Rotation0": 12.0f, "Pivot0": 0.0f, - "Pivot1": 7.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": 0.0f, - "Part": "LEFT_LEG", - "Rotation1": 0.0f, - "Rotation0": 18.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, + "Pivot1": 6.0f, "Rotation2": 0.0f } ], @@ -69,6 +69,15 @@ "ColorMarker": 16777215, "Comment": "", "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, { "Pivot2": 0.0f, "Part": "HEAD", @@ -78,33 +87,6 @@ "Pivot1": 0.0f, "Rotation2": 0.0f }, - { - "Pivot2": 0.0f, - "Part": "LEFT_ARM", - "Rotation1": 0.0f, - "Rotation0": -82.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": 0.0f, - "Part": "BODY", - "Rotation1": 0.0f, - "Rotation0": -6.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, - { - "Pivot2": 0.0f, - "Part": "RIGHT_ARM", - "Rotation1": 0.0f, - "Rotation0": -88.0f, - "Pivot0": 0.0f, - "Pivot1": 0.0f, - "Rotation2": 0.0f - }, { "Pivot2": -2.0f, "Part": "RIGHT_LEG", @@ -125,9 +107,27 @@ }, { "Pivot2": 0.0f, - "Part": "FULL_MODEL", + "Part": "LEFT_ARM", "Rotation1": 0.0f, - "Rotation0": 0.0f, + "Rotation0": -82.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -88.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -6.0f, "Pivot0": 0.0f, "Pivot1": 0.0f, "Rotation2": 0.0f @@ -139,7 +139,7 @@ "CurrentFrameTime": 0, "CurrentFrame": 0, "Smooth": 0b, - "ID": 4, + "ID": 15, "WhileAttacking": 1b, "WhileWalking": 1b, "Name": "Ability_Slam_Windup", From 1b8ad4dd904499f5b81ccf983b03d06fde4b34a9 Mon Sep 17 00:00:00 2001 From: Kam Date: Fri, 30 Jan 2026 05:01:54 -0500 Subject: [PATCH 314/358] Player Abilities --- src/api | 2 +- .../kamkeel/npcs/command/AbilityCommand.java | 53 +- .../npcs/controllers/SyncController.java | 45 +- .../controllers/data/ability/Ability.java | 220 +++++--- .../data/ability/AbilityController.java | 292 ++++++---- .../controllers/data/ability/AbilitySlot.java | 124 +++++ .../controllers/data/ability/AnchorPoint.java | 35 +- .../data/ability/BuiltInAbility.java | 63 +++ .../controllers/data/ability/Condition.java | 55 +- .../controllers/data/ability/UserType.java | 26 + .../data/ability/type/AbilityCharge.java | 128 ++--- .../data/ability/type/AbilityCutter.java | 28 +- .../data/ability/type/AbilityDash.java | 68 +-- .../data/ability/type/AbilityDisc.java | 20 +- .../data/ability/type/AbilityEnergyBeam.java | 20 +- .../data/ability/type/AbilityGuard.java | 26 +- .../data/ability/type/AbilityHazard.java | 66 +-- .../data/ability/type/AbilityHeal.java | 26 +- .../data/ability/type/AbilityHeavyHit.java | 6 +- .../data/ability/type/AbilityLaserShot.java | 16 +- .../data/ability/type/AbilityOrb.java | 20 +- .../data/ability/type/AbilityProjectile.java | 30 +- .../data/ability/type/AbilityShockwave.java | 16 +- .../data/ability/type/AbilitySlam.java | 158 +++--- .../data/ability/type/AbilitySweeper.java | 19 +- .../data/ability/type/AbilityTeleport.java | 91 ++-- .../data/ability/type/AbilityTrap.java | 56 +- .../data/ability/type/AbilityVortex.java | 32 +- .../npcs/entity/EntityAbilityBeam.java | 11 +- .../npcs/entity/EntityAbilityDisc.java | 7 +- .../npcs/entity/EntityAbilityLaser.java | 5 +- .../kamkeel/npcs/entity/EntityAbilityOrb.java | 7 +- .../npcs/entity/EntityAbilityProjectile.java | 17 +- .../npcs/entity/EntityAbilitySweeper.java | 14 +- .../kamkeel/npcs/network/PacketHandler.java | 18 +- .../npcs/network/enums/EnumDataPacket.java | 1 + .../npcs/network/enums/EnumRequestPacket.java | 8 +- .../npcs/network/enums/EnumSyncType.java | 3 +- .../data/ability/PlayerAbilitySyncPacket.java | 73 +++ .../packets/player/SpecialKeyStatePacket.java | 6 + ...ket.java => CustomAbilitiesGetPacket.java} | 12 +- ...acket.java => CustomAbilityGetPacket.java} | 24 +- ...et.java => CustomAbilityRemovePacket.java} | 24 +- ...cket.java => CustomAbilitySavePacket.java} | 20 +- .../kamkeel/npcs/util/AnchorPointHelper.java | 7 +- src/main/java/noppes/npcs/CustomNpcs.java | 1 - src/main/java/noppes/npcs/DataAbilities.java | 115 ++-- src/main/java/noppes/npcs/EventHooks.java | 6 + .../java/noppes/npcs/NoppesUtilServer.java | 10 +- .../noppes/npcs/ScriptPlayerEventHandler.java | 7 + .../client/gui/SubGuiNpcDialogVisual.java | 60 +-- .../client/gui/SubGuiNpcPartyOptions.java | 4 +- .../client/gui/SubGuiSpawningOptions.java | 4 +- .../client/gui/advanced/GuiNPCAbilities.java | 221 +++++--- .../gui/advanced/SubGuiAbilityConfig.java | 16 +- .../advanced/SubGuiAbilityDeleteConfirm.java | 4 +- .../gui/advanced/SubGuiAbilityEditMode.java | 56 ++ .../gui/advanced/SubGuiAbilityLoad.java | 121 +++-- .../gui/advanced/SubGuiAbilityLoadMode.java | 56 ++ .../advanced/SubGuiAbilitySaveConfirm.java | 4 +- .../gui/advanced/SubGuiConditionEdit.java | 6 +- .../advanced/ability/SubGuiAbilityDisc.java | 8 +- .../ability/SubGuiAbilityEffects.java | 4 +- .../ability/SubGuiAbilityEnergyBeam.java | 8 +- .../ability/SubGuiAbilityLaserShot.java | 4 +- .../advanced/ability/SubGuiAbilityOrb.java | 8 +- .../client/gui/global/GuiNPCManageLinked.java | 18 +- .../gui/global/GuiNpcManageAbilities.java | 60 ++- .../gui/global/GuiNpcNaturalSpawns.java | 8 +- .../gui/player/AuctionTooltipHandler.java | 4 +- .../client/gui/player/GuiAnvilRecipes.java | 6 +- .../client/gui/player/GuiAuctionBidding.java | 2 +- .../gui/player/GuiAuctionInterface.java | 4 +- .../client/gui/player/GuiAuctionSell.java | 4 +- .../npcs/client/gui/player/GuiNPCTrader.java | 6 +- .../client/gui/player/SubGuiAuctionBid.java | 6 +- .../gui/player/SubGuiAuctionBuyNow.java | 4 +- .../gui/player/inventory/GuiProfiles.java | 2 +- .../client/gui/roles/GuiNpcTraderSetup.java | 2 +- .../gui/roles/SubGuiNpcTraderStock.java | 2 +- .../gui/roles/SubGuiNpcTraderStockSlots.java | 2 +- .../client/gui/util/GuiAbilityInterface.java | 2 +- .../client/gui/util/GuiScriptTextArea.java | 4 +- .../gui/util/key/OverlayKeyPresetViewer.java | 2 +- .../gui/util/script/GoToLineDialog.java | 2 +- .../util/script/RenameRefactorHandler.java | 2 +- .../noppes/npcs/constants/EnumScrollData.java | 2 +- .../controllers/data/PlayerAbilityData.java | 503 ++++++++++++++++++ .../npcs/controllers/data/PlayerData.java | 11 + .../event/player/PlayerAbilityEvent.java | 101 ++++ .../scripted/wrapper/ScriptDataAbilities.java | 43 +- .../wrapper/ScriptPlayerAbilityData.java | 112 ++++ 92 files changed, 2610 insertions(+), 1025 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/UserType.java create mode 100644 src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java rename src/main/java/kamkeel/npcs/network/packets/request/ability/{SavedAbilitiesGetPacket.java => CustomAbilitiesGetPacket.java} (75%) rename src/main/java/kamkeel/npcs/network/packets/request/ability/{SavedAbilityGetPacket.java => CustomAbilityGetPacket.java} (70%) rename src/main/java/kamkeel/npcs/network/packets/request/ability/{SavedAbilityRemovePacket.java => CustomAbilityRemovePacket.java} (69%) rename src/main/java/kamkeel/npcs/network/packets/request/ability/{SavedAbilitySavePacket.java => CustomAbilitySavePacket.java} (73%) create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java create mode 100644 src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java create mode 100644 src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java create mode 100644 src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java diff --git a/src/api b/src/api index 06c9daf81..db6a68e43 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit 06c9daf81be8df0138a992ca6bcabc05e37189cb +Subproject commit db6a68e43073b2828ec05a3c2ede660453c4544e diff --git a/src/main/java/kamkeel/npcs/command/AbilityCommand.java b/src/main/java/kamkeel/npcs/command/AbilityCommand.java index cf5e1ca58..499f9491d 100644 --- a/src/main/java/kamkeel/npcs/command/AbilityCommand.java +++ b/src/main/java/kamkeel/npcs/command/AbilityCommand.java @@ -4,9 +4,6 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import net.minecraft.command.ICommandSender; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; import java.util.Set; import static kamkeel.npcs.util.ColorUtil.sendError; @@ -25,25 +22,23 @@ public class AbilityCommand extends CommandKamkeelBase { } @SubCommand( - desc = "List all saved abilities" + desc = "List all custom abilities" ) public void list(ICommandSender sender, String[] args) { - Set names = AbilityController.Instance.getSavedAbilityNames(); - if (names.isEmpty()) { - sendResult(sender, "No saved abilities found."); + Set uuids = AbilityController.Instance.getCustomAbilityIds(); + if (uuids.isEmpty()) { + sendResult(sender, "No custom abilities found."); return; } - List sortedNames = new ArrayList<>(names); - Collections.sort(sortedNames); - - sendResult(sender, "Saved Abilities (" + sortedNames.size() + "):"); - for (String name : sortedNames) { - Ability ability = AbilityController.Instance.getSavedAbility(name); + sendResult(sender, "Custom Abilities (" + uuids.size() + "):"); + for (String uuid : uuids) { + Ability ability = AbilityController.Instance.getCustomAbility(uuid); if (ability != null) { - sendResult(sender, " - \u00A7b" + name + "\u00A77 (" + ability.getTypeId() + ")"); + String name = ability.getName() != null ? ability.getName() : uuid; + sendResult(sender, " - \u00A7b" + name + "\u00A77 (" + ability.getTypeId() + ") [" + uuid + "]"); } else { - sendResult(sender, " - \u00A7b" + name); + sendResult(sender, " - \u00A7b" + uuid); } } } @@ -79,39 +74,39 @@ public class AbilityCommand extends CommandKamkeelBase { } @SubCommand( - desc = "Delete a saved ability", - usage = "" + desc = "Delete a custom ability by UUID", + usage = "" ) public void delete(ICommandSender sender, String[] args) { if (args.length < 1) { - sendError(sender, "Usage: /kam ability delete "); + sendError(sender, "Usage: /kam ability delete "); return; } - String name = args[0]; - if (!AbilityController.Instance.getSavedAbilityNames().contains(name)) { - sendError(sender, "No saved ability with name: " + name); + String uuid = args[0]; + if (!AbilityController.Instance.hasCustomAbility(uuid)) { + sendError(sender, "No custom ability with UUID: " + uuid); return; } - AbilityController.Instance.deleteAbility(name); - sendResult(sender, "Deleted ability: \u00A7b" + name); + AbilityController.Instance.deleteCustomAbility(uuid); + sendResult(sender, "Deleted ability: \u00A7b" + uuid); } @SubCommand( - desc = "Get info about a saved ability", - usage = "" + desc = "Get info about an ability", + usage = "" ) public void info(ICommandSender sender, String[] args) { if (args.length < 1) { - sendError(sender, "Usage: /kam ability info "); + sendError(sender, "Usage: /kam ability info "); return; } - String name = args[0]; - Ability ability = AbilityController.Instance.getSavedAbility(name); + String key = args[0]; + Ability ability = AbilityController.Instance.resolveAbility(key); if (ability == null) { - sendError(sender, "No saved ability with name: " + name); + sendError(sender, "No ability found for: " + key); return; } diff --git a/src/main/java/kamkeel/npcs/controllers/SyncController.java b/src/main/java/kamkeel/npcs/controllers/SyncController.java index 3247498b5..b6010ad86 100644 --- a/src/main/java/kamkeel/npcs/controllers/SyncController.java +++ b/src/main/java/kamkeel/npcs/controllers/SyncController.java @@ -39,11 +39,14 @@ import noppes.npcs.controllers.data.Quest; import noppes.npcs.controllers.data.QuestCategory; import noppes.npcs.controllers.data.RecipeAnvil; import noppes.npcs.controllers.data.RecipeCarpentry; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; import java.io.IOException; import java.util.EnumMap; import java.util.EnumSet; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; @@ -72,7 +75,8 @@ public class SyncController { EnumSyncType.ANVIL_RECIPES, EnumSyncType.CUSTOM_EFFECTS, EnumSyncType.MAGIC, - EnumSyncType.MAGIC_CYCLE + EnumSyncType.MAGIC_CYCLE, + EnumSyncType.CUSTOM_ABILITY }; public static void load() { @@ -88,6 +92,7 @@ public class SyncController { registerCache(EnumSyncType.CUSTOM_EFFECTS, SyncController::customEffectsNBT); registerCache(EnumSyncType.MAGIC, SyncController::magicsNBT); registerCache(EnumSyncType.MAGIC_CYCLE, SyncController::magicCyclesNBT); + registerCache(EnumSyncType.CUSTOM_ABILITY, SyncController::customAbilitiesNBT); } public static void syncPlayer(EntityPlayerMP player) { @@ -323,6 +328,27 @@ public class SyncController { return compound; } + public static NBTTagCompound customAbilitiesNBT() { + NBTTagList list = new NBTTagList(); + NBTTagCompound compound = new NBTTagCompound(); + for (Map.Entry entry : AbilityController.Instance.getCustomAbilities().entrySet()) { + NBTTagCompound abilityNBT = entry.getValue().writeNBT(); + abilityNBT.setString("CustomAbilityId", entry.getKey()); + list.appendTag(abilityNBT); + } + compound.setTag("Data", list); + return compound; + } + + public static void syncAllCustomAbilities() { + CachedSyncPayload payload = rebuildNow(EnumSyncType.CUSTOM_ABILITY); + if (payload == null) { + return; + } + PacketHandler.Instance.sendToAll(new SyncPacket(EnumSyncType.CUSTOM_ABILITY, payload)); + updateAllPlayerRevisions(EnumSyncType.CUSTOM_ABILITY, payload.getRevision()); + } + public static void syncPlayerData(EntityPlayerMP player, boolean update) { PlayerData data = PlayerData.get(player); if (data != null) { @@ -595,6 +621,21 @@ public class SyncController { ce.customEffectsSync.clear(); break; } + case CUSTOM_ABILITY: { + NBTTagList list = fullCompound.getTagList("Data", 10); + LinkedHashMap sync = new LinkedHashMap<>(); + for (int i = 0; i < list.tagCount(); i++) { + NBTTagCompound nbt = list.getCompoundTagAt(i); + String uuid = nbt.getString("CustomAbilityId"); + Ability ability = AbilityController.Instance.fromNBT(nbt); + if (ability != null) { + ability.setId(uuid); + sync.put(uuid, ability); + } + } + AbilityController.Instance.setCustomAbilities(sync); + break; + } } ClientCacheHandler.updateClientRevision(enumSyncType, revision); @@ -843,6 +884,8 @@ public class SyncController { return EnumSet.of(EnumSyncType.ANVIL_RECIPES); case CUSTOM_EFFECTS: return EnumSet.of(EnumSyncType.CUSTOM_EFFECTS); + case CUSTOM_ABILITY: + return EnumSet.of(EnumSyncType.CUSTOM_ABILITY); default: return EnumSet.noneOf(EnumSyncType.class); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index eb59edf7c..6f66614e0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -19,9 +19,14 @@ import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.data.Animation; +import net.minecraft.entity.player.EntityPlayer; import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.controllers.ScriptController; +import noppes.npcs.controllers.data.PlayerDataScript; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; +import noppes.npcs.scripted.event.player.PlayerAbilityEvent; import java.util.ArrayList; import java.util.List; @@ -81,6 +86,12 @@ public abstract class Ability implements IAbility { // Custom data for external mods protected NBTTagCompound customData = new NBTTagCompound(); + // User type restriction + protected UserType allowedBy = UserType.BOTH; + + // Cooldown override + protected boolean ignoreCooldown = false; + // Configurable potion effects protected List effects = new ArrayList<>(); @@ -101,12 +112,12 @@ public abstract class Ability implements IAbility { /** * Called first tick of ACTIVE phase */ - public abstract void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world); + public abstract void onExecute(EntityLivingBase caster, EntityLivingBase target, World world); /** * Called every tick of ACTIVE phase */ - public abstract void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick); + public abstract void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick); /** * Write type-specific config to NBT @@ -122,55 +133,55 @@ public abstract class Ability implements IAbility { // OPTIONAL OVERRIDES // ═══════════════════════════════════════════════════════════════════ - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { } - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { } - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { } /** * Apply damage to an entity with ability hit event support. * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. * - * @param npc The NPC executing the ability + * @param caster The entity executing the ability (NPC or Player) * @param hitEntity The entity being hit * @param damage The damage amount * @param knockback The horizontal knockback * @return true if damage was applied (not cancelled), false if cancelled */ - protected boolean applyAbilityDamage(EntityNPCInterface npc, EntityLivingBase hitEntity, + protected boolean applyAbilityDamage(EntityLivingBase caster, EntityLivingBase hitEntity, float damage, float knockback) { - double dx = hitEntity.posX - npc.posX; - double dz = hitEntity.posZ - npc.posZ; - return applyAbilityDamageInternal(npc, hitEntity, damage, knockback, 0.0f, dx, dz); + double dx = hitEntity.posX - caster.posX; + double dz = hitEntity.posZ - caster.posZ; + return applyAbilityDamageInternal(caster, hitEntity, damage, knockback, 0.0f, dx, dz); } /** * Apply damage to an entity with ability hit event support and vertical knockback. * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. * - * @param npc The NPC executing the ability + * @param caster The entity executing the ability (NPC or Player) * @param hitEntity The entity being hit * @param damage The damage amount * @param knockback The horizontal knockback * @param knockbackUp The vertical knockback * @return true if damage was applied (not cancelled), false if cancelled */ - protected boolean applyAbilityDamage(EntityNPCInterface npc, EntityLivingBase hitEntity, + protected boolean applyAbilityDamage(EntityLivingBase caster, EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { - double dx = hitEntity.posX - npc.posX; - double dz = hitEntity.posZ - npc.posZ; - return applyAbilityDamageInternal(npc, hitEntity, damage, knockback, knockbackUp, dx, dz); + double dx = hitEntity.posX - caster.posX; + double dz = hitEntity.posZ - caster.posZ; + return applyAbilityDamageInternal(caster, hitEntity, damage, knockback, knockbackUp, dx, dz); } /** * Apply damage to an entity with ability hit event support and custom knockback direction. * Fires the abilityHit script event, allowing scripts to modify or cancel the damage. * - * @param npc The NPC executing the ability + * @param caster The entity executing the ability (NPC or Player) * @param hitEntity The entity being hit * @param damage The damage amount * @param knockback The horizontal knockback @@ -178,46 +189,74 @@ public abstract class Ability implements IAbility { * @param knockbackDirZ The Z component of knockback direction * @return true if damage was applied (not cancelled), false if cancelled */ - protected boolean applyAbilityDamageWithDirection(EntityNPCInterface npc, EntityLivingBase hitEntity, + protected boolean applyAbilityDamageWithDirection(EntityLivingBase caster, EntityLivingBase hitEntity, float damage, float knockback, double knockbackDirX, double knockbackDirZ) { - return applyAbilityDamageInternal(npc, hitEntity, damage, knockback, 0.0f, knockbackDirX, knockbackDirZ); + return applyAbilityDamageInternal(caster, hitEntity, damage, knockback, 0.0f, knockbackDirX, knockbackDirZ); } /** * Internal unified damage application method. * All public damage methods delegate to this. + * Supports both NPC and Player casters. */ - private boolean applyAbilityDamageInternal(EntityNPCInterface npc, EntityLivingBase hitEntity, + private boolean applyAbilityDamageInternal(EntityLivingBase caster, EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp, double knockbackDirX, double knockbackDirZ) { - DataAbilities dataAbilities = npc.abilities; - AbilityEvent.HitEvent event = dataAbilities.fireHitEvent( - this, currentTarget, hitEntity, damage, knockback, knockbackUp); + // Fire hit event for NPC casters (NPC event system) + if (caster instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) caster; + DataAbilities dataAbilities = npc.abilities; + AbilityEvent.HitEvent event = dataAbilities.fireHitEvent( + this, currentTarget, hitEntity, damage, knockback, knockbackUp); - if (event == null) { - return false; // Cancelled + if (event == null) { + return false; // Cancelled + } + + damage = event.getDamage(); + knockback = event.getKnockback(); + knockbackUp = event.getKnockbackUp(); + } + // Fire hit event for Player casters (Player event system) + else if (caster instanceof EntityPlayer && ScriptController.Instance != null) { + EntityPlayer player = (EntityPlayer) caster; + PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(player); + if (handler != null) { + IPlayer iPlayer = (IPlayer) NpcAPI.Instance().getIEntity(player); + PlayerAbilityEvent.HitEvent event = new PlayerAbilityEvent.HitEvent( + iPlayer, this, currentTarget, hitEntity, damage, knockback, knockbackUp); + + if (noppes.npcs.EventHooks.onPlayerAbilityHit(handler, event)) { + return false; // Cancelled + } + + damage = event.getDamage(); + knockback = event.getKnockback(); + knockbackUp = event.getKnockbackUp(); + } } - // Apply damage with potentially modified values - float finalDamage = event.getDamage(); - float finalKnockback = event.getKnockback(); - float finalKnockbackUp = event.getKnockbackUp(); - // Apply damage - if (finalDamage > 0) { - hitEntity.attackEntityFrom(new NpcDamageSource("mob", npc), finalDamage); + if (damage > 0) { + if (caster instanceof EntityNPCInterface) { + hitEntity.attackEntityFrom(new NpcDamageSource("mob", (EntityNPCInterface) caster), damage); + } else if (caster instanceof EntityPlayer) { + hitEntity.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) caster), damage); + } else { + hitEntity.attackEntityFrom(DamageSource.causeMobDamage(caster), damage); + } } // Apply knockback if any - if (finalKnockback > 0 || finalKnockbackUp > 0) { - applyNpcKnockback(hitEntity, knockbackDirX, knockbackDirZ, finalKnockback, finalKnockbackUp); + if (knockback > 0 || knockbackUp > 0) { + applyKnockback(hitEntity, knockbackDirX, knockbackDirZ, knockback, knockbackUp); } return true; } - private void applyNpcKnockback(EntityLivingBase hitEntity, double dirX, double dirZ, float knockback, float knockbackUp) { + private void applyKnockback(EntityLivingBase hitEntity, double dirX, double dirZ, float knockback, float knockbackUp) { double len = Math.sqrt(dirX * dirX + dirZ * dirZ); double x = 0; double z = 0; @@ -334,33 +373,33 @@ public abstract class Ability implements IAbility { * @param target The target (for position calculation) * @return The telegraph instance, or null if no telegraph */ - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { if (!showTelegraph || telegraphType == TelegraphType.NONE) { return null; } Telegraph telegraph; double x, y, z; - float yaw = npc.rotationYaw; + float yaw = caster.rotationYaw; // Determine position based on targeting mode - boolean positionAtNpc = targetingMode == TargetingMode.AOE_SELF || + boolean positionAtCaster = targetingMode == TargetingMode.AOE_SELF || targetingMode == TargetingMode.SELF || telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE; - if (positionAtNpc) { - x = npc.posX; - y = findGroundLevel(npc.worldObj, npc.posX, npc.posY, npc.posZ); - z = npc.posZ; + if (positionAtCaster) { + x = caster.posX; + y = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + z = caster.posZ; } else if (target != null) { x = target.posX; - y = findGroundLevel(npc.worldObj, target.posX, target.posY, target.posZ); + y = findGroundLevel(caster.worldObj, target.posX, target.posY, target.posZ); z = target.posZ; } else { - x = npc.posX; - y = findGroundLevel(npc.worldObj, npc.posX, npc.posY, npc.posZ); - z = npc.posZ; + x = caster.posX; + y = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + z = caster.posZ; } // Create telegraph based on type @@ -391,12 +430,12 @@ public abstract class Ability implements IAbility { telegraph.setHeightOffset(telegraphHeightOffset); TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw); - instance.setCasterEntityId(npc.getEntityId()); + instance.setCasterEntityId(caster.getEntityId()); // Set entity to follow based on targeting mode - if (positionAtNpc) { - // AOE_SELF abilities: telegraph follows NPC during windup - instance.setEntityIdToFollow(npc.getEntityId()); + if (positionAtCaster) { + // AOE_SELF abilities: telegraph follows caster during windup + instance.setEntityIdToFollow(caster.getEntityId()); // For LINE/CONE telegraphs: track target direction during windup if ((telegraphType == TelegraphType.LINE || telegraphType == TelegraphType.CONE) && target != null) { @@ -729,9 +768,20 @@ public abstract class Ability implements IAbility { // CONDITION CHECKING // ═══════════════════════════════════════════════════════════════════ - public boolean checkConditions(EntityNPCInterface npc, EntityLivingBase target) { + public boolean checkConditions(EntityLivingBase caster, EntityLivingBase target) { for (Condition c : conditions) { - if (!c.check(npc, target)) return false; + if (!c.check(caster, target)) return false; + } + return true; + } + + /** + * Check conditions for a player caster, skipping target-requiring conditions. + */ + public boolean checkConditionsForPlayer(EntityLivingBase caster) { + for (Condition c : conditions) { + if (c.requiresTarget()) continue; + if (!c.check(caster, null)) return false; } return true; } @@ -768,6 +818,8 @@ public abstract class Ability implements IAbility { nbt.setString("telegraphType", telegraphType.name()); nbt.setFloat("telegraphHeightOffset", telegraphHeightOffset); nbt.setTag("customData", customData); + nbt.setInteger("allowedBy", allowedBy.ordinal()); + nbt.setBoolean("ignoreCooldown", ignoreCooldown); // Conditions NBTTagList condList = new NBTTagList(); @@ -803,38 +855,14 @@ public abstract class Ability implements IAbility { dazedTicks = nbt.getInteger("recovery"); interruptible = nbt.getBoolean("interruptible"); lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement")); - // Support old telegraphColor key for backwards compatibility - if (nbt.hasKey("windUpColor")) { - windUpColor = nbt.getInteger("windUpColor"); - } else if (nbt.hasKey("telegraphColor")) { - windUpColor = nbt.getInteger("telegraphColor"); - } - if (nbt.hasKey("activeColor")) { - activeColor = nbt.getInteger("activeColor"); - } else { - // Default active color to windUpColor with higher alpha - activeColor = (windUpColor & 0x00FFFFFF) | 0xC0000000; - } - // Sound fields (with backwards compatibility for old castSound) - if (nbt.hasKey("windUpSound")) { - windUpSound = nbt.getString("windUpSound"); - } else if (nbt.hasKey("castSound")) { - windUpSound = nbt.getString("castSound"); // Migrate old field - } else { - windUpSound = ""; - } - activeSound = nbt.hasKey("activeSound") ? nbt.getString("activeSound") : ""; - // Animation fields (with backwards compatibility for old animationId) - if (nbt.hasKey("windUpAnimationId")) { - windUpAnimationId = nbt.getInteger("windUpAnimationId"); - } else if (nbt.hasKey("animationId")) { - windUpAnimationId = nbt.getInteger("animationId"); // Migrate old field - } else { - windUpAnimationId = -1; - } + windUpColor = nbt.getInteger("windUpColor"); + activeColor = nbt.getInteger("activeColor"); + windUpSound = nbt.getString("windUpSound"); + activeSound = nbt.getString("activeSound"); + windUpAnimationId = nbt.hasKey("windUpAnimationId") ? nbt.getInteger("windUpAnimationId") : -1; activeAnimationId = nbt.hasKey("activeAnimationId") ? nbt.getInteger("activeAnimationId") : -1; - windUpAnimationName = nbt.hasKey("windUpAnimationName") ? nbt.getString("windUpAnimationName") : ""; - activeAnimationName = nbt.hasKey("activeAnimationName") ? nbt.getString("activeAnimationName") : ""; + windUpAnimationName = nbt.getString("windUpAnimationName"); + activeAnimationName = nbt.getString("activeAnimationName"); showTelegraph = !nbt.hasKey("showTelegraph") || nbt.getBoolean("showTelegraph"); if (nbt.hasKey("telegraphType")) { try { @@ -845,6 +873,8 @@ public abstract class Ability implements IAbility { } telegraphHeightOffset = nbt.hasKey("telegraphHeightOffset") ? nbt.getFloat("telegraphHeightOffset") : 0.1f; customData = nbt.getCompoundTag("customData"); + allowedBy = nbt.hasKey("allowedBy") ? UserType.fromOrdinal(nbt.getInteger("allowedBy")) : UserType.BOTH; + ignoreCooldown = nbt.hasKey("ignoreCooldown") && nbt.getBoolean("ignoreCooldown"); // Conditions conditions.clear(); @@ -1170,6 +1200,22 @@ public abstract class Ability implements IAbility { conditions.add(c); } + public UserType getAllowedBy() { + return allowedBy; + } + + public void setAllowedBy(UserType allowedBy) { + this.allowedBy = allowedBy; + } + + public boolean isIgnoreCooldown() { + return ignoreCooldown; + } + + public void setIgnoreCooldown(boolean ignoreCooldown) { + this.ignoreCooldown = ignoreCooldown; + } + public List getEffects() { return effects; } @@ -1192,6 +1238,16 @@ public abstract class Ability implements IAbility { // IAbility API METHODS // ═══════════════════════════════════════════════════════════════════ + @Override + public int getAllowedByType() { + return allowedBy.ordinal(); + } + + @Override + public void setAllowedByType(int type) { + this.allowedBy = UserType.fromOrdinal(type); + } + @Override public INbt getNbt() { return NpcAPI.Instance().getINbt(writeNBT()); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index a61df06fa..62d226bf7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -18,6 +18,7 @@ import kamkeel.npcs.controllers.data.ability.type.AbilitySlam; import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport; import kamkeel.npcs.controllers.data.ability.type.AbilityTrap; import kamkeel.npcs.controllers.data.ability.type.AbilityVortex; +import kamkeel.npcs.controllers.SyncController; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; @@ -29,30 +30,46 @@ import java.util.*; import java.util.function.Supplier; /** - * Controller for ability types. Allows external mods to register custom ability types. - * Initialize during FMLInitializationEvent. + * Central controller for ability types and presets. *

- * Also manages saved ability presets that can be reused across NPCs. + * Three maps: + *

    + *
  • {@code typeFactories} — keyed by typeId (lang key), creates new instances of each ability type
  • + *
  • {@code abilities} — built-in pre-configured abilities, keyed by unique name
  • + *
  • {@code customAbilities} — user-created presets, keyed by UUID
  • + *
*/ public class AbilityController implements IAbilityHandler { - public static AbilityController Instance; + public static AbilityController Instance = new AbilityController(); - private final Map> factories = new LinkedHashMap<>(); - private final Map> factoriesByTypeId = new LinkedHashMap<>(); - private final Map savedAbilities = new LinkedHashMap<>(); + /** Type factories keyed by typeId (lang key, e.g. "ability.cnpc.slam"). */ + private final Map> typeFactories = new LinkedHashMap<>(); + + /** Built-in pre-configured abilities keyed by unique name. */ + private final Map abilities = new LinkedHashMap<>(); + + /** User-created custom ability presets keyed by UUID. */ + private final Map customAbilities = new LinkedHashMap<>(); + + /** Version counter — incremented on any custom ability add/modify/delete. + * Used by AbilitySlot for cache invalidation. */ + private int version = 0; public AbilityController() { - Instance = this; - registerBuiltins(); + registerBuiltinTypes(); } + // ═══════════════════════════════════════════════════════════════════════════ + // LOADING / SAVING CUSTOM ABILITIES + // ═══════════════════════════════════════════════════════════════════════════ + /** - * Load saved abilities from world directory. - * Called when world loads. + * Load custom ability presets from world directory. + * Called when world loads. Files are named {@code .json}. */ public void load() { - savedAbilities.clear(); + customAbilities.clear(); File dir = getDir(); if (!dir.exists()) { return; @@ -64,22 +81,22 @@ public class AbilityController implements IAbilityHandler { for (File file : files) { if (!file.isFile() || !file.getName().endsWith(".json")) continue; try { + String filename = file.getName(); + String uuid = filename.substring(0, filename.length() - 5); // strip .json + NBTTagCompound nbt = NBTJsonUtil.LoadFile(file); Ability ability = fromNBT(nbt); - if (ability != null && ability.getName() != null && !ability.getName().isEmpty()) { - savedAbilities.put(ability.getName(), ability); - LogWriter.info("Loaded ability preset: " + ability.getName()); + if (ability != null) { + ability.setId(uuid); + customAbilities.put(uuid, ability); } } catch (Exception e) { - LogWriter.error("Error loading ability preset: " + file.getAbsolutePath(), e); + LogWriter.error("Error loading custom ability: " + file.getAbsolutePath(), e); } } - LogWriter.info("Loaded " + savedAbilities.size() + " ability presets"); + LogWriter.info("Loaded " + customAbilities.size() + " custom abilities"); } - /** - * Get the ability save directory. - */ private File getDir() { File dir = new File(CustomNpcs.getWorldSaveDirectory(), "abilities"); if (!dir.exists()) { @@ -89,26 +106,26 @@ public class AbilityController implements IAbilityHandler { } /** - * Save an ability preset to disk. - * - * @param ability The ability to save - * @return true if successful + * Save a custom ability preset. If the ability has no id, generates a UUID. + * Stores in customAbilities map and writes to disk. */ - public boolean saveAbility(Ability ability) { - if (ability == null || ability.getName() == null || ability.getName().isEmpty()) { - return false; + public boolean saveCustomAbility(Ability ability) { + if (ability == null) return false; + + String uuid = ability.getId(); + if (uuid == null || uuid.isEmpty()) { + uuid = UUID.randomUUID().toString(); + ability.setId(uuid); } File dir = getDir(); - String safeName = ability.getName().replaceAll("[^a-zA-Z0-9_-]", "_"); - File fileNew = new File(dir, safeName + ".json_new"); - File fileCurrent = new File(dir, safeName + ".json"); + File fileNew = new File(dir, uuid + ".json_new"); + File fileCurrent = new File(dir, uuid + ".json"); try { NBTTagCompound nbt = ability.writeNBT(); NBTJsonUtil.SaveFile(fileNew, nbt); - // Atomic rename if (fileCurrent.exists()) { fileCurrent.delete(); } @@ -117,76 +134,144 @@ public class AbilityController implements IAbilityHandler { fileNew.delete(); } - savedAbilities.put(ability.getName(), ability); - LogWriter.info("Saved ability preset: " + ability.getName()); + customAbilities.put(uuid, ability); + version++; + LogWriter.info("Saved custom ability: " + ability.getName() + " [" + uuid + "]"); + SyncController.syncAllCustomAbilities(); return true; } catch (Exception e) { - LogWriter.error("Error saving ability preset: " + ability.getName(), e); + LogWriter.error("Error saving custom ability: " + uuid, e); return false; } } /** - * Delete a saved ability preset. - * - * @param name The name of the ability to delete - * @return true if successful + * Delete a custom ability preset by UUID. */ - public boolean deleteAbility(String name) { - if (name == null || name.isEmpty()) { - return false; - } + public boolean deleteCustomAbility(String uuid) { + if (uuid == null || uuid.isEmpty()) return false; - Ability removed = savedAbilities.remove(name); - if (removed == null) { - return false; - } + Ability removed = customAbilities.remove(uuid); + if (removed == null) return false; File dir = getDir(); - String safeName = name.replaceAll("[^a-zA-Z0-9_-]", "_"); - File file = new File(dir, safeName + ".json"); + File file = new File(dir, uuid + ".json"); if (file.exists()) { file.delete(); } - LogWriter.info("Deleted ability preset: " + name); + version++; + LogWriter.info("Deleted custom ability: " + uuid); + SyncController.syncAllCustomAbilities(); return true; } + // ═══════════════════════════════════════════════════════════════════════════ + // BUILT-IN ABILITY REGISTRY + // ═══════════════════════════════════════════════════════════════════════════ + /** - * Get a saved ability preset by name. + * Register a pre-configured built-in ability by name. + * These are looked up by name in {@link #resolveAbility(String)}. * - * @param name The name of the ability - * @return A copy of the ability, or null if not found + * @param name Unique name (e.g., "Jump Attack", "Ki Blast") + * @param ability Pre-configured ability instance */ - public Ability getSavedAbility(String name) { - Ability saved = savedAbilities.get(name); - if (saved == null) { - return null; + public void registerAbility(String name, Ability ability) { + if (abilities.containsKey(name)) { + LogWriter.info("AbilityController: Overwriting built-in ability: " + name); } - // Return a copy so modifications don't affect the saved version - return fromNBT(saved.writeNBT()); + ability.setName(name); + abilities.put(name, ability); + LogWriter.info("Registered ability: " + name); + } + + // ═══════════════════════════════════════════════════════════════════════════ + // RESOLUTION (used by AbilitySlot, PlayerAbilityData, etc.) + // ═══════════════════════════════════════════════════════════════════════════ + + /** + * Resolve an ability by key. Checks built-in abilities first (by name), + * then custom abilities (by UUID). Returns a copy safe to modify. + * + * @param key The ability name (built-in) or UUID (custom) + * @return A copy of the resolved ability, or null if not found + */ + public Ability resolveAbility(String key) { + if (key == null || key.isEmpty()) return null; + + // Check built-in abilities first + Ability builtIn = abilities.get(key); + if (builtIn != null) { + return fromNBT(builtIn.writeNBT()); + } + + // Check custom abilities + Ability custom = customAbilities.get(key); + if (custom != null) { + return fromNBT(custom.writeNBT()); + } + + return null; } /** - * Check if a saved ability exists. + * Check if an ability key exists (either built-in or custom). */ - public boolean hasSavedAbility(String name) { - return savedAbilities.containsKey(name); + public boolean hasAbility(String key) { + return abilities.containsKey(key) || customAbilities.containsKey(key); } - /** - * Get all saved ability names. - */ - public Set getSavedAbilityNames() { - return new LinkedHashSet<>(savedAbilities.keySet()); + // ═══════════════════════════════════════════════════════════════════════════ + // ACCESSORS + // ═══════════════════════════════════════════════════════════════════════════ + + /** Get a built-in ability by name. Returns the stored instance (not a copy). */ + public Ability getAbility(String name) { + return abilities.get(name); } - /** - * Get all saved abilities. - */ - public Collection getSavedAbilities() { - return savedAbilities.values(); + /** Get all built-in ability names. */ + public Set getAbilityNames() { + return new LinkedHashSet<>(abilities.keySet()); + } + + /** Get a custom ability by UUID. Returns the stored instance (not a copy). */ + public Ability getCustomAbility(String uuid) { + return customAbilities.get(uuid); + } + + /** Check if a custom ability exists by UUID. */ + public boolean hasCustomAbility(String uuid) { + return customAbilities.containsKey(uuid); + } + + /** Get all custom ability UUIDs. */ + public Set getCustomAbilityIds() { + return new LinkedHashSet<>(customAbilities.keySet()); + } + + /** Get the display name for a custom ability by UUID. */ + public String getCustomAbilityName(String uuid) { + Ability a = customAbilities.get(uuid); + return a != null ? a.getName() : null; + } + + /** Get the custom abilities map (used by SyncController). */ + public Map getCustomAbilities() { + return customAbilities; + } + + /** Replace the custom abilities map (used by client-side sync). */ + public void setCustomAbilities(Map synced) { + customAbilities.clear(); + customAbilities.putAll(synced); + version++; + } + + /** Get the version counter (incremented on custom ability changes). */ + public int getVersion() { + return version; } // ═══════════════════════════════════════════════════════════════════════════ @@ -195,28 +280,37 @@ public class AbilityController implements IAbilityHandler { @Override public String[] getTypes() { - // Return the ability typeIds (lang keys), not the factory keys - return factoriesByTypeId.keySet().toArray(new String[0]); + return typeFactories.keySet().toArray(new String[0]); } @Override public boolean hasType(String typeId) { - return factories.containsKey(typeId) || factoriesByTypeId.containsKey(typeId); + return typeFactories.containsKey(typeId); } @Override - public String[] getSavedNames() { - return savedAbilities.keySet().toArray(new String[0]); + public String[] getAbilityNameArray() { + return abilities.keySet().toArray(new String[0]); } @Override - public boolean has(String name) { - return savedAbilities.containsKey(name); + public boolean hasAbilityName(String name) { + return abilities.containsKey(name); } @Override - public boolean delete(String name) { - return deleteAbility(name); + public String[] getCustomAbilityIdArray() { + return customAbilities.keySet().toArray(new String[0]); + } + + @Override + public boolean hasCustomAbilityId(String uuid) { + return customAbilities.containsKey(uuid); + } + + @Override + public boolean deleteCustomAbilityById(String uuid) { + return deleteCustomAbility(uuid); } // ═══════════════════════════════════════════════════════════════════════════ @@ -225,26 +319,23 @@ public class AbilityController implements IAbilityHandler { /** * Register a new ability type. - * Call during FMLInitializationEvent. + * Extracts the typeId from a temporary instance and stores in typeFactories. * - * @param factoryKey The factory key (e.g., "cnpc:slam", "mymod:custom_ability") + * @param factoryKey Unused legacy key — the typeId from the created instance is used * @param factory Factory lambda that creates a new instance */ public void registerType(String factoryKey, Supplier factory) { - if (factories.containsKey(factoryKey)) { - LogWriter.info("AbilityController: Overwriting existing ability type: " + factoryKey); - } - factories.put(factoryKey, factory); - - // Also register by the ability's actual typeId (lang key) Ability temp = factory.get(); - String abilityTypeId = temp.getTypeId(); - factoriesByTypeId.put(abilityTypeId, factory); + String typeId = temp.getTypeId(); + if (typeFactories.containsKey(typeId)) { + LogWriter.info("AbilityController: Overwriting type: " + typeId); + } + typeFactories.put(typeId, factory); } /** * Create an ability from NBT data. - * Uses the "typeId" field to determine which type to create. + * Uses the "typeId" field to look up in typeFactories. */ public Ability fromNBT(NBTTagCompound nbt) { if (nbt == null || !nbt.hasKey("typeId")) { @@ -252,11 +343,7 @@ public class AbilityController implements IAbilityHandler { } String typeId = nbt.getString("typeId"); - Supplier factory = factories.get(typeId); - if (factory == null) { - // Try looking up by ability typeId (lang key) - factory = factoriesByTypeId.get(typeId); - } + Supplier factory = typeFactories.get(typeId); if (factory == null) { LogWriter.info("AbilityController: Unknown ability type: " + typeId); return null; @@ -270,15 +357,10 @@ public class AbilityController implements IAbilityHandler { /** * Create a new empty ability of the given type. * - * @param factoryKey The factory key (e.g., "cnpc:slam") or the ability typeId (e.g., "ability.cnpc.slam") + * @param typeId The ability typeId (e.g., "ability.cnpc.slam") */ - public Ability create(String factoryKey) { - // Try direct lookup first (factory key) - Supplier factory = factories.get(factoryKey); - if (factory == null) { - // Try looking up by ability typeId - factory = factoriesByTypeId.get(factoryKey); - } + public Ability create(String typeId) { + Supplier factory = typeFactories.get(typeId); if (factory == null) { return null; } @@ -288,7 +370,7 @@ public class AbilityController implements IAbilityHandler { /** * Register built-in ability types. */ - private void registerBuiltins() { + private void registerBuiltinTypes() { // Melee/AOE damage abilities registerType("cnpc:slam", AbilitySlam::new); registerType("cnpc:heavy_hit", AbilityHeavyHit::new); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java new file mode 100644 index 000000000..ca4e7471e --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilitySlot.java @@ -0,0 +1,124 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Wrapper that stores either an inline ability (full NBT) or a reference + * to a built-in/custom ability (by name or UUID). + *

+ * References are lazy-resolved via {@link AbilityController#resolveAbility(String)} + * and cached until the controller version changes. + */ +public class AbilitySlot { + + private Ability inlineAbility; + private String referenceId; + + private transient Ability cachedAbility; + private transient int cachedVersion = -1; + + private AbilitySlot() {} + + /** + * Create a slot with an inline (embedded) ability. + */ + public static AbilitySlot inline(Ability ability) { + AbilitySlot slot = new AbilitySlot(); + slot.inlineAbility = ability; + slot.referenceId = null; + return slot; + } + + /** + * Create a slot that references a built-in ability (by name) or custom ability (by UUID). + */ + public static AbilitySlot reference(String referenceId) { + AbilitySlot slot = new AbilitySlot(); + slot.inlineAbility = null; + slot.referenceId = referenceId; + return slot; + } + + public boolean isReference() { + return referenceId != null; + } + + public String getReferenceId() { + return referenceId; + } + + /** + * Get the resolved ability. For inline slots, returns the ability directly. + * For reference slots, lazily resolves from AbilityController and caches. + * Returns null if the reference cannot be resolved (broken ref). + */ + public Ability getAbility() { + if (!isReference()) { + return inlineAbility; + } + + // Lazy resolve with version-based cache invalidation + AbilityController controller = AbilityController.Instance; + if (controller == null) return null; + + int currentVersion = controller.getVersion(); + if (cachedAbility != null && cachedVersion == currentVersion) { + return cachedAbility; + } + + cachedAbility = controller.resolveAbility(referenceId); + cachedVersion = currentVersion; + return cachedAbility; + } + + /** + * Get the inline ability directly (null for reference slots). + */ + public Ability getInlineAbility() { + return inlineAbility; + } + + /** + * Convert this reference slot to an inline slot by copying the resolved ability. + * No-op if already inline. Returns false if resolution fails. + */ + public boolean convertToInline() { + if (!isReference()) return true; + + Ability resolved = getAbility(); + if (resolved == null) return false; + + // Deep copy via NBT round-trip + inlineAbility = AbilityController.Instance.fromNBT(resolved.writeNBT()); + referenceId = null; + cachedAbility = null; + cachedVersion = -1; + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT + // ═══════════════════════════════════════════════════════════════════ + + public NBTTagCompound writeNBT() { + if (isReference()) { + NBTTagCompound nbt = new NBTTagCompound(); + nbt.setString("Reference", referenceId); + return nbt; + } + return inlineAbility != null ? inlineAbility.writeNBT() : new NBTTagCompound(); + } + + public static AbilitySlot fromNBT(NBTTagCompound nbt) { + if (nbt == null) return null; + + if (nbt.hasKey("Reference")) { + return reference(nbt.getString("Reference")); + } + + // Inline ability + Ability ability = AbilityController.Instance.fromNBT(nbt); + if (ability == null) return null; + return inline(ability); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java index 9bf408c27..01100c318 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java @@ -5,40 +5,33 @@ package kamkeel.npcs.controllers.data.ability; * Determines where the charging visual (orb, disc, etc.) appears relative to the entity. */ public enum AnchorPoint { - FRONT(0, "Front"), // In front of entity face (default) - CENTER(1, "Center"), // Entity center - RIGHT_HAND(2, "Right Hand"), // Right arm/hand position - LEFT_HAND(3, "Left Hand"), // Left arm/hand position - ABOVE_HEAD(4, "Above Head"), // Above the entity - CHEST(5, "Chest"); // Chest level, centered + FRONT("Front"), // In front of entity face (default) + CENTER("Center"), // Entity center + RIGHT_HAND("Right Hand"), // Right arm/hand position + LEFT_HAND("Left Hand"), // Left arm/hand position + ABOVE_HEAD("Above Head"), // Above the entity + CHEST("Chest"); // Chest level, centered - private final int id; private final String displayName; - AnchorPoint(int id, String displayName) { - this.id = id; + AnchorPoint(String displayName) { this.displayName = displayName; } - public int getId() { - return id; - } - public String getDisplayName() { return displayName; } /** - * Get AnchorPoint from NBT id value. - * Returns FRONT if id is invalid. + * Get AnchorPoint from ordinal value. + * Returns FRONT if ordinal is out of bounds. */ - public static AnchorPoint fromId(int id) { - for (AnchorPoint anchor : values()) { - if (anchor.id == id) { - return anchor; - } + public static AnchorPoint fromOrdinal(int ordinal) { + AnchorPoint[] vals = values(); + if (ordinal < 0 || ordinal >= vals.length) { + return FRONT; } - return FRONT; + return vals[ordinal]; } /** diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java new file mode 100644 index 000000000..69151291f --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java @@ -0,0 +1,63 @@ +package kamkeel.npcs.controllers.data.ability; + +import net.minecraft.nbt.NBTTagCompound; + +/** + * Abstract base class for built-in (non-customizable) abilities. + * Built-in abilities are registered by key, have fixed configurations, + * and cannot be modified through the preset GUI system. + *

+ * Examples: simple utility abilities like Jump, Sprint, etc. + * Third-party mods can extend this class to create their own built-in abilities. + */ +public abstract class BuiltInAbility extends Ability { + + /** + * The registry key used to look up this ability (e.g., "cnpc:jump", "dbc:ki_blast"). + */ + private final String registryKey; + + public BuiltInAbility(String registryKey) { + this.registryKey = registryKey; + this.id = registryKey; + this.name = registryKey; + this.typeId = registryKey; + } + + /** + * Get the registry key for this built-in ability. + */ + public String getRegistryKey() { + return registryKey; + } + + /** + * Built-in abilities are not customizable. + */ + @Override + public boolean isCustomizable() { + return false; + } + + /** + * Built-in abilities do not write type-specific NBT (they have fixed configs). + */ + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + // No-op: built-in abilities have fixed configurations + } + + /** + * Built-in abilities do not read type-specific NBT. + */ + @Override + public void readTypeNBT(NBTTagCompound nbt) { + // No-op: built-in abilities have fixed configurations + } + + /** + * Get the duration of the active phase in ticks. + * Built-in abilities must define their own active duration. + */ + public abstract int getActiveDurationTicks(); +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java index 8d078421a..1183fe858 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Condition.java @@ -13,11 +13,19 @@ public interface Condition { /** * Check if this condition is met. * - * @param npc The NPC that would use the ability - * @param target The current target (may be null for self-targeting abilities) + * @param caster The entity that would use the ability (NPC or Player) + * @param target The current target (may be null for self-targeting abilities or players) * @return true if the condition is satisfied */ - boolean check(EntityNPCInterface npc, EntityLivingBase target); + boolean check(EntityLivingBase caster, EntityLivingBase target); + + /** + * Whether this condition requires a target entity to evaluate. + * Conditions that return true here will be skipped for player ability checks. + */ + default boolean requiresTarget() { + return false; + } /** * Get the condition type ID for serialization. @@ -75,7 +83,7 @@ public interface Condition { // ═══════════════════════════════════════════════════════════════════ /** - * Condition: NPC HP is above a percentage threshold. + * Condition: Caster HP is above a percentage threshold. */ class ConditionHPAbove implements Condition { private float threshold = 0.5f; // 50% @@ -88,8 +96,8 @@ public interface Condition { } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { - return npc.getHealth() / npc.getMaxHealth() > threshold; + public boolean check(EntityLivingBase caster, EntityLivingBase target) { + return caster.getHealth() / caster.getMaxHealth() > threshold; } @Override @@ -112,7 +120,7 @@ public interface Condition { } /** - * Condition: NPC HP is below a percentage threshold. + * Condition: Caster HP is below a percentage threshold. */ class ConditionHPBelow implements Condition { private float threshold = 0.5f; @@ -125,8 +133,8 @@ public interface Condition { } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { - return npc.getHealth() / npc.getMaxHealth() < threshold; + public boolean check(EntityLivingBase caster, EntityLivingBase target) { + return caster.getHealth() / caster.getMaxHealth() < threshold; } @Override @@ -150,6 +158,7 @@ public interface Condition { /** * Condition: Target HP is above a percentage threshold. + * Requires a target - will be skipped for player ability checks. */ class ConditionTargetHPAbove implements Condition { private float threshold = 0.5f; @@ -162,11 +171,16 @@ public interface Condition { } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { + public boolean check(EntityLivingBase caster, EntityLivingBase target) { if (target == null) return false; return target.getHealth() / target.getMaxHealth() > threshold; } + @Override + public boolean requiresTarget() { + return true; + } + @Override public String getTypeId() { return "target_hp_above"; @@ -188,6 +202,7 @@ public interface Condition { /** * Condition: Target HP is below a percentage threshold. + * Requires a target - will be skipped for player ability checks. */ class ConditionTargetHPBelow implements Condition { private float threshold = 0.5f; @@ -200,11 +215,16 @@ public interface Condition { } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { + public boolean check(EntityLivingBase caster, EntityLivingBase target) { if (target == null) return false; return target.getHealth() / target.getMaxHealth() < threshold; } + @Override + public boolean requiresTarget() { + return true; + } + @Override public String getTypeId() { return "target_hp_below"; @@ -225,8 +245,9 @@ public interface Condition { } /** - * Condition: NPC has been hit X times within a time window. + * Condition: Caster has been hit X times within a time window. * Useful for reactive abilities that trigger after receiving multiple hits. + * Currently only works for NPCs (requires DataAbilities hit tracking). */ class ConditionHitCount implements Condition { private int requiredHits = 3; @@ -241,11 +262,15 @@ public interface Condition { } @Override - public boolean check(EntityNPCInterface npc, EntityLivingBase target) { + public boolean check(EntityLivingBase caster, EntityLivingBase target) { // Check NPC's recent hit count from DataAbilities - if (npc.abilities != null) { - return npc.abilities.getRecentHitCount(withinTicks) >= requiredHits; + if (caster instanceof EntityNPCInterface) { + EntityNPCInterface npc = (EntityNPCInterface) caster; + if (npc.abilities != null) { + return npc.abilities.getRecentHitCount(withinTicks) >= requiredHits; + } } + // For players, hit count tracking is not yet supported return false; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/UserType.java b/src/main/java/kamkeel/npcs/controllers/data/ability/UserType.java new file mode 100644 index 000000000..d68693f59 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/UserType.java @@ -0,0 +1,26 @@ +package kamkeel.npcs.controllers.data.ability; + +/** + * Defines which entity types can use an ability. + */ +public enum UserType { + NPC_ONLY, + PLAYER_ONLY, + BOTH; + + public boolean allowsNpc() { + return this == NPC_ONLY || this == BOTH; + } + + public boolean allowsPlayer() { + return this == PLAYER_ONLY || this == BOTH; + } + + public static UserType fromOrdinal(int ordinal) { + UserType[] values = values(); + if (ordinal >= 0 && ordinal < values.length) { + return values[ordinal]; + } + return BOTH; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 25941e0a5..37d88e1f7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -88,106 +88,106 @@ public class AbilityCharge extends Ability implements IAbilityCharge { * Called on the first tick of windup - lock direction here so it matches telegraph. */ @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (tick == 1) { // Lock direction on first windup tick (same time telegraph is created) - lockChargeDirection(npc, target); + lockChargeDirection(caster, target); } - // Keep NPC facing the locked direction during windup - enforceLockedRotation(npc); + // Keep caster facing the locked direction during windup + enforceLockedRotation(caster); } /** * Locks the charge direction based on current target position. * Called once at windup start - direction won't change even if target moves. */ - private void lockChargeDirection(EntityNPCInterface npc, EntityLivingBase target) { + private void lockChargeDirection(EntityLivingBase caster, EntityLivingBase target) { if (target != null) { - double dx = target.posX - npc.posX; - double dz = target.posZ - npc.posZ; + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; double len = Math.sqrt(dx * dx + dz * dz); if (len > 0) { chargeDirection = Vec3.createVectorHelper(dx / len, 0, dz / len); } else { - float yaw = (float) Math.toRadians(npc.rotationYaw); + float yaw = (float) Math.toRadians(caster.rotationYaw); chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw)); } } else { - float yaw = (float) Math.toRadians(npc.rotationYaw); + float yaw = (float) Math.toRadians(caster.rotationYaw); chargeDirection = Vec3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw)); } lockedYaw = (float) Math.toDegrees(Math.atan2(-chargeDirection.xCoord, chargeDirection.zCoord)); } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { // Initialize charge - direction was already locked during windup - startX = npc.posX; - startY = npc.posY; - startZ = npc.posZ; + startX = caster.posX; + startY = caster.posY; + startZ = caster.posZ; hitEntities.clear(); // If direction wasn't set during windup (shouldn't happen), set it now if (chargeDirection == null) { - lockChargeDirection(npc, target); + lockChargeDirection(caster, target); } - enforceLockedRotation(npc); + enforceLockedRotation(caster); } - private void enforceLockedRotation(EntityNPCInterface npc) { - npc.rotationYaw = lockedYaw; - npc.rotationYawHead = lockedYaw; - npc.prevRotationYaw = lockedYaw; - npc.prevRotationYawHead = lockedYaw; - npc.renderYawOffset = lockedYaw; - npc.prevRenderYawOffset = lockedYaw; + private void enforceLockedRotation(EntityLivingBase caster) { + caster.rotationYaw = lockedYaw; + caster.rotationYawHead = lockedYaw; + caster.prevRotationYaw = lockedYaw; + caster.prevRotationYawHead = lockedYaw; + caster.renderYawOffset = lockedYaw; + caster.prevRenderYawOffset = lockedYaw; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (chargeDirection == null) return; // Enforce rotation every tick - enforceLockedRotation(npc); + enforceLockedRotation(caster); // Calculate distance traveled double distanceTraveled = Math.sqrt( - Math.pow(npc.posX - startX, 2) + - Math.pow(npc.posZ - startZ, 2) + Math.pow(caster.posX - startX, 2) + + Math.pow(caster.posZ - startZ, 2) ); // Check if reached max distance if (distanceTraveled >= maxRange) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; signalCompletion(); return; } - if (isChargeBlocked(npc)) { - stopMomentum(npc); + if (isChargeBlocked(caster)) { + stopMomentum(caster); signalCompletion(); return; } - // Move NPC - npc.motionX = chargeDirection.xCoord * chargeSpeed; - npc.motionY = 0; - npc.motionZ = chargeDirection.zCoord * chargeSpeed; - npc.velocityChanged = true; + // Move caster + caster.motionX = chargeDirection.xCoord * chargeSpeed; + caster.motionY = 0; + caster.motionZ = chargeDirection.zCoord * chargeSpeed; + caster.velocityChanged = true; // Server-side collision damage if (!world.isRemote) { - AxisAlignedBB hitBox = npc.boundingBox.expand(hitWidth, hitWidth * 0.5, hitWidth); + AxisAlignedBB hitBox = caster.boundingBox.expand(hitWidth, hitWidth * 0.5, hitWidth); @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, hitBox); for (Entity entity : entities) { if (!(entity instanceof EntityLivingBase)) continue; - if (entity == npc) continue; + if (entity == caster) continue; if (hitEntities.contains(entity.getEntityId())) continue; EntityLivingBase livingEntity = (EntityLivingBase) entity; @@ -196,7 +196,7 @@ public class AbilityCharge extends Ability implements IAbilityCharge { hitEntities.add(entity.getEntityId()); // Apply damage with scripted event support - boolean wasHit = applyAbilityDamageWithDirection(npc, livingEntity, damage, knockback, + boolean wasHit = applyAbilityDamageWithDirection(caster, livingEntity, damage, knockback, chargeDirection.xCoord, chargeDirection.zCoord); // Play impact sound if hit wasn't cancelled @@ -208,28 +208,28 @@ public class AbilityCharge extends Ability implements IAbilityCharge { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - stopMomentum(npc); - super.onComplete(npc, target); + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + stopMomentum(caster); + super.onComplete(caster, target); } @Override - public void onInterrupt(EntityNPCInterface npc, net.minecraft.util.DamageSource source, float damage) { - stopMomentum(npc); - super.onInterrupt(npc, source, damage); + public void onInterrupt(EntityLivingBase caster, net.minecraft.util.DamageSource source, float damage) { + stopMomentum(caster); + super.onInterrupt(caster, source, damage); } - private void stopMomentum(EntityNPCInterface npc) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + private void stopMomentum(EntityLivingBase caster) { + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; } - private boolean isChargeBlocked(EntityNPCInterface npc) { + private boolean isChargeBlocked(EntityLivingBase caster) { double nextX = chargeDirection.xCoord * chargeSpeed; double nextZ = chargeDirection.zCoord * chargeSpeed; - AxisAlignedBB nextBox = npc.boundingBox.copy().offset(nextX, 0, nextZ); - return !npc.worldObj.getCollidingBoundingBoxes(npc, nextBox).isEmpty(); + AxisAlignedBB nextBox = caster.boundingBox.copy().offset(nextX, 0, nextZ); + return !caster.worldObj.getCollidingBoundingBoxes(caster, nextBox).isEmpty(); } @Override @@ -250,12 +250,12 @@ public class AbilityCharge extends Ability implements IAbilityCharge { } /** - * Creates a LINE telegraph from NPC position towards target. - * Telegraph follows NPC during windup (so NPC can reposition before charging). + * Creates a LINE telegraph from caster position towards target. + * Telegraph follows caster during windup (so caster can reposition before charging). * Direction is locked at creation based on target position. */ @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) { return null; } @@ -263,11 +263,11 @@ public class AbilityCharge extends Ability implements IAbilityCharge { // Calculate direction to target at this moment (locked) float yaw; if (target != null) { - double dx = target.posX - npc.posX; - double dz = target.posZ - npc.posZ; + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; yaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); } else { - yaw = npc.rotationYaw; + yaw = caster.rotationYaw; } // Create LINE telegraph @@ -278,12 +278,12 @@ public class AbilityCharge extends Ability implements IAbilityCharge { telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); telegraph.setHeightOffset(telegraphHeightOffset); - // Position at NPC ground level, direction towards target - double groundY = findGroundLevel(npc.worldObj, npc.posX, npc.posY, npc.posZ); - TelegraphInstance instance = new TelegraphInstance(telegraph, npc.posX, groundY, npc.posZ, yaw); - instance.setCasterEntityId(npc.getEntityId()); - // Telegraph follows NPC during windup - allows NPC to reposition - instance.setEntityIdToFollow(npc.getEntityId()); + // Position at caster ground level, direction towards target + double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ); + TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, groundY, caster.posZ, yaw); + instance.setCasterEntityId(caster.getEntityId()); + // Telegraph follows caster during windup - allows caster to reposition + instance.setEntityIdToFollow(caster.getEntityId()); return instance; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index 9db9d7700..ef2d2f687 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -100,13 +100,13 @@ public class AbilityCutter extends Ability implements IAbilityCutter { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { hitEntities.clear(); currentRotation = -arcAngle / 2.0f; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote) return; switch (sweepMode) { @@ -115,7 +115,7 @@ public class AbilityCutter extends Ability implements IAbilityCutter { signalCompletion(); // Swipe arc complete return; } - performSweepDamage(npc, world, innerRadius, range, currentRotation); + performSweepDamage(caster, world, innerRadius, range, currentRotation); currentRotation += sweepSpeed; break; @@ -126,30 +126,30 @@ public class AbilityCutter extends Ability implements IAbilityCutter { } currentRotation = (currentRotation + sweepSpeed) % 360.0f; hitEntities.clear(); - performSweepDamage(npc, world, innerRadius, range, currentRotation); + performSweepDamage(caster, world, innerRadius, range, currentRotation); break; } } - private void performSweepDamage(EntityNPCInterface npc, World world, float minDist, float maxDist, float angleOffset) { - float casterYaw = npc.rotationYaw + angleOffset; + private void performSweepDamage(EntityLivingBase caster, World world, float minDist, float maxDist, float angleOffset) { + float casterYaw = caster.rotationYaw + angleOffset; AxisAlignedBB searchBox = AxisAlignedBB.getBoundingBox( - npc.posX - maxDist, npc.posY - 1, npc.posZ - maxDist, - npc.posX + maxDist, npc.posY + 3, npc.posZ + maxDist + caster.posX - maxDist, caster.posY - 1, caster.posZ - maxDist, + caster.posX + maxDist, caster.posY + 3, caster.posZ + maxDist ); @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; if (hitEntities.contains(entity.getEntityId())) continue; // If not piercing, stop after hitting one entity this sweep if (!piercing && !hitEntities.isEmpty()) break; - double dx = entity.posX - npc.posX; - double dz = entity.posZ - npc.posZ; + double dx = entity.posX - caster.posX; + double dz = entity.posZ - caster.posZ; double dist = Math.sqrt(dx * dx + dz * dz); if (dist < minDist || dist > maxDist) continue; @@ -161,7 +161,7 @@ public class AbilityCutter extends Ability implements IAbilityCutter { float actualDamage = damage * distFactor; // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, entity, actualDamage, knockback); + boolean wasHit = applyAbilityDamage(caster, entity, actualDamage, knockback); // Apply effects if hit wasn't cancelled if (wasHit) { @@ -183,13 +183,13 @@ public class AbilityCutter extends Ability implements IAbilityCutter { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { hitEntities.clear(); currentRotation = 0.0f; } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { hitEntities.clear(); currentRotation = 0.0f; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 054176a7b..e070b099e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -8,6 +8,8 @@ import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.DamageSource; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -127,10 +129,10 @@ public class AbilityDash extends Ability implements IAbilityDash { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { - startX = npc.posX; - startY = npc.posY; - startZ = npc.posZ; + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + startX = caster.posX; + startY = caster.posY; + startZ = caster.posZ; // Choose random direction based on mode DashDirection[] directions = dashMode == DashMode.AGGRESSIVE @@ -141,11 +143,11 @@ public class AbilityDash extends Ability implements IAbilityDash { // Calculate dash direction relative to target float baseYaw; if (target != null) { - double dx = target.posX - npc.posX; - double dz = target.posZ - npc.posZ; + double dx = target.posX - caster.posX; + double dz = target.posZ - caster.posZ; baseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); } else { - baseYaw = npc.rotationYaw; + baseYaw = caster.rotationYaw; } // Apply direction offset @@ -161,54 +163,54 @@ public class AbilityDash extends Ability implements IAbilityDash { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (dashDirection == null) return; // Calculate distance traveled double distanceTraveled = Math.sqrt( - Math.pow(npc.posX - startX, 2) + - Math.pow(npc.posZ - startZ, 2) + Math.pow(caster.posX - startX, 2) + + Math.pow(caster.posZ - startZ, 2) ); // Check if reached max distance if (distanceTraveled >= dashDistance) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; signalCompletion(); return; } - if (isDashBlocked(npc)) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + if (isDashBlocked(caster)) { + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; signalCompletion(); return; } - // Move NPC - npc.motionX = dashDirection.xCoord * dashSpeed; - npc.motionY = 0; - npc.motionZ = dashDirection.zCoord * dashSpeed; - npc.velocityChanged = true; + // Move caster + caster.motionX = dashDirection.xCoord * dashSpeed; + caster.motionY = 0; + caster.motionZ = dashDirection.zCoord * dashSpeed; + caster.velocityChanged = true; // Trail particles - world.spawnParticle("smoke", npc.posX, npc.posY + 0.5, npc.posZ, 0, 0, 0); + world.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0); } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; } @Override - public void onInterrupt(EntityNPCInterface npc, net.minecraft.util.DamageSource source, float damage) { - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; } @Override @@ -217,11 +219,11 @@ public class AbilityDash extends Ability implements IAbilityDash { chosenDirection = null; } - private boolean isDashBlocked(EntityNPCInterface npc) { + private boolean isDashBlocked(EntityLivingBase caster) { if (dashDirection == null) return true; double nextX = dashDirection.xCoord * dashSpeed; double nextZ = dashDirection.zCoord * dashSpeed; - return !npc.worldObj.getCollidingBoundingBoxes(npc, npc.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty(); + return !caster.worldObj.getCollidingBoundingBoxes(caster, caster.boundingBox.copy().offset(nextX, 0, nextZ)).isEmpty(); } // ==================== PREVIEW MODE ==================== diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 3ef2e164d..74ce14e34 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -86,7 +86,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) { signalCompletion(); return; @@ -102,15 +102,15 @@ public class AbilityDisc extends Ability implements IAbilityDisc { } @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote) return; // Spawn disc in charging mode on first tick of windup if (tick == 1) { - // Create disc in charging mode - follows NPC based on anchor point during windup - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(npc, anchorData); + // Create disc in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); discEntity = new EntityAbilityDisc( - world, npc, target, + world, caster, target, spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, discRadius, discThickness, colorData, combatData, homingData, lightningData, lifespanData, @@ -123,7 +123,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Signal completion when entity dies if (discEntity == null || discEntity.isDead) { discEntity = null; @@ -132,7 +132,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { } @Override @@ -145,7 +145,7 @@ public class AbilityDisc extends Ability implements IAbilityDisc { } @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { return null; } @@ -159,8 +159,8 @@ public class AbilityDisc extends Ability implements IAbilityDisc { telegraph.setHeightOffset(telegraphHeightOffset); // Position at target and follow target during windup - TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, npc.rotationYaw); - instance.setCasterEntityId(npc.getEntityId()); + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); instance.setEntityIdToFollow(target.getEntityId()); return instance; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 53377161a..59e84f8f0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -83,7 +83,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) { signalCompletion(); return; @@ -99,17 +99,17 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { } @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote) return; // Spawn beam in charging mode on first tick of windup if (tick == 1) { float offsetDist = 1.0f; - // Create beam in charging mode - follows NPC based on anchor point during windup - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(npc, anchorData, offsetDist); + // Create beam in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData, offsetDist); beamEntity = new EntityAbilityBeam( - world, npc, target, + world, caster, target, spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, beamWidth, headSize, colorData, combatData, homingData, lightningData, lifespanData, @@ -122,7 +122,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Signal completion when entity dies if (beamEntity == null || beamEntity.isDead) { beamEntity = null; @@ -131,7 +131,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { } @Override @@ -144,7 +144,7 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { } @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { return null; } @@ -158,8 +158,8 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { telegraph.setHeightOffset(telegraphHeightOffset); // Position at target and follow target during windup - TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, npc.rotationYaw); - instance.setCasterEntityId(npc.getEntityId()); + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); instance.setEntityIdToFollow(target.getEntityId()); return instance; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index 92c7ca58e..223a1180c 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -5,6 +5,7 @@ import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.UserType; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -60,6 +61,7 @@ public class AbilityGuard extends Ability implements IAbilityGuard { this.showTelegraph = false; this.windUpSound = "random.anvil_use"; this.activeSound = "random.anvil_land"; + this.allowedBy = UserType.NPC_ONLY; } @Override @@ -85,17 +87,17 @@ public class AbilityGuard extends Ability implements IAbilityGuard { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { lastAttacker = null; counterTriggered = false; lastDamageTaken = 0.0f; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Counter attack logic if triggered if (canCounter && counterTriggered && lastAttacker != null && !world.isRemote) { - performCounter(npc, world); + performCounter(caster, world); } // Check if guard duration has ended @@ -105,13 +107,13 @@ public class AbilityGuard extends Ability implements IAbilityGuard { } /** - * Called externally when the NPC takes damage while guarding. + * Called externally when the caster takes damage while guarding. * This should be called from the damage handling code. * * @param attacker The entity that attacked * @param damage The damage amount (after reduction) */ - public void onDamageTaken(EntityNPCInterface npc, EntityLivingBase attacker, DamageSource source, float damage) { + public void onDamageTaken(EntityLivingBase caster, EntityLivingBase attacker, DamageSource source, float damage) { if (!canCounter || attacker == null) return; if (!isDirectHit(source)) return; if (RANDOM.nextFloat() >= counterChance) return; @@ -122,17 +124,19 @@ public class AbilityGuard extends Ability implements IAbilityGuard { // Counter will be performed in onActiveTick - don't self-interrupt } - private void performCounter(EntityNPCInterface npc, World world) { + private void performCounter(EntityLivingBase caster, World world) { float counterDamage = counterType == CounterType.PERCENT ? lastDamageTaken * (counterValue / 100.0f) : counterValue; - boolean wasHit = applyAbilityDamage(npc, lastAttacker, counterDamage, 0.5f); + boolean wasHit = applyAbilityDamage(caster, lastAttacker, counterDamage, 0.5f); if (wasHit && counterSound != null && !counterSound.isEmpty()) { - world.playSoundAtEntity(npc, counterSound, 1.0f, 1.2f); + world.playSoundAtEntity(caster, counterSound, 1.0f, 1.2f); + } + // Use shared animation utility - only available for NPCs + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).abilities.playAbilityAnimation(counterAnimationId); } - // Use shared animation utility - npc.abilities.playAbilityAnimation(counterAnimationId); counterTriggered = false; lastAttacker = null; @@ -157,7 +161,7 @@ public class AbilityGuard extends Ability implements IAbilityGuard { } /** - * Check if the NPC is currently in guard stance. + * Check if the caster is currently in guard stance. */ public boolean isGuarding() { return isExecuting() && getPhase() == kamkeel.npcs.controllers.data.ability.AbilityPhase.ACTIVE; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 61841ded4..0700b1f01 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -118,8 +118,8 @@ public class AbilityHazard extends Ability implements IAbilityHazard { } @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { - TelegraphInstance instance = super.createTelegraph(npc, target); + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + TelegraphInstance instance = super.createTelegraph(caster, target); if (instance == null) return null; // Control telegraph following based on placement mode @@ -128,9 +128,9 @@ public class AbilityHazard extends Ability implements IAbilityHazard { case FOLLOW_CASTER: // Telegraph stays at caster position, no following instance.setEntityIdToFollow(-1); - instance.setX(npc.posX); - instance.setY(npc.posY); - instance.setZ(npc.posZ); + instance.setX(caster.posX); + instance.setY(caster.posY); + instance.setZ(caster.posZ); break; case AT_TARGET: // Telegraph follows target during windup, locks on execute @@ -149,14 +149,14 @@ public class AbilityHazard extends Ability implements IAbilityHazard { } @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // AT_CASTER locks immediately, AT_TARGET follows during windup and locks on execute if (tick == 0) { switch (placement) { case AT_CASTER: - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; positionLocked = true; break; case AT_TARGET: @@ -172,7 +172,7 @@ public class AbilityHazard extends Ability implements IAbilityHazard { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { // Lock position from telegraph (which was following target) or calculate it now TelegraphInstance telegraph = getTelegraphInstance(); @@ -180,9 +180,9 @@ public class AbilityHazard extends Ability implements IAbilityHazard { case AT_CASTER: // Already locked during windup if (!positionLocked) { - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; } break; case AT_TARGET: @@ -204,15 +204,15 @@ public class AbilityHazard extends Ability implements IAbilityHazard { zoneY = pos[1]; zoneZ = pos[2]; } else { - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; } break; case FOLLOW_CASTER: - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; break; case FOLLOW_TARGET: if (target != null) { @@ -220,9 +220,9 @@ public class AbilityHazard extends Ability implements IAbilityHazard { zoneY = target.posY; zoneZ = target.posZ; } else { - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; } break; } @@ -231,7 +231,7 @@ public class AbilityHazard extends Ability implements IAbilityHazard { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote) return; // Check if hazard duration has ended @@ -245,9 +245,9 @@ public class AbilityHazard extends Ability implements IAbilityHazard { switch (placement) { case FOLLOW_CASTER: - zoneX = npc.posX; - zoneY = npc.posY; - zoneZ = npc.posZ; + zoneX = caster.posX; + zoneY = caster.posY; + zoneZ = caster.posZ; break; case FOLLOW_TARGET: if (target != null) { @@ -272,16 +272,16 @@ public class AbilityHazard extends Ability implements IAbilityHazard { List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); for (EntityLivingBase entity : entities) { - if (entity == npc && !affectsCaster) continue; + if (entity == caster && !affectsCaster) continue; if (damagedThisTick.contains(entity.getEntityId())) continue; - if (!isInZone(entity, npc)) continue; + if (!isInZone(entity, caster)) continue; if (damagePerSecond > 0) { if (ignoreInvulnFrames) { entity.hurtResistantTime = 0; } // Apply damage with scripted event support (no knockback for hazard) - boolean wasHit = applyAbilityDamage(npc, entity, damagePerSecond, 0); + boolean wasHit = applyAbilityDamage(caster, entity, damagePerSecond, 0); if (!wasHit) continue; // Skip debuffs if hit was cancelled } @@ -291,7 +291,7 @@ public class AbilityHazard extends Ability implements IAbilityHazard { } } - private boolean isInZone(EntityLivingBase entity, EntityNPCInterface npc) { + private boolean isInZone(EntityLivingBase entity, EntityLivingBase caster) { double dx = entity.posX - zoneX; double dz = entity.posZ - zoneZ; double dist = Math.sqrt(dx * dx + dz * dz); @@ -303,7 +303,7 @@ public class AbilityHazard extends Ability implements IAbilityHazard { return dist >= innerRadius && dist <= radius; case CONE: if (dist > radius) return false; - float casterYaw = npc.rotationYaw; + float casterYaw = caster.rotationYaw; double angleToEntity = Math.toDegrees(Math.atan2(-dx, dz)); double angleDiff = Math.abs(normalizeAngle(angleToEntity - casterYaw)); return angleDiff <= coneAngle / 2; @@ -337,13 +337,13 @@ public class AbilityHazard extends Ability implements IAbilityHazard { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { damagedThisTick.clear(); positionLocked = false; } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { damagedThisTick.clear(); positionLocked = false; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index 406be2f3a..898d54f57 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -80,21 +80,21 @@ public class AbilityHeal extends Ability implements IAbilityHeal { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) return; getHealedAllies().clear(); // Always find allies if we're healing them (needed for both instant and HoT) if (healAllies && healRadius > 0) { - findAlliesInRadius(npc, world); + findAlliesInRadius(caster, world); } if (instantHeal) { // Instant heal - apply all healing now if (healSelf) { - healEntity(npc); - spawnHealParticles(world, npc); + healEntity(caster); + spawnHealParticles(world, caster); } for (EntityLivingBase ally : getHealedAllies()) { @@ -109,7 +109,7 @@ public class AbilityHeal extends Ability implements IAbilityHeal { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote || instantHeal) return; // Heal over time - distribute heal across duration ticks @@ -121,11 +121,11 @@ public class AbilityHeal extends Ability implements IAbilityHeal { float selfTickHeal = tickHeal; // Add percentage-based heal portion if (healPercent > 0) { - selfTickHeal += (npc.getMaxHealth() * healPercent / (float) durationTicks) * 10; + selfTickHeal += (caster.getMaxHealth() * healPercent / (float) durationTicks) * 10; } - npc.heal(selfTickHeal); + caster.heal(selfTickHeal); if (tick % 20 == 0) { - spawnHealParticles(world, npc); + spawnHealParticles(world, caster); } } @@ -155,10 +155,10 @@ public class AbilityHeal extends Ability implements IAbilityHeal { /** * Find all allies in radius and add them to healedAllies list. */ - private void findAlliesInRadius(EntityNPCInterface npc, World world) { + private void findAlliesInRadius(EntityLivingBase caster, World world) { AxisAlignedBB aabb = AxisAlignedBB.getBoundingBox( - npc.posX - healRadius, npc.posY - 2, npc.posZ - healRadius, - npc.posX + healRadius, npc.posY + 3, npc.posZ + healRadius + caster.posX - healRadius, caster.posY - 2, caster.posZ - healRadius, + caster.posX + healRadius, caster.posY + 3, caster.posZ + healRadius ); @SuppressWarnings("unchecked") @@ -166,13 +166,13 @@ public class AbilityHeal extends Ability implements IAbilityHeal { for (Entity entity : entities) { if (!(entity instanceof EntityLivingBase)) continue; - if (entity == npc) continue; + if (entity == caster) continue; EntityLivingBase living = (EntityLivingBase) entity; // Only heal other NPCs (allies) if (living instanceof EntityNPCInterface) { - float dist = npc.getDistanceToEntity(living); + float dist = caster.getDistanceToEntity(living); if (dist <= healRadius) { getHealedAllies().add(living); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index 4ed4a0e78..7d0c217c0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -62,14 +62,14 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote || target == null) { signalCompletion(); return; } // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, target, damage, knockback); + boolean wasHit = applyAbilityDamage(caster, target, damage, knockback); // Apply effects if hit wasn't cancelled if (wasHit) { @@ -81,7 +81,7 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Heavy Hit is instant, no active tick needed } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index 3b45894b4..acf5f0aa4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -90,18 +90,18 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) { signalCompletion(); return; } - double spawnX = npc.posX; - double spawnY = npc.posY + npc.getEyeHeight() * 0.8; - double spawnZ = npc.posZ; + double spawnX = caster.posX; + double spawnY = caster.posY + caster.getEyeHeight(); + double spawnZ = caster.posZ; laserEntity = new EntityAbilityLaser( - world, npc, target, + world, caster, target, spawnX, spawnY, spawnZ, laserWidth, colorData, combatData, lightningData, lifespanData, @@ -116,7 +116,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Signal completion when entity dies if (laserEntity == null || laserEntity.isDead) { laserEntity = null; @@ -125,11 +125,11 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 78936adbf..b489f4b9d 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -82,7 +82,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) { signalCompletion(); return; @@ -98,15 +98,15 @@ public class AbilityOrb extends Ability implements IAbilityOrb { } @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (world.isRemote) return; // Spawn orb in charging mode on first tick of windup if (tick == 1) { - // Create orb in charging mode - follows NPC based on anchor point during windup - Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(npc, anchorData); + // Create orb in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); orbEntity = new EntityAbilityOrb( - world, npc, target, + world, caster, target, spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize, colorData, combatData, homingData, lightningData, lifespanData); orbEntity.setupCharging(anchorData, windUpTicks); @@ -117,7 +117,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Signal completion when entity dies if (orbEntity == null || orbEntity.isDead) { orbEntity = null; @@ -126,7 +126,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { // Nothing to clean up - entity manages itself } @@ -140,7 +140,7 @@ public class AbilityOrb extends Ability implements IAbilityOrb { } @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { return null; } @@ -154,8 +154,8 @@ public class AbilityOrb extends Ability implements IAbilityOrb { telegraph.setHeightOffset(telegraphHeightOffset); // Position at target and follow target during windup - TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, npc.rotationYaw); - instance.setCasterEntityId(npc.getEntityId()); + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); instance.setEntityIdToFollow(target.getEntityId()); return instance; diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index 6b81fabe7..c465bc0a5 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -13,8 +13,6 @@ import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityProjectile; import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.entity.EntityNPCInterface; - import noppes.npcs.api.ability.type.IAbilityProjectile; import java.util.List; @@ -71,13 +69,13 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote || target == null) return; // Calculate direction to target - double dx = target.posX - npc.posX; - double dy = (target.posY + target.height / 2) - (npc.posY + npc.getEyeHeight()); - double dz = target.posZ - npc.posZ; + double dx = target.posX - caster.posX; + double dy = (target.posY + target.height / 2) - (caster.posY + caster.getEyeHeight()); + double dz = target.posZ - caster.posZ; double len = Math.sqrt(dx * dx + dy * dy + dz * dz); if (len > 0) { @@ -88,10 +86,10 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { // Deal instant damage with scripted event support // TODO: Use custom EntityAbilityProjectile for actual tracking - applyAbilityDamageWithDirection(npc, target, damage, knockback, dx, dz); + applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz); // Play sound - world.playSoundAtEntity(npc, "random.bow", 1.0f, 0.8f); + world.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f); // Handle splash damage for explosive projectiles if (explosive && explosionRadius > 0) { @@ -100,13 +98,13 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { target.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius)); for (Entity entity : entities) { - if (entity instanceof EntityLivingBase && entity != npc) { + if (entity instanceof EntityLivingBase && entity != caster) { EntityLivingBase living = (EntityLivingBase) entity; float dist = target.getDistanceToEntity(living); if (dist < explosionRadius) { float falloff = 1.0f - (dist / explosionRadius); // Apply splash damage with scripted event support (no knockback) - applyAbilityDamage(npc, living, damage * falloff * 0.5f, 0); + applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0); } } } @@ -123,16 +121,16 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { } // Spawn projectile particles (visual trail) - spawnProjectileParticles(world, npc, target); + spawnProjectileParticles(world, caster, target); // Projectile is instant signalCompletion(); } - private void spawnProjectileParticles(World world, EntityNPCInterface npc, EntityLivingBase target) { - double startX = npc.posX; - double startY = npc.posY + npc.getEyeHeight(); - double startZ = npc.posZ; + private void spawnProjectileParticles(World world, EntityLivingBase caster, EntityLivingBase target) { + double startX = caster.posX; + double startY = caster.posY + caster.getEyeHeight(); + double startZ = caster.posZ; double endX = target.posX; double endY = target.posY + target.height / 2; double endZ = target.posZ; @@ -161,7 +159,7 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Projectile is instant, nothing to do per-tick } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index 006084f5e..6416cc0ba 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -71,28 +71,28 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { // Shockwave is instant - apply effect immediately after windup // Get all entities in radius - AxisAlignedBB box = npc.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius); + AxisAlignedBB box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius); @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); int count = 0; for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; if (entity.isDead) continue; - double dist = npc.getDistanceToEntity(entity); + double dist = caster.getDistanceToEntity(entity); if (dist > pushRadius) continue; count++; if (count > maxTargets) break; // Calculate push direction (away from caster) - double dx = entity.posX - npc.posX; - double dz = entity.posZ - npc.posZ; + double dx = entity.posX - caster.posX; + double dz = entity.posZ - caster.posZ; double len = Math.sqrt(dx * dx + dz * dz); if (len > 0) { @@ -109,7 +109,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f; float finalPush = pushStrength * distFactor; // Apply damage with custom knockback direction - boolean wasHit = applyAbilityDamageWithDirection(npc, entity, damage * distFactor, finalPush, dx, dz); + boolean wasHit = applyAbilityDamageWithDirection(caster, entity, damage * distFactor, finalPush, dx, dz); // Apply effects if hit connected if (wasHit) { @@ -122,7 +122,7 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Shockwave is instant - nothing to do per-tick } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 2dae47d05..64e0255ca 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -94,13 +94,13 @@ public class AbilitySlam extends Ability implements IAbilitySlam { } @Override - public void onWindUpTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Update target position during windup if (targetingMode == TargetingMode.AOE_SELF) { - // AOE_SELF: slam at NPC's current position - targetX = npc.posX; - targetY = npc.posY; - targetZ = npc.posZ; + // AOE_SELF: slam at caster's current position + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) { // AOE_TARGET: telegraph follows target via setEntityIdToFollow targetX = target.posX; @@ -110,13 +110,13 @@ public class AbilitySlam extends Ability implements IAbilitySlam { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { // Lock in the destination at moment of launch if (targetingMode == TargetingMode.AOE_SELF) { // AOE_SELF: slam in place (just jump up) - targetX = npc.posX; - targetY = npc.posY; - targetZ = npc.posZ; + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) { // AOE_TARGET: leap to target's current position targetX = target.posX; @@ -124,9 +124,9 @@ public class AbilitySlam extends Ability implements IAbilitySlam { targetZ = target.posZ; } else { // Fallback: slam in place - targetX = npc.posX; - targetY = npc.posY; - targetZ = npc.posZ; + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } hasLaunched = false; @@ -134,31 +134,33 @@ public class AbilitySlam extends Ability implements IAbilitySlam { airTicks = 0; // Reset fall distance to prevent fall damage during slam - npc.fallDistance = 0; + caster.fallDistance = 0; // Calculate and apply leap velocity - launchTowardTarget(npc); + launchTowardTarget(caster); } /** - * Calculate ballistic arc and launch NPC toward target. + * Calculate ballistic arc and launch caster toward target. * Accounts for Minecraft's drag physics to actually reach the destination. */ - private void launchTowardTarget(EntityNPCInterface npc) { - double dx = targetX - npc.posX; - double dy = targetY - npc.posY; - double dz = targetZ - npc.posZ; + private void launchTowardTarget(EntityLivingBase caster) { + double dx = targetX - caster.posX; + double dy = targetY - caster.posY; + double dz = targetZ - caster.posZ; double horizontalDist = Math.sqrt(dx * dx + dz * dz); // For AOE_SELF or very close targets, just hop in place if (horizontalDist < 0.5) { - npc.motionX = 0; - npc.motionZ = 0; - npc.motionY = 0.8 * leapSpeed; + caster.motionX = 0; + caster.motionZ = 0; + caster.motionY = 0.8 * leapSpeed; hasLaunched = true; - npc.setNpcJumpingState(true); - npc.velocityChanged = true; - npc.worldObj.playSoundAtEntity(npc, "mob.irongolem.throw", 0.8f, 0.8f); + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(true); + } + caster.velocityChanged = true; + caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f); return; } @@ -168,8 +170,8 @@ public class AbilitySlam extends Ability implements IAbilitySlam { dx *= scale; dz *= scale; horizontalDist = maxRange; - targetX = npc.posX + dx; - targetZ = npc.posZ + dz; + targetX = caster.posX + dx; + targetZ = caster.posZ + dz; } // Choose flight time based on distance - shorter distances = faster @@ -215,96 +217,100 @@ public class AbilitySlam extends Ability implements IAbilitySlam { double dirX = dx / horizontalDist; double dirZ = dz / horizontalDist; - npc.motionX = dirX * vHorizontal; - npc.motionZ = dirZ * vHorizontal; - npc.motionY = vy; + caster.motionX = dirX * vHorizontal; + caster.motionZ = dirZ * vHorizontal; + caster.motionY = vy; hasLaunched = true; - npc.setNpcJumpingState(true); - npc.velocityChanged = true; + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(true); + } + caster.velocityChanged = true; // Face the target float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); - npc.rotationYaw = targetYaw; - npc.rotationYawHead = targetYaw; + caster.rotationYaw = targetYaw; + caster.rotationYawHead = targetYaw; } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (!hasLaunched) return; if (hasLanded) return; airTicks++; // Continuously reset fall distance during slam to prevent fall damage - npc.fallDistance = 0; + caster.fallDistance = 0; // Check for landing - if (npc.onGround && airTicks > 3) { + if (caster.onGround && airTicks > 3) { // Landed! - onLanding(npc, world); + onLanding(caster, world); return; } // Timeout protection - force landing after max air time if (airTicks >= maxAirTicks) { - onLanding(npc, world); + onLanding(caster, world); return; } // While in air, face toward target - double dx = targetX - npc.posX; - double dz = targetZ - npc.posZ; + double dx = targetX - caster.posX; + double dz = targetZ - caster.posZ; float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI); - npc.rotationYaw = targetYaw; - npc.rotationYawHead = targetYaw; + caster.rotationYaw = targetYaw; + caster.rotationYawHead = targetYaw; } /** - * Called when NPC lands - deal AOE damage and signal completion. + * Called when caster lands - deal AOE damage and signal completion. */ - private void onLanding(EntityNPCInterface npc, World world) { + private void onLanding(EntityLivingBase caster, World world) { hasLanded = true; - npc.setNpcJumpingState(false); + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(false); + } // Signal that the ability has completed its active phase signalCompletion(); // Reset fall distance to prevent fall damage on landing - npc.fallDistance = 0; + caster.fallDistance = 0; // Stop horizontal momentum - npc.motionX = 0; - npc.motionZ = 0; - npc.velocityChanged = true; + caster.motionX = 0; + caster.motionZ = 0; + caster.velocityChanged = true; if (world.isRemote) return; // Play slam impact sound on landing - world.playSoundAtEntity(npc, "random.explode", 1.0f, 1.0f); + world.playSoundAtEntity(caster, "random.explode", 1.0f, 1.0f); // Find all entities in radius @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABBExcludingEntity( - npc, npc.boundingBox.expand(radius, radius / 2, radius)); + caster, caster.boundingBox.expand(radius, radius / 2, radius)); for (Entity entity : entities) { - if (entity instanceof EntityLivingBase && entity != npc) { + if (entity instanceof EntityLivingBase && entity != caster) { EntityLivingBase livingTarget = (EntityLivingBase) entity; // Check if actually within radius (bounding box is a cube, we want a circle) - double dx = livingTarget.posX - npc.posX; - double dz = livingTarget.posZ - npc.posZ; + double dx = livingTarget.posX - caster.posX; + double dz = livingTarget.posZ - caster.posZ; if (dx * dx + dz * dz <= radius * radius) { // Apply damage with scripted event support - applyAbilityDamage(npc, livingTarget, damage, knockbackStrength); + applyAbilityDamage(caster, livingTarget, damage, knockbackStrength); } } } // Spawn particles - spawnSlamParticles(world, npc.posX, npc.posY, npc.posZ); + spawnSlamParticles(world, caster.posX, caster.posY, caster.posZ); } /** @@ -344,16 +350,20 @@ public class AbilitySlam extends Ability implements IAbilitySlam { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { - npc.setNpcJumpingState(false); + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(false); + } hasLaunched = false; hasLanded = false; airTicks = 0; } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { - npc.setNpcJumpingState(false); + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { + if (caster instanceof EntityNPCInterface) { + ((EntityNPCInterface) caster).setNpcJumpingState(false); + } hasLaunched = false; hasLanded = false; airTicks = 0; @@ -462,7 +472,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam { } @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { // Check if telegraph should be shown if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) { return null; @@ -470,24 +480,24 @@ public class AbilitySlam extends Ability implements IAbilitySlam { // Telegraph shows at landing zone if (targetingMode == TargetingMode.AOE_SELF) { - // AOE_SELF: telegraph at NPC position, does NOT follow - targetX = npc.posX; - targetY = npc.posY; - targetZ = npc.posZ; + // AOE_SELF: telegraph at caster position, does NOT follow + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } else if (targetingMode == TargetingMode.AOE_TARGET && target != null) { // AOE_TARGET: telegraph at target position, follows target during windup targetX = target.posX; targetY = target.posY; targetZ = target.posZ; } else { - // Fallback to NPC position - targetX = npc.posX; - targetY = npc.posY; - targetZ = npc.posZ; + // Fallback to caster position + targetX = caster.posX; + targetY = caster.posY; + targetZ = caster.posZ; } // Create telegraph at the appropriate position - double groundY = findGroundLevel(npc.worldObj, targetX, targetY, targetZ); + double groundY = findGroundLevel(caster.worldObj, targetX, targetY, targetZ); Telegraph telegraph = Telegraph.circle(radius); telegraph.setDurationTicks(windUpTicks); @@ -496,14 +506,14 @@ public class AbilitySlam extends Ability implements IAbilitySlam { telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); telegraph.setHeightOffset(telegraphHeightOffset); - TelegraphInstance instance = new TelegraphInstance(telegraph, targetX, groundY, targetZ, npc.rotationYaw); - instance.setCasterEntityId(npc.getEntityId()); + TelegraphInstance instance = new TelegraphInstance(telegraph, targetX, groundY, targetZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); // AOE_TARGET: telegraph follows target during windup if (targetingMode == TargetingMode.AOE_TARGET && target != null) { instance.setEntityIdToFollow(target.getEntityId()); } - // AOE_SELF: telegraph stays at NPC position, no follow + // AOE_SELF: telegraph stays at caster position, no follow return instance; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index 976d9e44e..d2acc4df3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -18,7 +18,6 @@ import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilitySweeper; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.api.ability.type.IAbilitySweeper; -import noppes.npcs.entity.EntityNPCInterface; /** * Sweeper ability: A low sweeping beam that rotates around the NPC. @@ -83,14 +82,14 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) { signalCompletion(); return; } // Spawn the entity that handles BOTH visuals AND damage - activeEntity = new EntityAbilitySweeper(world, npc, target, + activeEntity = new EntityAbilitySweeper(world, caster, target, beamLength, beamWidth, beamHeight, colorData, sweepSpeed, numberOfRotations, @@ -103,7 +102,7 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Signal completion when entity dies if (activeEntity == null || activeEntity.isDead) { activeEntity = null; @@ -129,12 +128,12 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { * Override to position telegraph at NPC (not target) since the sweep is centered on NPC. */ @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { if (!showTelegraph || telegraphType == TelegraphType.NONE) { return null; } - // Create circle telegraph centered on NPC + // Create circle telegraph centered on caster Telegraph telegraph = Telegraph.circle(beamLength); telegraph.setDurationTicks(windUpTicks); telegraph.setColor(windUpColor); @@ -142,10 +141,10 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); telegraph.setHeightOffset(telegraphHeightOffset); - // Position at NPC, not target - TelegraphInstance instance = new TelegraphInstance(telegraph, npc.posX, npc.posY, npc.posZ, npc.rotationYaw); - instance.setCasterEntityId(npc.getEntityId()); - instance.setEntityIdToFollow(npc.getEntityId()); // Follow NPC + // Position at caster, not target + TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, caster.posY, caster.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(caster.getEntityId()); // Follow caster return instance; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index 1fef00a55..4e4a5dff7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -95,13 +95,13 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { currentBlink = 0; ticksSinceLastBlink = blinkDelayTicks; // Trigger first blink immediately } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1; if (currentBlink >= blinkLimit) { signalCompletion(); @@ -111,7 +111,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { ticksSinceLastBlink++; if (ticksSinceLastBlink >= blinkDelayTicks) { - performBlink(npc, target, world); + performBlink(caster, target, world); currentBlink++; ticksSinceLastBlink = 0; @@ -122,20 +122,20 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { } } - private void performBlink(EntityNPCInterface npc, EntityLivingBase target, World world) { + private void performBlink(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) return; - double oldX = npc.posX; - double oldY = npc.posY; - double oldZ = npc.posZ; + double oldX = caster.posX; + double oldY = caster.posY; + double oldZ = caster.posZ; - Vec3 destination = calculateDestination(npc, target, world); + Vec3 destination = calculateDestination(caster, target, world); if (destination == null) return; boolean mustHaveLOS = mode == TeleportMode.BEHIND || requireLineOfSight; - if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + npc.getEyeHeight(), oldZ, - destination.xCoord, destination.yCoord + npc.getEyeHeight(), destination.zCoord)) { - destination = findValidPositionAlongLine(world, npc, oldX, oldY, oldZ, + if (mustHaveLOS && !hasLineOfSight(world, oldX, oldY + caster.getEyeHeight(), oldZ, + destination.xCoord, destination.yCoord + caster.getEyeHeight(), destination.zCoord)) { + destination = findValidPositionAlongLine(world, caster, oldX, oldY, oldZ, destination.xCoord, destination.yCoord, destination.zCoord); if (destination == null) return; } @@ -146,22 +146,22 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { // Damage at origin if (damageAtStart) { - dealDamageAt(npc, world, oldX, oldY, oldZ); + dealDamageAt(caster, world, oldX, oldY, oldZ); } // Spawn particles at origin spawnTeleportParticles(world, oldX, oldY, oldZ); // Teleport - npc.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord); - npc.fallDistance = 0; + caster.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord); + caster.fallDistance = 0; // Spawn particles at destination spawnTeleportParticles(world, destination.xCoord, destination.yCoord, destination.zCoord); // Damage at destination if (damageAtEnd) { - dealDamageAt(npc, world, destination.xCoord, destination.yCoord, destination.zCoord); + dealDamageAt(caster, world, destination.xCoord, destination.yCoord, destination.zCoord); } // Face target after teleport @@ -169,8 +169,8 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { double dx = target.posX - destination.xCoord; double dz = target.posZ - destination.zCoord; float newYaw = (float) Math.toDegrees(Math.atan2(-dx, dz)); - npc.rotationYaw = newYaw; - npc.rotationYawHead = newYaw; + caster.rotationYaw = newYaw; + caster.rotationYawHead = newYaw; } } @@ -186,29 +186,50 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { } } - private Vec3 calculateDestination(EntityNPCInterface npc, EntityLivingBase target, World world) { - if (target == null) return null; - + private Vec3 calculateDestination(EntityLivingBase caster, EntityLivingBase target, World world) { double newX; double newY; double newZ; + if (target != null) { + switch (mode) { + case BEHIND: + double yaw = Math.toRadians(target.rotationYaw); + newX = target.posX + Math.sin(yaw) * behindDistance; + newZ = target.posZ - Math.cos(yaw) * behindDistance; + newY = target.posY; + return Vec3.createVectorHelper(newX, newY, newZ); + case SINGLE: + case BLINK: + default: + double angle = RANDOM.nextDouble() * Math.PI * 2; + double dist = Math.min(blinkRadius, maxRange); + double offset = RANDOM.nextDouble() * dist; + newX = target.posX + Math.cos(angle) * offset; + newZ = target.posZ + Math.sin(angle) * offset; + newY = target.posY; + return Vec3.createVectorHelper(newX, newY, newZ); + } + } + + // No target (player use) - teleport in caster's look direction + Vec3 look = caster.getLookVec(); switch (mode) { case BEHIND: - double yaw = Math.toRadians(target.rotationYaw); - newX = target.posX + Math.sin(yaw) * behindDistance; - newZ = target.posZ - Math.cos(yaw) * behindDistance; - newY = target.posY; + // Teleport forward in look direction by behindDistance + newX = caster.posX + look.xCoord * behindDistance; + newZ = caster.posZ + look.zCoord * behindDistance; + newY = caster.posY; return Vec3.createVectorHelper(newX, newY, newZ); case SINGLE: case BLINK: default: - double angle = RANDOM.nextDouble() * Math.PI * 2; + // Teleport forward in look direction within blinkRadius double dist = Math.min(blinkRadius, maxRange); - double offset = RANDOM.nextDouble() * dist; - newX = target.posX + Math.cos(angle) * offset; - newZ = target.posZ + Math.sin(angle) * offset; - newY = target.posY; + double offset = (RANDOM.nextDouble() * 0.5 + 0.5) * dist; // 50-100% of max distance + newX = caster.posX + look.xCoord * offset; + newZ = caster.posZ + look.zCoord * offset; + newY = caster.posY; return Vec3.createVectorHelper(newX, newY, newZ); } } @@ -285,7 +306,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { return true; } - private Vec3 findValidPositionAlongLine(World world, EntityNPCInterface npc, + private Vec3 findValidPositionAlongLine(World world, EntityLivingBase caster, double x1, double y1, double z1, double x2, double y2, double z2) { double dx = x2 - x1; @@ -307,8 +328,8 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { double checkY = y1 + dy * d; double checkZ = z1 + dz * d; - if (hasLineOfSight(world, x1, y1 + npc.getEyeHeight(), z1, - checkX, checkY + npc.getEyeHeight(), checkZ)) { + if (hasLineOfSight(world, x1, y1 + caster.getEyeHeight(), z1, + checkX, checkY + caster.getEyeHeight(), checkZ)) { int blockX = MathHelper.floor_double(checkX); int blockY = MathHelper.floor_double(checkY); int blockZ = MathHelper.floor_double(checkZ); @@ -361,7 +382,7 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { !headBlock.getMaterial().isSolid(); } - private void dealDamageAt(EntityNPCInterface npc, World world, double x, double y, double z) { + private void dealDamageAt(EntityLivingBase caster, World world, double x, double y, double z) { AxisAlignedBB aabb = AxisAlignedBB.getBoundingBox( x - damageRadius, y - 1, z - damageRadius, x + damageRadius, y + 2, z + damageRadius @@ -372,13 +393,13 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { for (Entity entity : entities) { if (!(entity instanceof EntityLivingBase)) continue; - if (entity == npc) continue; + if (entity == caster) continue; EntityLivingBase living = (EntityLivingBase) entity; double dist = Math.sqrt(Math.pow(living.posX - x, 2) + Math.pow(living.posZ - z, 2)); if (dist <= damageRadius) { // Apply damage with scripted event support (no knockback for teleport damage) - boolean wasHit = applyAbilityDamage(npc, living, damage, 0); + boolean wasHit = applyAbilityDamage(caster, living, damage, 0); if (wasHit) { applyEffects(living); } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 6bf69a60b..a50fe57a1 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -100,8 +100,8 @@ public class AbilityTrap extends Ability implements IAbilityTrap { } @Override - public TelegraphInstance createTelegraph(EntityNPCInterface npc, EntityLivingBase target) { - TelegraphInstance instance = super.createTelegraph(npc, target); + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + TelegraphInstance instance = super.createTelegraph(caster, target); if (instance == null) return null; // Control telegraph following based on placement mode @@ -111,14 +111,14 @@ public class AbilityTrap extends Ability implements IAbilityTrap { // Telegraph at caster or ahead, no following instance.setEntityIdToFollow(-1); if (placement == TrapPlacement.AHEAD_OF_CASTER) { - double yaw = Math.toRadians(npc.rotationYaw); - instance.setX(npc.posX - Math.sin(yaw) * placementDistance); - instance.setY(npc.posY); - instance.setZ(npc.posZ + Math.cos(yaw) * placementDistance); + double yaw = Math.toRadians(caster.rotationYaw); + instance.setX(caster.posX - Math.sin(yaw) * placementDistance); + instance.setY(caster.posY); + instance.setZ(caster.posZ + Math.cos(yaw) * placementDistance); } else { - instance.setX(npc.posX); - instance.setY(npc.posY); - instance.setZ(npc.posZ); + instance.setX(caster.posX); + instance.setY(caster.posY); + instance.setZ(caster.posZ); } break; case AT_TARGET: @@ -132,7 +132,7 @@ public class AbilityTrap extends Ability implements IAbilityTrap { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { armed = false; triggerCount = 0; ticksSinceLastTrigger = armTime; @@ -143,9 +143,9 @@ public class AbilityTrap extends Ability implements IAbilityTrap { switch (placement) { case AT_CASTER: - trapX = npc.posX; - trapY = npc.posY; - trapZ = npc.posZ; + trapX = caster.posX; + trapY = caster.posY; + trapZ = caster.posZ; break; case AT_TARGET: // Use telegraph position with offset @@ -166,22 +166,22 @@ public class AbilityTrap extends Ability implements IAbilityTrap { trapY = pos[1]; trapZ = pos[2]; } else { - trapX = npc.posX; - trapY = npc.posY; - trapZ = npc.posZ; + trapX = caster.posX; + trapY = caster.posY; + trapZ = caster.posZ; } break; case AHEAD_OF_CASTER: - double yaw = Math.toRadians(npc.rotationYaw); - trapX = npc.posX - Math.sin(yaw) * placementDistance; - trapY = npc.posY; - trapZ = npc.posZ + Math.cos(yaw) * placementDistance; + double yaw = Math.toRadians(caster.rotationYaw); + trapX = caster.posX - Math.sin(yaw) * placementDistance; + trapY = caster.posY; + trapZ = caster.posZ + Math.cos(yaw) * placementDistance; break; } } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { // Check if trap duration has ended if (tick >= durationTicks) { signalCompletion(); @@ -215,7 +215,7 @@ public class AbilityTrap extends Ability implements IAbilityTrap { List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; if (entity.isDead) continue; if (maxTriggers == 1 && triggeredEntities.contains(entity.getUniqueID())) continue; @@ -224,13 +224,13 @@ public class AbilityTrap extends Ability implements IAbilityTrap { double dist = Math.sqrt(dx * dx + dz * dz); if (dist <= triggerRadius) { - triggerTrap(npc, entity, world); + triggerTrap(caster, entity, world); return; } } } - private void triggerTrap(EntityNPCInterface npc, EntityLivingBase triggerer, World world) { + private void triggerTrap(EntityLivingBase caster, EntityLivingBase triggerer, World world) { triggerCount++; ticksSinceLastTrigger = 0; triggeredEntities.add(triggerer.getUniqueID()); @@ -247,7 +247,7 @@ public class AbilityTrap extends Ability implements IAbilityTrap { List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; double dx = entity.posX - trapX; double dz = entity.posZ - trapZ; double dist = Math.sqrt(dx * dx + dz * dz); @@ -261,7 +261,7 @@ public class AbilityTrap extends Ability implements IAbilityTrap { for (EntityLivingBase entity : affected) { // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, entity, damage, knockback); + boolean wasHit = applyAbilityDamage(caster, entity, damage, knockback); // Apply effects if the hit wasn't cancelled if (wasHit) { @@ -271,14 +271,14 @@ public class AbilityTrap extends Ability implements IAbilityTrap { } @Override - public void onComplete(EntityNPCInterface npc, EntityLivingBase target) { + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { armed = false; triggerCount = 0; triggeredEntities.clear(); } @Override - public void onInterrupt(EntityNPCInterface npc, DamageSource source, float damage) { + public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) { armed = false; triggerCount = 0; triggeredEntities.clear(); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index 1bcfb7cbc..f289f0f72 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -90,22 +90,22 @@ public class AbilityVortex extends Ability implements IAbilityVortex { } @Override - public void onExecute(EntityNPCInterface npc, EntityLivingBase target, World world) { + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { getPulledEntities().clear(); pullComplete = false; ticksSincePullDamage = 0; if (aoe) { - AxisAlignedBB box = npc.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); + AxisAlignedBB box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); @SuppressWarnings("unchecked") List entities = world.getEntitiesWithinAABB(EntityLivingBase.class, box); int count = 0; for (EntityLivingBase entity : entities) { - if (entity == npc) continue; + if (entity == caster) continue; if (entity.isDead) continue; - double dist = npc.getDistanceToEntity(entity); + double dist = caster.getDistanceToEntity(entity); if (dist <= pullRadius) { getPulledEntities().add(entity.getUniqueID()); count++; @@ -115,7 +115,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex { } else { // Single target mode - still check pullRadius if (target != null && !target.isDead) { - double dist = npc.getDistanceToEntity(target); + double dist = caster.getDistanceToEntity(target); if (dist <= pullRadius) { getPulledEntities().add(target.getUniqueID()); } @@ -124,21 +124,21 @@ public class AbilityVortex extends Ability implements IAbilityVortex { } @Override - public void onActiveTick(EntityNPCInterface npc, EntityLivingBase target, World world, int tick) { + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { if (pullComplete || getPulledEntities().isEmpty()) { signalCompletion(); return; } - double destX = npc.posX; - double destY = npc.posY; - double destZ = npc.posZ; + double destX = caster.posX; + double destY = caster.posY; + double destZ = caster.posZ; boolean anyStillPulling = false; ticksSincePullDamage++; for (UUID uuid : new HashSet<>(getPulledEntities())) { - EntityLivingBase entity = findEntity(npc, world, uuid); + EntityLivingBase entity = findEntity(caster, world, uuid); if (entity == null || entity.isDead) { getPulledEntities().remove(uuid); continue; @@ -151,7 +151,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex { if (dist <= 1.0f) { getPulledEntities().remove(uuid); - onTargetArrived(npc, entity, world); + onTargetArrived(caster, entity, world); continue; } @@ -176,7 +176,7 @@ public class AbilityVortex extends Ability implements IAbilityVortex { if (damageOnPull && pullDamage > 0 && ticksSincePullDamage >= 10) { ticksSincePullDamage = 0; entity.hurtResistantTime = 0; - applyAbilityDamage(npc, entity, pullDamage * 0.5f, 0); + applyAbilityDamage(caster, entity, pullDamage * 0.5f, 0); } } @@ -186,9 +186,9 @@ public class AbilityVortex extends Ability implements IAbilityVortex { } } - private void onTargetArrived(EntityNPCInterface npc, EntityLivingBase entity, World world) { + private void onTargetArrived(EntityLivingBase caster, EntityLivingBase entity, World world) { // Apply damage with scripted event support - boolean wasHit = applyAbilityDamage(npc, entity, damage, knockback * 0.5f); + boolean wasHit = applyAbilityDamage(caster, entity, damage, knockback * 0.5f); // Only apply effects if hit wasn't cancelled if (wasHit) { @@ -201,9 +201,9 @@ public class AbilityVortex extends Ability implements IAbilityVortex { * Uses AABB search instead of iterating all loaded entities. */ @SuppressWarnings("unchecked") - private EntityLivingBase findEntity(EntityNPCInterface npc, World world, UUID uuid) { + private EntityLivingBase findEntity(EntityLivingBase caster, World world, UUID uuid) { // Search within pull radius (entities being pulled should be within this area) - AxisAlignedBB searchBox = npc.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); + AxisAlignedBB searchBox = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius); List nearbyEntities = world.getEntitiesWithinAABB(EntityLivingBase.class, searchBox); for (EntityLivingBase entity : nearbyEntities) { diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index 75fa6ef7d..a9cfffc43 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -12,7 +12,6 @@ import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.LogWriter; -import noppes.npcs.entity.EntityNPCInterface; import java.util.ArrayList; import java.util.List; @@ -108,7 +107,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { * @param anchoredMode If true, origin follows owner and tail orb is rendered. * If false, beam is free-moving with trailing length (no tail orb). */ - public EntityAbilityBeam(World world, EntityNPCInterface owner, EntityLivingBase target, + public EntityAbilityBeam(World world, EntityLivingBase owner, EntityLivingBase target, double x, double y, double z, float beamWidth, float headSize, EnergyColorData color, EnergyCombatData combat, @@ -179,7 +178,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { this.motionZ = 0; } - public void setupPreview(EntityNPCInterface owner, float beamWidth, float headSize, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) { + public void setupPreview(EntityLivingBase owner, float beamWidth, float headSize, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration, float chargeOffsetDistance) { this.setPreviewMode(true); this.setPreviewOwner(owner); @@ -459,11 +458,13 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { ); } - setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + // Offset downward by half headSize to center + double centeredY = pos.yCoord - headSize * 0.5; + setPosition(pos.xCoord, centeredY, pos.zCoord); // Also update origin for when firing starts startX = pos.xCoord; - startY = pos.yCoord; + startY = centeredY; startZ = pos.zCoord; } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index f6003df80..7dc3a8a82 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -10,7 +10,6 @@ import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.entity.EntityNPCInterface; import java.util.List; @@ -84,7 +83,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { /** * Full constructor with all parameters using data classes. */ - public EntityAbilityDisc(World world, EntityNPCInterface owner, EntityLivingBase target, + public EntityAbilityDisc(World world, EntityLivingBase owner, EntityLivingBase target, double x, double y, double z, float discRadius, float discThickness, EnergyColorData color, EnergyCombatData combat, @@ -155,7 +154,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { * Follows anchor point and animations like in the real game. * Can be fired when transitioning to active phase. */ - public void setupPreview(EntityNPCInterface owner, float discRadius, float discThickness, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) { + public void setupPreview(EntityLivingBase owner, float discRadius, float discThickness, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) { this.setPreviewMode(true); this.setPreviewOwner(owner); @@ -512,7 +511,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { this.discThickness = targetDiscThickness * progress; this.size = this.discRadius; // Base size for interpolation - // Calculate position based on anchor point + // Calculate position based on anchor point, offset downward by half radius to center if (owner instanceof EntityLivingBase) { Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); setPosition(pos.xCoord, pos.yCoord, pos.zCoord); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index c45a6ee9e..3d888c698 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -11,7 +11,6 @@ import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.entity.EntityNPCInterface; import java.util.HashSet; import java.util.List; @@ -51,7 +50,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { /** * Full constructor with all parameters using data classes. */ - public EntityAbilityLaser(World world, EntityNPCInterface owner, EntityLivingBase target, + public EntityAbilityLaser(World world, EntityLivingBase owner, EntityLivingBase target, double x, double y, double z, float laserWidth, EnergyColorData color, EnergyCombatData combat, @@ -327,7 +326,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { * Setup this laser in preview mode for GUI display. * Laser doesn't have charging state - spawns at active phase and fires immediately. */ - public void setupPreview(EntityNPCInterface owner, float laserWidth, EnergyColorData color, + public void setupPreview(EntityLivingBase owner, float laserWidth, EnergyColorData color, EnergyLightningData lightning, float expansionSpeed, float maxDistance) { this.setPreviewMode(true); this.setPreviewOwner(owner); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index eff147448..c403f25de 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -10,7 +10,6 @@ import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.MovingObjectPosition; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.entity.EntityNPCInterface; import java.util.List; @@ -73,7 +72,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { /** * Full constructor using data classes for grouped parameters. */ - public EntityAbilityOrb(World world, EntityNPCInterface owner, EntityLivingBase target, + public EntityAbilityOrb(World world, EntityLivingBase owner, EntityLivingBase target, double x, double y, double z, float orbSize, EnergyColorData color, EnergyCombatData combat, EnergyHomingData homing, EnergyLightningData lightning, @@ -134,7 +133,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { * Follows anchor point and animations like in the real game. * Can be fired when transitioning to active phase. */ - public void setupPreview(EntityNPCInterface owner, float orbSize, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) { + public void setupPreview(EntityLivingBase owner, float orbSize, EnergyColorData color, EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration) { this.setPreviewMode(true); this.setPreviewOwner(owner); @@ -394,7 +393,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { float progress = getChargeProgress(); this.size = targetSize * progress; - // Calculate position based on anchor point + // Calculate position based on anchor point, offset downward by half size to center if (owner instanceof EntityLivingBase) { Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); setPosition(pos.xCoord, pos.yCoord, pos.zCoord); diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java index 5508ecd94..57f99e1c8 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityProjectile.java @@ -11,9 +11,11 @@ import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.DamageSource; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.NpcDamageSource; @@ -75,7 +77,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA // ==================== STATE ==================== protected boolean hasHit = false; protected boolean previewMode = false; // Client-side preview mode (no damage/effects) - protected EntityNPCInterface previewOwner = null; // Direct reference for GUI preview (no world lookup) + protected EntityLivingBase previewOwner = null; // Direct reference for GUI preview (no world lookup) // ==================== ROTATION INTERPOLATION ==================== public float prevRotationValX, prevRotationValY, prevRotationValZ; @@ -101,7 +103,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA /** * Initialize common properties using data classes. Call from subclass constructors. */ - protected void initProjectile(EntityNPCInterface owner, EntityLivingBase target, + protected void initProjectile(EntityLivingBase owner, EntityLivingBase target, double x, double y, double z, float size, EnergyColorData color, EnergyCombatData combat, EnergyLightningData lightning, EnergyLifespanData lifespan) { @@ -318,10 +320,13 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA protected void applyDamage(EntityLivingBase target, float dmg, float kb) { if (previewMode) return; // Skip damage in preview mode Entity owner = getOwner(); - EntityNPCInterface npc = (owner instanceof EntityNPCInterface) ? (EntityNPCInterface) owner : null; - if (npc != null) { - target.attackEntityFrom(new NpcDamageSource("npc_ability", npc), dmg); + if (owner instanceof EntityNPCInterface) { + target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), dmg); + } else if (owner instanceof EntityPlayer) { + target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), dmg); + } else if (owner instanceof EntityLivingBase) { + target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), dmg); } else { target.attackEntityFrom(new NpcDamageSource("npc_ability", null), dmg); } @@ -400,7 +405,7 @@ public abstract class EntityAbilityProjectile extends Entity implements IEntityA * Set the preview owner for GUI preview mode. * This allows anchor point calculations without world entity lookup. */ - public void setPreviewOwner(EntityNPCInterface owner) { + public void setPreviewOwner(EntityLivingBase owner) { this.previewOwner = owner; } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java index ffb5f558a..8540d4df9 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilitySweeper.java @@ -7,8 +7,10 @@ import io.netty.buffer.ByteBuf; import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; +import net.minecraft.util.DamageSource; import net.minecraft.world.World; import noppes.npcs.LogWriter; import noppes.npcs.NpcDamageSource; @@ -70,7 +72,7 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa this.ignoreFrustumCheck = true; } - public EntityAbilitySweeper(World world, EntityNPCInterface owner, EntityLivingBase target, + public EntityAbilitySweeper(World world, EntityLivingBase owner, EntityLivingBase target, float beamLength, float beamWidth, float beamHeight, EnergyColorData colorData, float sweepSpeed, int numberOfRotations, @@ -264,10 +266,12 @@ public class EntityAbilitySweeper extends Entity implements IEntityAdditionalSpa } private void applyDamage(EntityLivingBase target, Entity owner) { - EntityNPCInterface npc = (owner instanceof EntityNPCInterface) ? (EntityNPCInterface) owner : null; - - if (npc != null) { - target.attackEntityFrom(new NpcDamageSource("npc_ability", npc), damage); + if (owner instanceof EntityNPCInterface) { + target.attackEntityFrom(new NpcDamageSource("npc_ability", (EntityNPCInterface) owner), damage); + } else if (owner instanceof EntityPlayer) { + target.attackEntityFrom(DamageSource.causePlayerDamage((EntityPlayer) owner), damage); + } else if (owner instanceof EntityLivingBase) { + target.attackEntityFrom(DamageSource.causeMobDamage((EntityLivingBase) owner), damage); } else { target.attackEntityFrom(new NpcDamageSource("npc_ability", null), damage); } diff --git a/src/main/java/kamkeel/npcs/network/PacketHandler.java b/src/main/java/kamkeel/npcs/network/PacketHandler.java index 68c021559..63ab9b192 100644 --- a/src/main/java/kamkeel/npcs/network/PacketHandler.java +++ b/src/main/java/kamkeel/npcs/network/PacketHandler.java @@ -26,6 +26,7 @@ import kamkeel.npcs.network.packets.data.SoundManagementPacket; import kamkeel.npcs.network.packets.data.SwingPlayerArmPacket; import kamkeel.npcs.network.packets.data.UpdateAnimationsPacket; import kamkeel.npcs.network.packets.data.VillagerListPacket; +import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; import kamkeel.npcs.network.packets.data.gui.GuiClosePacket; @@ -92,10 +93,10 @@ import kamkeel.npcs.network.packets.request.TraderMarketSavePacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcSavePacket; -import kamkeel.npcs.network.packets.request.ability.SavedAbilitiesGetPacket; -import kamkeel.npcs.network.packets.request.ability.SavedAbilityGetPacket; -import kamkeel.npcs.network.packets.request.ability.SavedAbilityRemovePacket; -import kamkeel.npcs.network.packets.request.ability.SavedAbilitySavePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket; import kamkeel.npcs.network.packets.request.animation.AnimationGetPacket; import kamkeel.npcs.network.packets.request.animation.AnimationRemovePacket; import kamkeel.npcs.network.packets.request.animation.AnimationSavePacket; @@ -505,10 +506,10 @@ public class PacketHandler { REQUEST_PACKET.registerPacket(new AbilitiesGetAllPacket()); REQUEST_PACKET.registerPacket(new AbilitiesNpcGetPacket()); REQUEST_PACKET.registerPacket(new AbilitiesNpcSavePacket()); - REQUEST_PACKET.registerPacket(new SavedAbilitiesGetPacket()); - REQUEST_PACKET.registerPacket(new SavedAbilityGetPacket()); - REQUEST_PACKET.registerPacket(new SavedAbilityRemovePacket()); - REQUEST_PACKET.registerPacket(new SavedAbilitySavePacket()); + REQUEST_PACKET.registerPacket(new CustomAbilitiesGetPacket()); + REQUEST_PACKET.registerPacket(new CustomAbilityGetPacket()); + REQUEST_PACKET.registerPacket(new CustomAbilityRemovePacket()); + REQUEST_PACKET.registerPacket(new CustomAbilitySavePacket()); // Other Packets REQUEST_PACKET.registerPacket(new IsGuiOpenInform()); @@ -578,6 +579,7 @@ public class PacketHandler { // Data | Ability Packets DATA_PACKET.registerPacket(new TelegraphSpawnPacket()); DATA_PACKET.registerPacket(new TelegraphRemovePacket()); + DATA_PACKET.registerPacket(new PlayerAbilitySyncPacket()); } public void registerPlayerPackets() { diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java index a5c6c8ba9..18321d0a4 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumDataPacket.java @@ -62,6 +62,7 @@ public enum EnumDataPacket { // Ability System TELEGRAPH_SPAWN, TELEGRAPH_REMOVE, + PLAYER_ABILITY_SYNC, // Auction System AUCTION_DATA, diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java index d4968bc5c..8183941d0 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumRequestPacket.java @@ -198,10 +198,10 @@ public enum EnumRequestPacket { NpcAbilitiesGet, NpcAbilitiesSave, AbilitiesGetAll, - SavedAbilitiesGet, - SavedAbilityGet, - SavedAbilityRemove, - SavedAbilitySave, + CustomAbilitiesGet, + CustomAbilityGet, + CustomAbilityRemove, + CustomAbilitySave, Hammer, RecipeScript, diff --git a/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java b/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java index a84e62392..1ac0ec828 100644 --- a/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java +++ b/src/main/java/kamkeel/npcs/network/enums/EnumSyncType.java @@ -12,5 +12,6 @@ public enum EnumSyncType { WORKBENCH_RECIPES, CARPENTRY_RECIPES, ANVIL_RECIPES, - CUSTOM_EFFECTS + CUSTOM_EFFECTS, + CUSTOM_ABILITY } diff --git a/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java b/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java new file mode 100644 index 000000000..08df836aa --- /dev/null +++ b/src/main/java/kamkeel/npcs/network/packets/data/ability/PlayerAbilitySyncPacket.java @@ -0,0 +1,73 @@ +package kamkeel.npcs.network.packets.data.ability; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import io.netty.buffer.ByteBuf; +import kamkeel.npcs.network.AbstractPacket; +import kamkeel.npcs.network.PacketChannel; +import kamkeel.npcs.network.PacketHandler; +import kamkeel.npcs.network.enums.EnumDataPacket; +import kamkeel.npcs.util.ByteBufUtils; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import noppes.npcs.controllers.PlayerDataController; +import noppes.npcs.controllers.data.PlayerData; + +import java.io.IOException; + +/** + * Syncs the player's ability data (unlocked abilities, selected index) to the client. + * Sent on login and whenever abilities change (unlock/lock/select). + */ +public final class PlayerAbilitySyncPacket extends AbstractPacket { + public static final String packetName = "Data|PlayerAbilitySync"; + + private NBTTagCompound abilityNBT; + + public PlayerAbilitySyncPacket() { + } + + public PlayerAbilitySyncPacket(NBTTagCompound abilityNBT) { + this.abilityNBT = abilityNBT; + } + + @Override + public Enum getType() { + return EnumDataPacket.PLAYER_ABILITY_SYNC; + } + + @Override + public PacketChannel getChannel() { + return PacketHandler.DATA_PACKET; + } + + @Override + public void sendData(ByteBuf out) throws IOException { + ByteBufUtils.writeNBT(out, abilityNBT); + } + + @SideOnly(Side.CLIENT) + @Override + public void receiveData(ByteBuf in, EntityPlayer player) throws IOException { + NBTTagCompound nbt = ByteBufUtils.readNBT(in); + if (nbt == null) return; + + PlayerData data = PlayerData.get(player); + if (data != null) { + data.abilityData.readFromNBT(nbt); + } + } + + /** + * Send the player's ability data to their client. + */ + public static void sendToPlayer(EntityPlayerMP player) { + PlayerData data = PlayerDataController.Instance.getPlayerData(player); + if (data == null) return; + + NBTTagCompound nbt = new NBTTagCompound(); + data.abilityData.writeToNBT(nbt); + PacketHandler.Instance.sendToPlayer(new PlayerAbilitySyncPacket(nbt), player); + } +} diff --git a/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java b/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java index 3b9f56690..48ac3391b 100644 --- a/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/player/SpecialKeyStatePacket.java @@ -54,7 +54,13 @@ public final class SpecialKeyStatePacket extends AbstractPacket { PlayerData data = PlayerDataController.Instance.getPlayerData((EntityPlayerMP) player); if (data != null) { + boolean wasDown = data.isSpecialKeyDown(); data.setSpecialKeyDown(keyDown); + + // Trigger ability activation on key press (rising edge) + if (keyDown && !wasDown) { + data.abilityData.activateAbility(player); + } } } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitiesGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitiesGetPacket.java similarity index 75% rename from src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitiesGetPacket.java rename to src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitiesGetPacket.java index 0e8b547a3..8febd9fc0 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitiesGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitiesGetPacket.java @@ -16,17 +16,17 @@ import noppes.npcs.NoppesUtilServer; import java.io.IOException; /** - * Request packet to get list of all saved ability preset names. + * Request packet to get list of all custom ability preset UUIDs/names. */ -public final class SavedAbilitiesGetPacket extends AbstractPacket { - public static String packetName = "Request|SavedAbilitiesGet"; +public final class CustomAbilitiesGetPacket extends AbstractPacket { + public static String packetName = "Request|CustomAbilitiesGet"; - public SavedAbilitiesGetPacket() { + public CustomAbilitiesGetPacket() { } @Override public Enum getType() { - return EnumRequestPacket.SavedAbilitiesGet; + return EnumRequestPacket.CustomAbilitiesGet; } @Override @@ -46,6 +46,6 @@ public final class SavedAbilitiesGetPacket extends AbstractPacket { if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) return; - NoppesUtilServer.sendSavedAbilitiesData((EntityPlayerMP) player); + NoppesUtilServer.sendCustomAbilitiesData((EntityPlayerMP) player); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityGetPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java similarity index 70% rename from src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityGetPacket.java rename to src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java index 7bb2f1826..d95051a66 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityGetPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityGetPacket.java @@ -20,23 +20,23 @@ import net.minecraft.nbt.NBTTagCompound; import java.io.IOException; /** - * Request packet to get a specific saved ability by name. + * Request packet to get a specific custom ability by UUID. */ -public final class SavedAbilityGetPacket extends AbstractPacket { - public static String packetName = "Request|SavedAbilityGet"; +public final class CustomAbilityGetPacket extends AbstractPacket { + public static String packetName = "Request|CustomAbilityGet"; - private String abilityName; + private String uuid; - public SavedAbilityGetPacket() { + public CustomAbilityGetPacket() { } - public SavedAbilityGetPacket(String abilityName) { - this.abilityName = abilityName; + public CustomAbilityGetPacket(String uuid) { + this.uuid = uuid; } @Override public Enum getType() { - return EnumRequestPacket.SavedAbilityGet; + return EnumRequestPacket.CustomAbilityGet; } @Override @@ -47,7 +47,7 @@ public final class SavedAbilityGetPacket extends AbstractPacket { @SideOnly(Side.CLIENT) @Override public void sendData(ByteBuf out) throws IOException { - ByteBufUtils.writeString(out, abilityName); + ByteBufUtils.writeString(out, uuid); } @Override @@ -58,10 +58,12 @@ public final class SavedAbilityGetPacket extends AbstractPacket { if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) return; - String name = ByteBufUtils.readString(in); - Ability ability = AbilityController.Instance.getSavedAbility(name); + String id = ByteBufUtils.readString(in); + Ability ability = AbilityController.Instance.getCustomAbility(id); if (ability != null) { NBTTagCompound compound = ability.writeNBT(); + // Include the UUID so the client can track it + compound.setString("_uuid", id); GuiDataPacket.sendGuiData((EntityPlayerMP) player, compound); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityRemovePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java similarity index 69% rename from src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityRemovePacket.java rename to src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java index 3fe4f5a0c..3ef040700 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilityRemovePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilityRemovePacket.java @@ -19,23 +19,23 @@ import noppes.npcs.NoppesUtilServer; import java.io.IOException; /** - * Request packet to remove a saved ability by name. + * Request packet to remove a custom ability by UUID. */ -public final class SavedAbilityRemovePacket extends AbstractPacket { - public static String packetName = "Request|SavedAbilityRemove"; +public final class CustomAbilityRemovePacket extends AbstractPacket { + public static String packetName = "Request|CustomAbilityRemove"; - private String abilityName; + private String uuid; - public SavedAbilityRemovePacket() { + public CustomAbilityRemovePacket() { } - public SavedAbilityRemovePacket(String abilityName) { - this.abilityName = abilityName; + public CustomAbilityRemovePacket(String uuid) { + this.uuid = uuid; } @Override public Enum getType() { - return EnumRequestPacket.SavedAbilityRemove; + return EnumRequestPacket.CustomAbilityRemove; } @Override @@ -51,7 +51,7 @@ public final class SavedAbilityRemovePacket extends AbstractPacket { @SideOnly(Side.CLIENT) @Override public void sendData(ByteBuf out) throws IOException { - ByteBufUtils.writeString(out, abilityName); + ByteBufUtils.writeString(out, uuid); } @Override @@ -62,8 +62,8 @@ public final class SavedAbilityRemovePacket extends AbstractPacket { if (!PacketUtil.verifyItemPacket(packetName, EnumItemPacketType.WAND, player)) return; - String name = ByteBufUtils.readString(in); - AbilityController.Instance.deleteAbility(name); - NoppesUtilServer.sendSavedAbilitiesData((EntityPlayerMP) player); + String id = ByteBufUtils.readString(in); + AbilityController.Instance.deleteCustomAbility(id); + NoppesUtilServer.sendCustomAbilitiesData((EntityPlayerMP) player); } } diff --git a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitySavePacket.java b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java similarity index 73% rename from src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitySavePacket.java rename to src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java index 2cf2fa489..d5ca1c556 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/ability/SavedAbilitySavePacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/ability/CustomAbilitySavePacket.java @@ -22,23 +22,23 @@ import noppes.npcs.NoppesUtilServer; import java.io.IOException; /** - * Request packet to save an ability preset. + * Request packet to save a custom ability preset. */ -public final class SavedAbilitySavePacket extends AbstractPacket { - public static String packetName = "Request|SavedAbilitySave"; +public final class CustomAbilitySavePacket extends AbstractPacket { + public static String packetName = "Request|CustomAbilitySave"; private NBTTagCompound abilityNBT; - public SavedAbilitySavePacket() { + public CustomAbilitySavePacket() { } - public SavedAbilitySavePacket(NBTTagCompound abilityNBT) { + public CustomAbilitySavePacket(NBTTagCompound abilityNBT) { this.abilityNBT = abilityNBT; } @Override public Enum getType() { - return EnumRequestPacket.SavedAbilitySave; + return EnumRequestPacket.CustomAbilitySave; } @Override @@ -68,9 +68,11 @@ public final class SavedAbilitySavePacket extends AbstractPacket { NBTTagCompound compound = ByteBufUtils.readNBT(in); Ability ability = AbilityController.Instance.fromNBT(compound); if (ability != null) { - AbilityController.Instance.saveAbility(ability); - NoppesUtilServer.sendSavedAbilitiesData((EntityPlayerMP) player); - GuiDataPacket.sendGuiData((EntityPlayerMP) player, ability.writeNBT()); + AbilityController.Instance.saveCustomAbility(ability); + NoppesUtilServer.sendCustomAbilitiesData((EntityPlayerMP) player); + NBTTagCompound responseNbt = ability.writeNBT(); + responseNbt.setString("_uuid", ability.getId()); + GuiDataPacket.sendGuiData((EntityPlayerMP) player, responseNbt); } } } diff --git a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java index d9d584b64..e095dc61b 100644 --- a/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java +++ b/src/main/java/kamkeel/npcs/util/AnchorPointHelper.java @@ -75,9 +75,10 @@ public class AnchorPointHelper { switch (anchor) { case FRONT: - x -= Math.sin(headYaw) * frontOffset * scale; - y += height * FRONT_HEIGHT; - z += Math.cos(headYaw) * frontOffset * scale; + Vec3 look = entity.getLookVec(); + x += look.xCoord * frontOffset * scale; + y += height * FRONT_HEIGHT + look.yCoord * frontOffset * scale; + z += look.zCoord * frontOffset * scale; break; case CENTER: diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index 0005341f8..ff4617545 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -340,7 +340,6 @@ public class CustomNpcs { new SpawnController(); new LinkedNpcController(); new AnimationController(); - new AbilityController(); AbilityController.Instance.load(); TelegraphController.init(); diff --git a/src/main/java/noppes/npcs/DataAbilities.java b/src/main/java/noppes/npcs/DataAbilities.java index 526a3bdd7..8c44c482e 100644 --- a/src/main/java/noppes/npcs/DataAbilities.java +++ b/src/main/java/noppes/npcs/DataAbilities.java @@ -3,6 +3,7 @@ package noppes.npcs; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.AbilitySlot; import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; @@ -35,9 +36,9 @@ public class DataAbilities { // ═══════════════════════════════════════════════════════════════════ /** - * List of abilities this NPC can use + * List of ability slots this NPC can use (inline or reference). */ - private List abilities = new ArrayList<>(); + private List abilitySlots = new ArrayList<>(); /** * Whether the ability system is enabled for this NPC @@ -294,7 +295,7 @@ public class DataAbilities { * Check if an ability can be selected right now. */ public boolean canSelectAbility() { - if (!enabled || abilities.isEmpty()) { + if (!enabled || abilitySlots.isEmpty()) { return false; } if (currentAbility != null && currentAbility.isExecuting()) { @@ -314,7 +315,7 @@ public class DataAbilities { List eligible = new ArrayList<>(); int totalWeight = 0; - for (Ability ability : abilities) { + for (Ability ability : getAbilities()) { if (isAbilityEligible(ability, target)) { eligible.add(ability); totalWeight += ability.getWeight(); @@ -636,24 +637,24 @@ public class DataAbilities { } /** - * Execute a preset ability on this NPC. + * Execute an ability on this NPC by key (built-in name or custom UUID). * The NPC does NOT need to have this ability assigned. * - * @param presetName The name of the saved ability preset + * @param key The ability key (built-in name or custom UUID) * @param target The target entity (can be null for self-targeted abilities) * @return true if the ability was started successfully */ - public boolean executePresetAbility(String presetName, EntityLivingBase target) { - if (presetName == null || presetName.isEmpty() || npc.worldObj.isRemote) { + public boolean executeAbility(String key, EntityLivingBase target) { + if (key == null || key.isEmpty() || npc.worldObj.isRemote) { return false; } - Ability preset = AbilityController.Instance.getSavedAbility(presetName); - if (preset == null) { + Ability resolved = AbilityController.Instance.resolveAbility(key); + if (resolved == null) { return false; } - return forceStartAbility(preset, target); + return forceStartAbility(resolved, target); } // ═══════════════════════════════════════════════════════════════════ @@ -715,8 +716,8 @@ public class DataAbilities { // Roll cooldown so NPC doesn't immediately attack after reset rollCooldown(); - // Reset execution state on all abilities - for (Ability ability : abilities) { + // Reset execution state on all resolved abilities + for (Ability ability : getAbilities()) { ability.reset(); } } @@ -737,39 +738,79 @@ public class DataAbilities { // ABILITY LIST MANAGEMENT // ═══════════════════════════════════════════════════════════════════ + /** + * Get resolved abilities from all slots, filtering out broken references. + */ public List getAbilities() { - return abilities; + List resolved = new ArrayList<>(); + for (AbilitySlot slot : abilitySlots) { + Ability a = slot.getAbility(); + if (a != null) { + resolved.add(a); + } + } + return resolved; } + /** Get the raw slot list (for GUI access). */ + public List getAbilitySlots() { + return abilitySlots; + } + + /** Add an inline ability. */ public void addAbility(Ability ability) { - abilities.add(ability); + abilitySlots.add(AbilitySlot.inline(ability)); + } + + /** Add a reference ability by key (built-in name or custom UUID). */ + public void addAbilityReference(String key) { + abilitySlots.add(AbilitySlot.reference(key)); } public void removeAbility(int index) { - if (index >= 0 && index < abilities.size()) { - abilities.remove(index); + if (index >= 0 && index < abilitySlots.size()) { + abilitySlots.remove(index); } } public void removeAbility(String id) { - abilities.removeIf(a -> a.getId().equals(id)); + abilitySlots.removeIf(slot -> { + if (slot.isReference()) { + return slot.getReferenceId().equals(id); + } + Ability a = slot.getAbility(); + return a != null && a.getId().equals(id); + }); } public Ability getAbility(String id) { - for (Ability ability : abilities) { - if (ability.getId().equals(id)) { - return ability; + for (AbilitySlot slot : abilitySlots) { + Ability a = slot.getAbility(); + if (a != null && a.getId().equals(id)) { + return a; } } return null; } + /** Check if a slot at a given index is a reference. */ + public boolean isSlotReference(int index) { + if (index < 0 || index >= abilitySlots.size()) return false; + return abilitySlots.get(index).isReference(); + } + + /** Convert a reference slot to inline. Returns false if resolution fails. */ + public boolean convertToInline(int index) { + if (index < 0 || index >= abilitySlots.size()) return false; + return abilitySlots.get(index).convertToInline(); + } + public void clearAbilities() { - abilities.clear(); + abilitySlots.clear(); } public boolean isEmpty() { - return abilities.isEmpty(); + return abilitySlots.isEmpty(); } // ═══════════════════════════════════════════════════════════════════ @@ -920,8 +961,8 @@ public class DataAbilities { compound.setInteger("AbilityMaxCooldown", maxCooldown); NBTTagList abilityList = new NBTTagList(); - for (Ability ability : abilities) { - abilityList.appendTag(ability.writeNBT()); + for (AbilitySlot slot : abilitySlots) { + abilityList.appendTag(slot.writeNBT()); } compound.setTag("Abilities", abilityList); @@ -930,28 +971,16 @@ public class DataAbilities { public void readFromNBT(NBTTagCompound compound) { enabled = compound.getBoolean("AbilitiesEnabled"); + minCooldown = compound.getInteger("AbilityMinCooldown"); + maxCooldown = compound.getInteger("AbilityMaxCooldown"); - // Support old globalCooldown for backwards compatibility - if (compound.hasKey("AbilityMinCooldown")) { - minCooldown = compound.getInteger("AbilityMinCooldown"); - maxCooldown = compound.getInteger("AbilityMaxCooldown"); - } else if (compound.hasKey("AbilityGlobalCooldown")) { - // Migrate old single cooldown to min/max range - int oldCooldown = compound.getInteger("AbilityGlobalCooldown"); - minCooldown = oldCooldown; - maxCooldown = oldCooldown * 2; // Give some range - } else { - minCooldown = 20; - maxCooldown = 60; - } - - abilities.clear(); + abilitySlots.clear(); NBTTagList abilityList = compound.getTagList("Abilities", 10); for (int i = 0; i < abilityList.tagCount(); i++) { NBTTagCompound abilityNBT = abilityList.getCompoundTagAt(i); - Ability ability = AbilityController.Instance.fromNBT(abilityNBT); - if (ability != null) { - abilities.add(ability); + AbilitySlot slot = AbilitySlot.fromNBT(abilityNBT); + if (slot != null) { + abilitySlots.add(slot); } } } diff --git a/src/main/java/noppes/npcs/EventHooks.java b/src/main/java/noppes/npcs/EventHooks.java index 5e086abce..5f9d1388f 100644 --- a/src/main/java/noppes/npcs/EventHooks.java +++ b/src/main/java/noppes/npcs/EventHooks.java @@ -62,6 +62,7 @@ import noppes.npcs.scripted.event.RecipeScriptEvent; import noppes.npcs.scripted.event.player.CustomGuiEvent; import noppes.npcs.scripted.event.player.DialogEvent; import noppes.npcs.scripted.event.player.FactionEvent; +import noppes.npcs.scripted.event.player.PlayerAbilityEvent; import noppes.npcs.scripted.event.player.PlayerEvent; import noppes.npcs.scripted.event.player.PlayerEvent.ChatEvent; import noppes.npcs.scripted.event.player.PlayerEvent.ContainerOpen; @@ -692,6 +693,11 @@ public class EventHooks { return NpcAPI.EVENT_BUS.post(event); } + public static boolean onPlayerAbilityHit(PlayerDataScript handler, PlayerAbilityEvent.HitEvent event) { + handler.callScript(EnumScriptType.ABILITY_HIT, event); + return NpcAPI.EVENT_BUS.post(event); + } + public static void onPlayerChangeDim(PlayerDataScript handler, IPlayer player, int fromDim, int toDim) { PlayerEvent.ChangedDimension event = new PlayerEvent.ChangedDimension(player, fromDim, toDim); handler.callScript(EnumScriptType.CHANGED_DIM, event); diff --git a/src/main/java/noppes/npcs/NoppesUtilServer.java b/src/main/java/noppes/npcs/NoppesUtilServer.java index ae97f70b4..0799e7a6c 100644 --- a/src/main/java/noppes/npcs/NoppesUtilServer.java +++ b/src/main/java/noppes/npcs/NoppesUtilServer.java @@ -785,16 +785,18 @@ public class NoppesUtilServer { sendScrollData(player, map, EnumScrollData.ABILITY_TYPES); } - public static void sendSavedAbilitiesData(EntityPlayerMP player) { + public static void sendCustomAbilitiesData(EntityPlayerMP player) { HashMap map = new HashMap(); kamkeel.npcs.controllers.data.ability.AbilityController controller = kamkeel.npcs.controllers.data.ability.AbilityController.Instance; if (controller != null) { int index = 0; - for (String name : controller.getSavedAbilityNames()) { - map.put(name, index++); + for (String uuid : controller.getCustomAbilityIds()) { + String name = controller.getCustomAbilityName(uuid); + if (name == null || name.isEmpty()) name = uuid; + map.put(name + "\t" + uuid, index++); } } - sendScrollData(player, map, EnumScrollData.ABILITIES); + sendScrollData(player, map, EnumScrollData.CUSTOM_ABILITIES); } public static DialogOption setNpcDialog(int slot, int dialogId, EntityPlayer player) throws IOException { diff --git a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java index a37e6ba08..0541f64c4 100644 --- a/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java +++ b/src/main/java/noppes/npcs/ScriptPlayerEventHandler.java @@ -97,6 +97,13 @@ public class ScriptPlayerEventHandler { if (PlayerDataController.Instance != null) { PlayerData playerData = PlayerData.get(player); playerData.actionManager.tick(); + playerData.abilityData.tick(player); + + // Lock player movement during ability phases that require it + if (playerData.abilityData.isMovementLocked()) { + player.motionX = 0; + player.motionZ = 0; + } if (playerData.updateClient) { SyncController.syncPlayerData((EntityPlayerMP) player, true); diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java index 72481f133..807a3e719 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcDialogVisual.java @@ -98,7 +98,7 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis getTextField(18).setVisible(dialog.renderGradual); getLabel(18).enabled = dialog.renderGradual; - addTextField(new GuiNpcTextField(19, this, guiLeft + 125, y += 22, 40, 20, String.valueOf(dialog.textPitch))); + addTextField(new GuiNpcTextField(19, this, guiLeft + 125, y += 22, 40, 20, "" + dialog.textPitch)); addLabel(new GuiNpcLabel(19, "dialog.textPitch", guiLeft + 4, y + 5)); getTextField(19).floatsOnly = true; getTextField(19).setVisible(dialog.renderGradual); @@ -110,48 +110,48 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis addButton(new GuiNpcButton(24, guiLeft + 192, y, 50, 20, new String[]{"display.bottom", "display.top"}, dialog.alignment)); addLabel(new GuiNpcLabel(27, "display.alignment", guiLeft + 126, y + 5)); - addTextField(new GuiNpcTextField(11, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.textWidth))); - addTextField(new GuiNpcTextField(12, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.textHeight))); + addTextField(new GuiNpcTextField(11, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.textWidth)); + addTextField(new GuiNpcTextField(12, this, guiLeft + 165, y, 40, 20, "" + dialog.textHeight)); addLabel(new GuiNpcLabel(12, "gui.widthHeight", guiLeft + 4, y + 5)); getTextField(11).integersOnly = true; getTextField(11).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(12).integersOnly = true; getTextField(12).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(13, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.textOffsetX))); - addTextField(new GuiNpcTextField(14, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.textOffsetY))); + addTextField(new GuiNpcTextField(13, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.textOffsetX)); + addTextField(new GuiNpcTextField(14, this, guiLeft + 165, y, 40, 20, "" + dialog.textOffsetY)); addLabel(new GuiNpcLabel(15, "dialog.textOffset", guiLeft + 4, y + 5)); getTextField(13).integersOnly = true; getTextField(13).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(14).integersOnly = true; getTextField(14).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.titleOffsetX))); - addTextField(new GuiNpcTextField(16, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.titleOffsetY))); + addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.titleOffsetX)); + addTextField(new GuiNpcTextField(16, this, guiLeft + 165, y, 40, 20, "" + dialog.titleOffsetY)); addLabel(new GuiNpcLabel(18, "dialog.titleOffset", guiLeft + 4, y + 5)); getTextField(15).integersOnly = true; getTextField(15).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(16).integersOnly = true; getTextField(16).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(17, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.optionOffsetX))); - addTextField(new GuiNpcTextField(18, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.optionOffsetY))); + addTextField(new GuiNpcTextField(17, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.optionOffsetX)); + addTextField(new GuiNpcTextField(18, this, guiLeft + 165, y, 40, 20, "" + dialog.optionOffsetY)); addLabel(new GuiNpcLabel(21, "dialog.optionOffset", guiLeft + 4, y + 5)); getTextField(17).integersOnly = true; getTextField(17).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(18).integersOnly = true; getTextField(18).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(19, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.optionSpaceX))); - addTextField(new GuiNpcTextField(20, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.optionSpaceY))); + addTextField(new GuiNpcTextField(19, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.optionSpaceX)); + addTextField(new GuiNpcTextField(20, this, guiLeft + 165, y, 40, 20, "" + dialog.optionSpaceY)); addLabel(new GuiNpcLabel(24, "dialog.optionSpacing", guiLeft + 4, y + 5)); getTextField(19).integersOnly = true; getTextField(19).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(20).integersOnly = true; getTextField(20).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(21, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.npcOffsetX))); - addTextField(new GuiNpcTextField(22, this, guiLeft + 165, y, 40, 20, String.valueOf(dialog.npcOffsetY))); + addTextField(new GuiNpcTextField(21, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.npcOffsetX)); + addTextField(new GuiNpcTextField(22, this, guiLeft + 165, y, 40, 20, "" + dialog.npcOffsetY)); addLabel(new GuiNpcLabel(25, "dialog.npcOffset", guiLeft + 4, y + 5)); getTextField(21).integersOnly = true; getTextField(21).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); @@ -161,7 +161,7 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis getTextField(22).setVisible(!dialog.hideNPC); getLabel(25).enabled = !dialog.hideNPC; - addTextField(new GuiNpcTextField(23, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialog.npcScale))); + addTextField(new GuiNpcTextField(23, this, guiLeft + 120, y += 25, 40, 20, "" + dialog.npcScale)); addLabel(new GuiNpcLabel(26, "dialog.npcScale", guiLeft + 4, y + 5)); getTextField(23).floatsOnly = true; getTextField(23).setMinMaxDefaultFloat(-Float.MAX_VALUE, Float.MAX_VALUE, 0); @@ -182,30 +182,30 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis DialogImage dialogImage = getSelectedImage(); addButton(new GuiNpcButton(2, guiLeft + xSize + 24, guiTop + 5, 20, 20, "-")); - addTextField(new GuiNpcTextField(10, this, guiLeft + 17, y += 25, 30, 20, String.valueOf(dialogImage.id))); + addTextField(new GuiNpcTextField(10, this, guiLeft + 17, y += 25, 30, 20, "" + dialogImage.id)); addTextField(new GuiNpcTextField(11, this, guiLeft + 92, y, 150, 20, dialogImage.texture)); addLabel(new GuiNpcLabel(10, "gui.id", guiLeft + 4, y + 5)); addLabel(new GuiNpcLabel(11, "display.texture", guiLeft + 50, y + 5)); getTextField(10).integersOnly = true; - addTextField(new GuiNpcTextField(12, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialogImage.x))); - addTextField(new GuiNpcTextField(13, this, guiLeft + 165, y, 40, 20, String.valueOf(dialogImage.y))); + addTextField(new GuiNpcTextField(12, this, guiLeft + 120, y += 25, 40, 20, "" + dialogImage.x)); + addTextField(new GuiNpcTextField(13, this, guiLeft + 165, y, 40, 20, "" + dialogImage.y)); addLabel(new GuiNpcLabel(12, "gui.position", guiLeft + 4, y + 5)); getTextField(12).integersOnly = true; getTextField(12).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(13).integersOnly = true; getTextField(13).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialogImage.width))); - addTextField(new GuiNpcTextField(15, this, guiLeft + 165, y, 40, 20, String.valueOf(dialogImage.height))); + addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 25, 40, 20, "" + dialogImage.width)); + addTextField(new GuiNpcTextField(15, this, guiLeft + 165, y, 40, 20, "" + dialogImage.height)); addLabel(new GuiNpcLabel(13, "gui.widthHeight", guiLeft + 4, y + 5)); getTextField(14).integersOnly = true; getTextField(14).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); getTextField(15).integersOnly = true; getTextField(15).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); - addTextField(new GuiNpcTextField(16, this, guiLeft + 120, y += 25, 40, 20, String.valueOf(dialogImage.textureX))); - addTextField(new GuiNpcTextField(17, this, guiLeft + 165, y, 40, 20, String.valueOf(dialogImage.textureY))); + addTextField(new GuiNpcTextField(16, this, guiLeft + 120, y += 25, 40, 20, "" + dialogImage.textureX)); + addTextField(new GuiNpcTextField(17, this, guiLeft + 165, y, 40, 20, "" + dialogImage.textureY)); addLabel(new GuiNpcLabel(14, "dialog.textureOffset", guiLeft + 4, y + 5)); getTextField(16).integersOnly = true; getTextField(16).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); @@ -229,9 +229,9 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis getButton(19).setVisible(dialogImage.imageType == 2); getLabel(16).enabled = dialogImage.imageType == 2; - addTextField(new GuiNpcTextField(20, this, guiLeft + 33, y += 25, 30, 20, String.valueOf(dialogImage.scale))); - addTextField(new GuiNpcTextField(21, this, guiLeft + 102, y, 30, 20, String.valueOf(dialogImage.alpha))); - addTextField(new GuiNpcTextField(22, this, guiLeft + 183, y, 45, 20, String.valueOf(dialogImage.rotation))); + addTextField(new GuiNpcTextField(20, this, guiLeft + 33, y += 25, 30, 20, "" + dialogImage.scale)); + addTextField(new GuiNpcTextField(21, this, guiLeft + 102, y, 30, 20, "" + dialogImage.alpha)); + addTextField(new GuiNpcTextField(22, this, guiLeft + 183, y, 45, 20, "" + dialogImage.rotation)); addLabel(new GuiNpcLabel(17, "model.scale", guiLeft + 4, y + 5)); addLabel(new GuiNpcLabel(18, "display.alpha", guiLeft + 72, y + 5)); addLabel(new GuiNpcLabel(19, "movement.rotation", guiLeft + 140, y + 5)); @@ -306,13 +306,13 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis Collections.sort(ids); ArrayList strings = new ArrayList<>(); for (int i : ids) { - strings.add(String.valueOf(i)); + strings.add("" + i); } imageScroll.setUnsortedList(strings); if (changedId != -1) { - selected = imageScroll.getList().indexOf(String.valueOf(changedId)); - imageScroll.setSelected(String.valueOf(changedId)); + selected = imageScroll.getList().indexOf("" + changedId); + imageScroll.setSelected("" + changedId); changedId = -1; } @@ -402,7 +402,7 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis dialog.dialogImages.put(addId, new DialogImage(addId)); updateScrollData(); - selected = imageScroll.getList().indexOf(String.valueOf(addId)); + selected = imageScroll.getList().indexOf("" + addId); } if (button.id == 2) { if (imageScroll.getSelected() != null) { @@ -598,8 +598,8 @@ public class SubGuiNpcDialogVisual extends SubGuiInterface implements ISubGuiLis } if (textfield.id == 10) { - if (imageScroll.getList().contains(String.valueOf(textfield.getInteger()))) { - textfield.setText(String.valueOf(dialogImage.id)); + if (imageScroll.getList().contains("" + textfield.getInteger())) { + textfield.setText("" + dialogImage.id); return; } diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiNpcPartyOptions.java b/src/main/java/noppes/npcs/client/gui/SubGuiNpcPartyOptions.java index 10420a65d..c5a0769a3 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiNpcPartyOptions.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiNpcPartyOptions.java @@ -71,7 +71,7 @@ public class SubGuiNpcPartyOptions extends SubGuiInterface implements ITextfield y += 23; //min party size - number field GuiNpcTextField minField = new GuiNpcTextField(21, this, guiLeft + 60, guiTop + 10 + y, - 30, 20, String.valueOf(options.minPartySize)); + 30, 20, "" + options.minPartySize); minField.integersOnly = true; minField.setMinMaxDefault(1, Integer.MAX_VALUE, 1); addTextField(minField); @@ -79,7 +79,7 @@ public class SubGuiNpcPartyOptions extends SubGuiInterface implements ITextfield //max party size - number field GuiNpcTextField maxField = new GuiNpcTextField(20, this, guiLeft + 160, guiTop + 10 + y, - 30, 20, String.valueOf(options.maxPartySize)); + 30, 20, "" + options.maxPartySize); maxField.integersOnly = true; maxField.setMinMaxDefault(1, Integer.MAX_VALUE, 4); addTextField(maxField); diff --git a/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java b/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java index c15f9162f..29c33352b 100644 --- a/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java +++ b/src/main/java/noppes/npcs/client/gui/SubGuiSpawningOptions.java @@ -36,9 +36,9 @@ public class SubGuiSpawningOptions extends SubGuiInterface implements ITextfield addButton(new GuiNpcButtonYesNo(13, guiLeft + 120, y += 22, data.airSpawning)); addLabel(new GuiNpcLabel(13, "spawning.airSpawning", guiLeft + 4, y + 5)); - addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 22, 40, 20, String.valueOf(data.spawnHeightMin))); + addTextField(new GuiNpcTextField(14, this, guiLeft + 120, y += 22, 40, 20, "" + data.spawnHeightMin)); addLabel(new GuiNpcLabel(14, "spawning.minHeight", guiLeft + 4, y + 5)); - addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 22, 40, 20, String.valueOf(data.spawnHeightMax))); + addTextField(new GuiNpcTextField(15, this, guiLeft + 120, y += 22, 40, 20, "" + data.spawnHeightMax)); addLabel(new GuiNpcLabel(15, "spawning.maxHeight", guiLeft + 4, y + 5)); getTextField(14).integersOnly = true; getTextField(14).setMinMaxDefault(-Integer.MAX_VALUE, Integer.MAX_VALUE, 0); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java index 750fe282e..59046ef77 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/GuiNPCAbilities.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.ability.AbilitySlot; import kamkeel.npcs.network.PacketClient; import kamkeel.npcs.network.packets.request.ability.AbilitiesGetAllPacket; import kamkeel.npcs.network.packets.request.ability.AbilitiesNpcGetPacket; @@ -46,8 +47,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Display name to typeId mapping for the available types scroll private final HashMap displayNameToTypeId = new HashMap<>(); - // NPC's current abilities - private final List npcAbilities = new ArrayList<>(); + // NPC's current ability slots + private final List npcSlots = new ArrayList<>(); // Settings private boolean abilitiesEnabled = false; @@ -75,13 +76,13 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Cooldown range: Min Cooldown and Max Cooldown (spaced apart more) addLabel(new GuiNpcLabel(101, "ability.minCooldown", guiLeft + 115, y + 5)); - GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 165, y, 40, 20, String.valueOf(minCooldown)); + GuiNpcTextField minField = new GuiNpcTextField(101, this, fontRendererObj, guiLeft + 165, y, 40, 20, "" + minCooldown); minField.setIntegersOnly(); minField.setMinMaxDefault(0, 10000, 20); addTextField(minField); addLabel(new GuiNpcLabel(102, "ability.maxCooldown", guiLeft + 260, y + 5)); - GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 310, y, 40, 20, String.valueOf(maxCooldown)); + GuiNpcTextField maxField = new GuiNpcTextField(102, this, fontRendererObj, guiLeft + 310, y, 40, 20, "" + maxCooldown); maxField.setIntegersOnly(); maxField.setMinMaxDefault(0, 10000, 60); addTextField(maxField); @@ -124,8 +125,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC addButton(new GuiNpcButton(75, centerX, y + 90, 40, 20, "gui.load")); // On/Off toggle for selected ability (under Load button) - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - Ability selectedAbility = npcAbilities.get(selectedAbilityIndex); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + Ability selectedAbility = npcSlots.get(selectedAbilityIndex).getAbility(); GuiNpcButton toggleBtn = new GuiNpcButton(77, centerX, y + 112, 40, 20, new String[]{"gui.off", "gui.on"}, selectedAbility.isEnabled() ? 1 : 0); addButton(toggleBtn); @@ -139,7 +140,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC addButton(new GuiNpcButton(73, guiLeft + 270, y + 145, 20, 20, "<")); addButton(new GuiNpcButton(74, guiLeft + 292, y + 145, 20, 20, ">")); getButton(73).setEnabled(selectedAbilityIndex > 0); - getButton(74).setEnabled(selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size() - 1); + getButton(74).setEnabled(selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size() - 1); // Save button (to the right of carrots, only enabled when ability is selected) addButton(new GuiNpcButton(76, guiLeft + 320, y + 145, 50, 20, "gui.save")); @@ -166,10 +167,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC Ability newAbility = AbilityController.Instance.create(typeId); if (newAbility != null) { newAbility.setId(UUID.randomUUID().toString()); - // Don't set custom name - let it default to type name - npcAbilities.add(newAbility); - // Auto-select the newly added ability - selectedAbilityIndex = npcAbilities.size() - 1; + npcSlots.add(AbilitySlot.inline(newAbility)); + selectedAbilityIndex = npcSlots.size() - 1; updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); initGui(); @@ -182,8 +181,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Remove ability if (id == 71) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - npcAbilities.remove(selectedAbilityIndex); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + npcSlots.remove(selectedAbilityIndex); selectedAbilityIndex = -1; updateNpcAbilitiesList(); initGui(); @@ -194,9 +193,17 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Edit ability if (id == 72) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - Ability ability = npcAbilities.get(selectedAbilityIndex); - setSubGui(ability.createConfigGui(this)); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + if (slot.isReference()) { + // Reference slot - ask clone-and-modify vs modify-parent + setSubGui(new SubGuiAbilityEditMode()); + } else { + Ability ability = slot.getAbility(); + if (ability != null) { + setSubGui(ability.createConfigGui(this)); + } + } } return; } @@ -204,9 +211,9 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Move up (carrot left = earlier in list) if (id == 73) { if (selectedAbilityIndex > 0) { - Ability temp = npcAbilities.get(selectedAbilityIndex); - npcAbilities.set(selectedAbilityIndex, npcAbilities.get(selectedAbilityIndex - 1)); - npcAbilities.set(selectedAbilityIndex - 1, temp); + AbilitySlot temp = npcSlots.get(selectedAbilityIndex); + npcSlots.set(selectedAbilityIndex, npcSlots.get(selectedAbilityIndex - 1)); + npcSlots.set(selectedAbilityIndex - 1, temp); selectedAbilityIndex--; updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); @@ -218,10 +225,10 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Move down (carrot right = later in list) if (id == 74) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size() - 1) { - Ability temp = npcAbilities.get(selectedAbilityIndex); - npcAbilities.set(selectedAbilityIndex, npcAbilities.get(selectedAbilityIndex + 1)); - npcAbilities.set(selectedAbilityIndex + 1, temp); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size() - 1) { + AbilitySlot temp = npcSlots.get(selectedAbilityIndex); + npcSlots.set(selectedAbilityIndex, npcSlots.get(selectedAbilityIndex + 1)); + npcSlots.set(selectedAbilityIndex + 1, temp); selectedAbilityIndex++; updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); @@ -237,15 +244,16 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC return; } - // Save selected ability + // Save selected ability as custom preset if (id == 76) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - Ability abilityToSave = npcAbilities.get(selectedAbilityIndex); - if (abilityToSave.getName() == null || abilityToSave.getName().isEmpty()) { - // Need a name to save - open edit dialog first - setSubGui(abilityToSave.createConfigGui(this)); - } else { - setSubGui(new SubGuiAbilitySaveConfirm(abilityToSave)); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + Ability abilityToSave = npcSlots.get(selectedAbilityIndex).getAbility(); + if (abilityToSave != null) { + if (abilityToSave.getName() == null || abilityToSave.getName().isEmpty()) { + setSubGui(abilityToSave.createConfigGui(this)); + } else { + setSubGui(new SubGuiAbilitySaveConfirm(abilityToSave)); + } } } return; @@ -253,13 +261,15 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC // Toggle selected ability on/off if (id == 77) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - Ability ability = npcAbilities.get(selectedAbilityIndex); - ability.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); - updateNpcAbilitiesList(); - selectAbilityByIndex(selectedAbilityIndex); - initGui(); - save(); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + Ability ability = npcSlots.get(selectedAbilityIndex).getAbility(); + if (ability != null) { + ability.setEnabled(((GuiNpcButton) guibutton).getValue() == 1); + updateNpcAbilitiesList(); + selectAbilityByIndex(selectedAbilityIndex); + initGui(); + save(); + } } return; } @@ -290,26 +300,20 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC abilitiesEnabled = compound.getBoolean("AbilitiesEnabled"); - // Support old globalCooldown for backwards compatibility - if (compound.hasKey("AbilityMinCooldown")) { - minCooldown = compound.getInteger("AbilityMinCooldown"); - maxCooldown = compound.getInteger("AbilityMaxCooldown"); - } else if (compound.hasKey("AbilityGlobalCooldown")) { - int oldCooldown = compound.getInteger("AbilityGlobalCooldown"); - minCooldown = oldCooldown; - maxCooldown = oldCooldown * 2; - } else { + minCooldown = compound.getInteger("AbilityMinCooldown"); + maxCooldown = compound.getInteger("AbilityMaxCooldown"); + if (minCooldown == 0 && maxCooldown == 0) { minCooldown = 20; maxCooldown = 60; } - npcAbilities.clear(); + npcSlots.clear(); NBTTagList abilityList = compound.getTagList("Abilities", 10); for (int i = 0; i < abilityList.tagCount(); i++) { NBTTagCompound abilityNBT = abilityList.getCompoundTagAt(i); - Ability ability = AbilityController.Instance.fromNBT(abilityNBT); - if (ability != null) { - npcAbilities.add(ability); + AbilitySlot slot = AbilitySlot.fromNBT(abilityNBT); + if (slot != null) { + npcSlots.add(slot); } } @@ -352,7 +356,7 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC private void updateNpcAbilitiesList() { List list = new ArrayList<>(); - for (int i = 0; i < npcAbilities.size(); i++) { + for (int i = 0; i < npcSlots.size(); i++) { list.add(getAbilityListEntry(i)); } if (npcAbilitiesScroll != null) { @@ -361,26 +365,41 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC } /** - * Creates the display entry for an ability in the list. - * Format: "[●] 1. Custom Name (Type)" or "[●] 1. Type" if no custom name - * Circle is green (§a) for enabled, red (§c) for disabled + * Creates the display entry for an ability slot in the list. + * Inline: "[●] 1. Custom Name (Type)" or "[●] 1. Type" + * Reference: "[●] 1. > Name (Type)" in yellow + * Broken: "[●] 1. > [Missing: id...]" in red */ private String getAbilityListEntry(int index) { - Ability ability = npcAbilities.get(index); + AbilitySlot slot = npcSlots.get(index); + Ability ability = slot.getAbility(); + + if (ability == null) { + // Broken reference + String refId = slot.getReferenceId(); + String shortId = refId != null && refId.length() > 8 ? refId.substring(0, 8) + "..." : refId; + return "\u00A7c\u25CF\u00A7r " + (index + 1) + ". \u00A7c> [Missing: " + shortId + "]\u00A7r"; + } + String typeName = getDisplayName(ability.getTypeId()); String customName = ability.getName(); - - // Color prefix: green circle for enabled, red for disabled String colorPrefix = ability.isEnabled() ? "\u00A7a\u25CF\u00A7r " : "\u00A7c\u25CF\u00A7r "; + String nameDisplay; if (customName != null && !customName.isEmpty() && !customName.equals(typeName)) { - return colorPrefix + (index + 1) + ". " + customName + " (" + typeName + ")"; + nameDisplay = customName + " (" + typeName + ")"; + } else { + nameDisplay = typeName; } - return colorPrefix + (index + 1) + ". " + typeName; + + if (slot.isReference()) { + return colorPrefix + (index + 1) + ". \u00A7e> " + nameDisplay + "\u00A7r"; + } + return colorPrefix + (index + 1) + ". " + nameDisplay; } private void selectAbilityByIndex(int index) { - if (index >= 0 && index < npcAbilities.size()) { + if (index >= 0 && index < npcSlots.size()) { String entry = getAbilityListEntry(index); if (npcAbilitiesScroll != null) { npcAbilitiesScroll.setSelected(entry); @@ -403,8 +422,8 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC compound.setInteger("AbilityMaxCooldown", maxCooldown); NBTTagList abilityList = new NBTTagList(); - for (Ability ability : npcAbilities) { - abilityList.appendTag(ability.writeNBT()); + for (AbilitySlot slot : npcSlots) { + abilityList.appendTag(slot.writeNBT()); } compound.setTag("Abilities", abilityList); @@ -412,15 +431,29 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC } /** - * Called from SubGuiAbilityLoad when a saved ability is loaded. - * Adds the ability to the NPC's ability list. + * Called from SubGuiAbilityLoad when a cloned ability is loaded. + * Adds as an inline slot. */ public void loadAbility(Ability loadedAbility) { if (loadedAbility != null) { loadedAbility.setId(UUID.randomUUID().toString()); - npcAbilities.add(loadedAbility); - // Auto-select the newly added ability - selectedAbilityIndex = npcAbilities.size() - 1; + npcSlots.add(AbilitySlot.inline(loadedAbility)); + selectedAbilityIndex = npcSlots.size() - 1; + updateNpcAbilitiesList(); + selectAbilityByIndex(selectedAbilityIndex); + initGui(); + save(); + } + } + + /** + * Called from SubGuiAbilityLoad when a reference is loaded. + * Adds as a reference slot. + */ + public void loadAbilityReference(String referenceId) { + if (referenceId != null && !referenceId.isEmpty()) { + npcSlots.add(AbilitySlot.reference(referenceId)); + selectedAbilityIndex = npcSlots.size() - 1; updateNpcAbilitiesList(); selectAbilityByIndex(selectedAbilityIndex); initGui(); @@ -460,9 +493,16 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @Override public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { - if (scroll == npcAbilitiesScroll && selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - Ability ability = npcAbilities.get(selectedAbilityIndex); - setSubGui(ability.createConfigGui(this)); + if (scroll == npcAbilitiesScroll && selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + if (slot.isReference()) { + setSubGui(new SubGuiAbilityEditMode()); + } else { + Ability ability = slot.getAbility(); + if (ability != null) { + setSubGui(ability.createConfigGui(this)); + } + } } } @@ -489,8 +529,32 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC @Override public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiAbilityConfig) { - // Ability was saved in the SubGui, refresh list + if (subgui instanceof SubGuiAbilityEditMode) { + int mode = ((SubGuiAbilityEditMode) subgui).getResult(); + if (mode < 0) return; // cancelled + + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + if (mode == SubGuiAbilityEditMode.MODE_CLONE_MODIFY) { + // Convert reference to inline, then edit + if (slot.convertToInline()) { + Ability ability = slot.getAbility(); + if (ability != null) { + ability.setId(UUID.randomUUID().toString()); + setSubGui(ability.createConfigGui(this)); + } + updateNpcAbilitiesList(); + save(); + } + } else if (mode == SubGuiAbilityEditMode.MODE_MODIFY_PARENT) { + // Edit the resolved ability directly (changes propagate to controller) + Ability ability = slot.getAbility(); + if (ability != null) { + setSubGui(ability.createConfigGui(this)); + } + } + } + } else if (subgui instanceof SubGuiAbilityConfig) { updateNpcAbilitiesList(); save(); } @@ -500,8 +564,15 @@ public class GuiNPCAbilities extends GuiNPCInterface2 implements IScrollData, IC * Called from SubGuiAbilityConfig when ability is saved. */ public void onAbilitySaved(Ability ability) { - if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcAbilities.size()) { - npcAbilities.set(selectedAbilityIndex, ability); + if (selectedAbilityIndex >= 0 && selectedAbilityIndex < npcSlots.size()) { + AbilitySlot slot = npcSlots.get(selectedAbilityIndex); + if (slot.isReference()) { + // For reference slots, save back to controller + // (The resolved copy was edited in-place) + } else { + // For inline slots, replace with the updated ability + npcSlots.set(selectedAbilityIndex, AbilitySlot.inline(ability)); + } updateNpcAbilitiesList(); save(); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index 684138891..611f8c918 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -250,7 +250,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Row 2: Weight + Lock Movement addLabel(new GuiNpcLabel(4, "ability.weight", col1LabelX, y + 5)); - GuiNpcTextField weightField = new GuiNpcTextField(3, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(weight)); + GuiNpcTextField weightField = new GuiNpcTextField(3, this, fontRendererObj, col1FieldX, y, 40, 20, "" + weight); weightField.setIntegersOnly(); weightField.setMinMaxDefault(1, 1000, 10); addTextField(weightField); @@ -271,7 +271,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Only show dazed ticks if interruptible is enabled if (interruptible) { addLabel(new GuiNpcLabel(12, "ability.dazed", col2LabelX, y + 5)); - GuiNpcTextField dazedField = new GuiNpcTextField(13, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(dazedTicks)); + GuiNpcTextField dazedField = new GuiNpcTextField(13, this, fontRendererObj, col2FieldX, y, 40, 20, "" + dazedTicks); dazedField.setIntegersOnly(); dazedField.setMinMaxDefault(0, 1000, 80); addTextField(dazedField); @@ -291,13 +291,13 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Only show windup textbox if sync is off or no animation selected if (!syncWindupWithAnimation || !hasWindupAnimation) { - GuiNpcTextField windupField = new GuiNpcTextField(11, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(windUpTicks)); + GuiNpcTextField windupField = new GuiNpcTextField(11, this, fontRendererObj, col1FieldX, y, 40, 20, "" + windUpTicks); windupField.setIntegersOnly(); windupField.setMinMaxDefault(0, 1000, 20); addTextField(windupField); } else { // Show calculated value as label when synced - addLabel(new GuiNpcLabel(11, String.valueOf(windUpTicks), col1FieldX + 5, y + 5)); + addLabel(new GuiNpcLabel(11, "" + windUpTicks, col1FieldX + 5, y + 5)); } // Sync toggle button - only show if windup animation is selected @@ -309,7 +309,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi } addLabel(new GuiNpcLabel(13, "ability.cooldown", col2LabelX, y + 5)); - GuiNpcTextField cooldownField = new GuiNpcTextField(10, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(cooldownTicks)); + GuiNpcTextField cooldownField = new GuiNpcTextField(10, this, fontRendererObj, col2FieldX, y, 40, 20, "" + cooldownTicks); cooldownField.setIntegersOnly(); cooldownField.setMinMaxDefault(0, 10000, 0); addTextField(cooldownField); @@ -399,13 +399,13 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Row 1: Min Range + Max Range addLabel(new GuiNpcLabel(20, "ability.minRange", col1LabelX, y + 5)); - GuiNpcTextField minRangeField = new GuiNpcTextField(5, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf((int) minRange)); + GuiNpcTextField minRangeField = new GuiNpcTextField(5, this, fontRendererObj, col1FieldX, y, 40, 20, "" + (int) minRange); minRangeField.setIntegersOnly(); minRangeField.setMinMaxDefault(0, 100, 0); addTextField(minRangeField); addLabel(new GuiNpcLabel(21, "ability.maxRange", col2LabelX, y + 5)); - GuiNpcTextField maxRangeField = new GuiNpcTextField(6, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf((int) maxRange)); + GuiNpcTextField maxRangeField = new GuiNpcTextField(6, this, fontRendererObj, col2FieldX, y, 40, 20, "" + (int) maxRange); maxRangeField.setIntegersOnly(); maxRangeField.setMinMaxDefault(1, 100, 20); addTextField(maxRangeField); @@ -1039,7 +1039,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi * Creates an integer text field for type-specific settings. */ protected GuiNpcTextField createIntField(int id, int x, int y, int width, int value) { - GuiNpcTextField field = new GuiNpcTextField(id, this, fontRendererObj, x, y, width, 20, String.valueOf(value)); + GuiNpcTextField field = new GuiNpcTextField(id, this, fontRendererObj, x, y, width, 20, "" + value); field.setIntegersOnly(); return field; } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java index 3760666b4..c0517257b 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityDeleteConfirm.java @@ -1,7 +1,7 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.network.PacketClient; -import kamkeel.npcs.network.packets.request.ability.SavedAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket; import net.minecraft.client.gui.GuiButton; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -51,7 +51,7 @@ public class SubGuiAbilityDeleteConfirm extends SubGuiInterface { if (id == 0) { // Delete the ability via packet - PacketClient.sendClient(new SavedAbilityRemovePacket(abilityName)); + PacketClient.sendClient(new CustomAbilityRemovePacket(abilityName)); if (onDeleted != null) { onDeleted.run(); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java new file mode 100644 index 000000000..4d21f12ca --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityEditMode.java @@ -0,0 +1,56 @@ +package noppes.npcs.client.gui.advanced; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * Dialog asking the user whether to clone-and-modify (convert to inline) or modify the parent ability. + */ +public class SubGuiAbilityEditMode extends SubGuiInterface { + + public static final int MODE_CLONE_MODIFY = 0; + public static final int MODE_MODIFY_PARENT = 1; + + private int result = -1; + + public SubGuiAbilityEditMode() { + setBackground("menubg.png"); + xSize = 220; + ySize = 90; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 10; + addLabel(new GuiNpcLabel(0, "ability.edit.mode", guiLeft + 10, y)); + + y += 25; + addButton(new GuiNpcButton(0, guiLeft + 10, y, 95, 20, "ability.cloneModify")); + addButton(new GuiNpcButton(1, guiLeft + 115, y, 95, 20, "ability.modifyParent")); + + y += 25; + addButton(new GuiNpcButton(2, guiLeft + 65, y, 90, 20, "gui.cancel")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0) { + result = MODE_CLONE_MODIFY; + close(); + } else if (guibutton.id == 1) { + result = MODE_MODIFY_PARENT; + close(); + } else if (guibutton.id == 2) { + result = -1; + close(); + } + } + + public int getResult() { + return result; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java index 1e9133d9d..68a74cb8b 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoad.java @@ -3,8 +3,8 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.network.PacketClient; -import kamkeel.npcs.network.packets.request.ability.SavedAbilitiesGetPacket; -import kamkeel.npcs.network.packets.request.ability.SavedAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.client.gui.util.GuiCustomScroll; @@ -13,28 +13,34 @@ import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.ICustomScrollListener; import noppes.npcs.client.gui.util.IGuiData; import noppes.npcs.client.gui.util.IScrollData; +import noppes.npcs.client.gui.util.ISubGuiListener; import noppes.npcs.client.gui.util.SubGuiInterface; import noppes.npcs.constants.EnumScrollData; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.UUID; import java.util.Vector; /** * Dialog for loading a saved ability preset. - * Uses packet-based communication for proper client-server architecture. + * Shows custom abilities from the server, then asks Clone/Reference via SubGuiAbilityLoadMode. */ -public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollListener, IScrollData, IGuiData { +public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollListener, IScrollData, IGuiData, ISubGuiListener { private final SubGuiAbilityConfig parentConfig; private final GuiNPCAbilities parentAbilities; - private String selectedName = null; - private HashMap abilityData = new HashMap<>(); + private String selectedDisplayName = null; + private String selectedUuid = null; + + /** Maps display name -> UUID for custom abilities. */ + private final HashMap displayToUuid = new HashMap<>(); + private HashMap rawData = new HashMap<>(); private GuiCustomScroll scroll; - // Track if we're waiting for a load private boolean waitingForLoad = false; + private int pendingLoadMode = -1; public SubGuiAbilityLoad(SubGuiAbilityConfig parentConfig) { this.parentConfig = parentConfig; @@ -44,8 +50,7 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL xSize = 220; ySize = 180; - // Request ability list from server - PacketClient.sendClient(new SavedAbilitiesGetPacket()); + PacketClient.sendClient(new CustomAbilitiesGetPacket()); } public SubGuiAbilityLoad(GuiNPCAbilities parentAbilities) { @@ -56,8 +61,7 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL xSize = 220; ySize = 180; - // Request ability list from server - PacketClient.sendClient(new SavedAbilitiesGetPacket()); + PacketClient.sendClient(new CustomAbilitiesGetPacket()); } @Override @@ -69,7 +73,6 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL addLabel(new GuiNpcLabel(0, "ability.load.select", guiLeft + 10, y)); y += 14; - // Scroll list of saved abilities if (scroll == null) { scroll = new GuiCustomScroll(this, 0); scroll.setSize(200, 110); @@ -77,42 +80,70 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL scroll.guiLeft = guiLeft + 10; scroll.guiTop = y; scroll.setList(getFilteredList()); - if (selectedName != null && abilityData.containsKey(selectedName)) { - scroll.setSelected(selectedName); + if (selectedDisplayName != null) { + scroll.setSelected(selectedDisplayName); } addScroll(scroll); y += 115; - // Buttons addButton(new GuiNpcButton(0, guiLeft + 10, y, 95, 20, "gui.load")); addButton(new GuiNpcButton(2, guiLeft + 115, y, 95, 20, "gui.cancel")); - // Disable load if nothing selected - getButton(0).setEnabled(selectedName != null && !selectedName.isEmpty()); + getButton(0).setEnabled(selectedUuid != null); } private List getFilteredList() { - return new ArrayList<>(abilityData.keySet()); + return new ArrayList<>(displayToUuid.keySet()); } @Override public void buttonEvent(GuiButton guibutton) { int id = guibutton.id; - if (id == 0 && selectedName != null) { - // Request ability data from server - waitingForLoad = true; - PacketClient.sendClient(new SavedAbilityGetPacket(selectedName)); + if (id == 0 && selectedUuid != null) { + if (parentAbilities != null) { + // From NPC abilities GUI - ask clone/reference + setSubGui(new SubGuiAbilityLoadMode()); + } else { + // From config GUI - always clone + waitingForLoad = true; + pendingLoadMode = SubGuiAbilityLoadMode.MODE_CLONE; + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); + } } else if (id == 2) { close(); } } + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiAbilityLoadMode) { + int mode = ((SubGuiAbilityLoadMode) subgui).getResult(); + if (mode < 0) return; // cancelled + + if (mode == SubGuiAbilityLoadMode.MODE_REFERENCE) { + // Reference mode - just pass the reference ID + if (parentAbilities != null && selectedUuid != null) { + parentAbilities.loadAbilityReference(selectedUuid); + } + close(); + } else { + // Clone mode - request full data + waitingForLoad = true; + pendingLoadMode = SubGuiAbilityLoadMode.MODE_CLONE; + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); + } + } + } + @Override public void customScrollClicked(int i, int j, int k, GuiCustomScroll scroll) { if (scroll.id == 0) { - selectedName = scroll.getSelected(); + selectedDisplayName = scroll.getSelected(); + if (selectedDisplayName != null) { + selectedUuid = displayToUuid.get(selectedDisplayName); + } initGui(); } } @@ -120,22 +151,40 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL @Override public void customScrollDoubleClicked(String selection, GuiCustomScroll scroll) { if (scroll.id == 0 && selection != null && !selection.isEmpty()) { - // Double-click to load - request from server - selectedName = selection; - waitingForLoad = true; - PacketClient.sendClient(new SavedAbilityGetPacket(selection)); + selectedDisplayName = selection; + selectedUuid = displayToUuid.get(selectedDisplayName); + if (selectedUuid != null) { + if (parentAbilities != null) { + setSubGui(new SubGuiAbilityLoadMode()); + } else { + waitingForLoad = true; + pendingLoadMode = SubGuiAbilityLoadMode.MODE_CLONE; + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); + } + } } } @Override public void setData(Vector list, HashMap data, EnumScrollData type) { - if (type == EnumScrollData.ABILITIES) { - String name = scroll != null ? scroll.getSelected() : null; - this.abilityData = data; + if (type == EnumScrollData.CUSTOM_ABILITIES) { + displayToUuid.clear(); + rawData = data; + for (String key : data.keySet()) { + // key format: "displayName\tUUID" + int tabIndex = key.indexOf('\t'); + if (tabIndex > 0) { + String displayName = key.substring(0, tabIndex); + String uuid = key.substring(tabIndex + 1); + displayToUuid.put(displayName, uuid); + } else { + displayToUuid.put(key, key); + } + } if (scroll != null) { scroll.setList(getFilteredList()); - if (name != null && abilityData.containsKey(name)) { - scroll.setSelected(name); + if (selectedDisplayName != null && displayToUuid.containsKey(selectedDisplayName)) { + scroll.setSelected(selectedDisplayName); } } initGui(); @@ -144,11 +193,15 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL @Override public void setGuiData(NBTTagCompound compound) { - // Received ability data from server if (waitingForLoad) { waitingForLoad = false; Ability loaded = AbilityController.Instance.fromNBT(compound); if (loaded != null) { + if (pendingLoadMode == SubGuiAbilityLoadMode.MODE_CLONE) { + // Give it a new UUID so it's a unique inline copy + loaded.setId(UUID.randomUUID().toString()); + } + if (parentConfig != null) { parentConfig.loadAbility(loaded); } else if (parentAbilities != null) { @@ -161,7 +214,7 @@ public class SubGuiAbilityLoad extends SubGuiInterface implements ICustomScrollL @Override public void setSelected(String selected) { - this.selectedName = selected; + this.selectedDisplayName = selected; if (scroll != null) { scroll.setSelected(selected); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java new file mode 100644 index 000000000..28a7bd0bb --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityLoadMode.java @@ -0,0 +1,56 @@ +package noppes.npcs.client.gui.advanced; + +import net.minecraft.client.gui.GuiButton; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; + +/** + * Dialog asking the user whether to Clone (inline copy) or Reference the loaded ability. + */ +public class SubGuiAbilityLoadMode extends SubGuiInterface { + + public static final int MODE_CLONE = 0; + public static final int MODE_REFERENCE = 1; + + private int result = -1; + + public SubGuiAbilityLoadMode() { + setBackground("menubg.png"); + xSize = 200; + ySize = 90; + } + + @Override + public void initGui() { + super.initGui(); + + int y = guiTop + 10; + addLabel(new GuiNpcLabel(0, "ability.load.mode", guiLeft + 10, y)); + + y += 25; + addButton(new GuiNpcButton(0, guiLeft + 15, y, 80, 20, "ability.clone")); + addButton(new GuiNpcButton(1, guiLeft + 105, y, 80, 20, "ability.reference")); + + y += 25; + addButton(new GuiNpcButton(2, guiLeft + 55, y, 90, 20, "gui.cancel")); + } + + @Override + public void buttonEvent(GuiButton guibutton) { + if (guibutton.id == 0) { + result = MODE_CLONE; + close(); + } else if (guibutton.id == 1) { + result = MODE_REFERENCE; + close(); + } else if (guibutton.id == 2) { + result = -1; + close(); + } + } + + public int getResult() { + return result; + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java index 760f3eb9b..9407d20c5 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilitySaveConfirm.java @@ -2,7 +2,7 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.network.PacketClient; -import kamkeel.npcs.network.packets.request.ability.SavedAbilitySavePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket; import net.minecraft.client.gui.GuiButton; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; @@ -53,7 +53,7 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface { if (id == 0) { // Save the ability via packet - PacketClient.sendClient(new SavedAbilitySavePacket(ability.writeNBT())); + PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); if (callback != null) { callback.onAbilitySaved(ability); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java index f45858a99..d941f19ec 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiConditionEdit.java @@ -98,7 +98,7 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi if (typeId.equals("hit_count")) { // Hit count fields addLabel(new GuiNpcLabel(2, "condition.hits", labelX, y + 5)); - GuiNpcTextField hitsField = new GuiNpcTextField(2, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(requiredHits)); + GuiNpcTextField hitsField = new GuiNpcTextField(2, this, fontRendererObj, fieldX, y, 40, 20, "" + requiredHits); hitsField.setIntegersOnly(); hitsField.setMinMaxDefault(1, 100, 3); hitsField.setHoverText("condition.hover.hits"); @@ -106,7 +106,7 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi y += 24; addLabel(new GuiNpcLabel(3, "condition.within", labelX, y + 5)); - GuiNpcTextField ticksField = new GuiNpcTextField(3, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(withinTicks)); + GuiNpcTextField ticksField = new GuiNpcTextField(3, this, fontRendererObj, fieldX, y, 40, 20, "" + withinTicks); ticksField.setIntegersOnly(); ticksField.setMinMaxDefault(1, 1200, 60); ticksField.setHoverText("condition.hover.within"); @@ -115,7 +115,7 @@ public class SubGuiConditionEdit extends SubGuiInterface implements ITextfieldLi // HP threshold field (as percentage) addLabel(new GuiNpcLabel(2, "condition.percent", labelX, y + 5)); int percent = (int) (threshold * 100); - GuiNpcTextField thresholdField = new GuiNpcTextField(4, this, fontRendererObj, fieldX, y, 40, 20, String.valueOf(percent)); + GuiNpcTextField thresholdField = new GuiNpcTextField(4, this, fontRendererObj, fieldX, y, 40, 20, "" + percent); thresholdField.setIntegersOnly(); thresholdField.setMinMaxDefault(1, 99, 50); thresholdField.setHoverText("condition.hover.threshold"); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java index 5faf662b0..7f3d04d9e 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java @@ -122,7 +122,7 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { // Row 1: Anchor Point addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); - GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), disc.getAnchorPointEnum().getId()); + GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), disc.getAnchorPointEnum().ordinal()); anchorBtn.setHoverText("ability.hover.anchorPoint"); addButton(anchorBtn); @@ -182,12 +182,12 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { // Row 6: Density + Radius addLabel(new GuiNpcLabel(207, "ability.lightningDensity", labelX, y + 5)); - GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(disc.getLightningDensity())); + GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, "" + disc.getLightningDensity()); densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); addTextField(densityField); addLabel(new GuiNpcLabel(208, "ability.lightningRadius", col2LabelX, y + 5)); - GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(disc.getLightningRadius())); + GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, "" + disc.getLightningRadius()); radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); addTextField(radiusField); } @@ -236,7 +236,7 @@ public class SubGuiAbilityDisc extends SubGuiAbilityConfig { initGui(); break; case 210: - disc.setAnchorPointEnum(AnchorPoint.fromId(value)); + disc.setAnchorPointEnum(AnchorPoint.fromOrdinal(value)); break; } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEffects.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEffects.java index b4eced168..56ddaf9f6 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEffects.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEffects.java @@ -80,7 +80,7 @@ public class SubGuiAbilityEffects extends SubGuiInterface implements ITextfieldL // Duration text field GuiNpcTextField durField = new GuiNpcTextField(baseId + 1, this, fontRendererObj, - guiLeft + 112, y, 50, 20, String.valueOf(effect.getDurationTicks())); + guiLeft + 112, y, 50, 20, "" + effect.getDurationTicks()); durField.setIntegersOnly(); durField.setMinMaxDefault(1, 12000, 60); durField.setHoverText("effect.hover.duration"); @@ -89,7 +89,7 @@ public class SubGuiAbilityEffects extends SubGuiInterface implements ITextfieldL // Amplifier selector (0-10) String[] ampValues = new String[11]; for (int a = 0; a <= 10; a++) { - ampValues[a] = String.valueOf(a); + ampValues[a] = "" + a; } GuiNpcButton ampBtn = new GuiNpcButton(baseId + 2, guiLeft + 168, y, 40, 20, ampValues, effect.getAmplifier()); ampBtn.setHoverText("effect.hover.amplifier"); diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java index 49ab42e5d..3f70e5aca 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java @@ -118,7 +118,7 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { // Row 1: Anchor Point addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); - GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), beam.getAnchorPointEnum().getId()); + GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), beam.getAnchorPointEnum().ordinal()); anchorBtn.setHoverText("ability.hover.anchorPoint"); addButton(anchorBtn); @@ -175,12 +175,12 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { // Row 5: Density + Radius addLabel(new GuiNpcLabel(207, "ability.lightningDensity", labelX, y + 5)); - GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(beam.getLightningDensity())); + GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, "" + beam.getLightningDensity()); densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); addTextField(densityField); addLabel(new GuiNpcLabel(208, "ability.lightningRadius", col2LabelX, y + 5)); - GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(beam.getLightningRadius())); + GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, "" + beam.getLightningRadius()); radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); addTextField(radiusField); } @@ -225,7 +225,7 @@ public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { initGui(); break; case 210: - beam.setAnchorPointEnum(AnchorPoint.fromId(value)); + beam.setAnchorPointEnum(AnchorPoint.fromOrdinal(value)); break; } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java index e5184a3bb..b594a83c7 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java @@ -144,12 +144,12 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { // Row 4: Density + Radius addLabel(new GuiNpcLabel(206, "ability.lightningDensity", labelX, y + 5)); - GuiNpcTextField densityField = new GuiNpcTextField(206, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(laser.getLightningDensity())); + GuiNpcTextField densityField = new GuiNpcTextField(206, this, fontRendererObj, fieldX, y, 55, 18, "" + laser.getLightningDensity()); densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); addTextField(densityField); addLabel(new GuiNpcLabel(207, "ability.lightningRadius", col2LabelX, y + 5)); - GuiNpcTextField radiusField = new GuiNpcTextField(207, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(laser.getLightningRadius())); + GuiNpcTextField radiusField = new GuiNpcTextField(207, this, fontRendererObj, col2FieldX, y, 55, 18, "" + laser.getLightningRadius()); radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); addTextField(radiusField); } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java index 7cc0f9f2f..780661c6f 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java @@ -106,7 +106,7 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { // Row 1: Anchor Point addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); - GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), orb.getAnchorPointEnum().getId()); + GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), orb.getAnchorPointEnum().ordinal()); anchorBtn.setHoverText("ability.hover.anchorPoint"); addButton(anchorBtn); @@ -163,12 +163,12 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { // Row 61: Density + Radius addLabel(new GuiNpcLabel(207, "ability.lightningDensity", labelX, y + 5)); - GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(orb.getLightningDensity())); + GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, "" + orb.getLightningDensity()); densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); addTextField(densityField); addLabel(new GuiNpcLabel(208, "ability.lightningRadius", col2LabelX, y + 5)); - GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(orb.getLightningRadius())); + GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, "" + orb.getLightningRadius()); radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); addTextField(radiusField); } @@ -213,7 +213,7 @@ public class SubGuiAbilityOrb extends SubGuiAbilityConfig { initGui(); break; case 210: - orb.setAnchorPointEnum(AnchorPoint.fromId(value)); + orb.setAnchorPointEnum(AnchorPoint.fromOrdinal(value)); break; } } diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java index e07d2583c..52e78f7d6 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNPCManageLinked.java @@ -338,21 +338,21 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Health label = StatCollector.translateToLocal("stats.health") + ": "; - value = String.valueOf(npc.stats.maxHealth); + value = "" + npc.stats.maxHealth; fontRendererObj.drawString(label, xLabel, y, 0x29d6b9, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; // Damage (using getAttackStrength) label = StatCollector.translateToLocal("stats.meleestrength") + ": "; - value = String.valueOf(npc.stats.getAttackStrength()); + value = "" + npc.stats.getAttackStrength(); fontRendererObj.drawString(label, xLabel, y, 0xff5714, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; // Attack Speed label = StatCollector.translateToLocal("stats.meleespeed") + ": "; - value = String.valueOf(npc.stats.attackSpeed); + value = "" + npc.stats.attackSpeed; fontRendererObj.drawString(label, xLabel, y, 0xf7ca28, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -381,7 +381,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Walk Speed label = StatCollector.translateToLocal("stats.speed") + ": "; - value = String.valueOf(npc.ais.getWalkingSpeed()); + value = "" + npc.ais.getWalkingSpeed(); fontRendererObj.drawString(label, xLabel, y, 0xffae0d, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -415,7 +415,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Version label = StatCollector.translateToLocal("display.version") + ": "; - value = String.valueOf(linkedItem.version); + value = "" + linkedItem.version; fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -423,7 +423,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Max Stack Size labelColor = 0xff5714; label = StatCollector.translateToLocal("display.maxStack") + ": "; - value = String.valueOf(linkedItem.stackSize); + value = "" + linkedItem.stackSize; fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -431,7 +431,7 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, // Dig Speed labelColor = 0xf7ca28; label = StatCollector.translateToLocal("display.digSpeed") + ": "; - value = String.valueOf(linkedItem.digSpeed); + value = "" + linkedItem.digSpeed; fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; @@ -498,14 +498,14 @@ public class GuiNPCManageLinked extends GuiNPCInterface2 implements IScrollData, labelColor = 0x7cff54; // isTool label = StatCollector.translateToLocal("display.isTool") + ": "; - value = String.valueOf(linkedItem.isTool).toUpperCase(); + value = ("" + linkedItem.isTool).toUpperCase(); fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; // isNormalItem label = StatCollector.translateToLocal("display.isNormalItem") + ": "; - value = String.valueOf(linkedItem.isNormalItem).toUpperCase(); + value = ("" + linkedItem.isNormalItem).toUpperCase(); fontRendererObj.drawString(label, xLabel, y, labelColor, false); fontRendererObj.drawString(value, xValue, y, valueColor, false); y += 15; diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java index cb5220d54..8fa2e4e41 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcManageAbilities.java @@ -5,10 +5,10 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.network.PacketClient; -import kamkeel.npcs.network.packets.request.ability.SavedAbilitiesGetPacket; -import kamkeel.npcs.network.packets.request.ability.SavedAbilityGetPacket; -import kamkeel.npcs.network.packets.request.ability.SavedAbilityRemovePacket; -import kamkeel.npcs.network.packets.request.ability.SavedAbilitySavePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitiesGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityGetPacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilityRemovePacket; +import kamkeel.npcs.network.packets.request.ability.CustomAbilitySavePacket; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.GuiYesNo; import net.minecraft.client.gui.GuiYesNoCallback; @@ -53,7 +53,10 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface // ==================== DATA ==================== private GuiCustomScroll scroll; private HashMap abilityData = new HashMap<>(); + /** Maps display name -> UUID for custom abilities */ + private final HashMap displayToUuid = new HashMap<>(); private String selected = null; + private String selectedUuid = null; private String search = ""; private Ability selectedAbility = null; @@ -73,7 +76,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface previewExecutor.setParentGui(this); // Request ability data from server - PacketClient.sendClient(new SavedAbilitiesGetPacket()); + PacketClient.sendClient(new CustomAbilitiesGetPacket()); // Enable animation data AnimationData data = npc.display.animationData; @@ -90,7 +93,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface // Remove button addButton(new GuiNpcButton(1, guiLeft + 368, guiTop + 30, 45, 20, "gui.remove")); - getButton(1).setEnabled(selected != null && !selected.isEmpty() && abilityData.containsKey(selected)); + getButton(1).setEnabled(selected != null && !selected.isEmpty() && displayToUuid.containsKey(selected)); // Scroll list of saved abilities (right side) if (scroll == null) { @@ -103,7 +106,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface // Update scroll list scroll.setList(getSearchList()); - if (selected != null && abilityData.containsKey(selected)) { + if (selected != null && displayToUuid.containsKey(selected)) { scroll.setSelected(selected); } @@ -144,10 +147,10 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface private List getSearchList() { if (search.isEmpty()) { - return new ArrayList<>(abilityData.keySet()); + return new ArrayList<>(displayToUuid.keySet()); } List list = new ArrayList<>(); - for (String name : abilityData.keySet()) { + for (String name : displayToUuid.keySet()) { if (name.toLowerCase().contains(search.toLowerCase())) { list.add(name); } @@ -208,8 +211,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface int id = guibutton.id; - if (id == 1 && selected != null && abilityData.containsKey(selected)) { - // Remove - show confirmation + if (id == 1 && selected != null && selectedUuid != null) { GuiYesNo guiyesno = new GuiYesNo(this, selected, StatCollector.translateToLocal("gui.delete"), 1); displayGuiScreen(guiyesno); } else if (id == 100 && selectedAbility != null) { @@ -254,12 +256,13 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface if (scroll.id == 0) { String newSelection = scroll.getSelected(); if (newSelection != null && !newSelection.equals(selected)) { - // Stop any playing preview previewExecutor.stop(); selected = newSelection; - // Request ability data from server - PacketClient.sendClient(new SavedAbilityGetPacket(selected)); + selectedUuid = displayToUuid.get(selected); + if (selectedUuid != null) { + PacketClient.sendClient(new CustomAbilityGetPacket(selectedUuid)); + } } } } @@ -277,13 +280,25 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface @Override public void setData(Vector list, HashMap data, EnumScrollData type) { - if (type == EnumScrollData.ABILITIES) { - String name = scroll != null ? scroll.getSelected() : null; + if (type == EnumScrollData.CUSTOM_ABILITIES) { + String prevSelected = scroll != null ? scroll.getSelected() : null; this.abilityData = data; + displayToUuid.clear(); + for (String key : data.keySet()) { + // key format: "displayName\tUUID" + int tabIndex = key.indexOf('\t'); + if (tabIndex > 0) { + String displayName = key.substring(0, tabIndex); + String uuid = key.substring(tabIndex + 1); + displayToUuid.put(displayName, uuid); + } else { + displayToUuid.put(key, key); + } + } if (scroll != null) { scroll.setList(getSearchList()); - if (name != null && abilityData.containsKey(name)) { - scroll.setSelected(name); + if (prevSelected != null && displayToUuid.containsKey(prevSelected)) { + scroll.setSelected(prevSelected); } } initGui(); @@ -322,11 +337,11 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface NoppesUtil.openGUI(player, this); if (!result) return; - if (id == 1 && selected != null && abilityData.containsKey(selected)) { - // Delete ability via packet - PacketClient.sendClient(new SavedAbilityRemovePacket(selected)); + if (id == 1 && selected != null && selectedUuid != null) { + PacketClient.sendClient(new CustomAbilityRemovePacket(selectedUuid)); scroll.clear(); selected = null; + selectedUuid = null; selectedAbility = null; initGui(); } @@ -334,8 +349,7 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface @Override public void onAbilitySaved(Ability ability) { - // Save the edited ability via packet - PacketClient.sendClient(new SavedAbilitySavePacket(ability.writeNBT())); + PacketClient.sendClient(new CustomAbilitySavePacket(ability.writeNBT())); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java b/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java index 41e105a38..ecdec2990 100644 --- a/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java +++ b/src/main/java/noppes/npcs/client/gui/global/GuiNpcNaturalSpawns.java @@ -90,7 +90,7 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I ArrayList list = new ArrayList<>(); Set keySet = this.spawn.spawnCompounds.keySet(); for (int i : keySet) { - list.add(String.valueOf(i)); + list.add("" + i); } this.spawnEntryScroll.setList(list); addScroll(this.spawnEntryScroll); @@ -99,7 +99,7 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I if (this.spawnEntryScroll.hasSelected()) { int selected = Integer.parseInt(this.spawnEntryScroll.getSelected()); addButton(new GuiNpcButton(22, guiLeft + 32, y, 20, 20, "-")); - GuiNpcTextField num = new GuiNpcTextField(25, this, guiLeft + 60, y += 10, 30, 20, String.valueOf(selected)); + GuiNpcTextField num = new GuiNpcTextField(25, this, guiLeft + 60, y += 10, 30, 20, "" + selected); num.integersOnly = true; addTextField(num); addButton(new GuiNpcButton(26, guiLeft + 92, y, 100, 20, this.getTitle(this.spawn.spawnCompounds.get(selected)))); @@ -215,8 +215,8 @@ public class GuiNpcNaturalSpawns extends GuiNPCInterface2 implements IGuiData, I } if (textfield.id == 25 && this.spawnEntryScroll.hasSelected()) { int selected = Integer.parseInt(spawnEntryScroll.getSelected()); - if (spawnEntryScroll.getList().contains(String.valueOf(textfield.getInteger()))) { - textfield.setText(String.valueOf(selected)); + if (spawnEntryScroll.getList().contains("" + textfield.getInteger())) { + textfield.setText("" + selected); return; } NBTTagCompound compound = spawn.spawnCompounds.get(selected); diff --git a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java index 33966d1e2..a50622529 100644 --- a/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java +++ b/src/main/java/noppes/npcs/client/gui/player/AuctionTooltipHandler.java @@ -192,7 +192,7 @@ public class AuctionTooltipHandler { tooltip.add(EnumChatFormatting.RED + StatCollector.translateToLocal("auction.claim.expiresSoon")); } else { tooltip.add(EnumChatFormatting.GRAY + StatCollector.translateToLocal("auction.claim.expires") - .replace("%s", String.valueOf(daysLeft))); + .replace("%s", "" + daysLeft)); } tooltip.add(""); @@ -238,7 +238,7 @@ public class AuctionTooltipHandler { return amount + " " + AuctionClientConfig.getCurrencyName(); } StringBuilder sb = new StringBuilder(); - String str = String.valueOf(amount); + String str = "" + amount; int count = 0; for (int i = str.length() - 1; i >= 0; i--) { if (count > 0 && count % 3 == 0) { diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java b/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java index 683e21798..00551911a 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAnvilRecipes.java @@ -237,7 +237,7 @@ public class GuiAnvilRecipes extends GuiNPCInterface { fontRendererObj.drawString(percentText, percentTextX, percentTextY, CustomNpcResourceListener.DefaultTextColor); // Add text overlay for percentage with the full unrounded value. if (func_146978_c(percentTextX - guiLeft, percentTextY - guiTop, percentWidth, fontRendererObj.FONT_HEIGHT, mouseX, mouseY)) { - textOverlays.add(new TextOverlayData(percentTextX, percentTextY, percentWidth, fontRendererObj.FONT_HEIGHT, String.valueOf(recipe.getRepairPercentage()))); + textOverlays.add(new TextOverlayData(percentTextX, percentTextY, percentWidth, fontRendererObj.FONT_HEIGHT, "" + recipe.getRepairPercentage())); } if (!recipe.availability.isDefault()) { @@ -303,7 +303,7 @@ public class GuiAnvilRecipes extends GuiNPCInterface { int xpBoxWidth = scaledTextWidth; int xpBoxHeight = fontRendererObj.FONT_HEIGHT; if (func_146978_c(xpTextX - guiLeft, xpTextY - guiTop, xpBoxWidth, xpBoxHeight, mouseX, mouseY)) { - textOverlays.add(new TextOverlayData(xpTextX, xpTextY, xpBoxWidth, xpBoxHeight, String.valueOf(recipe.getXpCost()))); + textOverlays.add(new TextOverlayData(xpTextX, xpTextY, xpBoxWidth, xpBoxHeight, "" + recipe.getXpCost())); } } @@ -326,7 +326,7 @@ public class GuiAnvilRecipes extends GuiNPCInterface { private String formatXpCost(int xp) { if (xp < 1000) { - return String.valueOf(xp); + return "" + xp; } double xpK = xp / 1000.0; if (xpK == (int) xpK) { diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java index cc5e5d804..afe47fb44 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionBidding.java @@ -255,7 +255,7 @@ public class GuiAuctionBidding extends GuiAuctionInterface implements ISubGuiLis String bidCountLabel = StatCollector.translateToLocal("auction.info.bidCount"); fontRendererObj.drawString(EnumChatFormatting.GRAY + bidCountLabel, x, y, 0xFFFFFF); - label = String.valueOf(listing.bidCount); + label = "" + listing.bidCount; length = fontRendererObj.getStringWidth(label); fontRendererObj.drawString(EnumChatFormatting.WHITE + label, xRight - length, y, 0xFFFFFF); } diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java index 851c92263..7efdbe9c9 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionInterface.java @@ -196,9 +196,9 @@ public abstract class GuiAuctionInterface extends GuiContainerNPCInterface { /** Format currency with commas (e.g., 1000 -> "1,000") */ protected String formatCurrency(long amount) { - if (amount < 1000) return String.valueOf(amount); + if (amount < 1000) return "" + amount; StringBuilder sb = new StringBuilder(); - String str = String.valueOf(amount); + String str = "" + amount; int count = 0; for (int i = str.length() - 1; i >= 0; i--) { if (count > 0 && count % 3 == 0) sb.insert(0, ','); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java index 8b288c83f..a8784b3b0 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiAuctionSell.java @@ -67,7 +67,7 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis // Starting Price label and field addLabel(new GuiNpcLabel(1, "auction.sell.startingPrice", guiLeft + contentX, y + 5, 0xFFFFFF)); GuiNpcTextField startingField = new GuiNpcTextField(txtStartingPriceId, this, fontRendererObj, - guiLeft + contentX + 80, y, 80, 18, startingPrice > 0 ? String.valueOf(startingPrice) : ""); + guiLeft + contentX + 80, y, 80, 18, startingPrice > 0 ? "" + startingPrice : ""); startingField.setIntegersOnly(); addTextField(startingField); @@ -78,7 +78,7 @@ public class GuiAuctionSell extends GuiAuctionInterface implements ITextfieldLis addLabel(lblBuyout); txtBuyout = new GuiNpcTextField(txtBuyoutPriceId, this, fontRendererObj, - guiLeft + contentX + 80, y, 80, 18, buyoutPrice > 0 ? String.valueOf(buyoutPrice) : ""); + guiLeft + contentX + 80, y, 80, 18, buyoutPrice > 0 ? "" + buyoutPrice : ""); txtBuyout.setIntegersOnly(); addTextField(txtBuyout); diff --git a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java index f11bbb2e6..b0a1c6c32 100644 --- a/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java +++ b/src/main/java/noppes/npcs/client/gui/player/GuiNPCTrader.java @@ -172,7 +172,7 @@ public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData { // Draw stock count to the RIGHT of the trade (after output slot) if (stockEnabled && availableStock[slotIdx] < Integer.MAX_VALUE) { - String stockText = String.valueOf(availableStock[slotIdx]); + String stockText = "" + availableStock[slotIdx]; int stockColor = availableStock[slotIdx] > 0 ? 0x00AA00 : 0xAA0000; fontRendererObj.drawString(stockText, x + OUTPUT_OFFSET + 20, y + 5, stockColor); } @@ -317,10 +317,10 @@ public class GuiNPCTrader extends GuiContainerNPCInterface implements IGuiData { */ private String formatCurrency(long amount) { if (amount < 1000) { - return String.valueOf(amount); + return "" + amount; } StringBuilder sb = new StringBuilder(); - String str = String.valueOf(amount); + String str = "" + amount; int count = 0; for (int i = str.length() - 1; i >= 0; i--) { if (count > 0 && count % 3 == 0) { diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java index de7ab50e4..a1793c545 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBid.java @@ -62,7 +62,7 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe // Bid amount field addLabel(new GuiNpcLabel(3, "auction.bid.yourBid", guiLeft + 15, y + 4)); GuiNpcTextField bidField = new GuiNpcTextField(txtBidId, this, fontRendererObj, - guiLeft + 80, y, 100, 18, String.valueOf(minimumBid)); + guiLeft + 80, y, 100, 18, "" + minimumBid); bidField.setIntegersOnly(); addTextField(bidField); y += 28; @@ -146,9 +146,9 @@ public class SubGuiAuctionBid extends SubGuiInterface implements ITextfieldListe } private String formatCurrency(long amount) { - if (amount < 1000) return String.valueOf(amount); + if (amount < 1000) return "" + amount; StringBuilder sb = new StringBuilder(); - String str = String.valueOf(amount); + String str = "" + amount; int count = 0; for (int i = str.length() - 1; i >= 0; i--) { if (count > 0 && count % 3 == 0) sb.insert(0, ','); diff --git a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java index f4061299b..b63471047 100644 --- a/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java +++ b/src/main/java/noppes/npcs/client/gui/player/SubGuiAuctionBuyNow.java @@ -107,9 +107,9 @@ public class SubGuiAuctionBuyNow extends SubGuiInterface { } private String formatCurrency(long amount) { - if (amount < 1000) return String.valueOf(amount); + if (amount < 1000) return "" + amount; StringBuilder sb = new StringBuilder(); - String str = String.valueOf(amount); + String str = "" + amount; int count = 0; for (int i = str.length() - 1; i >= 0; i--) { if (count > 0 && count % 3 == 0) sb.insert(0, ','); diff --git a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java index 7ffb7aff2..f9e627fe7 100644 --- a/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java +++ b/src/main/java/noppes/npcs/client/gui/player/inventory/GuiProfiles.java @@ -180,7 +180,7 @@ public class GuiProfiles extends GuiCNPCInventory implements ISubGuiListener, IC if (slot.isTemporary()) { label = StatCollector.translateToLocal("profile.temporary"); - result = (String.valueOf(slot.isTemporary())).toUpperCase(); + result = ("" + slot.isTemporary()).toUpperCase(); labelColor = 0xfc60dd; resultColor = 0x35fc81; fontRendererObj.drawString(label, xLabel, y, labelColor, false); diff --git a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java index 6c4745d42..7b54518e3 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java +++ b/src/main/java/noppes/npcs/client/gui/roles/GuiNpcTraderSetup.java @@ -56,7 +56,7 @@ public class GuiNpcTraderSetup extends GuiContainerNPCInterface2 implements ITex int x = guiLeft + i % 3 * 130 + 15; // First position after "$" label int y = guiTop + i / 3 * 22 + 14; long cost = role.getCurrencyCost(i); - GuiNpcTextField field = new GuiNpcTextField(CURRENCY_FIELD_ID_START + i, this, x, y, 36, 16, cost > 0 ? String.valueOf(cost) : ""); + GuiNpcTextField field = new GuiNpcTextField(CURRENCY_FIELD_ID_START + i, this, x, y, 36, 16, cost > 0 ? "" + cost : ""); field.setMaxStringLength(8); field.setIntegersOnly(); addTextField(field); diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java index 2e369b7e6..4e88c9b21 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStock.java @@ -95,7 +95,7 @@ public class SubGuiNpcTraderStock extends SubGuiInterface implements ISubGuiList return "-1"; // Mixed values, return unlimited } } - return String.valueOf(first); + return "" + first; } @Override diff --git a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java index b4a76ba48..c9d40c5df 100644 --- a/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java +++ b/src/main/java/noppes/npcs/client/gui/roles/SubGuiNpcTraderStockSlots.java @@ -47,7 +47,7 @@ public class SubGuiNpcTraderStockSlots extends SubGuiInterface implements ITextf // Max stock textfield int maxStock = role.stock.maxStock[i]; - String value = maxStock < 0 ? "-1" : String.valueOf(maxStock); + String value = maxStock < 0 ? "-1" : "" + maxStock; GuiNpcTextField field = new GuiNpcTextField(i, this, fontRendererObj, x + 42, y, 32, 18, value); field.setIntegersOnly(); field.setMinMaxDefault(-1, Integer.MAX_VALUE, -1); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java b/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java index c9c44ff93..ea8dcc0b5 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiAbilityInterface.java @@ -169,7 +169,7 @@ public class GuiAbilityInterface extends GuiNPCInterface2 { GL11.glPushMatrix(); // Move to NPC render position on screen - GL11.glTranslatef(npcScreenX, npcScreenY, 50F); + GL11.glTranslatef(npcScreenX, npcScreenY, 300F); // Apply zoom (scale) GL11.glScalef(-CAMERA_ZOOM, CAMERA_ZOOM, CAMERA_ZOOM); diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java index 77be0dd7c..be148c11d 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiScriptTextArea.java @@ -477,7 +477,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Dynamically calculate gutter width based on line count digits if (container != null && container.linesCount > 0) { int maxLineNum = container.linesCount; - String maxLineStr = String.valueOf(maxLineNum); + String maxLineStr = "" + maxLineNum; int digitWidth = ClientProxy.Font.width(maxLineStr); LINE_NUMBER_GUTTER_WIDTH = digitWidth + 10; // 10px total padding (5px left + 5px right) } @@ -645,7 +645,7 @@ public class GuiScriptTextArea extends GuiNpcTextField { // Render LINE GUTTER numbers for (int i = renderStart; i <= renderEnd; i++) { int posY = y + (i - scroll.getScrolledLine()) * container.lineHeight + stringYOffset; - String lineNum = String.valueOf(i + 1); + String lineNum = "" + (i + 1); int lineNumWidth = ClientProxy.Font.width(lineNum); int lineNumX = x + LINE_NUMBER_GUTTER_WIDTH - lineNumWidth - 5; // right-align with 5px padding int lineNumY = posY + 1; diff --git a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java index 7ebc098be..5d9cfdaa0 100644 --- a/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java +++ b/src/main/java/noppes/npcs/client/gui/util/key/OverlayKeyPresetViewer.java @@ -361,7 +361,7 @@ public class OverlayKeyPresetViewer { GL11.glPushMatrix(); GL11.glScalef(scale, scale, 1); GL11.glTranslatef(letterX / scale, nameY / scale, 1); - font.drawStringWithShadow(String.valueOf(letter), 0, 0, isDefault ? 0xff8a8a8a : 0xffffffff); + font.drawStringWithShadow("" + letter, 0, 0, isDefault ? 0xff8a8a8a : 0xffffffff); GL11.glPopMatrix(); //////////////////////////////////////////// diff --git a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java index f054f32a7..e3e023dee 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/GoToLineDialog.java @@ -470,7 +470,7 @@ public class GoToLineDialog { // Type character - only allow digits and colon if (ChatAllowedCharacters.isAllowedCharacter(c)) { if (Character.isDigit(c) || c == ':') { - insertText(String.valueOf(c)); + insertText("" + c); markActivity(); return true; } diff --git a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java index 3d5c9909d..81c346f8f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java +++ b/src/main/java/noppes/npcs/client/gui/util/script/RenameRefactorHandler.java @@ -366,7 +366,7 @@ public class RenameRefactorHandler { if (isValidIdentifierChar(c, isFirstChar)) { if (hasSelection) { // Replace entire selection with this character - currentWord = String.valueOf(c); + currentWord = "" + c; applyLiveRename(); callback.setCursorPosition(primaryOccurrenceStart + 1); sel.setSelection(0, 0); // Clear selection diff --git a/src/main/java/noppes/npcs/constants/EnumScrollData.java b/src/main/java/noppes/npcs/constants/EnumScrollData.java index baf994b4e..06d5a6ec3 100644 --- a/src/main/java/noppes/npcs/constants/EnumScrollData.java +++ b/src/main/java/noppes/npcs/constants/EnumScrollData.java @@ -7,7 +7,7 @@ public enum EnumScrollData { DIALOG_GROUP, QUEST_GROUP, ABILITY_TYPES, - ABILITIES, + CUSTOM_ABILITIES, ANIMATIONS, BUILTIN_ANIMATIONS } diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java new file mode 100644 index 000000000..45f9dfe44 --- /dev/null +++ b/src/main/java/noppes/npcs/controllers/data/PlayerAbilityData.java @@ -0,0 +1,503 @@ +package noppes.npcs.controllers.data; + +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.ability.AbilityPhase; +import kamkeel.npcs.controllers.data.ability.UserType; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket; +import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.nbt.NBTTagList; +import net.minecraft.nbt.NBTTagString; +import net.minecraft.util.DamageSource; +import noppes.npcs.api.ability.IPlayerAbilityData; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; + +import kamkeel.npcs.network.packets.data.ability.PlayerAbilitySyncPacket; + +import java.util.ArrayList; +import java.util.List; + +/** + * Manages player ability data - unlocked abilities, selection, cooldowns, and execution. + * Players reference abilities by key (built-in keys or preset names), never customized copies. + *

+ * Key differences from NPC DataAbilities: + * - Universal cooldown (shared across all abilities) + * - Player manually selects ability (no weighted random) + * - Conditions requiring a target are skipped + * - Uses ability's base cooldownTicks only + */ +public class PlayerAbilityData implements IPlayerAbilityData { + + private final PlayerData playerData; + + // ═══════════════════════════════════════════════════════════════════ + // CONFIGURATION (saved to NBT) + // ═══════════════════════════════════════════════════════════════════ + + /** + * List of unlocked ability keys (preset names or built-in keys). + * Order matters - determines selection index. + */ + private List unlockedAbilities = new ArrayList<>(); + + /** + * Currently selected ability index in the unlocked list. + */ + private int selectedIndex = 0; + + // ═══════════════════════════════════════════════════════════════════ + // RUNTIME STATE (not saved) + // ═══════════════════════════════════════════════════════════════════ + + /** + * Universal cooldown end time (shared across all abilities). + */ + private transient long cooldownEndTime = 0; + + /** + * Currently executing ability instance (null if none). + */ + private transient Ability currentAbility; + + /** + * Current ability's key (for cooldown tracking after completion). + */ + private transient String currentAbilityKey; + + /** + * Target for the current ability (can be null for self-targeting). + */ + private transient EntityLivingBase currentTarget; + + // ═══════════════════════════════════════════════════════════════════ + // CONSTRUCTOR + // ═══════════════════════════════════════════════════════════════════ + + public PlayerAbilityData(PlayerData playerData) { + this.playerData = playerData; + } + + // ═══════════════════════════════════════════════════════════════════ + // TICK - Called from player tick handler + // ═══════════════════════════════════════════════════════════════════ + + /** + * Main tick method - handles ability execution. + * Should be called every server tick. + */ + public void tick(EntityPlayer player) { + if (player.worldObj.isRemote || player.isDead) { + return; + } + + if (currentAbility != null && currentAbility.isExecuting()) { + tickCurrentAbility(player); + } + } + + /** + * Tick the currently executing ability. + */ + private void tickCurrentAbility(EntityPlayer player) { + AbilityPhase oldPhase = currentAbility.getPhase(); + + // Tick advances time and possibly changes phase + boolean phaseChanged = currentAbility.tick(); + + switch (currentAbility.getPhase()) { + case WINDUP: + currentAbility.onWindUpTick(player, currentTarget, player.worldObj, currentAbility.getCurrentTick()); + break; + + case ACTIVE: + if (phaseChanged && oldPhase == AbilityPhase.WINDUP) { + // Just entered ACTIVE phase + TelegraphInstance telegraph = currentAbility.getTelegraphInstance(); + if (telegraph != null) { + telegraph.lockPosition(); + } + removeTelegraph(currentAbility, player); + + // Play active sound and animation + playAbilitySound(player, currentAbility.getActiveSound()); + playAbilityAnimation(currentAbility.getActiveAnimation()); + + // Call onExecute + currentAbility.onExecute(player, currentTarget, player.worldObj); + + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(player); + return; + } + } + currentAbility.onActiveTick(player, currentTarget, player.worldObj, currentAbility.getCurrentTick()); + + if (currentAbility.getPhase() == AbilityPhase.IDLE) { + handleAbilityCompletion(player); + return; + } + break; + + case DAZED: + // Player is dazed - just wait + break; + + case IDLE: + handleAbilityCompletion(player); + break; + } + } + + /** + * Handle ability completion. + */ + private void handleAbilityCompletion(EntityPlayer player) { + if (currentAbility == null) return; + + currentAbility.onComplete(player, currentTarget); + + // Apply universal cooldown (using base cooldownTicks only, not min/max random) + if (!currentAbility.isIgnoreCooldown()) { + cooldownEndTime = player.worldObj.getTotalWorldTime() + currentAbility.getCooldownTicks(); + } + + // Stop animation + stopAbilityAnimation(); + + currentAbility = null; + currentAbilityKey = null; + currentTarget = null; + } + + // ═══════════════════════════════════════════════════════════════════ + // ABILITY ACTIVATION - Called on Special Key press + // ═══════════════════════════════════════════════════════════════════ + + /** + * Activate the currently selected ability. + * Called when the player presses the Special Key. + * + * @param player The player + * @return true if the ability was started + */ + public boolean activateAbility(EntityPlayer player) { + if (player.worldObj.isRemote) return false; + if (unlockedAbilities.isEmpty()) return false; + if (selectedIndex < 0 || selectedIndex >= unlockedAbilities.size()) return false; + + String key = unlockedAbilities.get(selectedIndex); + return activateAbility(player, key); + } + + /** + * Activate a specific ability by key. + * + * @param player The player + * @param key The ability key (built-in or preset name) + * @return true if the ability was started + */ + public boolean activateAbility(EntityPlayer player, String key) { + if (player.worldObj.isRemote || key == null || key.isEmpty()) return false; + + // Can't activate if already executing + if (currentAbility != null && currentAbility.isExecuting()) return false; + + // Resolve ability from controller + if (AbilityController.Instance == null) return false; + Ability ability = AbilityController.Instance.resolveAbility(key); + if (ability == null) return false; + + // Check user type allows player + if (!ability.getAllowedBy().allowsPlayer()) return false; + + // Check universal cooldown + if (!ability.isIgnoreCooldown() && isOnCooldown(player)) return false; + + // Check conditions (skip target-requiring ones) + if (!ability.checkConditionsForPlayer(player)) return false; + + // Start the ability + currentAbility = ability; + currentAbilityKey = key; + currentTarget = null; // Players don't have auto-targets + ability.start(null); + + // Spawn telegraph + spawnTelegraph(ability, player, null); + + // Play wind up sound and animation + playAbilitySound(player, ability.getWindUpSound()); + playAbilityAnimation(ability.getWindUpAnimation()); + + return true; + } + + // ═══════════════════════════════════════════════════════════════════ + // CLIENT SYNC + // ═══════════════════════════════════════════════════════════════════ + + /** + * Sync ability data to the client. + * Called when abilities change (unlock/lock/select) and on login. + */ + public void syncToClient() { + EntityPlayer player = playerData.player; + if (player instanceof EntityPlayerMP) { + PlayerAbilitySyncPacket.sendToPlayer((EntityPlayerMP) player); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // SELECTION + // ═══════════════════════════════════════════════════════════════════ + + public int getSelectedIndex() { + return selectedIndex; + } + + public void setSelectedIndex(int index) { + if (index >= 0 && index < unlockedAbilities.size()) { + this.selectedIndex = index; + syncToClient(); + } + } + + public void selectNext() { + if (unlockedAbilities.isEmpty()) return; + selectedIndex = (selectedIndex + 1) % unlockedAbilities.size(); + syncToClient(); + } + + public void selectPrevious() { + if (unlockedAbilities.isEmpty()) return; + selectedIndex = (selectedIndex - 1 + unlockedAbilities.size()) % unlockedAbilities.size(); + syncToClient(); + } + + public String getSelectedAbilityKey() { + if (unlockedAbilities.isEmpty() || selectedIndex < 0 || selectedIndex >= unlockedAbilities.size()) { + return null; + } + return unlockedAbilities.get(selectedIndex); + } + + // ═══════════════════════════════════════════════════════════════════ + // UNLOCKED ABILITIES + // ═══════════════════════════════════════════════════════════════════ + + public List getUnlockedAbilityList() { + return unlockedAbilities; + } + + @Override + public String[] getUnlockedAbilities() { + return unlockedAbilities.toArray(new String[0]); + } + + public void setUnlockedAbilities(List abilities) { + this.unlockedAbilities = abilities != null ? new ArrayList<>(abilities) : new ArrayList<>(); + // Clamp selected index + if (selectedIndex >= unlockedAbilities.size()) { + selectedIndex = Math.max(0, unlockedAbilities.size() - 1); + } + syncToClient(); + } + + public void unlockAbility(String key) { + if (key != null && !key.isEmpty() && !unlockedAbilities.contains(key)) { + unlockedAbilities.add(key); + syncToClient(); + } + } + + public void lockAbility(String key) { + unlockedAbilities.remove(key); + if (selectedIndex >= unlockedAbilities.size()) { + selectedIndex = Math.max(0, unlockedAbilities.size() - 1); + } + syncToClient(); + } + + public boolean hasUnlockedAbility(String key) { + return unlockedAbilities.contains(key); + } + + // ═══════════════════════════════════════════════════════════════════ + // COOLDOWNS + // ═══════════════════════════════════════════════════════════════════ + + public boolean isOnCooldown(EntityPlayer player) { + return player.worldObj.getTotalWorldTime() < cooldownEndTime; + } + + /** + * Check if a specific ability key is on cooldown. + * Currently uses universal cooldown (same for all abilities). + */ + public boolean isOnCooldown(String key, EntityPlayer player) { + return isOnCooldown(player); + } + + public long getRemainingCooldown(EntityPlayer player) { + long remaining = cooldownEndTime - player.worldObj.getTotalWorldTime(); + return remaining > 0 ? remaining : 0; + } + + /** + * Reset cooldown for a specific ability key. + * Currently resets the universal cooldown. + */ + public void resetCooldown(String key) { + cooldownEndTime = 0; + } + + /** + * Reset all cooldowns. + */ + public void resetAllCooldowns() { + cooldownEndTime = 0; + } + + @Override + public boolean activateAbility() { + return activateAbility(playerData.player); + } + + @Override + public boolean activateAbility(String key) { + return activateAbility(playerData.player, key); + } + + // ═══════════════════════════════════════════════════════════════════ + // STATE QUERIES + // ═══════════════════════════════════════════════════════════════════ + + public boolean isExecutingAbility() { + return currentAbility != null && currentAbility.isExecuting(); + } + + public Ability getCurrentAbility() { + return currentAbility; + } + + public void interruptCurrentAbility() { + if (currentAbility != null && currentAbility.isExecuting()) { + currentAbility.interrupt(); + stopAbilityAnimation(); + currentAbility = null; + currentAbilityKey = null; + currentTarget = null; + } + } + + @Override + public boolean isOnCooldown() { + EntityPlayer player = playerData.player; + if (player == null) return false; + return isOnCooldown(player); + } + + @Override + public boolean isOnCooldown(String key) { + EntityPlayer player = playerData.player; + if (player == null) return false; + return isOnCooldown(key, player); + } + + @Override + public void resetCooldown() { + cooldownEndTime = 0; + } + + /** + * Check if the player's movement should be locked due to an executing ability. + * Used by the tick handler to restrict player movement during ability phases. + */ + public boolean isMovementLocked() { + return currentAbility != null && currentAbility.isExecuting() + && currentAbility.isMovementLockedForCurrentPhase(); + } + + // ═══════════════════════════════════════════════════════════════════ + // TELEGRAPH + // ═══════════════════════════════════════════════════════════════════ + + private void spawnTelegraph(Ability ability, EntityPlayer player, EntityLivingBase target) { + TelegraphInstance telegraph = ability.createTelegraph(player, target); + if (telegraph != null) { + ability.setTelegraphInstance(telegraph); + if (player instanceof EntityPlayerMP) { + TelegraphSpawnPacket.sendToTracking(telegraph, player); + } + } + } + + private void removeTelegraph(Ability ability, EntityPlayer player) { + TelegraphInstance telegraph = ability.getTelegraphInstance(); + if (telegraph != null) { + if (player instanceof EntityPlayerMP) { + TelegraphRemovePacket.sendToTracking(telegraph.getInstanceId(), player); + } + ability.setTelegraphInstance(null); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // ANIMATION + // ═══════════════════════════════════════════════════════════════════ + + private void playAbilityAnimation(Animation animation) { + if (animation == null || AnimationController.Instance == null) return; + playerData.animationData.setEnabled(true); + playerData.animationData.setAnimation(animation); + playerData.animationData.updateClient(); + } + + private void stopAbilityAnimation() { + playerData.animationData.setAnimation(null); + playerData.animationData.updateClient(); + } + + private void playAbilitySound(EntityPlayer player, String sound) { + if (sound != null && !sound.isEmpty()) { + player.worldObj.playSoundAtEntity(player, sound, 1.0f, 1.0f); + } + } + + // ═══════════════════════════════════════════════════════════════════ + // NBT SERIALIZATION + // ═══════════════════════════════════════════════════════════════════ + + public void writeToNBT(NBTTagCompound compound) { + NBTTagList list = new NBTTagList(); + for (String key : unlockedAbilities) { + list.appendTag(new NBTTagString(key)); + } + compound.setTag("PlayerAbilities", list); + compound.setInteger("PlayerAbilitySelected", selectedIndex); + } + + public void readFromNBT(NBTTagCompound compound) { + unlockedAbilities.clear(); + if (compound.hasKey("PlayerAbilities")) { + NBTTagList list = compound.getTagList("PlayerAbilities", 8); // 8 = string tag + for (int i = 0; i < list.tagCount(); i++) { + String key = list.getStringTagAt(i); + if (key != null && !key.isEmpty()) { + unlockedAbilities.add(key); + } + } + } + selectedIndex = compound.getInteger("PlayerAbilitySelected"); + if (selectedIndex >= unlockedAbilities.size()) { + selectedIndex = Math.max(0, unlockedAbilities.size() - 1); + } + } +} diff --git a/src/main/java/noppes/npcs/controllers/data/PlayerData.java b/src/main/java/noppes/npcs/controllers/data/PlayerData.java index 04e06a41e..fa4e7d961 100644 --- a/src/main/java/noppes/npcs/controllers/data/PlayerData.java +++ b/src/main/java/noppes/npcs/controllers/data/PlayerData.java @@ -15,6 +15,7 @@ import net.minecraft.world.World; import net.minecraftforge.common.IExtendedEntityProperties; import noppes.npcs.CustomNpcs; import noppes.npcs.LogWriter; +import noppes.npcs.api.ability.IPlayerAbilityData; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.handler.IPlayerBankData; import noppes.npcs.api.handler.IPlayerTradeData; @@ -65,6 +66,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { // Trade data (currency + auction claims) - shared across all profile slots public PlayerTradeData tradeData = new PlayerTradeData(this); + public PlayerAbilityData abilityData = new PlayerAbilityData(this); public ActionManager actionManager = new ActionManager(); public PlayerDataScript scriptData; @@ -108,6 +110,9 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { if (!controller.playerEffects.containsKey(playerID)) { controller.playerEffects.put(playerID, effectData.getEffects()); } + + // Sync player ability data to client + abilityData.syncToClient(); } public void onLogout() { @@ -137,6 +142,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { effectData.readFromNBT(data); magicData.readToNBT(data); tradeData.readFromNBT(data); + abilityData.readFromNBT(data); if (player != null) { playername = player.getCommandSenderName(); @@ -180,6 +186,7 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { effectData.writeToNBT(compound); magicData.writeToNBT(compound); tradeData.writeToNBT(compound); + abilityData.writeToNBT(compound); compound.setString("PlayerName", playername); compound.setString("UUID", uuid); @@ -411,6 +418,10 @@ public class PlayerData implements IExtendedEntityProperties, IPlayerData { return tradeData; } + public IPlayerAbilityData getAbilityData() { + return abilityData; + } + public synchronized void save() { // Don't Save this is a Modification of a Profile's PlayerData Profile profile = ProfileController.Instance.getProfile(UUID.fromString(uuid)); diff --git a/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java b/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java new file mode 100644 index 000000000..cb7def405 --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/event/player/PlayerAbilityEvent.java @@ -0,0 +1,101 @@ +package noppes.npcs.scripted.event.player; + +import cpw.mods.fml.common.eventhandler.Cancelable; +import kamkeel.npcs.controllers.data.ability.Ability; +import net.minecraft.entity.EntityLivingBase; +import noppes.npcs.api.ability.IAbility; +import noppes.npcs.api.entity.IEntityLivingBase; +import noppes.npcs.api.entity.IPlayer; +import noppes.npcs.api.event.IPlayerAbilityEvent; +import noppes.npcs.constants.EnumScriptType; +import noppes.npcs.scripted.NpcAPI; + +/** + * Events fired during player ability execution lifecycle. + */ +public class PlayerAbilityEvent extends PlayerEvent implements IPlayerAbilityEvent { + protected final Ability ability; + protected final IEntityLivingBase target; + + public PlayerAbilityEvent(IPlayer player, Ability ability, EntityLivingBase target) { + super(player); + this.ability = ability; + this.target = target != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(target) : null; + } + + @Override + public IAbility getAbility() { + return ability; + } + + @Override + public IEntityLivingBase getTarget() { + return target; + } + + @Override + public String getHookName() { + return "playerAbilityEvent"; + } + + /** + * Fired when a player's ability hits an entity with damage. + * Canceling prevents the damage from being applied. + */ + @Cancelable + public static class HitEvent extends PlayerAbilityEvent implements IPlayerAbilityEvent.HitEvent { + private final IEntityLivingBase hitEntity; + private float damage; + private float knockback; + private float knockbackUp; + + public HitEvent(IPlayer player, Ability ability, EntityLivingBase target, + EntityLivingBase hitEntity, float damage, float knockback, float knockbackUp) { + super(player, ability, target); + this.hitEntity = hitEntity != null ? (IEntityLivingBase) NpcAPI.Instance().getIEntity(hitEntity) : null; + this.damage = damage; + this.knockback = knockback; + this.knockbackUp = knockbackUp; + } + + @Override + public IEntityLivingBase getHitEntity() { + return hitEntity; + } + + @Override + public float getDamage() { + return damage; + } + + @Override + public void setDamage(float damage) { + this.damage = damage; + } + + @Override + public float getKnockback() { + return knockback; + } + + @Override + public void setKnockback(float knockback) { + this.knockback = knockback; + } + + @Override + public float getKnockbackUp() { + return knockbackUp; + } + + @Override + public void setKnockbackUp(float knockbackUp) { + this.knockbackUp = knockbackUp; + } + + @Override + public String getHookName() { + return EnumScriptType.ABILITY_HIT.function; + } + } +} diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java index d097a892c..6cbbb4dae 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java @@ -43,6 +43,11 @@ public class ScriptDataAbilities implements IDataAbilities { } } + @Override + public void addAbilityReference(String key) { + data.addAbilityReference(key); + } + @Override public void removeAbility(String abilityId) { data.removeAbility(abilityId); @@ -58,6 +63,33 @@ public class ScriptDataAbilities implements IDataAbilities { return data.getAbility(abilityId) != null; } + @Override + public boolean isAbilityReference(String abilityId) { + List slots = data.getAbilitySlots(); + for (int i = 0; i < slots.size(); i++) { + kamkeel.npcs.controllers.data.ability.AbilitySlot slot = slots.get(i); + if (slot.isReference()) { + if (slot.getReferenceId().equals(abilityId)) return true; + } else { + Ability a = slot.getAbility(); + if (a != null && a.getId().equals(abilityId)) return false; + } + } + return false; + } + + @Override + public boolean convertToInline(String abilityId) { + List slots = data.getAbilitySlots(); + for (int i = 0; i < slots.size(); i++) { + kamkeel.npcs.controllers.data.ability.AbilitySlot slot = slots.get(i); + if (slot.isReference() && slot.getReferenceId().equals(abilityId)) { + return data.convertToInline(i); + } + } + return false; + } + @Override public void clearAbilities() { data.clearAbilities(); @@ -117,17 +149,14 @@ public class ScriptDataAbilities implements IDataAbilities { } @Override - public boolean executePresetAbility(String presetName) { - return executePresetAbility(presetName, null); + public boolean executeAbility(String key) { + return executeAbility(key, null); } @Override - public boolean executePresetAbility(String presetName, Object target) { - // Resolve target + public boolean executeAbility(String key, Object target) { EntityLivingBase targetEntity = resolveTarget(target); - - // Use the direct preset execution method - return data.executePresetAbility(presetName, targetEntity); + return data.executeAbility(key, targetEntity); } @Override diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java new file mode 100644 index 000000000..2570653fc --- /dev/null +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java @@ -0,0 +1,112 @@ +package noppes.npcs.scripted.wrapper; + +import net.minecraft.entity.player.EntityPlayer; +import noppes.npcs.api.ability.IAbility; +import noppes.npcs.api.ability.IPlayerAbilityData; +import noppes.npcs.controllers.data.PlayerAbilityData; +import noppes.npcs.controllers.data.PlayerData; + +/** + * Script wrapper for PlayerAbilityData providing API access to player abilities. + */ +public class ScriptPlayerAbilityData implements IPlayerAbilityData { + + private final PlayerAbilityData data; + private final PlayerData playerData; + + public ScriptPlayerAbilityData(PlayerData playerData) { + this.playerData = playerData; + this.data = playerData.abilityData; + } + + @Override + public String[] getUnlockedAbilities() { + return data.getUnlockedAbilities().toArray(new String[0]); + } + + @Override + public void unlockAbility(String key) { + data.unlockAbility(key); + } + + @Override + public void lockAbility(String key) { + data.lockAbility(key); + } + + @Override + public boolean hasUnlockedAbility(String key) { + return data.hasUnlockedAbility(key); + } + + @Override + public int getSelectedIndex() { + return data.getSelectedIndex(); + } + + @Override + public void setSelectedIndex(int index) { + data.setSelectedIndex(index); + } + + @Override + public String getSelectedAbilityKey() { + return data.getSelectedAbilityKey(); + } + + @Override + public void selectNext() { + data.selectNext(); + } + + @Override + public void selectPrevious() { + data.selectPrevious(); + } + + @Override + public boolean isExecutingAbility() { + return data.isExecutingAbility(); + } + + @Override + public IAbility getCurrentAbility() { + return data.getCurrentAbility(); + } + + @Override + public void interruptCurrentAbility() { + data.interruptCurrentAbility(); + } + + @Override + public boolean isOnCooldown(String key) { + EntityPlayer player = playerData.player; + if (player == null) return false; + return data.isOnCooldown(key, player); + } + + @Override + public void resetCooldown(String key) { + data.resetCooldown(key); + } + + @Override + public void resetAllCooldowns() { + data.resetAllCooldowns(); + } + + @Override + public boolean activateAbility() { + EntityPlayer player = playerData.player; + if (player == null) return false; + return data.activateAbility(player); + } + + @Override + public boolean activateAbility(String key) { + EntityPlayer player = playerData.player; + if (player == null) return false; + return data.activateAbility(player, key); + } +} From 2cac2e4add829beab330955ff8993aa08d3115eb Mon Sep 17 00:00:00 2001 From: Vidal Date: Fri, 30 Jan 2026 12:37:57 -0300 Subject: [PATCH 315/358] Changed Special Key from C to B --- src/main/java/noppes/npcs/client/ClientProxy.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/client/ClientProxy.java b/src/main/java/noppes/npcs/client/ClientProxy.java index 01c226812..705e261c5 100644 --- a/src/main/java/noppes/npcs/client/ClientProxy.java +++ b/src/main/java/noppes/npcs/client/ClientProxy.java @@ -320,7 +320,7 @@ public class ClientProxy extends CommonProxy { Minecraft mc = Minecraft.getMinecraft(); NPCButton = new KeyBinding("NPC Inventory", Keyboard.KEY_N, "key.categories.customnpc"); - SpecialKey = new KeyBinding("key.customnpcs.special", Keyboard.KEY_C, "key.categories.customnpc"); + SpecialKey = new KeyBinding("key.customnpcs.special", Keyboard.KEY_B, "key.categories.customnpc"); ClientRegistry.registerKeyBinding(NPCButton); ClientRegistry.registerKeyBinding(SpecialKey); From 01e06c22ac31b48d4144b211ebc056b721582538 Mon Sep 17 00:00:00 2001 From: Vidal Date: Fri, 30 Jan 2026 13:30:27 -0300 Subject: [PATCH 316/358] Added alpha to the head and tail of the beam --- .../npcs/client/renderer/RenderAbilityBeam.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java index 2fcb58d54..4912e4b72 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityBeam.java @@ -63,6 +63,7 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { float headSize = beam.getHeadSize(); int innerColor = beam.getInnerColor(); int outerColor = beam.getOuterColor(); + float outerAlpha = beam.getOuterColorAlpha(); // Render everything relative to origin position GL11.glPushMatrix(); @@ -74,7 +75,7 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { // Render tail orb at origin (0,0,0) - only in anchored mode // Don't render if head is too close to origin (would cause overlap/extra orb appearance) if (beam.shouldRenderTailOrb() && headDistFromOrigin > headSize * 0.5) { - renderTailOrb(headSize * 0.8f, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth()); + renderTailOrb(headSize * 0.8f, innerColor, outerColor, beam.isOuterColorEnabled(), beam.getOuterColorWidth(), outerAlpha); } // Render trail segments with smooth connections (with fading for non-anchored) @@ -85,7 +86,7 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { } // Render head at interpolated offset position - renderHead(beam, headOffsetX, headOffsetY, headOffsetZ, headSize, innerColor, outerColor, partialTicks); + renderHead(beam, headOffsetX, headOffsetY, headOffsetZ, headSize, innerColor, outerColor, outerAlpha, partialTicks); GL11.glPopMatrix(); @@ -128,10 +129,11 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { // Render outer glow only if enabled if (beam.isOuterColorEnabled()) { float outerScale = innerScale + beam.getOuterColorWidth(); + float outerAlpha = beam.getOuterColorAlpha(); GL11.glDepthMask(false); GL11.glPushMatrix(); GL11.glScalef(outerScale, outerScale, outerScale); - renderCube(outerColor, 0.5f, 0.5f); + renderCube(outerColor, outerAlpha, 0.5f); GL11.glPopMatrix(); GL11.glDepthMask(true); } @@ -148,7 +150,7 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { * Render the tail orb at the origin (where the beam emanates from). * This orb does not rotate - it's a stable anchor point. */ - private void renderTailOrb(float size, int innerColor, int outerColor, boolean outerColorEnabled, float outerColorWidth) { + private void renderTailOrb(float size, int innerColor, int outerColor, boolean outerColorEnabled, float outerColorWidth, float outerAlpha) { GL11.glPushMatrix(); // Already at origin (0,0,0) @@ -162,7 +164,7 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { GL11.glDepthMask(false); GL11.glPushMatrix(); GL11.glScalef(size * outerScale, size * outerScale, size * outerScale); - renderCube(outerColor, 0.4f, 0.5f); + renderCube(outerColor, outerAlpha, 0.5f); GL11.glPopMatrix(); GL11.glDepthMask(true); } @@ -532,7 +534,7 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { * Coordinates are relative to origin. */ private void renderHead(EntityAbilityBeam beam, double x, double y, double z, - float size, int innerColor, int outerColor, float partialTicks) { + float size, int innerColor, int outerColor, float outerAlpha, float partialTicks) { GL11.glPushMatrix(); GL11.glTranslated(x, y, z); @@ -564,7 +566,7 @@ public class RenderAbilityBeam extends RenderAbilityProjectile { GL11.glDepthMask(false); GL11.glPushMatrix(); GL11.glScalef(outerScale, outerScale, outerScale); - renderCube(outerColor, 0.5f, 0.5f); + renderCube(outerColor, outerAlpha, 0.5f); GL11.glPopMatrix(); GL11.glDepthMask(true); } From d9bf5132e7f72a607df540a3850fd9f849a37474 Mon Sep 17 00:00:00 2001 From: Vidal Date: Fri, 30 Jan 2026 13:46:53 -0300 Subject: [PATCH 317/358] Updated old anchor point method calls --- .../npcs/controllers/data/ability/type/AbilityDisc.java | 4 ++-- .../npcs/controllers/data/ability/type/AbilityEnergyBeam.java | 4 ++-- .../npcs/controllers/data/ability/type/AbilityOrb.java | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 74ce14e34..36bc898b2 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -260,10 +260,10 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } @Override - public int getAnchorPoint() { return anchorData.anchorPoint.getId(); } + public int getAnchorPoint() { return anchorData.anchorPoint.ordinal(); } @Override - public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromId(point); } + public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } @Override @SideOnly(Side.CLIENT) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 59e84f8f0..700f771c7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -263,10 +263,10 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } @Override - public int getAnchorPoint() { return anchorData.anchorPoint.getId(); } + public int getAnchorPoint() { return anchorData.anchorPoint.ordinal(); } @Override - public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromId(point); } + public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } @Override @SideOnly(Side.CLIENT) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index b489f4b9d..63c7c9005 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -396,12 +396,12 @@ public class AbilityOrb extends Ability implements IAbilityOrb { public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } public int getAnchorPoint() { - return anchorData.anchorPoint.getId(); + return anchorData.anchorPoint.ordinal(); } @Override public void setAnchorPoint(int point) { - this.anchorData.anchorPoint = AnchorPoint.fromId(point); + this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } @Override From 4a5b0f49956e745c04d12a1fe6064a7e235d563b Mon Sep 17 00:00:00 2001 From: Vidal Date: Fri, 30 Jan 2026 20:21:45 -0300 Subject: [PATCH 318/358] Updated overrides and imports --- .../java/kamkeel/npcs/command/AbilityCommand.java | 3 +++ .../npcs/scripted/wrapper/ScriptDataAbilities.java | 5 ++++- .../scripted/wrapper/ScriptPlayerAbilityData.java | 12 +++++++++++- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/kamkeel/npcs/command/AbilityCommand.java b/src/main/java/kamkeel/npcs/command/AbilityCommand.java index 499f9491d..d5b9420cc 100644 --- a/src/main/java/kamkeel/npcs/command/AbilityCommand.java +++ b/src/main/java/kamkeel/npcs/command/AbilityCommand.java @@ -4,6 +4,9 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; import net.minecraft.command.ICommandSender; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; import java.util.Set; import static kamkeel.npcs.util.ColorUtil.sendError; diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java index 6cbbb4dae..d0b5259ea 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptDataAbilities.java @@ -2,12 +2,15 @@ package noppes.npcs.scripted.wrapper; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; +import kamkeel.npcs.controllers.data.ability.AbilitySlot; import net.minecraft.entity.EntityLivingBase; import noppes.npcs.DataAbilities; import noppes.npcs.api.ability.IAbility; import noppes.npcs.api.ability.IDataAbilities; import noppes.npcs.entity.EntityNPCInterface; +import java.util.List; + /** * Script wrapper for DataAbilities providing API access to NPC abilities. */ @@ -65,7 +68,7 @@ public class ScriptDataAbilities implements IDataAbilities { @Override public boolean isAbilityReference(String abilityId) { - List slots = data.getAbilitySlots(); + List slots = data.getAbilitySlots(); for (int i = 0; i < slots.size(); i++) { kamkeel.npcs.controllers.data.ability.AbilitySlot slot = slots.get(i); if (slot.isReference()) { diff --git a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java index 2570653fc..a36c65d51 100644 --- a/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java +++ b/src/main/java/noppes/npcs/scripted/wrapper/ScriptPlayerAbilityData.java @@ -21,7 +21,7 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData { @Override public String[] getUnlockedAbilities() { - return data.getUnlockedAbilities().toArray(new String[0]); + return data.getUnlockedAbilities(); } @Override @@ -79,6 +79,11 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData { data.interruptCurrentAbility(); } + @Override + public boolean isOnCooldown() { + return data.isOnCooldown(); + } + @Override public boolean isOnCooldown(String key) { EntityPlayer player = playerData.player; @@ -86,6 +91,11 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData { return data.isOnCooldown(key, player); } + @Override + public void resetCooldown() { + data.resetCooldown(); + } + @Override public void resetCooldown(String key) { data.resetCooldown(key); From a8ca3293fdea207d6e03b9219dc3a309ffd0f850 Mon Sep 17 00:00:00 2001 From: Vidal Date: Fri, 30 Jan 2026 20:25:15 -0300 Subject: [PATCH 319/358] Removed wrong @param --- .../java/kamkeel/npcs/controllers/data/ability/Ability.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index 6f66614e0..e31b8f8fd 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -369,7 +369,7 @@ public abstract class Ability implements IAbility { * Create a telegraph instance for this ability. * Override for custom telegraph shapes. * - * @param npc The caster + * @param caster The caster * @param target The target (for position calculation) * @return The telegraph instance, or null if no telegraph */ From 5182a3474e2e7334e65f4b5adb7e5f20517075c9 Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Sat, 31 Jan 2026 04:58:27 +0100 Subject: [PATCH 320/358] Field handler for automatic GUI creation --- .../java/somehussar/gui/FieldConstraint.java | 5 + .../java/somehussar/gui/FieldConstraints.java | 27 ++++ src/main/java/somehussar/gui/GuiEditable.java | 13 ++ .../java/somehussar/gui/GuiFieldHandler.java | 128 ++++++++++++++++++ .../gui/constraints/ConstraintFactory.java | 10 ++ .../gui/constraints/NumericConstraint.java | 13 ++ .../constraints/NumericConstraintFactory.java | 13 ++ .../constraints/NumericRangeConstraint.java | 35 +++++ .../somehussar/gui/field/EditableField.java | 71 ++++++++++ 9 files changed, 315 insertions(+) create mode 100644 src/main/java/somehussar/gui/FieldConstraint.java create mode 100644 src/main/java/somehussar/gui/FieldConstraints.java create mode 100644 src/main/java/somehussar/gui/GuiEditable.java create mode 100644 src/main/java/somehussar/gui/GuiFieldHandler.java create mode 100644 src/main/java/somehussar/gui/constraints/ConstraintFactory.java create mode 100644 src/main/java/somehussar/gui/constraints/NumericConstraint.java create mode 100644 src/main/java/somehussar/gui/constraints/NumericConstraintFactory.java create mode 100644 src/main/java/somehussar/gui/constraints/NumericRangeConstraint.java create mode 100644 src/main/java/somehussar/gui/field/EditableField.java diff --git a/src/main/java/somehussar/gui/FieldConstraint.java b/src/main/java/somehussar/gui/FieldConstraint.java new file mode 100644 index 000000000..af46df61d --- /dev/null +++ b/src/main/java/somehussar/gui/FieldConstraint.java @@ -0,0 +1,5 @@ +package somehussar.gui; + +public interface FieldConstraint { + void validate(Object value); +} diff --git a/src/main/java/somehussar/gui/FieldConstraints.java b/src/main/java/somehussar/gui/FieldConstraints.java new file mode 100644 index 000000000..113cf4460 --- /dev/null +++ b/src/main/java/somehussar/gui/FieldConstraints.java @@ -0,0 +1,27 @@ +package somehussar.gui; + +public final class FieldConstraints { + + private static final FieldConstraints EMPTY = + new FieldConstraints(new FieldConstraint[0]); + + private final FieldConstraint[] constraints; + + public FieldConstraints(FieldConstraint[] constraints) { + this.constraints = constraints; + } + + public static FieldConstraints empty() { + return EMPTY; + } + + public void validate(Object value) { + for (FieldConstraint c : constraints) { + c.validate(value); + } + } + + public boolean isEmpty() { + return constraints.length == 0; + } +} diff --git a/src/main/java/somehussar/gui/GuiEditable.java b/src/main/java/somehussar/gui/GuiEditable.java new file mode 100644 index 000000000..b32d4f7c4 --- /dev/null +++ b/src/main/java/somehussar/gui/GuiEditable.java @@ -0,0 +1,13 @@ +package somehussar.gui; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface GuiEditable { + + @Retention(RetentionPolicy.RUNTIME) + @interface Field { + String name() default ""; + } +} diff --git a/src/main/java/somehussar/gui/GuiFieldHandler.java b/src/main/java/somehussar/gui/GuiFieldHandler.java new file mode 100644 index 000000000..f217e2b5a --- /dev/null +++ b/src/main/java/somehussar/gui/GuiFieldHandler.java @@ -0,0 +1,128 @@ +package somehussar.gui; + +import somehussar.gui.constraints.*; +import somehussar.gui.field.EditableField; + +import java.lang.annotation.Annotation; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class GuiFieldHandler { + + private static final Map, ClassMetadata> CACHE = new HashMap<>(); + private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup(); + + public static final Map, ConstraintFactory> registry = new HashMap<>(); + + static { + registry.put(NumericConstraint.class, new NumericConstraintFactory()); + } + + + private GuiFieldHandler() {} + + public static ClassMetadata getMetadata(Class type) { + return CACHE.computeIfAbsent(type, GuiFieldHandler::scanClass); + } + + + private static ClassMetadata scanClass(Class type) { + if (!type.isAnnotationPresent(GuiEditable.class)) { + return null; + } + + ClassMetadata parent = null; + + Class superCls = type.getSuperclass(); + if (superCls != null && superCls != Object.class) { + parent = getMetadata(superCls); + } + + List list = new ArrayList<>(); + for (Field field : type.getDeclaredFields()) { + EditableField editableField = createEditableField(field); + + if (editableField != null) { + list.add(editableField); + } + } + + return new ClassMetadata( + type, + list.toArray(list.toArray(new EditableField[0])), + parent + ); + } + + @SuppressWarnings({"RawUseOfParameterized", "unchecked"}) + private static EditableField createEditableField(Field field) { + try { + GuiEditable.Field fieldConfig = field.getAnnotation(GuiEditable.Field.class); + if (fieldConfig == null) return null; + if (Modifier.isFinal(field.getModifiers())) return null; + + field.setAccessible(true); + EditableField.Builder builder = new EditableField.Builder(); + builder.name(fieldConfig.name()); + builder.type(field.getType()); + builder.getter(LOOKUP.unreflectGetter(field)); + builder.setter(LOOKUP.unreflectSetter(field)); + builder.constraints(buildConstraints(field)); + + + return builder.build(); + } catch (IllegalAccessException e) { + throw new RuntimeException( + "Failed to bind field: " + field, e); + } + } + + @SuppressWarnings({"RawUseOfParameterized", "unchecked"}) + private static FieldConstraints buildConstraints(Field field) { + List list = new ArrayList<>(); + + for (Annotation ann : field.getAnnotations()) { + ConstraintFactory factory = registry.get(ann.annotationType()); + if (factory != null) { + FieldConstraint c = factory.create(field, ann); + list.add(c); + } + } + + if (list.isEmpty()) return FieldConstraints.empty(); + return new FieldConstraints(list.toArray(new FieldConstraint[0])); + } + + + @SuppressWarnings({"RawUseOfParameterized"}) + public static final class ClassMetadata { + final Class type; + final EditableField[] fields; // only declared fields + final ClassMetadata parent; // superclass metadata (nullable) + + ClassMetadata(Class type, EditableField[] fields, ClassMetadata parent) { + this.type = type; + this.fields = fields; + this.parent = parent; + } + + public ClassMetadata getParent() { + return parent; + } + + public EditableField[] getDeclaredFields() { + return fields; + } + + public Class getType() { + return type; + } + } + + +} diff --git a/src/main/java/somehussar/gui/constraints/ConstraintFactory.java b/src/main/java/somehussar/gui/constraints/ConstraintFactory.java new file mode 100644 index 000000000..efc3cb90a --- /dev/null +++ b/src/main/java/somehussar/gui/constraints/ConstraintFactory.java @@ -0,0 +1,10 @@ +package somehussar.gui.constraints; + +import somehussar.gui.FieldConstraint; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Field; + +public interface ConstraintFactory { + FieldConstraint create(Field field, A annotation); +} diff --git a/src/main/java/somehussar/gui/constraints/NumericConstraint.java b/src/main/java/somehussar/gui/constraints/NumericConstraint.java new file mode 100644 index 000000000..773129be3 --- /dev/null +++ b/src/main/java/somehussar/gui/constraints/NumericConstraint.java @@ -0,0 +1,13 @@ +package somehussar.gui.constraints; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(value = RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +public @interface NumericConstraint { + double min() default Double.NEGATIVE_INFINITY; + double max() default Double.POSITIVE_INFINITY; +} diff --git a/src/main/java/somehussar/gui/constraints/NumericConstraintFactory.java b/src/main/java/somehussar/gui/constraints/NumericConstraintFactory.java new file mode 100644 index 000000000..bd0a024e6 --- /dev/null +++ b/src/main/java/somehussar/gui/constraints/NumericConstraintFactory.java @@ -0,0 +1,13 @@ +package somehussar.gui.constraints; + +import somehussar.gui.FieldConstraint; + +import java.lang.reflect.Field; + +public class NumericConstraintFactory implements ConstraintFactory { + + @Override + public FieldConstraint create(Field field, NumericConstraint ann) { + return new NumericRangeConstraint(ann.min(), ann.max()); + } +} diff --git a/src/main/java/somehussar/gui/constraints/NumericRangeConstraint.java b/src/main/java/somehussar/gui/constraints/NumericRangeConstraint.java new file mode 100644 index 000000000..16bf95895 --- /dev/null +++ b/src/main/java/somehussar/gui/constraints/NumericRangeConstraint.java @@ -0,0 +1,35 @@ +package somehussar.gui.constraints; + +import somehussar.gui.FieldConstraint; + +public final class NumericRangeConstraint implements FieldConstraint { + + private final double min; + private final double max; + + public NumericRangeConstraint(double min, double max) { + this.min = min; + this.max = max; + } + + @Override + public void validate(Object value) { + if (!(value instanceof Number)) { + throw new ConstraintViolationException("Expected number, got " + value); + } + + double v = ((Number) value).doubleValue(); + if (v < min || v > max) { + throw new ConstraintViolationException( + "Value " + v + " must be between " + min + " and " + max + ); + } + } + + public static final class ConstraintViolationException extends RuntimeException { + public ConstraintViolationException(String message) { + super(message); + } + } + +} diff --git a/src/main/java/somehussar/gui/field/EditableField.java b/src/main/java/somehussar/gui/field/EditableField.java new file mode 100644 index 000000000..a1a076efd --- /dev/null +++ b/src/main/java/somehussar/gui/field/EditableField.java @@ -0,0 +1,71 @@ +package somehussar.gui.field; + +import somehussar.gui.FieldConstraints; + +import java.lang.invoke.MethodHandle; + +public final class EditableField { + + private final String name; + private final Class type; + private final MethodHandle getter; + private final MethodHandle setter; + private final FieldConstraints constraints; + + public String getName() { + return name; + } + + public Class getType() { + return type; + } + + @SuppressWarnings({"unchecked"}) + public T read(T instance) { + try { + return (T) getter.invoke(instance); + } catch (Throwable t) { + throw new RuntimeException("Failed to read field " + name, t); + } + } + + public void write(T instance, T value) { + constraints.validate(value); + try { + setter.invoke(instance, value); + } catch (Throwable t) { + throw new RuntimeException("Failed to write field " + name, t); + } + } + + public FieldConstraints getConstraints() { + return this.constraints; + } + + EditableField(Builder b) { + this.name = b.name; + this.type = b.type; + this.getter = b.getter; + this.setter = b.setter; + this.constraints = b.constraints; + } + + public static class Builder { + private String name; + private Class type; + private MethodHandle getter; + private MethodHandle setter; + private FieldConstraints constraints; + + public Builder name(String v) { this.name = v; return this; } + public Builder type(Class v) { this.type = v; return this; } + public Builder getter(MethodHandle v) { this.getter = v; return this; } + public Builder setter(MethodHandle v) { this.setter = v; return this; } + public Builder constraints(FieldConstraints v) { this.constraints = v; return this; } + + public EditableField build() { + return new EditableField(this); + } + + } +} From 9480212c4d5d31e2c39d6241c58e03154fb71c15 Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Sat, 31 Jan 2026 05:26:02 +0100 Subject: [PATCH 321/358] Group ability --- src/main/java/somehussar/gui/GuiEditable.java | 13 ++++++++++++- .../java/somehussar/gui/GuiFieldHandler.java | 17 +++++++++++------ .../somehussar/gui/field/EditableField.java | 12 +++++++++++- 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/src/main/java/somehussar/gui/GuiEditable.java b/src/main/java/somehussar/gui/GuiEditable.java index b32d4f7c4..52a8d4067 100644 --- a/src/main/java/somehussar/gui/GuiEditable.java +++ b/src/main/java/somehussar/gui/GuiEditable.java @@ -8,6 +8,17 @@ public @interface GuiEditable { @Retention(RetentionPolicy.RUNTIME) @interface Field { - String name() default ""; + String value(); + + /** + * Display order + * Higher value = higher priority. + */ + int order() default 0; + } + + @Retention(RetentionPolicy.RUNTIME) + @interface Group { + String value(); } } diff --git a/src/main/java/somehussar/gui/GuiFieldHandler.java b/src/main/java/somehussar/gui/GuiFieldHandler.java index f217e2b5a..ba5356696 100644 --- a/src/main/java/somehussar/gui/GuiFieldHandler.java +++ b/src/main/java/somehussar/gui/GuiFieldHandler.java @@ -67,12 +67,17 @@ public final class GuiFieldHandler { if (Modifier.isFinal(field.getModifiers())) return null; field.setAccessible(true); - EditableField.Builder builder = new EditableField.Builder(); - builder.name(fieldConfig.name()); - builder.type(field.getType()); - builder.getter(LOOKUP.unreflectGetter(field)); - builder.setter(LOOKUP.unreflectSetter(field)); - builder.constraints(buildConstraints(field)); + EditableField.Builder builder = new EditableField.Builder() + .name(fieldConfig.value()) + .type(field.getType()) + .getter(LOOKUP.unreflectGetter(field)) + .setter(LOOKUP.unreflectSetter(field)) + .constraints(buildConstraints(field)) + .order(fieldConfig.order()); + + GuiEditable.Group group = field.getAnnotation(GuiEditable.Group.class); + if (group != null) + builder.group(group.value()); return builder.build(); diff --git a/src/main/java/somehussar/gui/field/EditableField.java b/src/main/java/somehussar/gui/field/EditableField.java index a1a076efd..c0ace4303 100644 --- a/src/main/java/somehussar/gui/field/EditableField.java +++ b/src/main/java/somehussar/gui/field/EditableField.java @@ -11,6 +11,8 @@ public final class EditableField { private final MethodHandle getter; private final MethodHandle setter; private final FieldConstraints constraints; + private final int order; + private final String group; public String getName() { return name; @@ -42,12 +44,16 @@ public final class EditableField { return this.constraints; } + public int getOrder() { return this.order; } + EditableField(Builder b) { this.name = b.name; this.type = b.type; this.getter = b.getter; this.setter = b.setter; this.constraints = b.constraints; + this.order = b.order; + this.group = b.group; } public static class Builder { @@ -56,15 +62,19 @@ public final class EditableField { private MethodHandle getter; private MethodHandle setter; private FieldConstraints constraints; + private int order; + private String group = null; public Builder name(String v) { this.name = v; return this; } public Builder type(Class v) { this.type = v; return this; } public Builder getter(MethodHandle v) { this.getter = v; return this; } public Builder setter(MethodHandle v) { this.setter = v; return this; } public Builder constraints(FieldConstraints v) { this.constraints = v; return this; } + public Builder order(int order) { this.order = order; return this; } + public Builder group(String value) { this.group = value; return this; } public EditableField build() { - return new EditableField(this); + return new EditableField<>(this); } } From 3ea6303fc380be2aab81d7379f412a16d321b8bc Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Sat, 31 Jan 2026 05:39:28 +0100 Subject: [PATCH 322/358] Sizing --- src/main/java/somehussar/gui/GuiEditable.java | 22 +++++++++++++++++++ .../java/somehussar/gui/GuiFieldHandler.java | 9 ++++++-- .../somehussar/gui/field/EditableField.java | 6 ++++- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/main/java/somehussar/gui/GuiEditable.java b/src/main/java/somehussar/gui/GuiEditable.java index 52a8d4067..6e0a826cf 100644 --- a/src/main/java/somehussar/gui/GuiEditable.java +++ b/src/main/java/somehussar/gui/GuiEditable.java @@ -1,5 +1,6 @@ package somehussar.gui; +import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -15,10 +16,31 @@ public @interface GuiEditable { * Higher value = higher priority. */ int order() default 0; + } @Retention(RetentionPolicy.RUNTIME) @interface Group { String value(); } + + @Retention(RetentionPolicy.RUNTIME) + @interface Size { + int labelWidth() default 60; + int fieldWidth() default 50; + + GuiEditable.Size DEFAULT_SIZE = new GuiEditable.Size() { + @Override + public int labelWidth() { + return 60; + } + + @Override + public int fieldWidth() { + return 50; + } + @Override public Class annotationType() { return GuiEditable.Size.class; } + }; + + } } diff --git a/src/main/java/somehussar/gui/GuiFieldHandler.java b/src/main/java/somehussar/gui/GuiFieldHandler.java index ba5356696..5cf39bda5 100644 --- a/src/main/java/somehussar/gui/GuiFieldHandler.java +++ b/src/main/java/somehussar/gui/GuiFieldHandler.java @@ -30,7 +30,7 @@ public final class GuiFieldHandler { return CACHE.computeIfAbsent(type, GuiFieldHandler::scanClass); } - + @SuppressWarnings({"RawUseOfParameterized"}) private static ClassMetadata scanClass(Class type) { if (!type.isAnnotationPresent(GuiEditable.class)) { return null; @@ -66,6 +66,9 @@ public final class GuiFieldHandler { if (fieldConfig == null) return null; if (Modifier.isFinal(field.getModifiers())) return null; + GuiEditable.Size size = field.getAnnotation(GuiEditable.Size.class); + if (size == null) size = GuiEditable.Size.DEFAULT_SIZE;; + field.setAccessible(true); EditableField.Builder builder = new EditableField.Builder() .name(fieldConfig.value()) @@ -73,7 +76,9 @@ public final class GuiFieldHandler { .getter(LOOKUP.unreflectGetter(field)) .setter(LOOKUP.unreflectSetter(field)) .constraints(buildConstraints(field)) - .order(fieldConfig.order()); + .order(fieldConfig.order()) + .size(size); + GuiEditable.Group group = field.getAnnotation(GuiEditable.Group.class); if (group != null) diff --git a/src/main/java/somehussar/gui/field/EditableField.java b/src/main/java/somehussar/gui/field/EditableField.java index c0ace4303..782416663 100644 --- a/src/main/java/somehussar/gui/field/EditableField.java +++ b/src/main/java/somehussar/gui/field/EditableField.java @@ -1,6 +1,7 @@ package somehussar.gui.field; import somehussar.gui.FieldConstraints; +import somehussar.gui.GuiEditable; import java.lang.invoke.MethodHandle; @@ -13,6 +14,7 @@ public final class EditableField { private final FieldConstraints constraints; private final int order; private final String group; + private final GuiEditable.Size size; public String getName() { return name; @@ -54,6 +56,7 @@ public final class EditableField { this.constraints = b.constraints; this.order = b.order; this.group = b.group; + this.size = b.size; } public static class Builder { @@ -64,6 +67,7 @@ public final class EditableField { private FieldConstraints constraints; private int order; private String group = null; + private GuiEditable.Size size; public Builder name(String v) { this.name = v; return this; } public Builder type(Class v) { this.type = v; return this; } @@ -72,10 +76,10 @@ public final class EditableField { public Builder constraints(FieldConstraints v) { this.constraints = v; return this; } public Builder order(int order) { this.order = order; return this; } public Builder group(String value) { this.group = value; return this; } + public Builder size(GuiEditable.Size size) { this.size = size; return this; } public EditableField build() { return new EditableField<>(this); } - } } From a1549abff1b46de1af32ff6e8247d70860826262 Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Sat, 31 Jan 2026 05:47:17 +0100 Subject: [PATCH 323/358] Move classes around --- .../gui/{ => annotationHandling}/FieldConstraint.java | 2 +- .../gui/{ => annotationHandling}/FieldConstraints.java | 2 +- .../gui/{ => annotationHandling}/GuiEditable.java | 2 +- .../gui/{ => annotationHandling}/GuiFieldHandler.java | 8 +++++--- .../constraints/ConstraintFactory.java | 4 ++-- .../constraints/NumericConstraint.java | 2 +- .../constraints/NumericConstraintFactory.java | 4 ++-- .../constraints/NumericRangeConstraint.java | 4 ++-- .../gui/{ => annotationHandling}/field/EditableField.java | 6 +++--- 9 files changed, 18 insertions(+), 16 deletions(-) rename src/main/java/somehussar/gui/{ => annotationHandling}/FieldConstraint.java (62%) rename src/main/java/somehussar/gui/{ => annotationHandling}/FieldConstraints.java (93%) rename src/main/java/somehussar/gui/{ => annotationHandling}/GuiEditable.java (96%) rename src/main/java/somehussar/gui/{ => annotationHandling}/GuiFieldHandler.java (92%) rename src/main/java/somehussar/gui/{ => annotationHandling}/constraints/ConstraintFactory.java (62%) rename src/main/java/somehussar/gui/{ => annotationHandling}/constraints/NumericConstraint.java (87%) rename src/main/java/somehussar/gui/{ => annotationHandling}/constraints/NumericConstraintFactory.java (71%) rename src/main/java/somehussar/gui/{ => annotationHandling}/constraints/NumericRangeConstraint.java (88%) rename src/main/java/somehussar/gui/{ => annotationHandling}/field/EditableField.java (93%) diff --git a/src/main/java/somehussar/gui/FieldConstraint.java b/src/main/java/somehussar/gui/annotationHandling/FieldConstraint.java similarity index 62% rename from src/main/java/somehussar/gui/FieldConstraint.java rename to src/main/java/somehussar/gui/annotationHandling/FieldConstraint.java index af46df61d..7a9b124e4 100644 --- a/src/main/java/somehussar/gui/FieldConstraint.java +++ b/src/main/java/somehussar/gui/annotationHandling/FieldConstraint.java @@ -1,4 +1,4 @@ -package somehussar.gui; +package somehussar.gui.annotationHandling; public interface FieldConstraint { void validate(Object value); diff --git a/src/main/java/somehussar/gui/FieldConstraints.java b/src/main/java/somehussar/gui/annotationHandling/FieldConstraints.java similarity index 93% rename from src/main/java/somehussar/gui/FieldConstraints.java rename to src/main/java/somehussar/gui/annotationHandling/FieldConstraints.java index 113cf4460..9a48fd1e3 100644 --- a/src/main/java/somehussar/gui/FieldConstraints.java +++ b/src/main/java/somehussar/gui/annotationHandling/FieldConstraints.java @@ -1,4 +1,4 @@ -package somehussar.gui; +package somehussar.gui.annotationHandling; public final class FieldConstraints { diff --git a/src/main/java/somehussar/gui/GuiEditable.java b/src/main/java/somehussar/gui/annotationHandling/GuiEditable.java similarity index 96% rename from src/main/java/somehussar/gui/GuiEditable.java rename to src/main/java/somehussar/gui/annotationHandling/GuiEditable.java index 6e0a826cf..596f3ab2f 100644 --- a/src/main/java/somehussar/gui/GuiEditable.java +++ b/src/main/java/somehussar/gui/annotationHandling/GuiEditable.java @@ -1,4 +1,4 @@ -package somehussar.gui; +package somehussar.gui.annotationHandling; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; diff --git a/src/main/java/somehussar/gui/GuiFieldHandler.java b/src/main/java/somehussar/gui/annotationHandling/GuiFieldHandler.java similarity index 92% rename from src/main/java/somehussar/gui/GuiFieldHandler.java rename to src/main/java/somehussar/gui/annotationHandling/GuiFieldHandler.java index 5cf39bda5..c6b7f3625 100644 --- a/src/main/java/somehussar/gui/GuiFieldHandler.java +++ b/src/main/java/somehussar/gui/annotationHandling/GuiFieldHandler.java @@ -1,7 +1,9 @@ -package somehussar.gui; +package somehussar.gui.annotationHandling; -import somehussar.gui.constraints.*; -import somehussar.gui.field.EditableField; +import somehussar.gui.annotationHandling.constraints.ConstraintFactory; +import somehussar.gui.annotationHandling.constraints.NumericConstraint; +import somehussar.gui.annotationHandling.constraints.NumericConstraintFactory; +import somehussar.gui.annotationHandling.field.EditableField; import java.lang.annotation.Annotation; import java.lang.invoke.MethodHandles; diff --git a/src/main/java/somehussar/gui/constraints/ConstraintFactory.java b/src/main/java/somehussar/gui/annotationHandling/constraints/ConstraintFactory.java similarity index 62% rename from src/main/java/somehussar/gui/constraints/ConstraintFactory.java rename to src/main/java/somehussar/gui/annotationHandling/constraints/ConstraintFactory.java index efc3cb90a..d62de94b4 100644 --- a/src/main/java/somehussar/gui/constraints/ConstraintFactory.java +++ b/src/main/java/somehussar/gui/annotationHandling/constraints/ConstraintFactory.java @@ -1,6 +1,6 @@ -package somehussar.gui.constraints; +package somehussar.gui.annotationHandling.constraints; -import somehussar.gui.FieldConstraint; +import somehussar.gui.annotationHandling.FieldConstraint; import java.lang.annotation.Annotation; import java.lang.reflect.Field; diff --git a/src/main/java/somehussar/gui/constraints/NumericConstraint.java b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraint.java similarity index 87% rename from src/main/java/somehussar/gui/constraints/NumericConstraint.java rename to src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraint.java index 773129be3..3b9268a30 100644 --- a/src/main/java/somehussar/gui/constraints/NumericConstraint.java +++ b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraint.java @@ -1,4 +1,4 @@ -package somehussar.gui.constraints; +package somehussar.gui.annotationHandling.constraints; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/src/main/java/somehussar/gui/constraints/NumericConstraintFactory.java b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraintFactory.java similarity index 71% rename from src/main/java/somehussar/gui/constraints/NumericConstraintFactory.java rename to src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraintFactory.java index bd0a024e6..24b08ceb9 100644 --- a/src/main/java/somehussar/gui/constraints/NumericConstraintFactory.java +++ b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericConstraintFactory.java @@ -1,6 +1,6 @@ -package somehussar.gui.constraints; +package somehussar.gui.annotationHandling.constraints; -import somehussar.gui.FieldConstraint; +import somehussar.gui.annotationHandling.FieldConstraint; import java.lang.reflect.Field; diff --git a/src/main/java/somehussar/gui/constraints/NumericRangeConstraint.java b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericRangeConstraint.java similarity index 88% rename from src/main/java/somehussar/gui/constraints/NumericRangeConstraint.java rename to src/main/java/somehussar/gui/annotationHandling/constraints/NumericRangeConstraint.java index 16bf95895..198888d25 100644 --- a/src/main/java/somehussar/gui/constraints/NumericRangeConstraint.java +++ b/src/main/java/somehussar/gui/annotationHandling/constraints/NumericRangeConstraint.java @@ -1,6 +1,6 @@ -package somehussar.gui.constraints; +package somehussar.gui.annotationHandling.constraints; -import somehussar.gui.FieldConstraint; +import somehussar.gui.annotationHandling.FieldConstraint; public final class NumericRangeConstraint implements FieldConstraint { diff --git a/src/main/java/somehussar/gui/field/EditableField.java b/src/main/java/somehussar/gui/annotationHandling/field/EditableField.java similarity index 93% rename from src/main/java/somehussar/gui/field/EditableField.java rename to src/main/java/somehussar/gui/annotationHandling/field/EditableField.java index 782416663..73172ef42 100644 --- a/src/main/java/somehussar/gui/field/EditableField.java +++ b/src/main/java/somehussar/gui/annotationHandling/field/EditableField.java @@ -1,7 +1,7 @@ -package somehussar.gui.field; +package somehussar.gui.annotationHandling.field; -import somehussar.gui.FieldConstraints; -import somehussar.gui.GuiEditable; +import somehussar.gui.annotationHandling.FieldConstraints; +import somehussar.gui.annotationHandling.GuiEditable; import java.lang.invoke.MethodHandle; From 43c0a8e2968ec19b71c39705c8505df7fbc4676f Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Sat, 31 Jan 2026 06:00:37 +0100 Subject: [PATCH 324/358] Shitty test command do not fucking use xd --- .../java/kamkeel/npcs/command/TestGui.java | 25 +++++++++++++ .../controllers/data/ability/Ability.java | 5 +++ .../java/somehussar/gui/AnnotatedGui.java | 35 +++++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 src/main/java/kamkeel/npcs/command/TestGui.java create mode 100644 src/main/java/somehussar/gui/AnnotatedGui.java diff --git a/src/main/java/kamkeel/npcs/command/TestGui.java b/src/main/java/kamkeel/npcs/command/TestGui.java new file mode 100644 index 000000000..d34f64b0a --- /dev/null +++ b/src/main/java/kamkeel/npcs/command/TestGui.java @@ -0,0 +1,25 @@ +package kamkeel.npcs.command; + +import kamkeel.npcs.controllers.data.ability.Ability; +import net.minecraft.client.Minecraft; +import net.minecraft.command.CommandException; +import net.minecraft.command.ICommandSender; +import somehussar.gui.AnnotatedGui; + +public class TestGui extends CommandKamkeelBase { + + @Override + public void processCommand(ICommandSender sender, String[] args) throws CommandException { + Minecraft.getMinecraft().displayGuiScreen(new AnnotatedGui(Ability.class)); + } + + @Override + public String getDescription() { + return ""; + } + + @Override + public String getCommandName() { + return "mytestgui"; + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index eb59edf7c..b67ed95ac 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -27,15 +27,20 @@ import java.util.ArrayList; import java.util.List; import net.minecraft.entity.Entity; +import somehussar.gui.annotationHandling.GuiEditable; +@GuiEditable public abstract class Ability implements IAbility { // ═══════════════════════════════════════════════════════════════════ // CONFIGURATION (saved to NBT) // ═══════════════════════════════════════════════════════════════════ + @GuiEditable.Field("id") protected String id; + @GuiEditable.Field("name") protected String name; + @GuiEditable.Field("test") protected String typeId; // e.g., "ability.cnpc.slam" (also used as lang key) // Selection diff --git a/src/main/java/somehussar/gui/AnnotatedGui.java b/src/main/java/somehussar/gui/AnnotatedGui.java new file mode 100644 index 000000000..7bb4bf777 --- /dev/null +++ b/src/main/java/somehussar/gui/AnnotatedGui.java @@ -0,0 +1,35 @@ +package somehussar.gui; + +import noppes.npcs.client.gui.util.GuiNPCInterface; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import somehussar.gui.annotationHandling.GuiFieldHandler; +import somehussar.gui.annotationHandling.field.EditableField; + +public class AnnotatedGui extends GuiNPCInterface { + private final GuiFieldHandler.ClassMetadata metadata; + + public AnnotatedGui(Class type) { + metadata = GuiFieldHandler.getMetadata(type); + setBackground("menubg.png"); + xSize = 256; + ySize = 216; + closeOnEsc = true; + } + + @Override + public void initGui() { + super.initGui(); + if (metadata == null) + return; + + int i = 0; + for (EditableField field : metadata.getDeclaredFields()) { + addLabel(new GuiNpcLabel(i++, field.getName(), guiLeft+8, guiTop+i*15+5)); + } + } + + @Override + public void save() { + + } +} From d0eef8b271a0cc3d089ff4eca9f8785f22b833b8 Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Sat, 31 Jan 2026 06:00:46 +0100 Subject: [PATCH 325/358] oops --- src/main/java/kamkeel/npcs/command/CommandKamkeel.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java index d0341f211..a7cc9acd5 100644 --- a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java +++ b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java @@ -30,6 +30,7 @@ public class CommandKamkeel extends CommandBase { public CommandKamkeel() { registerCommand(help); + registerCommand(new TestGui()); registerCommand(new ScriptCommand()); registerCommand(new SlayCommand()); registerCommand(new QuestCommand()); From 5dc27f7212edce361c1c4f7970df03165305c41c Mon Sep 17 00:00:00 2001 From: Vidal Date: Sat, 31 Jan 2026 02:02:18 -0300 Subject: [PATCH 326/358] Updated anchor point method calls --- .../controllers/data/ability/data/EnergyAnchorData.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java index 39c7db9e7..66983f89b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/data/EnergyAnchorData.java @@ -24,25 +24,25 @@ public class EnergyAnchorData { } public EnergyAnchorData(int anchorPoint) { - this.anchorPoint = AnchorPoint.fromId(anchorPoint); + this.anchorPoint = AnchorPoint.fromOrdinal(anchorPoint); } public EnergyAnchorData(int anchorPoint, float anchorOffsetX, float anchorOffsetY, float anchorOffsetZ) { - this.anchorPoint = AnchorPoint.fromId(anchorPoint); + this.anchorPoint = AnchorPoint.fromOrdinal(anchorPoint); this.anchorOffsetX = anchorOffsetX; this.anchorOffsetY = anchorOffsetY; this.anchorOffsetZ = anchorOffsetZ; } public void writeNBT(NBTTagCompound nbt) { - nbt.setInteger("anchorPoint", anchorPoint.getId()); + nbt.setInteger("anchorPoint", anchorPoint.ordinal()); nbt.setFloat("anchorOffsetX", anchorOffsetX); nbt.setFloat("anchorOffsetY", anchorOffsetY); nbt.setFloat("anchorOffsetZ", anchorOffsetZ); } public void readNBT(NBTTagCompound nbt) { - this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromId(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; + this.anchorPoint = nbt.hasKey("anchorPoint") ? AnchorPoint.fromOrdinal(nbt.getInteger("anchorPoint")) : AnchorPoint.FRONT; this.anchorOffsetX = nbt.hasKey("anchorOffsetX") ? nbt.getFloat("anchorOffsetX") : 0; this.anchorOffsetY = nbt.hasKey("anchorOffsetY") ? nbt.getFloat("anchorOffsetY") : 0; this.anchorOffsetZ = nbt.hasKey("anchorOffsetZ") ? nbt.getFloat("anchorOffsetZ") : 0; From 3ffd62c2e7760da8a116a9d85c2c6802e2421321 Mon Sep 17 00:00:00 2001 From: Vidal Date: Sat, 31 Jan 2026 02:03:07 -0300 Subject: [PATCH 327/358] More animations --- .../animations/Ability_DualBeam_Active.json | 203 ++++++++++++++++++ .../animations/Ability_DualBeam_Windup.json | 201 +++++++++++++++++ 2 files changed, 404 insertions(+) create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_DualBeam_Active.json create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_DualBeam_Windup.json diff --git a/src/main/resources/assets/customnpcs/animations/Ability_DualBeam_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_DualBeam_Active.json new file mode 100644 index 000000000..fa47349f9 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_DualBeam_Active.json @@ -0,0 +1,203 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": 31.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": -25.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 25.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": 31.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": -25.0f, + "Rotation0": -85.0f, + "Pivot0": 1.0f, + "Pivot1": 1.0f, + "Rotation2": 20.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 25.0f, + "Rotation0": -85.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": -20.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": 31.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": -25.0f, + "Rotation0": -85.0f, + "Pivot0": 1.0f, + "Pivot1": 1.0f, + "Rotation2": -20.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 25.0f, + "Rotation0": -85.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 20.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 3, + "Smooth": 0b, + "ID": 12, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_DualBeam_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_DualBeam_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_DualBeam_Windup.json new file mode 100644 index 000000000..99437fc2d --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_DualBeam_Windup.json @@ -0,0 +1,201 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 90.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "LEFT_ARM", + "Rotation1": -90.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Duration": 60 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 18.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 75.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": -25.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -75.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 25.0f + }, + { + "Pivot2": 3.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": -18.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 3.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -31.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 5 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 3.0f, + "FrameParts": [ + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": 31.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_ARM", + "Rotation1": -25.0f, + "Rotation0": -90.0f, + "Pivot0": 1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 25.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 0.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 15 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 2, + "Smooth": 0b, + "ID": 11, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_DualBeam_Windup", + "WhileStanding": 1b +} From 00e62be79df67c7c06cce7a6f95fa75af916b7ac Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Sat, 31 Jan 2026 06:06:10 +0100 Subject: [PATCH 328/358] Revert "oops" This reverts commit d0eef8b271a0cc3d089ff4eca9f8785f22b833b8. --- src/main/java/kamkeel/npcs/command/CommandKamkeel.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java index a7cc9acd5..d0341f211 100644 --- a/src/main/java/kamkeel/npcs/command/CommandKamkeel.java +++ b/src/main/java/kamkeel/npcs/command/CommandKamkeel.java @@ -30,7 +30,6 @@ public class CommandKamkeel extends CommandBase { public CommandKamkeel() { registerCommand(help); - registerCommand(new TestGui()); registerCommand(new ScriptCommand()); registerCommand(new SlayCommand()); registerCommand(new QuestCommand()); From 289baf4131ceab531e03c42bcbc5f6f63a10763b Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Sat, 31 Jan 2026 06:06:54 +0100 Subject: [PATCH 329/358] Revert test gui command --- .../java/kamkeel/npcs/command/TestGui.java | 25 ------------------- 1 file changed, 25 deletions(-) delete mode 100644 src/main/java/kamkeel/npcs/command/TestGui.java diff --git a/src/main/java/kamkeel/npcs/command/TestGui.java b/src/main/java/kamkeel/npcs/command/TestGui.java deleted file mode 100644 index d34f64b0a..000000000 --- a/src/main/java/kamkeel/npcs/command/TestGui.java +++ /dev/null @@ -1,25 +0,0 @@ -package kamkeel.npcs.command; - -import kamkeel.npcs.controllers.data.ability.Ability; -import net.minecraft.client.Minecraft; -import net.minecraft.command.CommandException; -import net.minecraft.command.ICommandSender; -import somehussar.gui.AnnotatedGui; - -public class TestGui extends CommandKamkeelBase { - - @Override - public void processCommand(ICommandSender sender, String[] args) throws CommandException { - Minecraft.getMinecraft().displayGuiScreen(new AnnotatedGui(Ability.class)); - } - - @Override - public String getDescription() { - return ""; - } - - @Override - public String getCommandName() { - return "mytestgui"; - } -} From 17eb003a12097fb9748e85ad7d3a35954a10ef1e Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Sat, 31 Jan 2026 07:18:54 +0100 Subject: [PATCH 330/358] i fucking give up, this will not hold up. --- .../data/ability/type/AbilitySlam.java | 2 ++ .../java/somehussar/gui/AnnotatedGui.java | 23 +++++++++---- .../somehussar/gui/ImmediateTextField.java | 32 +++++++++++++++++++ 3 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 src/main/java/somehussar/gui/ImmediateTextField.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index 2dae47d05..b3c800d81 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -19,6 +19,7 @@ import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilitySlam; +import somehussar.gui.annotationHandling.GuiEditable; import java.util.List; @@ -31,6 +32,7 @@ import java.util.List; * - Landing: When NPC hits ground, deal AOE damage * - RECOVERY: Landing recovery animation */ +@GuiEditable public class AbilitySlam extends Ability implements IAbilitySlam { // Type-specific config diff --git a/src/main/java/somehussar/gui/AnnotatedGui.java b/src/main/java/somehussar/gui/AnnotatedGui.java index 7bb4bf777..8cdf224d5 100644 --- a/src/main/java/somehussar/gui/AnnotatedGui.java +++ b/src/main/java/somehussar/gui/AnnotatedGui.java @@ -1,19 +1,28 @@ package somehussar.gui; -import noppes.npcs.client.gui.util.GuiNPCInterface; +import net.minecraft.client.gui.GuiScreen; import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.SubGuiInterface; import somehussar.gui.annotationHandling.GuiFieldHandler; import somehussar.gui.annotationHandling.field.EditableField; -public class AnnotatedGui extends GuiNPCInterface { +public class AnnotatedGui extends SubGuiInterface { private final GuiFieldHandler.ClassMetadata metadata; - public AnnotatedGui(Class type) { - metadata = GuiFieldHandler.getMetadata(type); + public AnnotatedGui(T object, String[] groups, GuiScreen parent) { + this(object, groups, parent, 256, 216); + } + + public AnnotatedGui(T object, String[] groups, GuiScreen parent, int xSize, int ySize) { + if (object != null) + metadata = GuiFieldHandler.getMetadata(object.getClass()); + else + metadata = null; setBackground("menubg.png"); - xSize = 256; - ySize = 216; + this.xSize = xSize; + this.ySize = ySize; closeOnEsc = true; + this.parent = parent; } @Override @@ -23,7 +32,7 @@ public class AnnotatedGui extends GuiNPCInterface { return; int i = 0; - for (EditableField field : metadata.getDeclaredFields()) { + for (EditableField field : metadata.getParent().getDeclaredFields()) { addLabel(new GuiNpcLabel(i++, field.getName(), guiLeft+8, guiTop+i*15+5)); } } diff --git a/src/main/java/somehussar/gui/ImmediateTextField.java b/src/main/java/somehussar/gui/ImmediateTextField.java new file mode 100644 index 000000000..5f49a9d84 --- /dev/null +++ b/src/main/java/somehussar/gui/ImmediateTextField.java @@ -0,0 +1,32 @@ +package somehussar.gui; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import somehussar.gui.annotationHandling.field.EditableField; + +public class ImmediateTextField extends GuiNpcTextField{ + private final EditableField editableField; +// +// private final GuiNpcTextField encapsulatedField; + + public ImmediateTextField(int id, EditableField field, int xPos, int yPos) { + this(id, field, Minecraft.getMinecraft().fontRenderer, xPos, yPos); + } + public ImmediateTextField(int id, EditableField field, FontRenderer fontRenderer, int xPos, int yPos) { + super(id, null, fontRenderer, xPos, yPos, 0, 0, null); + + this.editableField = field; + } + + @Override + public void drawTextBox(int mousX, int mousY) { + + } + + @Override + public void mouseClicked(int xPos, int yPos, int k) { + xPos -= this.xPosition; + yPos -= this.yPosition; + } +} From b9e82e4c5b1bded76ddab8d12dcaa08729b2dca0 Mon Sep 17 00:00:00 2001 From: Vidal Date: Sat, 31 Jan 2026 16:37:28 -0300 Subject: [PATCH 331/358] Fixed mysterious override stopping the game from running Kam let's please stop using claude for everything this BuiltInAbility is not even used --- src/api | 2 +- .../npcs/controllers/data/ability/BuiltInAbility.java | 8 -------- src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java | 6 ++++-- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/api b/src/api index db6a68e43..bf650319d 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit db6a68e43073b2828ec05a3c2ede660453c4544e +Subproject commit bf650319da422790bff294d570dd879143fe8d4c diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java b/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java index 69151291f..904fad0b9 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/BuiltInAbility.java @@ -31,14 +31,6 @@ public abstract class BuiltInAbility extends Ability { return registryKey; } - /** - * Built-in abilities are not customizable. - */ - @Override - public boolean isCustomizable() { - return false; - } - /** * Built-in abilities do not write type-specific NBT (they have fixed configs). */ diff --git a/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java b/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java index 26f7a1ba7..f534f9dbc 100644 --- a/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java +++ b/src/main/java/noppes/npcs/scripted/entity/ScriptNpc.java @@ -5,6 +5,7 @@ import net.minecraft.item.ItemStack; import noppes.npcs.NoppesUtilServer; import noppes.npcs.api.IPos; import noppes.npcs.api.ITimers; +import noppes.npcs.api.ability.IDataAbilities; import noppes.npcs.api.entity.ICustomNpc; import noppes.npcs.api.entity.IEntityLivingBase; import noppes.npcs.api.entity.IPlayer; @@ -51,6 +52,7 @@ import noppes.npcs.scripted.roles.ScriptRoleInterface; import noppes.npcs.scripted.roles.ScriptRoleMailman; import noppes.npcs.scripted.roles.ScriptRoleTrader; import noppes.npcs.scripted.roles.ScriptRoleTransporter; +import noppes.npcs.scripted.wrapper.ScriptDataAbilities; import noppes.npcs.util.ValueUtil; import java.util.ArrayList; @@ -1458,7 +1460,7 @@ public class ScriptNpc extends ScriptLiving imp return npc.stats.magicData; } - public noppes.npcs.api.ability.IDataAbilities getAbilityData() { - return new noppes.npcs.scripted.wrapper.ScriptDataAbilities(npc); + public IDataAbilities getAbilityData() { + return new ScriptDataAbilities(npc); } } From 9df8fbb88d9d0775cf4a0cbccc67f9f7141e0d7e Mon Sep 17 00:00:00 2001 From: Vidal Date: Sat, 31 Jan 2026 19:22:43 -0300 Subject: [PATCH 332/358] Registry to add new Abilities, could possibly be used to register other stuff like animations --- src/main/java/kamkeel/npcs/util/Register.java | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/main/java/kamkeel/npcs/util/Register.java diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java new file mode 100644 index 000000000..d12e49146 --- /dev/null +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -0,0 +1,36 @@ +package kamkeel.npcs.util; + +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityController; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.function.Supplier; + +public class Register { + private final String registryKey; + private final String namespace; + protected final Map> entries = new LinkedHashMap<>(); + + public Register(String registryKey, String namespace) { + this.registryKey = registryKey; + this.namespace = namespace; + } + + public T register(String factoryName, Supplier factory) { + entries.put(registryKey + "." + namespace + ":" + factoryName, factory); + return factory.get(); + } + + public static class Abilities extends Register { + public Abilities(String namespace) { + super("ability", namespace); + } + + public void register() { + for (Map.Entry> entry : entries.entrySet()) { + AbilityController.Instance.registerType(entry.getKey(), entry.getValue()); + } + } + } +} From 245e8f81626acb117692098e2a09b164564e9987 Mon Sep 17 00:00:00 2001 From: Vidal Date: Sat, 31 Jan 2026 21:55:39 -0300 Subject: [PATCH 333/358] Added Dual Beam Ability --- .../data/ability/AbilityController.java | 20 +- .../data/ability/type/AbilityDualBeam.java | 401 ++++++++++++++++++ .../npcs/entity/EntityAbilityBeam.java | 23 + .../ability/SubGuiAbilityDualBeam.java | 328 ++++++++++++++ 4 files changed, 754 insertions(+), 18 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualBeam.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualBeam.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 62d226bf7..5b3c15e05 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -1,23 +1,6 @@ package kamkeel.npcs.controllers.data.ability; -import kamkeel.npcs.controllers.data.ability.type.AbilitySweeper; -import kamkeel.npcs.controllers.data.ability.type.AbilityCharge; -import kamkeel.npcs.controllers.data.ability.type.AbilityCutter; -import kamkeel.npcs.controllers.data.ability.type.AbilityDash; -import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; -import kamkeel.npcs.controllers.data.ability.type.AbilityHazard; -import kamkeel.npcs.controllers.data.ability.type.AbilityHeal; -import kamkeel.npcs.controllers.data.ability.type.AbilityHeavyHit; -import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; -import kamkeel.npcs.controllers.data.ability.type.AbilityDisc; -import kamkeel.npcs.controllers.data.ability.type.AbilityLaserShot; -import kamkeel.npcs.controllers.data.ability.type.AbilityEnergyBeam; -import kamkeel.npcs.controllers.data.ability.type.AbilityProjectile; -import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave; -import kamkeel.npcs.controllers.data.ability.type.AbilitySlam; -import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport; -import kamkeel.npcs.controllers.data.ability.type.AbilityTrap; -import kamkeel.npcs.controllers.data.ability.type.AbilityVortex; +import kamkeel.npcs.controllers.data.ability.type.*; import kamkeel.npcs.controllers.SyncController; import net.minecraft.nbt.NBTTagCompound; import noppes.npcs.CustomNpcs; @@ -383,6 +366,7 @@ public class AbilityController implements IAbilityHandler { registerType("cnpc:disc", AbilityDisc::new); registerType("cnpc:laser_shot", AbilityLaserShot::new); registerType("cnpc:beam", AbilityEnergyBeam::new); + registerType("cnpc:dual_beam", AbilityDualBeam::new); // Movement abilities registerType("cnpc:charge", AbilityCharge::new); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualBeam.java new file mode 100644 index 000000000..6704b0e33 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualBeam.java @@ -0,0 +1,401 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityBeam; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDualBeam; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.util.ValueUtil; + +public class AbilityDualBeam extends Ability { + private float beamWidth = 0.4f; + private float headSize = 0.6f; + private boolean dualFire= true; + private int dualFireDelay = 0; + + // Data classes + private EnergyColorData[] colorData = new EnergyColorData[]{ + new EnergyColorData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 6.0f), + new EnergyColorData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 6.0f), + }; + private EnergyCombatData combatData = new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f); + private EnergyHomingData homingData = new EnergyHomingData(0.4f, true, 0.1f, 15.0f); + private EnergyLightningData[] lightningData = new EnergyLightningData[]{ + new EnergyLightningData(), new EnergyLightningData() + }; + private EnergyLifespanData lifespanData = new EnergyLifespanData(25.0f, 200); + private EnergyAnchorData[] anchorData = new EnergyAnchorData[]{ + new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND) + }; + + // Transient state for beam entity (used during windup charging) + private transient EntityAbilityBeam beamEntity = null; + private transient EntityAbilityBeam beamEntity2 = null; + + public AbilityDualBeam() { + this.typeId = "ability.cnpc.dual_beam"; + this.name = "Dual Beam"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 20.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 80; + this.lockMovement = LockMovementType.WINDUP_AND_ACTIVE; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + // Default built-in animations + this.windUpAnimationName = "Ability_DualBeam_Windup"; + this.activeAnimationName = "Ability_DualBeam_Active"; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + @SideOnly(Side.CLIENT) + public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { + return new SubGuiAbilityDualBeam(this, callback); + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + return; + } + + // Start firing the beam that was spawned during windup + if (beamEntity != null && !beamEntity.isDead) { + beamEntity.startFiring(target); + } + + if (dualFire) { + if (beamEntity2 != null && !beamEntity2.isDead && dualFireDelay <= 0) { + beamEntity2.startFiring(target); + } + } else { + beamEntity2.setDead(); + } + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; + + // Spawn beam in charging mode on first tick of windup + if (tick == 1) { + float offsetDist = 1.0f; + + // Create beam in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0], offsetDist); + beamEntity = new EntityAbilityBeam( + world, caster, target, + spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord, + beamWidth, headSize, + colorData[0], combatData, homingData, lightningData[0], lifespanData, + lockMovement.locksActive()); + + Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1], offsetDist); + beamEntity2 = new EntityAbilityBeam( + world, caster, target, + spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord, + beamWidth, headSize, + colorData[1], combatData, homingData, lightningData[1], lifespanData, + lockMovement.locksActive()); + + beamEntity.setSiblingUUID(beamEntity2.getPersistentID()); + beamEntity2.setSiblingUUID(beamEntity.getPersistentID()); + + beamEntity.setupCharging(anchorData[0], windUpTicks, offsetDist); + beamEntity2.setupCharging(anchorData[1], windUpTicks, offsetDist); + + beamEntity.setEffects(this.effects); + if (dualFire) beamEntity2.setEffects(this.effects); + + world.spawnEntityInWorld(beamEntity); + world.spawnEntityInWorld(beamEntity2); + } + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (dualFire) { + if ((beamEntity == null || beamEntity.isDead) && (beamEntity2 == null || beamEntity2.isDead)) { + beamEntity = null; + beamEntity2 = null; + signalCompletion(); + } + + if (tick == dualFireDelay && beamEntity2 != null && !beamEntity2.isDead) { + beamEntity2.startFiring(target); + } + } else { + if (beamEntity == null || beamEntity.isDead) { + beamEntity = null; + signalCompletion(); + } + } + } + + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + } + + @Override + public void cleanup() { + // Despawn beam entity if still alive + if (beamEntity != null && !beamEntity.isDead) { + beamEntity.setDead(); + } + + if (beamEntity2 != null && !beamEntity2.isDead) { + beamEntity2.setDead(); + } + + beamEntity = null; + beamEntity2 = null; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create circle telegraph at target position + Telegraph telegraph = Telegraph.circle(headSize * 2.0f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return headSize * 2.0f; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("beamWidth", beamWidth); + nbt.setFloat("headSize", headSize); + nbt.setBoolean("dualFire", dualFire); + nbt.setInteger("dualFireDelay", dualFireDelay); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + + NBTTagCompound beam1 = new NBTTagCompound(); + NBTTagCompound beam2 = new NBTTagCompound(); + + anchorData[0].writeNBT(beam1); + anchorData[1].writeNBT(beam2); + colorData[0].writeNBT(beam1); + colorData[1].writeNBT(beam2); + lightningData[0].writeNBT(beam1); + lightningData[1].writeNBT(beam2); + + nbt.setTag("Beam_1", beam1); + nbt.setTag("Beam_2", beam2); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + NBTTagCompound beam1 = nbt.hasKey("Beam_1") ? nbt.getCompoundTag("Beam_1") : new NBTTagCompound(); + NBTTagCompound beam2 = nbt.hasKey("Beam_2") ? nbt.getCompoundTag("Beam_2") : new NBTTagCompound(); + + this.beamWidth = nbt.hasKey("beamWidth") ? nbt.getFloat("beamWidth") : 0.4f; + this.headSize = nbt.hasKey("headSize") ? nbt.getFloat("headSize") : 0.6f; + this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire"); + this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0; + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lifespanData.readNBT(nbt); + + anchorData[0].readNBT(beam1); + anchorData[1].readNBT(beam2); + colorData[0].readNBT(beam1); + colorData[1].readNBT(beam2); + lightningData[0].readNBT(beam1); + lightningData[1].readNBT(beam2); + } + + // Getters & Setters - Standalone fields + public float getBeamWidth() { return beamWidth; } + public void setBeamWidth(float beamWidth) { this.beamWidth = beamWidth; } + public float getHeadSize() { return headSize; } + public void setHeadSize(float headSize) { this.headSize = headSize; } + public boolean isDualFire() { return dualFire; } + public void setDualFire(boolean dualFire) { this.dualFire = dualFire; } + public int getDualFireDelay() { return dualFireDelay; } + public void setDualFireDelay(int dualFireDelay) { this.dualFireDelay = dualFireDelay; } + + private EnergyColorData getColorData(int beam) { + beam = ValueUtil.clamp(beam, 0, 1); + return colorData[beam]; + } + private EnergyLightningData getLightningData(int beam) { + beam = ValueUtil.clamp(beam, 0, 1); + return lightningData[beam]; + } + private EnergyAnchorData getAnchorData(int beam) { + beam = ValueUtil.clamp(beam, 0, 1); + return anchorData[beam]; + } + + // Getters & Setters - Color data + public int getInnerColor(int beam) { return getColorData(beam).innerColor; } + public void setInnerColor(int beam, int innerColor) { getColorData(beam).innerColor = innerColor; } + public int getOuterColor(int beam) { return getColorData(beam).outerColor; } + public void setOuterColor(int beam, int outerColor) { getColorData(beam).outerColor = outerColor; } + public boolean isOuterColorEnabled(int beam) { return getColorData(beam).outerColorEnabled; } + public void setOuterColorEnabled(int beam, boolean enabled) { getColorData(beam).outerColorEnabled = enabled; } + public float getOuterColorWidth(int beam) { return getColorData(beam).outerColorWidth; } + public void setOuterColorWidth(int beam, float width) { getColorData(beam).outerColorWidth = width; } + public float getOuterColorAlpha(int beam) { return getColorData(beam).outerColorAlpha; } + public void setOuterColorAlpha(int beam, float alpha) { getColorData(beam).outerColorAlpha = alpha; } + public float getRotationSpeed(int beam) { return getColorData(beam).rotationSpeed; } + public void setRotationSpeed(int beam, float speed) { getColorData(beam).rotationSpeed = speed; } + + + public int getInnerColor() { return getInnerColor(0); } + public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); } + public int getOuterColor() { return getOuterColor(0); } + public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); } + public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); } + public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); } + public float getOuterColorWidth() { return getOuterColorWidth(0); } + public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); } + public float getOuterColorAlpha() { return getOuterColorAlpha(0); } + public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); } + public float getRotationSpeed() { return getRotationSpeed(0); } + public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); } + + // Getters & Setters - Combat data + public float getDamage() { return combatData.damage; } + public void setDamage(float damage) { this.combatData.damage = damage; } + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float knockback) { this.combatData.knockback = knockback; } + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float knockbackUp) { this.combatData.knockbackUp = knockbackUp; } + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean explosive) { this.combatData.explosive = explosive; } + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float explosionRadius) { this.combatData.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { this.combatData.explosionDamageFalloff = explosionDamageFalloff; } + + // Getters & Setters - Homing data + public float getSpeed() { return homingData.speed; } + public void setSpeed(float speed) { this.homingData.speed = speed; } + public boolean isHoming() { return homingData.homing; } + public void setHoming(boolean homing) { this.homingData.homing = homing; } + public float getHomingStrength() { return homingData.homingStrength; } + public void setHomingStrength(float homingStrength) { this.homingData.homingStrength = homingStrength; } + public float getHomingRange() { return homingData.homingRange; } + public void setHomingRange(float homingRange) { this.homingData.homingRange = homingRange; } + + // Getters & Setters - Lightning data + public boolean hasLightningEffect(int beam) { return getLightningData(beam).lightningEffect; } + public void setLightningEffect(int beam, boolean lightningEffect) { getLightningData(beam).lightningEffect = lightningEffect; } + public float getLightningDensity(int beam) { return getLightningData(beam).lightningDensity; } + public void setLightningDensity(int beam, float lightningDensity) { getLightningData(beam).lightningDensity = lightningDensity; } + public float getLightningRadius(int beam) { return getLightningData(beam).lightningRadius; } + public void setLightningRadius(int beam, float lightningRadius) { getLightningData(beam).lightningRadius = lightningRadius; } + + public boolean hasLightningEffect() { return hasLightningEffect(0); } + public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); } + public float getLightningDensity() { return getLightningDensity(0); } + public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); } + public float getLightningRadius() { return getLightningRadius(0); } + public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); } + + + // Getters & Setters - Lifespan data + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float maxDistance) { this.lifespanData.maxDistance = maxDistance; } + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int maxLifetime) { this.lifespanData.maxLifetime = maxLifetime; } + + // Getters & Setters - Anchor point + public AnchorPoint getAnchorPointEnum(int beam) { return getAnchorData(beam).anchorPoint; } + public void setAnchorPointEnum(int beam, AnchorPoint anchorPoint) { getAnchorData(beam).anchorPoint = anchorPoint; } + public float getAnchorOffsetX(int beam) { return getAnchorData(beam).anchorOffsetX; } + public void setAnchorOffsetX(int beam, float x) { getAnchorData(beam).anchorOffsetX = x; } + public float getAnchorOffsetY(int beam) { return getAnchorData(beam).anchorOffsetY; } + public void setAnchorOffsetY(int beam, float y) { getAnchorData(beam).anchorOffsetY = y; } + public float getAnchorOffsetZ(int beam) { return getAnchorData(beam).anchorOffsetZ; } + public void setAnchorOffsetZ(int beam, float z) { getAnchorData(beam).anchorOffsetZ = z; } + + public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); } + public float getAnchorOffsetX() { return getAnchorOffsetX(0); } + public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); } + public float getAnchorOffsetY() { return getAnchorOffsetY(0); } + public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); } + public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); } + public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); } + + //@Override + public int getAnchorPoint(int beam) { return getAnchorData(beam).anchorPoint.ordinal(); } + public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); } + + //@Override + public void setAnchorPoint(int beam, int point) { getAnchorData(beam).anchorPoint = AnchorPoint.fromOrdinal(point); } + public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityBeam beam = new EntityAbilityBeam(npc.worldObj); + beam.setupPreview(npc, beamWidth, headSize, colorData[0], lightningData[0], anchorData[0], windUpTicks, 1.0f); + return beam; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index a9cfffc43..bceae89aa 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -15,6 +15,7 @@ import noppes.npcs.LogWriter; import java.util.ArrayList; import java.util.List; +import java.util.UUID; /** * Beam projectile - head with trailing path that curves with homing. @@ -32,6 +33,7 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { private boolean homing = true; private float homingStrength = 0.35f; // Increased from 0.15 for better tracking private float homingRange = 20.0f; + private UUID siblingUUID = null; // Beam shape properties private float beamWidth = 0.3f; @@ -646,6 +648,12 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { } } + @Override + protected boolean shouldIgnoreEntity(Entity entity) { + if (entity.getPersistentID().equals(siblingUUID)) return true; + return super.shouldIgnoreEntity(entity); + } + // ==================== GETTERS FOR RENDERER ==================== public float getBeamWidth() { @@ -744,6 +752,18 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { return trailFadeTime; } + public UUID getSiblingUUID() { + return siblingUUID; + } + + public void setSiblingUUID(UUID siblingUUID) { + if (siblingUUID == null) + return; + + if (this.siblingUUID == null) + this.siblingUUID = siblingUUID; + } + // ==================== NBT ==================== @Override @@ -787,6 +807,8 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { )); } } + + this.siblingUUID = nbt.hasKey("SiblingUUID") ? UUID.fromString(nbt.getString("SiblingUUID")) : null; } @Override @@ -821,5 +843,6 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { trailList.appendTag(pointNbt); } nbt.setTag("Trail", trailList); + nbt.setString("SiblingUUID", siblingUUID.toString()); } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualBeam.java new file mode 100644 index 000000000..87144adfc --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualBeam.java @@ -0,0 +1,328 @@ +package noppes.npcs.client.gui.advanced.ability; + +import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.type.AbilityDualBeam; +import noppes.npcs.client.gui.SubGuiColorSelector; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.util.*; + +import java.util.List; + +public class SubGuiAbilityDualBeam extends SubGuiAbilityConfig { + private final AbilityDualBeam beam; + private int editingVisualColorId = 0; + private int editingBeam = 0; + + public SubGuiAbilityDualBeam(AbilityDualBeam ability, IAbilityConfigCallback callback) { + super(ability, callback); + this.beam = ability; + } + + @Override + protected boolean hasVisualSettings() { + return true; + } + + @Override + protected void initTypeTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 145; + int col2FieldX = guiLeft + 205; + + // Row 1: Damage + Speed + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); + addTextField(createFloatField(100, fieldX, y, 50, beam.getDamage())); + + addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); + addTextField(createFloatField(101, col2FieldX, y, 50, beam.getSpeed())); + + y += 24; + + // Row 2: Beam Width + Head Size + addLabel(new GuiNpcLabel(102, "ability.beamWidth", labelX, y + 5)); + addTextField(createFloatField(102, fieldX, y, 50, beam.getBeamWidth())); + + addLabel(new GuiNpcLabel(103, "ability.headSize", col2LabelX, y + 5)); + addTextField(createFloatField(103, col2FieldX, y, 50, beam.getHeadSize())); + + y += 24; + + // Row 3: Homing + Homing Strength (only show strength if homing) + addLabel(new GuiNpcLabel(104, "ability.homing", labelX, y + 5)); + GuiNpcButton homingBtn = new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isHoming() ? 1 : 0); + homingBtn.setHoverText("ability.hover.homing"); + addButton(homingBtn); + + if (beam.isHoming()) { + addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); + addTextField(createFloatField(105, col2FieldX, y, 50, beam.getHomingStrength())); + } + + y += 24; + + // Row 4: Max Distance (+ Homing Range only if homing) + if (beam.isHoming()) { + addLabel(new GuiNpcLabel(106, "ability.homingRange", labelX, y + 5)); + addTextField(createFloatField(106, fieldX, y, 50, beam.getHomingRange())); + } + + addLabel(new GuiNpcLabel(107, "ability.maxDist", beam.isHoming() ? col2LabelX : labelX, y + 5)); + addTextField(createFloatField(107, beam.isHoming() ? col2FieldX : fieldX, y, 50, beam.getMaxDistance())); + + y += 24; + + // Row 5: Explosive + Explosion Radius (only show radius if explosive) + addLabel(new GuiNpcLabel(108, "ability.explosive", labelX, y + 5)); + GuiNpcButton explosiveBtn = new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isExplosive() ? 1 : 0); + explosiveBtn.setHoverText("ability.hover.explosive"); + addButton(explosiveBtn); + + if (beam.isExplosive()) { + addLabel(new GuiNpcLabel(109, "ability.explosionRad", col2LabelX, y + 5)); + addTextField(createFloatField(109, col2FieldX, y, 50, beam.getExplosionRadius())); + } + + y += 24; + + // Row 6: Knockback + Max Lifetime + addLabel(new GuiNpcLabel(110, "ability.knockback", labelX, y + 5)); + addTextField(createFloatField(110, fieldX, y, 50, beam.getKnockback())); + + addLabel(new GuiNpcLabel(111, "ability.lifetime", col2LabelX, y + 5)); + addTextField(createIntField(111, col2FieldX, y, 50, beam.getMaxLifetime())); + + y += 24; + + addLabel(new GuiNpcLabel(112, "ability.dualFire", labelX, y + 5)); + addButton(new GuiNpcButtonYesNo(112, fieldX, y, beam.isDualFire())); + + if (beam.isDualFire()) { + addLabel(new GuiNpcLabel(113, "ability.dualFireDelay", col2LabelX, y + 5)); + addTextField(createIntField(113, col2FieldX, y, 50, beam.getDualFireDelay())); + } + + y += 24; + + addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); + } + + @Override + protected void initVisualTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 180; + int col2FieldX = guiLeft + 260; + + // Row 1: Anchor Point + addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); + GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), beam.getAnchorPointEnum(editingBeam).ordinal()); + anchorBtn.setHoverText("ability.hover.anchorPoint"); + addButton(anchorBtn); + + y += 24; + + // Row 2: Inner Color + Outer Color + addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); + String innerHex = String.format("%06X", beam.getInnerColor(editingBeam) & 0xFFFFFF); + GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); + innerColorBtn.setTextColor(beam.getInnerColor(editingBeam) & 0xFFFFFF); + addButton(innerColorBtn); + + addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); + String outerHex = String.format("%06X", beam.getOuterColor(editingBeam) & 0xFFFFFF); + GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); + outerColorBtn.setTextColor(beam.getOuterColor(editingBeam) & 0xFFFFFF); + outerColorBtn.setEnabled(beam.isOuterColorEnabled(editingBeam)); + addButton(outerColorBtn); + + y += 24; + + // Row 2: Outer Color Enabled + Outer Color Width + addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); + addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isOuterColorEnabled(editingBeam) ? 1 : 0)); + + addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); + GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, beam.getOuterColorWidth(editingBeam)); + widthField.setEnabled(beam.isOuterColorEnabled(editingBeam)); + addTextField(widthField); + + y += 24; + + // Row 3: Rotation Speed + addLabel(new GuiNpcLabel(204, "ability.outerAlpha", labelX, y + 5)); + GuiNpcTextField alphaField = createFloatField(204, fieldX, y, 55, beam.getOuterColorAlpha(editingBeam)); + alphaField.setEnabled(beam.isOuterColorEnabled(editingBeam)); + alphaField.setMinMaxDefaultFloat(0, 1, 0.5f); + addTextField(alphaField); + + addLabel(new GuiNpcLabel(205, "ability.rotationSpeed", col2LabelX, y + 5)); + addTextField(createFloatField(205, col2FieldX, y, 50, beam.getRotationSpeed(editingBeam))); + + y += 24; + + // Row 4: Lightning Effect Enabled (only affects head) + addLabel(new GuiNpcLabel(206, "ability.lightning", labelX, y + 5)); + GuiNpcButton lightningBtn = new GuiNpcButton(206, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.hasLightningEffect(editingBeam) ? 1 : 0); + lightningBtn.setHoverText("ability.hover.lightning"); + addButton(lightningBtn); + + // Only show Lightning settings if Lightning is enabled + if (beam.hasLightningEffect(editingBeam)) { + y += 24; + + // Row 5: Density + Radius + addLabel(new GuiNpcLabel(207, "ability.lightningDensity", labelX, y + 5)); + GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, "" + beam.getLightningDensity(editingBeam)); + densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); + addTextField(densityField); + + addLabel(new GuiNpcLabel(208, "ability.lightningRadius", col2LabelX, y + 5)); + GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, "" + beam.getLightningRadius(editingBeam)); + radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); + addTextField(radiusField); + } + + y += 24; + + addButton(new GuiNpcButton(400, col2FieldX, y, 55, 20, new String[]{"Beam 1", "Beam 2"}, editingBeam)); + } + + @Override + protected void handleTypeButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 104: + beam.setHoming(value == 1); + initGui(); + break; + case 108: + beam.setExplosive(value == 1); + initGui(); + break; + case 112: + beam.setDualFire(value == 1); + initGui(); + break; + case 150: + setSubGui(new SubGuiAbilityEffects(beam.getEffects())); + break; + } + } + + @Override + protected void handleVisualButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 200: + editingVisualColorId = 200; + setSubGui(new SubGuiColorSelector(beam.getInnerColor(editingBeam))); + break; + case 201: + editingVisualColorId = 201; + setSubGui(new SubGuiColorSelector(beam.getOuterColor(editingBeam))); + break; + case 202: + beam.setOuterColorEnabled(editingBeam, value == 1); + initGui(); + break; + case 206: + beam.setLightningEffect(editingBeam, value == 1); + initGui(); + break; + case 210: + beam.setAnchorPointEnum(editingBeam, AnchorPoint.fromOrdinal(value)); + break; + case 400: + editingBeam = (editingBeam + 1) % 2; + initGui(); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { + SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; + int rgb = colorSelector.color & 0x00FFFFFF; + if (editingVisualColorId == 200) { + beam.setInnerColor(editingBeam, rgb); + } else if (editingVisualColorId == 201) { + beam.setOuterColor(editingBeam, rgb); + } + editingVisualColorId = 0; + initGui(); + } else if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + beam.setEffects(result); + } + } else { + super.subGuiClosed(subgui); + } + } + + @Override + protected void handleTypeTextField(int id, GuiNpcTextField field) { + switch (id) { + case 100: + beam.setDamage(parseFloat(field, beam.getDamage())); + break; + case 101: + beam.setSpeed(parseFloat(field, beam.getSpeed())); + break; + case 102: + beam.setBeamWidth(parseFloat(field, beam.getBeamWidth())); + break; + case 103: + beam.setHeadSize(parseFloat(field, beam.getHeadSize())); + break; + case 105: + beam.setHomingStrength(parseFloat(field, beam.getHomingStrength())); + break; + case 106: + beam.setHomingRange(parseFloat(field, beam.getHomingRange())); + break; + case 107: + beam.setMaxDistance(parseFloat(field, beam.getMaxDistance())); + break; + case 109: + beam.setExplosionRadius(parseFloat(field, beam.getExplosionRadius())); + break; + case 110: + beam.setKnockback(parseFloat(field, beam.getKnockback())); + break; + case 111: + beam.setMaxLifetime(field.getInteger()); + break; + case 113: + beam.setDualFireDelay(field.getInteger()); + break; + } + } + + @Override + protected void handleVisualTextField(int id, GuiNpcTextField field) { + switch (id) { + case 203: + beam.setOuterColorWidth(editingBeam, parseFloat(field, beam.getOuterColorWidth(editingBeam))); + break; + case 204: + beam.setOuterColorAlpha(editingBeam, parseFloat(field, beam.getOuterColorAlpha(editingBeam))); + break; + case 205: + beam.setRotationSpeed(editingBeam, parseFloat(field, beam.getRotationSpeed(editingBeam))); + break; + case 207: + beam.setLightningDensity(editingBeam, parseFloat(field, beam.getLightningDensity(editingBeam))); + break; + case 208: + beam.setLightningRadius(editingBeam, parseFloat(field, beam.getLightningRadius(editingBeam))); + break; + } + } +} From e9f22dab2a59ab3fb4e98edeb0c70ccb923c7627 Mon Sep 17 00:00:00 2001 From: Vidal Date: Sat, 31 Jan 2026 21:56:14 -0300 Subject: [PATCH 334/358] Added Dual Orb Ability --- .../data/ability/AbilityController.java | 1 + .../data/ability/type/AbilityDualOrb.java | 481 ++++++++++++++++++ .../kamkeel/npcs/entity/EntityAbilityOrb.java | 15 + .../ability/SubGuiAbilityDualOrb.java | 313 ++++++++++++ 4 files changed, 810 insertions(+) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualOrb.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualOrb.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 5b3c15e05..5603ac4ff 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -363,6 +363,7 @@ public class AbilityController implements IAbilityHandler { registerType("cnpc:sweeper", AbilitySweeper::new); registerType("cnpc:projectile", AbilityProjectile::new); registerType("cnpc:orb", AbilityOrb::new); + registerType("cnpc:dual_orb", AbilityDualOrb::new); registerType("cnpc:disc", AbilityDisc::new); registerType("cnpc:laser_shot", AbilityLaserShot::new); registerType("cnpc:beam", AbilityEnergyBeam::new); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualOrb.java new file mode 100644 index 000000000..baddfb910 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualOrb.java @@ -0,0 +1,481 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityOrb; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDualOrb; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.util.ValueUtil; + +public class AbilityDualOrb extends Ability { + // Ability-specific properties + private float orbSize = 1.0f; + private boolean dualFire= true; + private int dualFireDelay = 20; + + // Data classes for energy properties + public final EnergyColorData[] colorData = new EnergyColorData[]{ + new EnergyColorData(), new EnergyColorData() + }; + public final EnergyCombatData combatData = new EnergyCombatData(); + public final EnergyHomingData homingData = new EnergyHomingData(); + public final EnergyLightningData[] lightningData = new EnergyLightningData[]{ + new EnergyLightningData(), new EnergyLightningData() + }; + public final EnergyLifespanData lifespanData = new EnergyLifespanData(); + private EnergyAnchorData[] anchorData = new EnergyAnchorData[]{ + new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND) + }; + + // Transient state for orb entity (used during windup charging) + private transient EntityAbilityOrb orbEntity = null; + private transient EntityAbilityOrb orbEntity2 = null; + + public AbilityDualOrb() { + this.typeId = "ability.cnpc.dual_orb"; + this.name = "Dual Orb"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 25.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 30; + this.lockMovement = LockMovementType.WINDUP; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + // Default built-in animation + this.windUpAnimationName = "Ability_DualBeam_Windup"; + this.activeAnimationName = "Ability_DualBeam_Active"; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + @SideOnly(Side.CLIENT) + public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { + return new SubGuiAbilityDualOrb(this, callback); + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + return; + } + + // Start moving the orb that was spawned during windup + if (orbEntity != null && !orbEntity.isDead) { + orbEntity.startMoving(target); + } + + if (dualFire) { + if (orbEntity2 != null && !orbEntity2.isDead && dualFireDelay <= 0) { + orbEntity2.startMoving(target); + } + } else { + orbEntity2.setDead(); + } + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; + + // Spawn orb in charging mode on first tick of windup + if (tick == 1) { + // Create orb in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]); + orbEntity = new EntityAbilityOrb( + world, caster, target, + spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord, orbSize, + colorData[0], combatData, homingData, lightningData[0], lifespanData); + + Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1]); + orbEntity2 = new EntityAbilityOrb( + world, caster, target, + spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord, orbSize, + colorData[1], combatData, homingData, lightningData[1], lifespanData); + + orbEntity.setSiblingUUID(orbEntity2.getPersistentID()); + orbEntity2.setSiblingUUID(orbEntity.getPersistentID()); + + orbEntity.setupCharging(anchorData[0], windUpTicks); + orbEntity2.setupCharging(anchorData[1], windUpTicks); + + orbEntity.setEffects(this.effects); + + if (dualFire) { + orbEntity2.setEffects(this.effects); + } + + world.spawnEntityInWorld(orbEntity); + world.spawnEntityInWorld(orbEntity2); + } + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (dualFire) { + if ((orbEntity == null || orbEntity.isDead) && (orbEntity2 == null || orbEntity2.isDead)) { + orbEntity = null; + orbEntity2 = null; + signalCompletion(); + } + + if (tick == dualFireDelay && orbEntity2 != null && !orbEntity2.isDead) { + orbEntity2.startMoving(target); + } + } else { + if (orbEntity == null || orbEntity.isDead) { + orbEntity = null; + signalCompletion(); + } + } + } + + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + // Nothing to clean up - entity manages itself + } + + @Override + public void cleanup() { + // Despawn orb entity if still alive + if (orbEntity != null && !orbEntity.isDead) { + orbEntity.setDead(); + } + + if (orbEntity2 != null && !orbEntity2.isDead) { + orbEntity2.setDead(); + } + + orbEntity = null; + orbEntity2 = null; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create small circle telegraph at target position + Telegraph telegraph = Telegraph.circle(orbSize * 1.5f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return orbSize * 1.5f; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("orbSize", orbSize); + nbt.setBoolean("dualFire", dualFire); + nbt.setInteger("dualFireDelay", dualFireDelay); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + // Backward compat: old key was "orbSpeed" + nbt.setFloat("orbSpeed", homingData.speed); + + NBTTagCompound orb1 = new NBTTagCompound(); + NBTTagCompound orb2 = new NBTTagCompound(); + + anchorData[0].writeNBT(orb1); + anchorData[1].writeNBT(orb2); + colorData[0].writeNBT(orb1); + colorData[1].writeNBT(orb2); + lightningData[0].writeNBT(orb1); + lightningData[1].writeNBT(orb2); + + nbt.setTag("Orb_1", orb1); + nbt.setTag("Orb_2", orb2); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + NBTTagCompound orb1 = nbt.hasKey("Orb_1") ? nbt.getCompoundTag("Orb_1") : new NBTTagCompound(); + NBTTagCompound orb2 = nbt.hasKey("Orb_2") ? nbt.getCompoundTag("Orb_2") : new NBTTagCompound(); + + this.orbSize = nbt.hasKey("orbSize") ? nbt.getFloat("orbSize") : 1.0f; + this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire"); + this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0; + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lifespanData.readNBT(nbt); + // Backward compat: old key was "orbSpeed" + if (nbt.hasKey("orbSpeed")) { + homingData.speed = nbt.getFloat("orbSpeed"); + } + + anchorData[0].readNBT(orb1); + anchorData[1].readNBT(orb2); + colorData[0].readNBT(orb1); + colorData[1].readNBT(orb2); + lightningData[0].readNBT(orb1); + lightningData[1].readNBT(orb2); + } + + // Getters & Setters + private EnergyColorData getColorData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return colorData[orb]; + } + private EnergyLightningData getLightningData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return lightningData[orb]; + } + private EnergyAnchorData getAnchorData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return anchorData[orb]; + } + + public float getOrbSpeed() { + return homingData.speed; + } + + public void setOrbSpeed(float orbSpeed) { + homingData.speed = orbSpeed; + } + + public float getOrbSize() { + return orbSize; + } + + public void setOrbSize(float orbSize) { + this.orbSize = orbSize; + } + + public boolean isDualFire() { + return dualFire; + } + + public void setDualFire(boolean dualFire) { + this.dualFire = dualFire; + } + + public int getDualFireDelay() { + return dualFireDelay; + } + + public void setDualFireDelay(int dualFireDelay) { + this.dualFireDelay = dualFireDelay; + } + + public float getMaxDistance() { + return lifespanData.maxDistance; + } + + public void setMaxDistance(float maxDistance) { + lifespanData.maxDistance = maxDistance; + } + + public int getMaxLifetime() { + return lifespanData.maxLifetime; + } + + public void setMaxLifetime(int maxLifetime) { + lifespanData.maxLifetime = maxLifetime; + } + + public float getDamage() { + return combatData.damage; + } + + public void setDamage(float damage) { + combatData.damage = damage; + } + + public float getKnockback() { + return combatData.knockback; + } + + public void setKnockback(float knockback) { + combatData.knockback = knockback; + } + + public float getKnockbackUp() { + return combatData.knockbackUp; + } + + public void setKnockbackUp(float knockbackUp) { + combatData.knockbackUp = knockbackUp; + } + + public boolean isHoming() { + return homingData.homing; + } + + public void setHoming(boolean homing) { + homingData.homing = homing; + } + + public float getHomingStrength() { + return homingData.homingStrength; + } + + public void setHomingStrength(float homingStrength) { + homingData.homingStrength = homingStrength; + } + + public float getHomingRange() { + return homingData.homingRange; + } + + public void setHomingRange(float homingRange) { + homingData.homingRange = homingRange; + } + + public boolean isExplosive() { + return combatData.explosive; + } + + public void setExplosive(boolean explosive) { + combatData.explosive = explosive; + } + + public float getExplosionRadius() { + return combatData.explosionRadius; + } + + public void setExplosionRadius(float explosionRadius) { + combatData.explosionRadius = explosionRadius; + } + + public float getExplosionDamageFalloff() { + return combatData.explosionDamageFalloff; + } + + public void setExplosionDamageFalloff(float explosionDamageFalloff) { + combatData.explosionDamageFalloff = explosionDamageFalloff; + } + + + public int getInnerColor(int orb) { return getColorData(orb).innerColor; } + public void setInnerColor(int orb, int innerColor) { getColorData(orb).innerColor = innerColor; } + public int getOuterColor(int orb) { return getColorData(orb).outerColor; } + public void setOuterColor(int orb, int outerColor) { getColorData(orb).outerColor = outerColor; } + public boolean isOuterColorEnabled(int orb) { return getColorData(orb).outerColorEnabled; } + public void setOuterColorEnabled(int orb, boolean enabled) { getColorData(orb).outerColorEnabled = enabled; } + public float getOuterColorWidth(int orb) { return getColorData(orb).outerColorWidth; } + public void setOuterColorWidth(int orb, float width) { getColorData(orb).outerColorWidth = width; } + public float getOuterColorAlpha(int orb) { return getColorData(orb).outerColorAlpha; } + public void setOuterColorAlpha(int orb, float alpha) { getColorData(orb).outerColorAlpha = alpha; } + public float getRotationSpeed(int orb) { return getColorData(orb).rotationSpeed; } + public void setRotationSpeed(int orb, float speed) { getColorData(orb).rotationSpeed = speed; } + + public int getInnerColor() { return getInnerColor(0); } + public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); } + public int getOuterColor() { return getOuterColor(0); } + public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); } + public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); } + public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); } + public float getOuterColorWidth() { return getOuterColorWidth(0); } + public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); } + public float getOuterColorAlpha() { return getOuterColorAlpha(0); } + public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); } + public float getRotationSpeed() { return getRotationSpeed(0); } + public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); } + + + public boolean hasLightningEffect(int orb) { return getLightningData(orb).lightningEffect; } + public void setLightningEffect(int orb, boolean lightningEffect) { getLightningData(orb).lightningEffect = lightningEffect; } + public float getLightningDensity(int orb) { return getLightningData(orb).lightningDensity; } + public void setLightningDensity(int orb, float lightningDensity) { getLightningData(orb).lightningDensity = lightningDensity; } + public float getLightningRadius(int orb) { return getLightningData(orb).lightningRadius; } + public void setLightningRadius(int orb, float lightningRadius) { getLightningData(orb).lightningRadius = lightningRadius; } + + public boolean hasLightningEffect() { return hasLightningEffect(0); } + public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); } + public float getLightningDensity() { return getLightningDensity(0); } + public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); } + public float getLightningRadius() { return getLightningRadius(0); } + public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); } + + + public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; } + public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; } + public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; } + public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; } + public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; } + public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; } + public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; } + public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; } + + public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); } + public float getAnchorOffsetX() { return getAnchorOffsetX(0); } + public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); } + public float getAnchorOffsetY() { return getAnchorOffsetY(0); } + public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); } + public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); } + public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); } + + //@Override + public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); } + public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); } + + //@Override + public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); } + public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityOrb orb = new EntityAbilityOrb(npc.worldObj); + orb.setupPreview(npc, orbSize, colorData[0], lightningData[0], anchorData[0], windUpTicks); + return orb; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index c403f25de..a4384d035 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -12,6 +12,7 @@ import net.minecraft.util.Vec3; import net.minecraft.world.World; import java.util.List; +import java.util.UUID; /** * Orb projectile - spherical homing energy ball. @@ -37,6 +38,8 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { // Data watcher index for charging state (synced to clients) private static final int DW_CHARGING = 20; + private UUID siblingUUID = null; + public EntityAbilityOrb(World world) { super(world); } @@ -441,6 +444,18 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { return size; } + public UUID getSiblingUUID() { + return siblingUUID; + } + + public void setSiblingUUID(UUID siblingUUID) { + if (siblingUUID == null) + return; + + if (this.siblingUUID == null) + this.siblingUUID = siblingUUID; + } + // ==================== NBT ==================== @Override diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualOrb.java new file mode 100644 index 000000000..03a868a05 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualOrb.java @@ -0,0 +1,313 @@ +package noppes.npcs.client.gui.advanced.ability; + +import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.type.AbilityDualOrb; +import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; +import noppes.npcs.client.gui.SubGuiColorSelector; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.util.*; + +import java.util.List; + +public class SubGuiAbilityDualOrb extends SubGuiAbilityConfig { + private final AbilityDualOrb orb; + private int editingVisualColorId = 0; + private int editingOrb = 0; + + public SubGuiAbilityDualOrb(AbilityDualOrb ability, IAbilityConfigCallback callback) { + super(ability, callback); + this.orb = ability; + } + + @Override + protected boolean hasVisualSettings() { + return true; + } + + @Override + protected void initTypeTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 145; + int col2FieldX = guiLeft + 205; + + // Row 1: Damage + Speed + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); + addTextField(createFloatField(100, fieldX, y, 50, orb.getDamage())); + + addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); + addTextField(createFloatField(101, col2FieldX, y, 50, orb.getOrbSpeed())); + + y += 24; + + // Row 2: Orb Size + Max Distance + addLabel(new GuiNpcLabel(102, "stats.size", labelX, y + 5)); + addTextField(createFloatField(102, fieldX, y, 50, orb.getOrbSize())); + + addLabel(new GuiNpcLabel(103, "ability.maxDist", col2LabelX, y + 5)); + addTextField(createFloatField(103, col2FieldX, y, 50, orb.getMaxDistance())); + + y += 24; + + // Row 3: Homing + Homing Strength (only show strength if homing) + addLabel(new GuiNpcLabel(104, "ability.homing", labelX, y + 5)); + GuiNpcButton homingBtn = new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isHoming() ? 1 : 0); + homingBtn.setHoverText("ability.hover.homing"); + addButton(homingBtn); + + if (orb.isHoming()) { + addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); + addTextField(createFloatField(105, col2FieldX, y, 50, orb.getHomingStrength())); + } + + y += 24; + + // Row 4: Explosive + Explosion Radius (only show radius if explosive) + addLabel(new GuiNpcLabel(106, "ability.explosive", labelX, y + 5)); + GuiNpcButton explosiveBtn = new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isExplosive() ? 1 : 0); + explosiveBtn.setHoverText("ability.hover.explosive"); + addButton(explosiveBtn); + + if (orb.isExplosive()) { + addLabel(new GuiNpcLabel(107, "ability.explosionRad", col2LabelX, y + 5)); + addTextField(createFloatField(107, col2FieldX, y, 50, orb.getExplosionRadius())); + } + + y += 24; + + // Row 5: Knockback + Max Lifetime + addLabel(new GuiNpcLabel(108, "ability.knockback", labelX, y + 5)); + addTextField(createFloatField(108, fieldX, y, 50, orb.getKnockback())); + + addLabel(new GuiNpcLabel(109, "ability.lifetime", col2LabelX, y + 5)); + addTextField(createIntField(109, col2FieldX, y, 50, orb.getMaxLifetime())); + + y += 24; + + addLabel(new GuiNpcLabel(110, "ability.dualFire", labelX, y + 5)); + addButton(new GuiNpcButtonYesNo(110, fieldX, y, orb.isDualFire())); + + if (orb.isDualFire()) { + addLabel(new GuiNpcLabel(111, "ability.dualFireDelay", col2LabelX, y + 5)); + addTextField(createIntField(111, col2FieldX, y, 50, orb.getDualFireDelay())); + } + + y += 24; + + // Row 7: Effects button + addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); + } + + @Override + protected void initVisualTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 180; + int col2FieldX = guiLeft + 260; + + // Row 1: Anchor Point + addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); + GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), orb.getAnchorPointEnum(editingOrb).ordinal()); + anchorBtn.setHoverText("ability.hover.anchorPoint"); + addButton(anchorBtn); + + y += 24; + + // Row 2: Inner Color + Rotation Speed + addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); + String innerHex = String.format("%06X", orb.getInnerColor(editingOrb) & 0xFFFFFF); + GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); + innerColorBtn.setTextColor(orb.getInnerColor(editingOrb) & 0xFFFFFF); + addButton(innerColorBtn); + + addLabel(new GuiNpcLabel(205, "ability.rotationSpeed", col2LabelX, y + 5)); + addTextField(createFloatField(205, col2FieldX, y, 55, orb.getRotationSpeed(editingOrb))); + + y += 24; + + // Row 3: Outer Glow Enabled + addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); + addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isOuterColorEnabled(editingOrb) ? 1 : 0)); + + // Only show Outer Color and Width if Outer Glow is enabled + if (orb.isOuterColorEnabled()) { + addLabel(new GuiNpcLabel(204, "ability.outerAlpha", col2LabelX, y + 5)); + GuiNpcTextField alphaField = createFloatField(204, col2FieldX, y, 55, orb.getOuterColorAlpha(editingOrb)); + alphaField.setEnabled(orb.isOuterColorEnabled(editingOrb)); + alphaField.setMinMaxDefaultFloat(0, 1, 0.5f); + addTextField(alphaField); + + y += 24; + + // Row 4: Outer Color + Outer Width + addLabel(new GuiNpcLabel(201, "ability.outerColor", labelX, y + 5)); + String outerHex = String.format("%06X", orb.getOuterColor(editingOrb) & 0xFFFFFF); + GuiNpcButton outerColorBtn = new GuiNpcButton(201, fieldX, y, 55, 20, outerHex); + outerColorBtn.setTextColor(orb.getOuterColor(editingOrb) & 0xFFFFFF); + addButton(outerColorBtn); + + addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); + addTextField(createFloatField(203, col2FieldX, y, 55, orb.getOuterColorWidth(editingOrb))); + } + + y += 24; + + // Row 5: Lightning Effect Enabled + addLabel(new GuiNpcLabel(206, "ability.lightning", labelX, y + 5)); + GuiNpcButton lightningBtn = new GuiNpcButton(206, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.hasLightningEffect(editingOrb) ? 1 : 0); + lightningBtn.setHoverText("ability.hover.lightning"); + addButton(lightningBtn); + + // Only show Lightning settings if Lightning is enabled + if (orb.hasLightningEffect(editingOrb)) { + y += 24; + + // Row 61: Density + Radius + addLabel(new GuiNpcLabel(207, "ability.lightningDensity", labelX, y + 5)); + GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, "" + orb.getLightningDensity(editingOrb)); + densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); + addTextField(densityField); + + addLabel(new GuiNpcLabel(208, "ability.lightningRadius", col2LabelX, y + 5)); + GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, "" + orb.getLightningRadius(editingOrb)); + radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); + addTextField(radiusField); + } + + y += 24; + + addButton(new GuiNpcButton(400, col2FieldX, y, 55, 20, new String[]{"Orb 1", "Orb 2"}, editingOrb)); + } + + @Override + protected void handleTypeButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 104: + orb.setHoming(value == 1); + initGui(); + break; + case 106: + orb.setExplosive(value == 1); + initGui(); + break; + case 110: + orb.setDualFire(value == 1); + initGui(); + break; + case 150: + setSubGui(new SubGuiAbilityEffects(orb.getEffects())); + break; + } + } + + @Override + protected void handleVisualButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 200: + editingVisualColorId = 200; + setSubGui(new SubGuiColorSelector(orb.getInnerColor(editingOrb))); + break; + case 201: + editingVisualColorId = 201; + setSubGui(new SubGuiColorSelector(orb.getOuterColor(editingOrb))); + break; + case 202: + orb.setOuterColorEnabled(editingOrb, value == 1); + initGui(); + break; + case 206: + orb.setLightningEffect(editingOrb, value == 1); + initGui(); + break; + case 210: + orb.setAnchorPointEnum(editingOrb, AnchorPoint.fromOrdinal(value)); + break; + case 400: + editingOrb = (editingOrb + 1) % 2; + initGui(); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { + SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; + int rgb = colorSelector.color & 0x00FFFFFF; + if (editingVisualColorId == 200) { + orb.setInnerColor(rgb); + } else if (editingVisualColorId == 201) { + orb.setOuterColor(rgb); + } + editingVisualColorId = 0; + initGui(); + } else if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + orb.setEffects(result); + } + } else { + super.subGuiClosed(subgui); + } + } + + @Override + protected void handleTypeTextField(int id, GuiNpcTextField field) { + switch (id) { + case 100: + orb.setDamage(parseFloat(field, orb.getDamage())); + break; + case 101: + orb.setOrbSpeed(parseFloat(field, orb.getOrbSpeed())); + break; + case 102: + orb.setOrbSize(parseFloat(field, orb.getOrbSize())); + break; + case 103: + orb.setMaxDistance(parseFloat(field, orb.getMaxDistance())); + break; + case 105: + orb.setHomingStrength(parseFloat(field, orb.getHomingStrength())); + break; + case 107: + orb.setExplosionRadius(parseFloat(field, orb.getExplosionRadius())); + break; + case 108: + orb.setKnockback(parseFloat(field, orb.getKnockback())); + break; + case 109: + orb.setMaxLifetime(field.getInteger()); + break; + case 111: + orb.setDualFireDelay(field.getInteger()); + break; + } + } + + @Override + protected void handleVisualTextField(int id, GuiNpcTextField field) { + switch (id) { + case 203: + orb.setOuterColorWidth(editingOrb, parseFloat(field, orb.getOuterColorWidth(editingOrb))); + break; + case 204: + orb.setOuterColorAlpha(editingOrb, parseFloat(field, orb.getOuterColorAlpha(editingOrb))); + break; + case 205: + orb.setRotationSpeed(editingOrb, parseFloat(field, orb.getRotationSpeed(editingOrb))); + break; + case 207: + orb.setLightningDensity(editingOrb, parseFloat(field, orb.getLightningDensity(editingOrb))); + break; + case 208: + orb.setLightningRadius(editingOrb, parseFloat(field, orb.getLightningRadius(editingOrb))); + break; + } + } +} From 71065fc5208b29de4f8ed4ba44c709a429ffe4d1 Mon Sep 17 00:00:00 2001 From: Vidal Date: Sat, 31 Jan 2026 22:36:12 -0300 Subject: [PATCH 335/358] Added Dual Disc Ability --- .../data/ability/AbilityController.java | 3 +- .../data/ability/type/AbilityDualDisc.java | 410 ++++++++++++++++++ .../npcs/entity/EntityAbilityDisc.java | 17 + .../kamkeel/npcs/entity/EntityAbilityOrb.java | 4 + .../ability/SubGuiAbilityDualDisc.java | 342 +++++++++++++++ .../ability/SubGuiAbilityDualOrb.java | 5 +- .../assets/customnpcs/lang/en_US.lang | 7 + 7 files changed, 784 insertions(+), 4 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualDisc.java create mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualDisc.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 5603ac4ff..43d7bd8bb 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -364,7 +364,8 @@ public class AbilityController implements IAbilityHandler { registerType("cnpc:projectile", AbilityProjectile::new); registerType("cnpc:orb", AbilityOrb::new); registerType("cnpc:dual_orb", AbilityDualOrb::new); - registerType("cnpc:disc", AbilityDisc::new); + registerType("cnpc:dual_disc", AbilityDisc::new); + registerType("cnpc:dual_disc", AbilityDualDisc::new); registerType("cnpc:laser_shot", AbilityLaserShot::new); registerType("cnpc:beam", AbilityEnergyBeam::new); registerType("cnpc:dual_beam", AbilityDualBeam::new); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualDisc.java new file mode 100644 index 000000000..873530119 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualDisc.java @@ -0,0 +1,410 @@ +package kamkeel.npcs.controllers.data.ability.type; + +import cpw.mods.fml.relauncher.Side; +import cpw.mods.fml.relauncher.SideOnly; +import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.LockMovementType; +import kamkeel.npcs.controllers.data.ability.TargetingMode; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.controllers.data.telegraph.Telegraph; +import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; +import kamkeel.npcs.controllers.data.telegraph.TelegraphType; +import kamkeel.npcs.entity.EntityAbilityDisc; +import kamkeel.npcs.util.AnchorPointHelper; +import net.minecraft.entity.Entity; +import net.minecraft.entity.EntityLivingBase; +import net.minecraft.nbt.NBTTagCompound; +import net.minecraft.util.Vec3; +import net.minecraft.world.World; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDualDisc; +import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import noppes.npcs.entity.EntityNPCInterface; +import noppes.npcs.util.ValueUtil; + +public class AbilityDualDisc extends Ability { + + // Disc geometry + private float discRadius = 1.0f; + private float discThickness = 0.2f; + + // Boomerang properties + private boolean boomerang = false; + private int boomerangDelay = 40; + + // Dual properties + private boolean dualFire= true; + private int dualFireDelay = 10; + + // Energy data classes + private EnergyColorData[] colorData = new EnergyColorData[]{ + new EnergyColorData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 5.0f), + new EnergyColorData(0xFFFFFF, 0x8800FF, true, 0.4f, 0.5f, 5.0f) + }; + private EnergyCombatData combatData = new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f); + private EnergyHomingData homingData = new EnergyHomingData(0.6f, true, 0.12f, 18.0f); + public final EnergyLightningData[] lightningData = new EnergyLightningData[]{ + new EnergyLightningData(), new EnergyLightningData() + }; + private EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200); + private EnergyAnchorData[] anchorData = new EnergyAnchorData[]{ + new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND) + }; + + // Transient state for disc entity (used during windup charging) + private transient EntityAbilityDisc discEntity = null; + private transient EntityAbilityDisc discEntity2 = null; + + public AbilityDualDisc() { + this.typeId = "ability.cnpc.dual_disc"; + this.name = "Dual Disc"; + this.targetingMode = TargetingMode.AGGRO_TARGET; + this.maxRange = 30.0f; + this.minRange = 5.0f; + this.cooldownTicks = 0; + this.windUpTicks = 60; + this.lockMovement = LockMovementType.WINDUP; + this.telegraphType = TelegraphType.CIRCLE; + this.showTelegraph = true; + // Default built-in animations + this.windUpAnimationName = "Ability_DualBeam_Windup"; + this.activeAnimationName = "Ability_DualBeam_Active"; + } + + @Override + public boolean hasTypeSettings() { + return true; + } + + @Override + @SideOnly(Side.CLIENT) + public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { + return new SubGuiAbilityDualDisc(this, callback); + } + + @Override + public boolean isTargetingModeLocked() { + return true; + } + + @Override + public TargetingMode[] getAllowedTargetingModes() { + return new TargetingMode[]{TargetingMode.AGGRO_TARGET}; + } + + @Override + public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { + if (world.isRemote) { + signalCompletion(); + return; + } + + // Start moving the orb that was spawned during windup + if (discEntity != null && !discEntity.isDead) { + discEntity.startMoving(target); + } + + if (dualFire) { + if (discEntity2 != null && !discEntity2.isDead && dualFireDelay <= 0) { + discEntity2.startMoving(target); + } + } else { + discEntity2.setDead(); + } + + // Ability stays active until entity dies (prevents firing another while projectile is alive) + // Movement locking is handled separately by the base class + } + + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; + + // Spawn disc in charging mode on first tick of windup + if (tick == 1) { + // Create disc in charging mode - follows caster based on anchor point during windup + Vec3 spawnPos1 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[0]); + discEntity = new EntityAbilityDisc( + world, caster, target, + spawnPos1.xCoord, spawnPos1.yCoord, spawnPos1.zCoord, + discRadius, discThickness, + colorData[0], combatData, homingData, lightningData[0], lifespanData, + boomerang, boomerangDelay); + + Vec3 spawnPos2 = AnchorPointHelper.calculateAnchorPosition(caster, anchorData[1]); + discEntity2 = new EntityAbilityDisc( + world, caster, target, + spawnPos2.xCoord, spawnPos2.yCoord, spawnPos2.zCoord, + discRadius, discThickness, + colorData[1], combatData, homingData, lightningData[1], lifespanData, + boomerang, boomerangDelay); + + discEntity.setSiblingUUID(discEntity2.getPersistentID()); + discEntity2.setSiblingUUID(discEntity.getPersistentID()); + + discEntity.setupCharging(anchorData[0], windUpTicks); + discEntity2.setupCharging(anchorData[1], windUpTicks); + + discEntity.setEffects(this.effects); + + if (dualFire) { + discEntity2.setEffects(this.effects); + } + + world.spawnEntityInWorld(discEntity); + world.spawnEntityInWorld(discEntity2); + } + } + + @Override + public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + // Signal completion when entity dies + if (dualFire) { + if ((discEntity == null || discEntity.isDead) && (discEntity2 == null || discEntity2.isDead)) { + discEntity = null; + discEntity2 = null; + signalCompletion(); + } + + if (tick == dualFireDelay && discEntity2 != null && !discEntity2.isDead) { + discEntity2.startMoving(target); + } + } else { + if (discEntity == null || discEntity.isDead) { + discEntity = null; + signalCompletion(); + } + } + } + + @Override + public void onComplete(EntityLivingBase caster, EntityLivingBase target) { + } + + @Override + public void cleanup() { + // Despawn disc entity if still alive + if (discEntity != null && !discEntity.isDead) { + discEntity.setDead(); + } + + if (discEntity2 != null && !discEntity2.isDead) { + discEntity2.setDead(); + } + + discEntity = null; + discEntity2 = null; + } + + @Override + public TelegraphInstance createTelegraph(EntityLivingBase caster, EntityLivingBase target) { + if (!showTelegraph || telegraphType == TelegraphType.NONE || target == null) { + return null; + } + + // Create circle telegraph at target position + Telegraph telegraph = Telegraph.circle(discRadius * 1.5f); + telegraph.setDurationTicks(windUpTicks); + telegraph.setColor(windUpColor); + telegraph.setWarningColor(activeColor); + telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4)); + telegraph.setHeightOffset(telegraphHeightOffset); + + // Position at target and follow target during windup + TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, target.posY, target.posZ, caster.rotationYaw); + instance.setCasterEntityId(caster.getEntityId()); + instance.setEntityIdToFollow(target.getEntityId()); + + return instance; + } + + @Override + public float getTelegraphRadius() { + return discRadius * 1.5f; + } + + @Override + public void writeTypeNBT(NBTTagCompound nbt) { + nbt.setFloat("discRadius", discRadius); + nbt.setFloat("discThickness", discThickness); + nbt.setBoolean("boomerang", boomerang); + nbt.setInteger("boomerangDelay", boomerangDelay); + nbt.setBoolean("dualFire", dualFire); + nbt.setInteger("dualFireDelay", dualFireDelay); + combatData.writeNBT(nbt); + homingData.writeNBT(nbt); + lifespanData.writeNBT(nbt); + + NBTTagCompound disc1 = new NBTTagCompound(); + NBTTagCompound disc2 = new NBTTagCompound(); + + anchorData[0].writeNBT(disc1); + anchorData[1].writeNBT(disc2); + colorData[0].writeNBT(disc1); + colorData[1].writeNBT(disc2); + lightningData[0].writeNBT(disc1); + lightningData[1].writeNBT(disc2); + + nbt.setTag("Disc_1", disc1); + nbt.setTag("Disc_2", disc2); + } + + @Override + public void readTypeNBT(NBTTagCompound nbt) { + NBTTagCompound disc1 = nbt.hasKey("Disc_1") ? nbt.getCompoundTag("Disc_1") : new NBTTagCompound(); + NBTTagCompound disc2 = nbt.hasKey("Disc_2") ? nbt.getCompoundTag("Disc_2") : new NBTTagCompound(); + + this.discRadius = nbt.hasKey("discRadius") ? nbt.getFloat("discRadius") : 1.0f; + this.discThickness = nbt.hasKey("discThickness") ? nbt.getFloat("discThickness") : 0.2f; + this.boomerang = nbt.hasKey("boomerang") && nbt.getBoolean("boomerang"); + this.boomerangDelay = nbt.hasKey("boomerangDelay") ? nbt.getInteger("boomerangDelay") : 40; + this.dualFire = !nbt.hasKey("dualFire") || nbt.getBoolean("dualFire"); + this.dualFireDelay = nbt.hasKey("dualFireDelay") ? nbt.getInteger("dualFireDelay") : 0; + combatData.readNBT(nbt); + homingData.readNBT(nbt); + lifespanData.readNBT(nbt); + + anchorData[0].readNBT(disc1); + anchorData[1].readNBT(disc2); + colorData[0].readNBT(disc1); + colorData[1].readNBT(disc2); + lightningData[0].readNBT(disc1); + lightningData[1].readNBT(disc2); + } + + // Getters & Setters + private EnergyColorData getColorData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return colorData[orb]; + } + private EnergyLightningData getLightningData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return lightningData[orb]; + } + private EnergyAnchorData getAnchorData(int orb) { + orb = ValueUtil.clamp(orb, 0, 1); + return anchorData[orb]; + } + + public boolean isDualFire() { return dualFire; } + public void setDualFire(boolean dualFire) { this.dualFire = dualFire; } + public int getDualFireDelay() { return dualFireDelay; } + public void setDualFireDelay(int dualFireDelay) { this.dualFireDelay = dualFireDelay; } + public float getSpeed() { return homingData.speed; } + public void setSpeed(float speed) { homingData.speed = speed; } + public float getDiscRadius() { return discRadius; } + public void setDiscRadius(float discRadius) { this.discRadius = discRadius; } + public float getDiscThickness() { return discThickness; } + public void setDiscThickness(float discThickness) { this.discThickness = discThickness; } + public float getMaxDistance() { return lifespanData.maxDistance; } + public void setMaxDistance(float maxDistance) { lifespanData.maxDistance = maxDistance; } + public int getMaxLifetime() { return lifespanData.maxLifetime; } + public void setMaxLifetime(int maxLifetime) { lifespanData.maxLifetime = maxLifetime; } + public float getDamage() { return combatData.damage; } + public void setDamage(float damage) { combatData.damage = damage; } + public float getKnockback() { return combatData.knockback; } + public void setKnockback(float knockback) { combatData.knockback = knockback; } + public float getKnockbackUp() { return combatData.knockbackUp; } + public void setKnockbackUp(float knockbackUp) { combatData.knockbackUp = knockbackUp; } + public boolean isHoming() { return homingData.homing; } + public void setHoming(boolean homing) { homingData.homing = homing; } + public float getHomingStrength() { return homingData.homingStrength; } + public void setHomingStrength(float homingStrength) { homingData.homingStrength = homingStrength; } + public float getHomingRange() { return homingData.homingRange; } + public void setHomingRange(float homingRange) { homingData.homingRange = homingRange; } + public boolean isBoomerang() { return boomerang; } + public void setBoomerang(boolean boomerang) { this.boomerang = boomerang; } + public int getBoomerangDelay() { return boomerangDelay; } + public void setBoomerangDelay(int boomerangDelay) { this.boomerangDelay = boomerangDelay; } + public boolean isExplosive() { return combatData.explosive; } + public void setExplosive(boolean explosive) { combatData.explosive = explosive; } + public float getExplosionRadius() { return combatData.explosionRadius; } + public void setExplosionRadius(float explosionRadius) { combatData.explosionRadius = explosionRadius; } + public float getExplosionDamageFalloff() { return combatData.explosionDamageFalloff; } + public void setExplosionDamageFalloff(float explosionDamageFalloff) { combatData.explosionDamageFalloff = explosionDamageFalloff; } + + + public int getInnerColor(int orb) { return getColorData(orb).innerColor; } + public void setInnerColor(int orb, int innerColor) { getColorData(orb).innerColor = innerColor; } + public int getOuterColor(int orb) { return getColorData(orb).outerColor; } + public void setOuterColor(int orb, int outerColor) { getColorData(orb).outerColor = outerColor; } + public boolean isOuterColorEnabled(int orb) { return getColorData(orb).outerColorEnabled; } + public void setOuterColorEnabled(int orb, boolean enabled) { getColorData(orb).outerColorEnabled = enabled; } + public float getOuterColorWidth(int orb) { return getColorData(orb).outerColorWidth; } + public void setOuterColorWidth(int orb, float width) { getColorData(orb).outerColorWidth = width; } + public float getOuterColorAlpha(int orb) { return getColorData(orb).outerColorAlpha; } + public void setOuterColorAlpha(int orb, float alpha) { getColorData(orb).outerColorAlpha = alpha; } + public float getRotationSpeed(int orb) { return getColorData(orb).rotationSpeed; } + public void setRotationSpeed(int orb, float speed) { getColorData(orb).rotationSpeed = speed; } + + public int getInnerColor() { return getInnerColor(0); } + public void setInnerColor(int innerColor) { setInnerColor(0, innerColor); } + public int getOuterColor() { return getOuterColor(0); } + public void setOuterColor(int outerColor) { setOuterColor(0, outerColor); } + public boolean isOuterColorEnabled() { return isOuterColorEnabled(0); } + public void setOuterColorEnabled(boolean enabled) { setOuterColorEnabled(0, enabled); } + public float getOuterColorWidth() { return getOuterColorWidth(0); } + public void setOuterColorWidth(float width) { setOuterColorWidth(0, width); } + public float getOuterColorAlpha() { return getOuterColorAlpha(0); } + public void setOuterColorAlpha(float alpha) { setOuterColorAlpha(0, alpha); } + public float getRotationSpeed() { return getRotationSpeed(0); } + public void setRotationSpeed(float speed) { setRotationSpeed(0, speed); } + + + public boolean hasLightningEffect(int orb) { return getLightningData(orb).lightningEffect; } + public void setLightningEffect(int orb, boolean lightningEffect) { getLightningData(orb).lightningEffect = lightningEffect; } + public float getLightningDensity(int orb) { return getLightningData(orb).lightningDensity; } + public void setLightningDensity(int orb, float lightningDensity) { getLightningData(orb).lightningDensity = lightningDensity; } + public float getLightningRadius(int orb) { return getLightningData(orb).lightningRadius; } + public void setLightningRadius(int orb, float lightningRadius) { getLightningData(orb).lightningRadius = lightningRadius; } + + public boolean hasLightningEffect() { return hasLightningEffect(0); } + public void setLightningEffect(boolean lightningEffect) { setLightningEffect(0, lightningEffect); } + public float getLightningDensity() { return getLightningDensity(0); } + public void setLightningDensity(float lightningDensity) { setLightningDensity(0, lightningDensity); } + public float getLightningRadius() { return getLightningRadius(0); } + public void setLightningRadius(float lightningRadius) { setLightningRadius(0, lightningRadius); } + + + public AnchorPoint getAnchorPointEnum(int orb) { return getAnchorData(orb).anchorPoint; } + public void setAnchorPointEnum(int orb, AnchorPoint anchorPoint) { getAnchorData(orb).anchorPoint = anchorPoint; } + public float getAnchorOffsetX(int orb) { return getAnchorData(orb).anchorOffsetX; } + public void setAnchorOffsetX(int orb, float x) { getAnchorData(orb).anchorOffsetX = x; } + public float getAnchorOffsetY(int orb) { return getAnchorData(orb).anchorOffsetY; } + public void setAnchorOffsetY(int orb, float y) { getAnchorData(orb).anchorOffsetY = y; } + public float getAnchorOffsetZ(int orb) { return getAnchorData(orb).anchorOffsetZ; } + public void setAnchorOffsetZ(int orb, float z) { getAnchorData(orb).anchorOffsetZ = z; } + + public AnchorPoint getAnchorPointEnum() { return getAnchorPointEnum(0); } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { setAnchorPointEnum(0, anchorPoint); } + public float getAnchorOffsetX() { return getAnchorOffsetX(0); } + public void setAnchorOffsetX(float x) { setAnchorOffsetX(0, x); } + public float getAnchorOffsetY() { return getAnchorOffsetY(0); } + public void setAnchorOffsetY(float y) { setAnchorOffsetY(0, y); } + public float getAnchorOffsetZ() { return getAnchorOffsetZ(0); } + public void setAnchorOffsetZ(float z) { setAnchorOffsetZ(0, z); } + + //@Override + public int getAnchorPoint(int orb) { return getAnchorData(orb).anchorPoint.ordinal(); } + public int getAnchorPoint() { return getAnchorData(0).anchorPoint.ordinal(); } + + //@Override + public void setAnchorPoint(int orb, int point) { getAnchorData(orb).anchorPoint = AnchorPoint.fromOrdinal(point); } + public void setAnchorPoint(int point) { getAnchorData(0).anchorPoint = AnchorPoint.fromOrdinal(point); } + + @Override + @SideOnly(Side.CLIENT) + public Entity createPreviewEntity(EntityNPCInterface npc) { + if (npc == null || npc.worldObj == null) return null; + + EntityAbilityDisc disc = new EntityAbilityDisc(npc.worldObj); + disc.setupPreview(npc, discRadius, discThickness, colorData[0], lightningData[0], anchorData[0], windUpTicks); + return disc; + } + + @Override + public int getPreviewActiveDuration() { + return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; + } +} diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index 7dc3a8a82..d0005c7e0 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -12,6 +12,7 @@ import net.minecraft.util.Vec3; import net.minecraft.world.World; import java.util.List; +import java.util.UUID; /** * Disc projectile - flat spinning disc with optional boomerang behavior. @@ -48,6 +49,8 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { // Data watcher index for charging state (synced to clients) private static final int DW_CHARGING = 20; + private UUID siblingUUID = null; + public EntityAbilityDisc(World world) { super(world); } @@ -565,6 +568,18 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { return returning; } + public UUID getSiblingUUID() { + return siblingUUID; + } + + public void setSiblingUUID(UUID siblingUUID) { + if (siblingUUID == null) + return; + + if (this.siblingUUID == null) + this.siblingUUID = siblingUUID; + } + // ==================== NBT ==================== @Override @@ -587,6 +602,7 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { this.targetDiscRadius = nbt.hasKey("TargetDiscRadius") ? nbt.getFloat("TargetDiscRadius") : this.discRadius; this.targetDiscThickness = nbt.hasKey("TargetDiscThickness") ? nbt.getFloat("TargetDiscThickness") : this.discThickness; this.anchorData.readNBT(nbt); + this.siblingUUID = nbt.hasKey("SiblingUUID") ? UUID.fromString(nbt.getString("SiblingUUID")) : null; } @Override @@ -609,5 +625,6 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { // Anchor data this.anchorData.writeNBT(nbt); + nbt.setString("SiblingUUID", siblingUUID.toString()); } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index a4384d035..250b1d629 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -473,6 +473,8 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size; // Anchor data this.anchorData.readNBT(nbt); + + this.siblingUUID = nbt.hasKey("SiblingUUID") ? UUID.fromString(nbt.getString("SiblingUUID")) : null; } @Override @@ -487,5 +489,7 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { nbt.setInteger("ChargeTick", chargeTick); anchorData.writeNBT(nbt); nbt.setFloat("TargetSize", targetSize); + + nbt.setString("SiblingUUID", siblingUUID.toString()); } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualDisc.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualDisc.java new file mode 100644 index 000000000..f8bdbf9f2 --- /dev/null +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualDisc.java @@ -0,0 +1,342 @@ +package noppes.npcs.client.gui.advanced.ability; + +import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.type.AbilityDisc; +import kamkeel.npcs.controllers.data.ability.type.AbilityDualDisc; +import noppes.npcs.client.gui.SubGuiColorSelector; +import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; +import noppes.npcs.client.gui.util.*; + +import java.util.List; + +public class SubGuiAbilityDualDisc extends SubGuiAbilityConfig { + private final AbilityDualDisc disc; + private int editingVisualColorId = 0; + private int editingDisc = 0; + + public SubGuiAbilityDualDisc(AbilityDualDisc ability, IAbilityConfigCallback callback) { + super(ability, callback); + this.disc = ability; + } + + @Override + protected boolean hasVisualSettings() { + return true; + } + + @Override + protected void initTypeTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 145; + int col2FieldX = guiLeft + 205; + + // Row 1: Damage + Speed + addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); + addTextField(createFloatField(100, fieldX, y, 50, disc.getDamage())); + + addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); + addTextField(createFloatField(101, col2FieldX, y, 50, disc.getSpeed())); + + y += 24; + + // Row 2: Disc Radius + Disc Thickness + addLabel(new GuiNpcLabel(102, "ability.discRadius", labelX, y + 5)); + addTextField(createFloatField(102, fieldX, y, 50, disc.getDiscRadius())); + + addLabel(new GuiNpcLabel(103, "ability.discThickness", col2LabelX, y + 5)); + addTextField(createFloatField(103, col2FieldX, y, 50, disc.getDiscThickness())); + + y += 24; + + // Row 3: Homing + Homing Strength (only show strength if homing) + addLabel(new GuiNpcLabel(104, "ability.homing", labelX, y + 5)); + GuiNpcButton homingBtn = new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isHoming() ? 1 : 0); + homingBtn.setHoverText("ability.hover.homing"); + addButton(homingBtn); + + if (disc.isHoming()) { + addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); + addTextField(createFloatField(105, col2FieldX, y, 50, disc.getHomingStrength())); + } + + y += 24; + + // Row 4: Boomerang + Boomerang Delay (only show delay if boomerang) + addLabel(new GuiNpcLabel(106, "ability.boomerang", labelX, y + 5)); + GuiNpcButton boomerangBtn = new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isBoomerang() ? 1 : 0); + boomerangBtn.setHoverText("ability.hover.boomerang"); + addButton(boomerangBtn); + + if (disc.isBoomerang()) { + addLabel(new GuiNpcLabel(107, "ability.boomerangDelay", col2LabelX, y + 5)); + addTextField(createIntField(107, col2FieldX, y, 50, disc.getBoomerangDelay())); + } + + y += 24; + + // Row 5: Explosive + Explosion Radius (only show radius if explosive) + addLabel(new GuiNpcLabel(108, "ability.explosive", labelX, y + 5)); + GuiNpcButton explosiveBtn = new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isExplosive() ? 1 : 0); + explosiveBtn.setHoverText("ability.hover.explosive"); + addButton(explosiveBtn); + + if (disc.isExplosive()) { + addLabel(new GuiNpcLabel(109, "ability.explosionRad", col2LabelX, y + 5)); + addTextField(createFloatField(109, col2FieldX, y, 50, disc.getExplosionRadius())); + } + + y += 24; + + // Row 6: Knockback + Max Distance + addLabel(new GuiNpcLabel(110, "ability.knockback", labelX, y + 5)); + addTextField(createFloatField(110, fieldX, y, 50, disc.getKnockback())); + + addLabel(new GuiNpcLabel(111, "ability.maxDist", col2LabelX, y + 5)); + addTextField(createFloatField(111, col2FieldX, y, 50, disc.getMaxDistance())); + + y += 24; + + // Row 7: Max Lifetime + Effects button + addLabel(new GuiNpcLabel(112, "ability.lifetime", labelX, y + 5)); + addTextField(createIntField(112, fieldX, y, 50, disc.getMaxLifetime())); + + y += 24; + + addLabel(new GuiNpcLabel(113, "ability.dualFire", labelX, y + 5)); + addButton(new GuiNpcButtonYesNo(113, fieldX, y, disc.isDualFire())); + + if (disc.isDualFire()) { + addLabel(new GuiNpcLabel(114, "ability.dualFireDelay", col2LabelX, y + 5)); + addTextField(createIntField(114, col2FieldX, y, 50, disc.getDualFireDelay())); + } + + addButton(new GuiNpcButton(150, col2LabelX + 120, y, 80, 20, "ability.effects")); + } + + @Override + protected void initVisualTab(int startY) { + int y = startY; + int labelX = guiLeft + 8; + int fieldX = guiLeft + 85; + int col2LabelX = guiLeft + 180; + int col2FieldX = guiLeft + 260; + + // Row 1: Anchor Point + addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); + GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), disc.getAnchorPointEnum(editingDisc).ordinal()); + anchorBtn.setHoverText("ability.hover.anchorPoint"); + addButton(anchorBtn); + + y += 24; + + // Row 2: Inner Color + Outer Color + addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); + String innerHex = String.format("%06X", disc.getInnerColor(editingDisc) & 0xFFFFFF); + GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); + innerColorBtn.setTextColor(disc.getInnerColor(editingDisc) & 0xFFFFFF); + addButton(innerColorBtn); + + addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); + String outerHex = String.format("%06X", disc.getOuterColor(editingDisc) & 0xFFFFFF); + GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); + outerColorBtn.setTextColor(disc.getOuterColor(editingDisc) & 0xFFFFFF); + outerColorBtn.setEnabled(disc.isOuterColorEnabled(editingDisc)); + addButton(outerColorBtn); + + y += 24; + + // Row 2: Outer Color Enabled + Outer Color Width + addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); + addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isOuterColorEnabled(editingDisc) ? 1 : 0)); + + addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); + GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, disc.getOuterColorWidth(editingDisc)); + widthField.setEnabled(disc.isOuterColorEnabled(editingDisc)); + addTextField(widthField); + + y += 24; + + // Row 3: Outer Color Alpha + addLabel(new GuiNpcLabel(204, "ability.outerAlpha", labelX, y + 5)); + GuiNpcTextField alphaField = createFloatField(204, fieldX, y, 55, disc.getOuterColorAlpha(editingDisc)); + alphaField.setEnabled(disc.isOuterColorEnabled(editingDisc)); + alphaField.setMinMaxDefaultFloat(0, 1, 0.5f); + addTextField(alphaField); + + y += 24; + + // Row 4: Rotation Speed + addLabel(new GuiNpcLabel(205, "ability.rotationSpeed", labelX, y + 5)); + addTextField(createFloatField(205, fieldX, y, 50, disc.getRotationSpeed(editingDisc))); + + y += 24; + + // Row 5: Lightning Effect Enabled + addLabel(new GuiNpcLabel(206, "ability.lightning", labelX, y + 5)); + GuiNpcButton lightningBtn = new GuiNpcButton(206, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.hasLightningEffect(editingDisc) ? 1 : 0); + lightningBtn.setHoverText("ability.hover.lightning"); + addButton(lightningBtn); + + // Only show Lightning settings if Lightning is enabled + if (disc.hasLightningEffect(editingDisc)) { + y += 24; + + // Row 6: Density + Radius + addLabel(new GuiNpcLabel(207, "ability.lightningDensity", labelX, y + 5)); + GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, "" + disc.getLightningDensity(editingDisc)); + densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); + addTextField(densityField); + + addLabel(new GuiNpcLabel(208, "ability.lightningRadius", col2LabelX, y + 5)); + GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, "" + disc.getLightningRadius(editingDisc)); + radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); + addTextField(radiusField); + } + + y += 24; + + addButton(new GuiNpcButton(400, col2FieldX, y, 55, 20, new String[]{"Disc 1", "Disc 2"}, editingDisc)); + } + + @Override + protected void handleTypeButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 104: + disc.setHoming(value == 1); + initGui(); + break; + case 106: + disc.setBoomerang(value == 1); + initGui(); + break; + case 108: + disc.setExplosive(value == 1); + initGui(); + break; + case 113: + disc.setDualFire(value == 1); + initGui(); + break; + case 150: + setSubGui(new SubGuiAbilityEffects(disc.getEffects())); + break; + } + } + + @Override + protected void handleVisualButton(int id, GuiNpcButton button) { + int value = button.getValue(); + switch (id) { + case 200: + editingVisualColorId = 200; + setSubGui(new SubGuiColorSelector(disc.getInnerColor(editingDisc))); + break; + case 201: + editingVisualColorId = 201; + setSubGui(new SubGuiColorSelector(disc.getOuterColor(editingDisc))); + break; + case 202: + disc.setOuterColorEnabled(editingDisc, value == 1); + initGui(); + break; + case 206: + disc.setLightningEffect(editingDisc, value == 1); + initGui(); + break; + case 210: + disc.setAnchorPointEnum(editingDisc, AnchorPoint.fromOrdinal(value)); + break; + case 400: + editingDisc = (editingDisc + 1) % 2; + initGui(); + break; + } + } + + @Override + public void subGuiClosed(SubGuiInterface subgui) { + if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { + SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; + int rgb = colorSelector.color & 0x00FFFFFF; + if (editingVisualColorId == 200) { + disc.setInnerColor(editingDisc, rgb); + } else if (editingVisualColorId == 201) { + disc.setOuterColor(editingDisc, rgb); + } + editingVisualColorId = 0; + initGui(); + } else if (subgui instanceof SubGuiAbilityEffects) { + SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; + List result = effectsGui.getResult(); + if (result != null) { + disc.setEffects(result); + } + } else { + super.subGuiClosed(subgui); + } + } + + @Override + protected void handleTypeTextField(int id, GuiNpcTextField field) { + switch (id) { + case 100: + disc.setDamage(parseFloat(field, disc.getDamage())); + break; + case 101: + disc.setSpeed(parseFloat(field, disc.getSpeed())); + break; + case 102: + disc.setDiscRadius(parseFloat(field, disc.getDiscRadius())); + break; + case 103: + disc.setDiscThickness(parseFloat(field, disc.getDiscThickness())); + break; + case 105: + disc.setHomingStrength(parseFloat(field, disc.getHomingStrength())); + break; + case 107: + disc.setBoomerangDelay(field.getInteger()); + break; + case 109: + disc.setExplosionRadius(parseFloat(field, disc.getExplosionRadius())); + break; + case 110: + disc.setKnockback(parseFloat(field, disc.getKnockback())); + break; + case 111: + disc.setMaxDistance(parseFloat(field, disc.getMaxDistance())); + break; + case 112: + disc.setMaxLifetime(field.getInteger()); + break; + case 114: + disc.setDualFireDelay(field.getInteger()); + break; + } + } + + @Override + protected void handleVisualTextField(int id, GuiNpcTextField field) { + switch (id) { + case 203: + disc.setOuterColorWidth(editingDisc, parseFloat(field, disc.getOuterColorWidth(editingDisc))); + break; + case 204: + disc.setOuterColorAlpha(editingDisc, parseFloat(field, disc.getOuterColorAlpha(editingDisc))); + break; + case 205: + disc.setRotationSpeed(editingDisc, parseFloat(field, disc.getRotationSpeed(editingDisc))); + break; + case 207: + disc.setLightningDensity(editingDisc, parseFloat(field, disc.getLightningDensity(editingDisc))); + break; + case 208: + disc.setLightningRadius(editingDisc, parseFloat(field, disc.getLightningRadius(editingDisc))); + break; + } + } +} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualOrb.java index 03a868a05..a8b9caadf 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualOrb.java @@ -3,7 +3,6 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.type.AbilityDualOrb; -import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.*; @@ -240,9 +239,9 @@ public class SubGuiAbilityDualOrb extends SubGuiAbilityConfig { SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; int rgb = colorSelector.color & 0x00FFFFFF; if (editingVisualColorId == 200) { - orb.setInnerColor(rgb); + orb.setInnerColor(editingOrb, rgb); } else if (editingVisualColorId == 201) { - orb.setOuterColor(rgb); + orb.setOuterColor(editingOrb, rgb); } editingVisualColorId = 0; initGui(); diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index fef02e070..f64e3820f 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1562,8 +1562,10 @@ ability.cnpc.slam=Slam ability.cnpc.heavy_hit=Heavy Hit ability.cnpc.cutter=Cutter ability.cnpc.beam=Beam +ability.cnpc.dual_beam=Dual Beam ability.cnpc.projectile=Projectile ability.cnpc.orb=Orb +ability.cnpc.dual_orb=Dual Orb ability.cnpc.charge=Charge ability.cnpc.dash=Dash ability.cnpc.teleport=Teleport @@ -1574,6 +1576,7 @@ ability.cnpc.heal=Heal ability.cnpc.hazard=Hazard ability.cnpc.trap=Trap ability.cnpc.disc=Disc +ability.cnpc.dual_disc=Dual Disc ability.cnpc.laser_shot=Laser Shot ability.cnpc.sweeper=Sweeper @@ -1699,6 +1702,10 @@ ability.lightningRadius=Radius ability.anchorPoint=Anchor Point ability.noVisualSettings=No visual settings for this ability type +# Dual Energy Projectile Settings +ability.dualFire=Dual Fire +ability.dualFireDelay=Fire Delay + # Laser Shot Settings ability.laserWidth=Laser Width ability.expansionSpeed=Expansion Spd From d6b7b969b316b6ef3598326407e01300bfeb831b Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:39:03 +0100 Subject: [PATCH 336/358] Fix crash on server --- src/main/java/noppes/npcs/CustomNpcs.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/noppes/npcs/CustomNpcs.java b/src/main/java/noppes/npcs/CustomNpcs.java index ff4617545..0bc081429 100644 --- a/src/main/java/noppes/npcs/CustomNpcs.java +++ b/src/main/java/noppes/npcs/CustomNpcs.java @@ -158,7 +158,13 @@ public class CustomNpcs { @SideOnly(Side.CLIENT) private static IDynamicCompiler clientJaninoCompiler; @SideOnly(Side.CLIENT) - private final static Set> clientClassFilterConsumer = new HashSet<>(); + private static Set> clientClassFilterConsumer; + + static { + if (FMLCommonHandler.instance().getEffectiveSide().isClient()) { + clientClassFilterConsumer = new HashSet<>(); + } + } @SideOnly(Side.CLIENT) public static void addClassesToClientClassFilter(Consumer consumer) { From 86a4c15dbb8e3d63745473020bc4fec0a458a56f Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 1 Feb 2026 00:59:54 -0300 Subject: [PATCH 337/358] Fixed energy abilities not rendering --- src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java | 5 ++++- src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java | 5 ++++- src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java | 4 +++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java index bceae89aa..6aed40c3f 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityBeam.java @@ -843,6 +843,9 @@ public class EntityAbilityBeam extends EntityAbilityProjectile { trailList.appendTag(pointNbt); } nbt.setTag("Trail", trailList); - nbt.setString("SiblingUUID", siblingUUID.toString()); + + if (siblingUUID instanceof UUID) { + nbt.setString("SiblingUUID", siblingUUID.toString()); + } } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java index d0005c7e0..04afb2224 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityDisc.java @@ -625,6 +625,9 @@ public class EntityAbilityDisc extends EntityAbilityProjectile { // Anchor data this.anchorData.writeNBT(nbt); - nbt.setString("SiblingUUID", siblingUUID.toString()); + + if (siblingUUID instanceof UUID) { + nbt.setString("SiblingUUID", siblingUUID.toString()); + } } } diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java index 250b1d629..3c5895206 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityOrb.java @@ -490,6 +490,8 @@ public class EntityAbilityOrb extends EntityAbilityProjectile { anchorData.writeNBT(nbt); nbt.setFloat("TargetSize", targetSize); - nbt.setString("SiblingUUID", siblingUUID.toString()); + if (siblingUUID instanceof UUID) { + nbt.setString("SiblingUUID", siblingUUID.toString()); + } } } From 44f7161c9ffe6aafbf204da8cd2f534483fc4a50 Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 1 Feb 2026 01:50:28 -0300 Subject: [PATCH 338/358] Added charge to animation and fixed some access modifiers --- .../client/renderer/RenderAbilityLaser.java | 75 +++++++- .../data/ability/type/AbilityLaserShot.java | 70 +++++--- .../data/ability/type/AbilityOrb.java | 12 +- .../npcs/entity/EntityAbilityLaser.java | 167 +++++++++++++++++- .../ability/SubGuiAbilityLaserShot.java | 20 ++- 5 files changed, 302 insertions(+), 42 deletions(-) diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java index bf4df79eb..e70e5c6ba 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityLaser.java @@ -3,6 +3,7 @@ package kamkeel.npcs.client.renderer; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.client.renderer.lightning.AttachedLightningRenderer; +import kamkeel.npcs.entity.EntityAbilityBeam; import kamkeel.npcs.entity.EntityAbilityLaser; import net.minecraft.client.renderer.Tessellator; import net.minecraft.entity.Entity; @@ -20,11 +21,14 @@ public class RenderAbilityLaser extends RenderAbilityProjectile { public void doRender(Entity entity, double x, double y, double z, float yaw, float partialTicks) { EntityAbilityLaser laser = (EntityAbilityLaser) entity; - // Don't render if length is 0 - if (laser.getCurrentLength() <= 0) return; - setupRenderState(); + if (laser.isCharging()) { + renderChargingOrb(laser, x, y, z, partialTicks); + restoreRenderState(); + return; + } + // Get laser parameters double startX = laser.getStartX(); double startY = laser.getStartY(); @@ -123,6 +127,55 @@ public class RenderAbilityLaser extends RenderAbilityProjectile { restoreRenderState(); } + private void renderChargingOrb(EntityAbilityLaser laser, double x, double y, double z, float partialTicks) { + float headSize = laser.getLaserWidth() * 1.5f; + float chargeProgress = laser.getInterpolatedChargeProgress(partialTicks); + float size = headSize * chargeProgress; + + if (size <= 0.01f) return; + + int innerColor = laser.getInnerColor(); + int outerColor = laser.getOuterColor(); + + GL11.glPushMatrix(); + GL11.glTranslated(x, y, z); + + // Pulsing effect + float pulseTime = laser.ticksExisted + partialTicks; + float scaleModifier = (float) Math.sin(pulseTime * 0.2f) * 0.08f; + float scale = size * (1.0f + scaleModifier); + + // Render lightning during charging if enabled + if (laser.hasLightningEffect()) { + renderAttachedLightning(laser, scale); + } + + GL11.glPushMatrix(); + GL11.glScalef(scale, scale, scale); + + // Inner scale defines the core size + float innerScale = 0.6f; + + // Render outer glow only if enabled + if (laser.isOuterColorEnabled()) { + float outerScale = innerScale + laser.getOuterColorWidth(); + float outerAlpha = laser.getOuterColorAlpha(); + GL11.glDepthMask(false); + GL11.glPushMatrix(); + GL11.glScalef(outerScale, outerScale, outerScale); + renderCube(outerColor, outerAlpha, 0.5f); + GL11.glPopMatrix(); + GL11.glDepthMask(true); + } + + // Render inner core + GL11.glScalef(innerScale, innerScale, innerScale); + renderCube(innerColor, 1.0f, 0.5f); + + GL11.glPopMatrix(); + GL11.glPopMatrix(); + } + /** * Render lightning effects along the laser beam at multiple points. */ @@ -161,6 +214,22 @@ public class RenderAbilityLaser extends RenderAbilityProjectile { GL11.glPopMatrix(); } + + private void renderAttachedLightning(EntityAbilityLaser laser, float headScale) { + AttachedLightningRenderer.LightningState state = getLightningState(laser); + + float density = laser.getLightningDensity(); + // Lightning radius extends outward from inner surface (innerScale = 0.6) + float innerRadius = 0.6f * headScale * 0.5f; + float radius = innerRadius + laser.getLightningRadius() * headScale; + int outerColor = laser.getOuterColor(); + int innerColor = laser.getInnerColor(); + int fadeTime = laser.getLightningFadeTime(); + + state.update(density, radius, outerColor, innerColor, fadeTime); + state.render(); + } + /** * Get or create the main lightning state for the laser. */ diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index acf5f0aa4..5d42b23cc 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -3,18 +3,18 @@ package kamkeel.npcs.controllers.data.ability.type; import cpw.mods.fml.relauncher.Side; import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; -import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; -import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; -import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; +import kamkeel.npcs.controllers.data.ability.data.*; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import kamkeel.npcs.entity.EntityAbilityLaser; +import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; +import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityLaserShot; @@ -34,10 +34,11 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { private int lingerTicks = 8; // Data classes - private EnergyColorData colorData = new EnergyColorData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f); - private EnergyCombatData combatData = new EnergyCombatData(6.0f, 0.5f, 0.05f, false, 2.0f, 0.5f); - private EnergyLightningData lightningData = new EnergyLightningData(); - private EnergyLifespanData lifespanData = new EnergyLifespanData(40.0f, 100); + private final EnergyColorData colorData = new EnergyColorData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f); + private final EnergyCombatData combatData = new EnergyCombatData(6.0f, 0.5f, 0.05f, false, 2.0f, 0.5f); + private final EnergyLightningData lightningData = new EnergyLightningData(); + private final EnergyLifespanData lifespanData = new EnergyLifespanData(40.0f, 100); + private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); // Transient state for laser entity (used for movement locking) private transient EntityAbilityLaser laserEntity = null; @@ -89,27 +90,34 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { return laserWidth * 2.0f; // Make telegraph slightly wider for visibility } + @Override + public void onWindUpTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { + if (world.isRemote) return; + + if (tick == 1) { + Vec3 spawnPos = AnchorPointHelper.calculateAnchorPosition(caster, anchorData); + laserEntity = new EntityAbilityLaser( + world, caster, target, + spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, + laserWidth, + colorData, combatData, lightningData, lifespanData, + expansionSpeed, lingerTicks + ); + + laserEntity.setupCharging(anchorData, windUpTicks); + + laserEntity.setEffects(this.effects); + world.spawnEntityInWorld(laserEntity); + } + } + @Override public void onExecute(EntityLivingBase caster, EntityLivingBase target, World world) { if (world.isRemote) { signalCompletion(); - return; } - double spawnX = caster.posX; - double spawnY = caster.posY + caster.getEyeHeight(); - double spawnZ = caster.posZ; - - laserEntity = new EntityAbilityLaser( - world, caster, target, - spawnX, spawnY, spawnZ, - laserWidth, - colorData, combatData, lightningData, lifespanData, - expansionSpeed, lingerTicks - ); - - laserEntity.setEffects(this.effects); - world.spawnEntityInWorld(laserEntity); + laserEntity.startMoving(target); // Ability stays active until entity dies (prevents firing another while projectile is alive) // Movement locking is handled separately by the base class @@ -146,6 +154,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { nbt.setFloat("laserWidth", laserWidth); nbt.setFloat("expansionSpeed", expansionSpeed); nbt.setInteger("lingerTicks", lingerTicks); + anchorData.writeNBT(nbt); colorData.writeNBT(nbt); combatData.writeNBT(nbt); lightningData.writeNBT(nbt); @@ -157,6 +166,7 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { this.laserWidth = nbt.hasKey("laserWidth") ? nbt.getFloat("laserWidth") : 0.3f; this.expansionSpeed = nbt.hasKey("expansionSpeed") ? nbt.getFloat("expansionSpeed") : 3.0f; this.lingerTicks = nbt.hasKey("lingerTicks") ? nbt.getInteger("lingerTicks") : 8; + anchorData.readNBT(nbt); colorData.readNBT(nbt); combatData.readNBT(nbt); lightningData.readNBT(nbt); @@ -210,6 +220,20 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { public float getLightningRadius() { return lightningData.lightningRadius; } public void setLightningRadius(float lightningRadius) { this.lightningData.lightningRadius = lightningRadius; } + public AnchorPoint getAnchorPointEnum() { return anchorData.anchorPoint; } + + public float getAnchorOffsetX() { return anchorData.anchorOffsetX; } + public float getAnchorOffsetY() { return anchorData.anchorOffsetY; } + public float getAnchorOffsetZ() { return anchorData.anchorOffsetZ; } + public void setAnchorPointEnum(AnchorPoint anchorPoint) { this.anchorData.anchorPoint = anchorPoint; } + public void setAnchorOffsetX(float x) { this.anchorData.anchorOffsetX = x; } + public void setAnchorOffsetY(float y) { this.anchorData.anchorOffsetY = y; } + public void setAnchorOffsetZ(float z) { this.anchorData.anchorOffsetZ = z; } + public int getAnchorPoint() { + return anchorData.anchorPoint.ordinal(); + } + public void setAnchorPoint(int point) { this.anchorData.anchorPoint = AnchorPoint.fromOrdinal(point); } + @Override @SideOnly(Side.CLIENT) public Entity createPreviewEntity(EntityNPCInterface npc) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 63c7c9005..0ae1c6182 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -34,12 +34,12 @@ public class AbilityOrb extends Ability implements IAbilityOrb { private float orbSize = 1.0f; // Data classes for energy properties - public final EnergyColorData colorData = new EnergyColorData(); - public final EnergyCombatData combatData = new EnergyCombatData(); - public final EnergyHomingData homingData = new EnergyHomingData(); - public final EnergyLightningData lightningData = new EnergyLightningData(); - public final EnergyLifespanData lifespanData = new EnergyLifespanData(); - private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); + private final EnergyColorData colorData = new EnergyColorData(); + private final EnergyCombatData combatData = new EnergyCombatData(); + private final EnergyHomingData homingData = new EnergyHomingData(); + private final EnergyLightningData lightningData = new EnergyLightningData(); + private final EnergyLifespanData lifespanData = new EnergyLifespanData(); + private final EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.RIGHT_HAND); // Transient state for orb entity (used during windup charging) private transient EntityAbilityOrb orbEntity = null; diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index 3d888c698..d8aeb725c 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -1,9 +1,8 @@ package kamkeel.npcs.entity; -import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; -import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; -import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; -import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; +import kamkeel.npcs.controllers.data.ability.data.*; +import kamkeel.npcs.util.AnchorPointHelper; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; @@ -43,10 +42,48 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { // End point for rendering private double endX, endY, endZ; + // Charging state (during windup) + private boolean charging = false; + private int chargeDuration = 40; + private int chargeTick = 0; + private EnergyAnchorData anchorData = new EnergyAnchorData(AnchorPoint.FRONT); + private float targetSize = 1.0f; // Full size to grow to during charging + + // Data watcher index for charging state (synced to clients) + private static final int DW_CHARGING = 20; + public EntityAbilityLaser(World world) { super(world); } + @Override + protected void entityInit() { + super.entityInit(); + // Register data watcher for charging state + this.dataWatcher.addObject(DW_CHARGING, (byte) 0); + } + + /** + * Check if orb is in charging state (synced via data watcher). + * In preview mode, uses local field since data watcher isn't synced. + */ + public boolean isCharging() { + if (previewMode) { + return this.charging; + } + return this.dataWatcher.getWatchableObjectByte(DW_CHARGING) == 1; + } + + /** + * Set charging state (server only, synced to clients via data watcher). + */ + private void setCharging(boolean value) { + this.charging = value; + if (!worldObj.isRemote) { + this.dataWatcher.updateObject(DW_CHARGING, (byte) (value ? 1 : 0)); + } + } + /** * Full constructor with all parameters using data classes. */ @@ -70,7 +107,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { // Calculate direction toward target or forward if (target != null) { double dx = target.posX - x; - double dy = (target.posY + target.getEyeHeight()) - y; + double dy = (target.posY + target.getEyeHeight() - 0.4) - y; double dz = target.posZ - z; double len = Math.sqrt(dx * dx + dy * dy + dz * dz); if (len > 0) { @@ -93,6 +130,25 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { this.endZ = z; } + /** + * Setup this orb in charging mode (for windup phase). + * The orb will grow from 0 to orbSize over chargeDuration ticks. + * Position follows the owner based on anchor point. + */ + public void setupCharging(EnergyAnchorData anchor, int chargeDuration) { + setCharging(true); + this.chargeDuration = chargeDuration; + this.chargeTick = 0; + this.anchorData = anchor; + this.targetSize = this.size; + this.size = 0.01f; + this.renderCurrentSize = 0.01f; + this.prevRenderSize = 0.01f; + this.motionX = 0; + this.motionY = 0; + this.motionZ = 0; + } + @Override protected void updateRotation() { // Laser doesn't rotate @@ -106,6 +162,11 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { @Override protected void updateProjectile() { + if (isCharging()) { + updateCharging(); + return; + } + if (!fullyExtended) { // Expand the laser currentLength += expansionSpeed; @@ -134,6 +195,46 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { } } + public void startMoving(EntityLivingBase target) { + if (!isCharging()) return; + + setCharging(false); + + // Update start position to current position + startX = posX; + startY = posY; + startZ = posZ; + + // Calculate velocity toward target + Entity owner = getOwner(); + + if (target != null) { + double dx = target.posX - startX; + double dy = (target.posY + target.getEyeHeight() - 0.4) - startY; + double dz = target.posZ - startZ; + double len = Math.sqrt(dx * dx + dy * dy + dz * dz); + if (len > 0) { + this.dirX = dx / len; + this.dirY = dy / len; + this.dirZ = dz / len; + } + } else if (owner != null) { + // Fire in NPC's facing direction + float yaw = (float) Math.toRadians(owner.rotationYaw); + float pitch = (float) Math.toRadians(owner.rotationPitch); + this.dirX = -Math.sin(yaw) * Math.cos(pitch); + this.dirY = -Math.sin(pitch); + this.dirZ = Math.cos(yaw) * Math.cos(pitch);; + } + + setPosition(endX, endY, endZ); + + // Initialize end point + this.endX = posX; + this.endY = posY; + this.endZ = posZ; + } + private void checkBlockCollision() { Vec3 start = Vec3.createVectorHelper(startX, startY, startZ); Vec3 end = Vec3.createVectorHelper(endX, endY, endZ); @@ -168,6 +269,44 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { } } + /** + * Update during charging state - follow owner based on anchor point. + */ + private void updateCharging() { + chargeTick++; + + Entity owner = getOwner(); + if (owner == null || owner.isDead) { + setDead(); + return; + } + + // Grow size based on charge progress + float progress = getChargeProgress(); + this.size = targetSize * progress; + + // Calculate position based on anchor point, offset downward by half size to center + if (owner instanceof EntityLivingBase) { + Vec3 pos = AnchorPointHelper.calculateAnchorPosition((EntityLivingBase) owner, anchorData); + setPosition(pos.xCoord, pos.yCoord, pos.zCoord); + } + } + + /** + * Get the charge progress (0-1). + */ + public float getChargeProgress() { + if (chargeDuration <= 0) return 1.0f; + return Math.min(1.0f, (float) chargeTick / chargeDuration); + } + + public float getInterpolatedChargeProgress(float partialTicks) { + if (chargeDuration <= 0) return 1.0f; + float prevProgress = Math.max(0, (float) (chargeTick - 1) / chargeDuration); + float currProgress = Math.min(1.0f, (float) chargeTick / chargeDuration); + return prevProgress + (currProgress - prevProgress) * partialTicks; + } + private void checkEntityCollisionAlongLine() { // Check collision along the entire laser line // Use a slightly expanded AABB for the full line @@ -190,7 +329,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { // Check if entity is close to the laser line if (isEntityOnLine(entity)) { hitEntities.add(entity.getEntityId()); - applyDamage(entity); + applyDamage(entity); // TODO find out why ts aint hittin // Piercing - don't stop, continue to next entity } @@ -305,6 +444,15 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { this.endX = nbt.getDouble("EndX"); this.endY = nbt.getDouble("EndY"); this.endZ = nbt.getDouble("EndZ"); + + // Charging state + boolean isCharging = nbt.hasKey("Charging") && nbt.getBoolean("Charging"); + this.charging = isCharging; + this.dataWatcher.updateObject(DW_CHARGING, (byte) (isCharging ? 1 : 0)); + this.chargeDuration = nbt.hasKey("ChargeDuration") ? nbt.getInteger("ChargeDuration") : 40; + this.chargeTick = nbt.hasKey("ChargeTick") ? nbt.getInteger("ChargeTick") : 0; + this.targetSize = nbt.hasKey("TargetSize") ? nbt.getFloat("TargetSize") : this.size; + anchorData.readNBT(nbt); } @Override @@ -320,6 +468,13 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { nbt.setDouble("EndX", endX); nbt.setDouble("EndY", endY); nbt.setDouble("EndZ", endZ); + + // Charging state + nbt.setBoolean("Charging", isCharging()); + nbt.setInteger("ChargeDuration", chargeDuration); + nbt.setInteger("ChargeTick", chargeTick); + nbt.setFloat("TargetSize", targetSize); + anchorData.writeNBT(nbt); } /** diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java index b594a83c7..71a03e97e 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java @@ -1,6 +1,7 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import kamkeel.npcs.controllers.data.ability.AnchorPoint; import kamkeel.npcs.controllers.data.ability.type.AbilityLaserShot; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; @@ -99,7 +100,15 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { int col2LabelX = guiLeft + 180; int col2FieldX = guiLeft + 260; - // Row 1: Inner Color + Outer Color + // Row 1: Anchor Point + addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); + GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), laser.getAnchorPointEnum().ordinal()); + anchorBtn.setHoverText("ability.hover.anchorPoint"); + addButton(anchorBtn); + + y += 24; + + // Row 2: Inner Color + Outer Color addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); String innerHex = String.format("%06X", laser.getInnerColor() & 0xFFFFFF); GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); @@ -115,7 +124,7 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { y += 24; - // Row 2: Outer Color Enabled + Outer Color Width + // Row 3: Outer Color Enabled + Outer Color Width addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, laser.isOuterColorEnabled() ? 1 : 0)); @@ -126,7 +135,7 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { y += 24; - // Row 3: Lightning Effect Enabled + // Row 4: Lightning Effect Enabled addLabel(new GuiNpcLabel(204, "ability.outerAlpha", labelX, y + 5)); GuiNpcTextField alphaField = createFloatField(204, fieldX, y, 55, laser.getOuterColorAlpha()); alphaField.setEnabled(laser.isOuterColorEnabled()); @@ -142,7 +151,7 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { if (laser.hasLightningEffect()) { y += 24; - // Row 4: Density + Radius + // Row 5: Density + Radius addLabel(new GuiNpcLabel(206, "ability.lightningDensity", labelX, y + 5)); GuiNpcTextField densityField = new GuiNpcTextField(206, this, fontRendererObj, fieldX, y, 55, 18, "" + laser.getLightningDensity()); densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); @@ -189,6 +198,9 @@ public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { laser.setLightningEffect(value == 1); initGui(); break; + case 210: + laser.setAnchorPointEnum(AnchorPoint.fromOrdinal(value)); + break; } } From ffc6a070dcce96c78a03ffcda9e65db219caf69c Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 1 Feb 2026 01:50:39 -0300 Subject: [PATCH 339/358] Removed todo --- src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java index d8aeb725c..e93ea5a77 100644 --- a/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java +++ b/src/main/java/kamkeel/npcs/entity/EntityAbilityLaser.java @@ -329,7 +329,7 @@ public class EntityAbilityLaser extends EntityAbilityProjectile { // Check if entity is close to the laser line if (isEntityOnLine(entity)) { hitEntities.add(entity.getEntityId()); - applyDamage(entity); // TODO find out why ts aint hittin + applyDamage(entity); // Piercing - don't stop, continue to next entity } From bd0f8d8383ada8d97a609411f20b2e944e89fc78 Mon Sep 17 00:00:00 2001 From: somehussar <148253652+somehussar@users.noreply.github.com> Date: Sun, 1 Feb 2026 06:00:59 +0100 Subject: [PATCH 340/358] Goatee, are you fucking retarded? --- .../npcs/network/packets/request/script/ScriptFilesPacket.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java index 37383ea6c..96268e915 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java @@ -47,7 +47,6 @@ public final class ScriptFilesPacket extends AbstractPacket { return CustomNpcsPermissions.SCRIPT_GLOBAL; } - @SideOnly(Side.CLIENT) @Override public void sendData(ByteBuf out) throws IOException { ByteBufUtils.writeNBT(out, getScriptsNbt(lang)); From 119c3170295e14cd3ca92310b9936b85ae82e125 Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 1 Feb 2026 02:01:29 -0300 Subject: [PATCH 341/358] Fixed wrong packet channel and misplaced SideOnly --- .../npcs/network/packets/request/script/ScriptFilesPacket.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java index 37383ea6c..700513d78 100644 --- a/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java +++ b/src/main/java/kamkeel/npcs/network/packets/request/script/ScriptFilesPacket.java @@ -39,7 +39,7 @@ public final class ScriptFilesPacket extends AbstractPacket { @Override public PacketChannel getChannel() { - return PacketHandler.DATA_PACKET; + return PacketHandler.REQUEST_PACKET; } @Override @@ -47,7 +47,6 @@ public final class ScriptFilesPacket extends AbstractPacket { return CustomNpcsPermissions.SCRIPT_GLOBAL; } - @SideOnly(Side.CLIENT) @Override public void sendData(ByteBuf out) throws IOException { ByteBufUtils.writeNBT(out, getScriptsNbt(lang)); From 00ebb626a68a3b113d75b94091e2caa24a85fe64 Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 1 Feb 2026 04:27:56 -0300 Subject: [PATCH 342/358] HOW DID I MISS THIS --- .../java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java index e2e6f8234..048b3642f 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityOrb.java @@ -56,7 +56,7 @@ public class RenderAbilityOrb extends RenderAbilityProjectile { GL11.glDepthMask(false); GL11.glPushMatrix(); GL11.glScalef(outerScale, outerScale, outerScale); - renderCube(orb.getOuterColor(), 0.5f, 0.5f); + renderCube(orb.getOuterColor(), orb.getOuterColorAlpha(), 0.5f); GL11.glPopMatrix(); GL11.glDepthMask(true); } From f4db57875a129b758041c1509e805dc6ba2b6256 Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 1 Feb 2026 06:45:29 -0300 Subject: [PATCH 343/358] Changed dual ability names --- .../data/ability/AbilityController.java | 6 +++--- ...AbilityDualBeam.java => AbilityBeamDual.java} | 16 ++++++++-------- ...AbilityDualDisc.java => AbilityDiscDual.java} | 16 ++++++++-------- .../{AbilityDualOrb.java => AbilityOrbDual.java} | 16 ++++++++-------- ...yDualBeam.java => SubGuiAbilityBeamDual.java} | 8 ++++---- ...yDualDisc.java => SubGuiAbilityDiscDual.java} | 9 ++++----- ...ityDualOrb.java => SubGuiAbilityOrbDual.java} | 8 ++++---- ..._Active.json => Ability_BeamDual_Active.json} | 2 +- ..._Windup.json => Ability_BeamDual_Windup.json} | 2 +- 9 files changed, 41 insertions(+), 42 deletions(-) rename src/main/java/kamkeel/npcs/controllers/data/ability/type/{AbilityDualBeam.java => AbilityBeamDual.java} (97%) rename src/main/java/kamkeel/npcs/controllers/data/ability/type/{AbilityDualDisc.java => AbilityDiscDual.java} (97%) rename src/main/java/kamkeel/npcs/controllers/data/ability/type/{AbilityDualOrb.java => AbilityOrbDual.java} (97%) rename src/main/java/noppes/npcs/client/gui/advanced/ability/{SubGuiAbilityDualBeam.java => SubGuiAbilityBeamDual.java} (98%) rename src/main/java/noppes/npcs/client/gui/advanced/ability/{SubGuiAbilityDualDisc.java => SubGuiAbilityDiscDual.java} (97%) rename src/main/java/noppes/npcs/client/gui/advanced/ability/{SubGuiAbilityDualOrb.java => SubGuiAbilityOrbDual.java} (98%) rename src/main/resources/assets/customnpcs/animations/{Ability_DualBeam_Active.json => Ability_BeamDual_Active.json} (99%) rename src/main/resources/assets/customnpcs/animations/{Ability_DualBeam_Windup.json => Ability_BeamDual_Windup.json} (99%) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java index 43d7bd8bb..eba8795f7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityController.java @@ -363,12 +363,12 @@ public class AbilityController implements IAbilityHandler { registerType("cnpc:sweeper", AbilitySweeper::new); registerType("cnpc:projectile", AbilityProjectile::new); registerType("cnpc:orb", AbilityOrb::new); - registerType("cnpc:dual_orb", AbilityDualOrb::new); + registerType("cnpc:dual_orb", AbilityOrbDual::new); registerType("cnpc:dual_disc", AbilityDisc::new); - registerType("cnpc:dual_disc", AbilityDualDisc::new); + registerType("cnpc:dual_disc", AbilityDiscDual::new); registerType("cnpc:laser_shot", AbilityLaserShot::new); registerType("cnpc:beam", AbilityEnergyBeam::new); - registerType("cnpc:dual_beam", AbilityDualBeam::new); + registerType("cnpc:dual_beam", AbilityBeamDual::new); // Movement abilities registerType("cnpc:charge", AbilityCharge::new); diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java similarity index 97% rename from src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualBeam.java rename to src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java index 6704b0e33..54c806604 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityBeamDual.java @@ -18,12 +18,12 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDualBeam; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityBeamDual; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.util.ValueUtil; -public class AbilityDualBeam extends Ability { +public class AbilityBeamDual extends Ability { private float beamWidth = 0.4f; private float headSize = 0.6f; private boolean dualFire= true; @@ -48,9 +48,9 @@ public class AbilityDualBeam extends Ability { private transient EntityAbilityBeam beamEntity = null; private transient EntityAbilityBeam beamEntity2 = null; - public AbilityDualBeam() { - this.typeId = "ability.cnpc.dual_beam"; - this.name = "Dual Beam"; + public AbilityBeamDual() { + this.typeId = "ability.cnpc.beam_dual"; + this.name = "Beam Dual"; this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 20.0f; this.minRange = 5.0f; @@ -60,8 +60,8 @@ public class AbilityDualBeam extends Ability { this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; // Default built-in animations - this.windUpAnimationName = "Ability_DualBeam_Windup"; - this.activeAnimationName = "Ability_DualBeam_Active"; + this.windUpAnimationName = "Ability_BeamDual_Windup"; + this.activeAnimationName = "Ability_BeamDual_Active"; } @Override @@ -72,7 +72,7 @@ public class AbilityDualBeam extends Ability { @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityDualBeam(this, callback); + return new SubGuiAbilityBeamDual(this, callback); } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java similarity index 97% rename from src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualDisc.java rename to src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java index 873530119..0fc3ab061 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java @@ -18,12 +18,12 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDualDisc; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDiscDual; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.util.ValueUtil; -public class AbilityDualDisc extends Ability { +public class AbilityDiscDual extends Ability { // Disc geometry private float discRadius = 1.0f; @@ -56,9 +56,9 @@ public class AbilityDualDisc extends Ability { private transient EntityAbilityDisc discEntity = null; private transient EntityAbilityDisc discEntity2 = null; - public AbilityDualDisc() { - this.typeId = "ability.cnpc.dual_disc"; - this.name = "Dual Disc"; + public AbilityDiscDual() { + this.typeId = "ability.cnpc.disc_dual"; + this.name = "Disc Dual"; this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 30.0f; this.minRange = 5.0f; @@ -68,8 +68,8 @@ public class AbilityDualDisc extends Ability { this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; // Default built-in animations - this.windUpAnimationName = "Ability_DualBeam_Windup"; - this.activeAnimationName = "Ability_DualBeam_Active"; + this.windUpAnimationName = "Ability_BeamDual_Windup"; + this.activeAnimationName = "Ability_BeamDual_Active"; } @Override @@ -80,7 +80,7 @@ public class AbilityDualDisc extends Ability { @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityDualDisc(this, callback); + return new SubGuiAbilityDiscDual(this, callback); } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java similarity index 97% rename from src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualOrb.java rename to src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java index baddfb910..1e28d5820 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDualOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrbDual.java @@ -18,12 +18,12 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDualOrb; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityOrbDual; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.util.ValueUtil; -public class AbilityDualOrb extends Ability { +public class AbilityOrbDual extends Ability { // Ability-specific properties private float orbSize = 1.0f; private boolean dualFire= true; @@ -47,9 +47,9 @@ public class AbilityDualOrb extends Ability { private transient EntityAbilityOrb orbEntity = null; private transient EntityAbilityOrb orbEntity2 = null; - public AbilityDualOrb() { - this.typeId = "ability.cnpc.dual_orb"; - this.name = "Dual Orb"; + public AbilityOrbDual() { + this.typeId = "ability.cnpc.orb_dual"; + this.name = "Orb Dual"; this.targetingMode = TargetingMode.AGGRO_TARGET; this.maxRange = 25.0f; this.minRange = 5.0f; @@ -59,8 +59,8 @@ public class AbilityDualOrb extends Ability { this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; // Default built-in animation - this.windUpAnimationName = "Ability_DualBeam_Windup"; - this.activeAnimationName = "Ability_DualBeam_Active"; + this.windUpAnimationName = "Ability_BeamDual_Windup"; + this.activeAnimationName = "Ability_BeamDual_Active"; } @Override @@ -71,7 +71,7 @@ public class AbilityDualOrb extends Ability { @Override @SideOnly(Side.CLIENT) public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityDualOrb(this, callback); + return new SubGuiAbilityOrbDual(this, callback); } @Override diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeamDual.java similarity index 98% rename from src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualBeam.java rename to src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeamDual.java index 87144adfc..3e1886c53 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualBeam.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityBeamDual.java @@ -2,19 +2,19 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.AnchorPoint; -import kamkeel.npcs.controllers.data.ability.type.AbilityDualBeam; +import kamkeel.npcs.controllers.data.ability.type.AbilityBeamDual; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.*; import java.util.List; -public class SubGuiAbilityDualBeam extends SubGuiAbilityConfig { - private final AbilityDualBeam beam; +public class SubGuiAbilityBeamDual extends SubGuiAbilityConfig { + private final AbilityBeamDual beam; private int editingVisualColorId = 0; private int editingBeam = 0; - public SubGuiAbilityDualBeam(AbilityDualBeam ability, IAbilityConfigCallback callback) { + public SubGuiAbilityBeamDual(AbilityBeamDual ability, IAbilityConfigCallback callback) { super(ability, callback); this.beam = ability; } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualDisc.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDiscDual.java similarity index 97% rename from src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualDisc.java rename to src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDiscDual.java index f8bdbf9f2..c8ab5a352 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualDisc.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDiscDual.java @@ -2,20 +2,19 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.AnchorPoint; -import kamkeel.npcs.controllers.data.ability.type.AbilityDisc; -import kamkeel.npcs.controllers.data.ability.type.AbilityDualDisc; +import kamkeel.npcs.controllers.data.ability.type.AbilityDiscDual; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.*; import java.util.List; -public class SubGuiAbilityDualDisc extends SubGuiAbilityConfig { - private final AbilityDualDisc disc; +public class SubGuiAbilityDiscDual extends SubGuiAbilityConfig { + private final AbilityDiscDual disc; private int editingVisualColorId = 0; private int editingDisc = 0; - public SubGuiAbilityDualDisc(AbilityDualDisc ability, IAbilityConfigCallback callback) { + public SubGuiAbilityDiscDual(AbilityDiscDual ability, IAbilityConfigCallback callback) { super(ability, callback); this.disc = ability; } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrbDual.java similarity index 98% rename from src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualOrb.java rename to src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrbDual.java index a8b9caadf..cdf805f53 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDualOrb.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrbDual.java @@ -2,19 +2,19 @@ package noppes.npcs.client.gui.advanced.ability; import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.AnchorPoint; -import kamkeel.npcs.controllers.data.ability.type.AbilityDualOrb; +import kamkeel.npcs.controllers.data.ability.type.AbilityOrbDual; import noppes.npcs.client.gui.SubGuiColorSelector; import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.*; import java.util.List; -public class SubGuiAbilityDualOrb extends SubGuiAbilityConfig { - private final AbilityDualOrb orb; +public class SubGuiAbilityOrbDual extends SubGuiAbilityConfig { + private final AbilityOrbDual orb; private int editingVisualColorId = 0; private int editingOrb = 0; - public SubGuiAbilityDualOrb(AbilityDualOrb ability, IAbilityConfigCallback callback) { + public SubGuiAbilityOrbDual(AbilityOrbDual ability, IAbilityConfigCallback callback) { super(ability, callback); this.orb = ability; } diff --git a/src/main/resources/assets/customnpcs/animations/Ability_DualBeam_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Active.json similarity index 99% rename from src/main/resources/assets/customnpcs/animations/Ability_DualBeam_Active.json rename to src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Active.json index fa47349f9..c98f89236 100644 --- a/src/main/resources/assets/customnpcs/animations/Ability_DualBeam_Active.json +++ b/src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Active.json @@ -198,6 +198,6 @@ "ID": 12, "WhileAttacking": 1b, "WhileWalking": 1b, - "Name": "Ability_DualBeam_Active", + "Name": "Ability_BeamDual_Active", "WhileStanding": 1b } diff --git a/src/main/resources/assets/customnpcs/animations/Ability_DualBeam_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Windup.json similarity index 99% rename from src/main/resources/assets/customnpcs/animations/Ability_DualBeam_Windup.json rename to src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Windup.json index 99437fc2d..d793e3f22 100644 --- a/src/main/resources/assets/customnpcs/animations/Ability_DualBeam_Windup.json +++ b/src/main/resources/assets/customnpcs/animations/Ability_BeamDual_Windup.json @@ -196,6 +196,6 @@ "ID": 11, "WhileAttacking": 1b, "WhileWalking": 1b, - "Name": "Ability_DualBeam_Windup", + "Name": "Ability_BeamDual_Windup", "WhileStanding": 1b } From b7fdda690576c1bc7b387d907fb2a92878084b4f Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 1 Feb 2026 06:51:26 -0300 Subject: [PATCH 344/358] Updated translation --- src/main/resources/assets/customnpcs/lang/en_US.lang | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index f64e3820f..ab6ee1904 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1562,10 +1562,10 @@ ability.cnpc.slam=Slam ability.cnpc.heavy_hit=Heavy Hit ability.cnpc.cutter=Cutter ability.cnpc.beam=Beam -ability.cnpc.dual_beam=Dual Beam +ability.cnpc.beam_dual=Beam Dual ability.cnpc.projectile=Projectile ability.cnpc.orb=Orb -ability.cnpc.dual_orb=Dual Orb +ability.cnpc.orb_dual=Orb Dual ability.cnpc.charge=Charge ability.cnpc.dash=Dash ability.cnpc.teleport=Teleport @@ -1576,7 +1576,7 @@ ability.cnpc.heal=Heal ability.cnpc.hazard=Hazard ability.cnpc.trap=Trap ability.cnpc.disc=Disc -ability.cnpc.dual_disc=Dual Disc +ability.cnpc.disc_dual=Disc Dual ability.cnpc.laser_shot=Laser Shot ability.cnpc.sweeper=Sweeper @@ -1696,6 +1696,7 @@ ability.innerColor=Inner Color ability.outerColor=Outer Color ability.outerEnabled=Outer Glow ability.outerWidth=Outer Width +ability.outerAlpha=Outer Alpha ability.lightning=Lightning ability.lightningDensity=Density ability.lightningRadius=Radius From c39ce6b6d16edd14c3a4332a4638c9f9ef988242 Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 1 Feb 2026 17:08:24 -0300 Subject: [PATCH 345/358] Fixed sync windup animation not being an ability property and getting overridden on gui init --- .../controllers/data/ability/Ability.java | 49 ++++++++++++++++++- src/main/java/kamkeel/npcs/util/Register.java | 2 + .../gui/advanced/SubGuiAbilityConfig.java | 4 +- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index e31b8f8fd..b33426ba9 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -20,6 +20,7 @@ import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.data.Animation; import net.minecraft.entity.player.EntityPlayer; +import noppes.npcs.controllers.data.Frame; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.entity.IPlayer; import noppes.npcs.controllers.ScriptController; @@ -57,6 +58,7 @@ public abstract class Ability implements IAbility { protected int cooldownTicks = 0; // Added to global cooldown after ability completes protected int windUpTicks = 20; protected int dazedTicks = 80; // Only used when interrupted during WINDUP (if interruptible) + protected boolean syncWindupWithAnimation = true; // Interruption protected boolean interruptible = true; @@ -972,7 +974,7 @@ public abstract class Ability implements IAbility { } public int getWindUpTicks() { - return windUpTicks; + return this.calculateWindupFromAnimation(); } public void setWindUpTicks(int windUpTicks) { @@ -987,6 +989,14 @@ public abstract class Ability implements IAbility { this.dazedTicks = Math.max(0, dazedTicks); } + public boolean isSyncWindupWithAnimation() { + return syncWindupWithAnimation; + } + + public void setSyncWindupWithAnimation(boolean syncWindupWithAnimation) { + this.syncWindupWithAnimation = syncWindupWithAnimation; + } + public boolean isInterruptible() { return interruptible; } @@ -1234,6 +1244,43 @@ public abstract class Ability implements IAbility { effects.clear(); } + // ═══════════════════════════════════════════════════════════════════ + // SHIT FROM THE ASS + // ═══════════════════════════════════════════════════════════════════ + + private int calculateWindupFromAnimation() { + if (!syncWindupWithAnimation) { + return windUpTicks; + } + + if (AnimationController.Instance == null) { + return this.windUpTicks; + } + + Animation animation = null; + // Check for built-in animation (by name) first + if (windUpAnimationName != null && !windUpAnimationName.isEmpty()) { + animation = (Animation) AnimationController.Instance.get(windUpAnimationName); + } + // Fall back to user animation (by ID) + else if (windUpAnimationId >= 0) { + animation = (Animation) AnimationController.Instance.get(windUpAnimationId); + } + + if (animation == null || animation.frames.isEmpty()) { + return this.windUpTicks; + } + + // Calculate total duration by summing all frame durations + int totalDuration = 0; + for (Frame frame : animation.frames) { + totalDuration += frame.getDuration(); + } + + // Return total duration + return totalDuration; + } + // ═══════════════════════════════════════════════════════════════════ // IAbility API METHODS // ═══════════════════════════════════════════════════════════════════ diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index d12e49146..6485b9b81 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -33,4 +33,6 @@ public class Register { } } } + + // TODO ADD ANIMATION REGISTER } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index 611f8c918..ef799961d 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -66,7 +66,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi protected int cooldownTicks; protected int windUpTicks; protected int dazedTicks; - protected boolean syncWindupWithAnimation = true; // Sync windup ticks with animation duration + protected boolean syncWindupWithAnimation; // Sync windup ticks with animation duration // ═══════════════════════════════════════════════════════════════════════════ // CACHED VALUES - Target Tab @@ -120,6 +120,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi this.cooldownTicks = ability.getCooldownTicks(); this.windUpTicks = ability.getWindUpTicks(); this.dazedTicks = ability.getDazedTicks(); + this.syncWindupWithAnimation = ability.isSyncWindupWithAnimation(); // Target this.minRange = ability.getMinRange(); @@ -923,6 +924,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi ability.setCooldownTicks(cooldownTicks); ability.setWindUpTicks(windUpTicks); ability.setDazedTicks(dazedTicks); + ability.setSyncWindupWithAnimation(syncWindupWithAnimation); // Target ability.setMinRange(minRange); From c168702970b666f4bfe357e2f163cfefdba9d3eb Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 1 Feb 2026 20:09:12 -0300 Subject: [PATCH 346/358] WIP Animations Register --- .../controllers/data/ability/Ability.java | 6 ++-- src/main/java/kamkeel/npcs/util/Register.java | 20 ++++++++++++- .../gui/advanced/SubGuiAbilityConfig.java | 2 +- .../npcs/controllers/AnimationController.java | 29 +++++++++++++------ 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index b33426ba9..f7325a24f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -1098,7 +1098,7 @@ public abstract class Ability implements IAbility { // Built-in animation by name takes priority if (windUpAnimationName != null && !windUpAnimationName.isEmpty()) { - return (Animation) AnimationController.Instance.get(windUpAnimationName); + return (Animation) AnimationController.Instance.get(windUpAnimationName, true); } // Fall back to user animation by ID if (windUpAnimationId >= 0) { @@ -1128,7 +1128,7 @@ public abstract class Ability implements IAbility { // Built-in animation by name takes priority if (activeAnimationName != null && !activeAnimationName.isEmpty()) { - return (Animation) AnimationController.Instance.get(activeAnimationName); + return (Animation) AnimationController.Instance.get(activeAnimationName, true); } // Fall back to user animation by ID if (activeAnimationId >= 0) { @@ -1260,7 +1260,7 @@ public abstract class Ability implements IAbility { Animation animation = null; // Check for built-in animation (by name) first if (windUpAnimationName != null && !windUpAnimationName.isEmpty()) { - animation = (Animation) AnimationController.Instance.get(windUpAnimationName); + animation = (Animation) AnimationController.Instance.get(windUpAnimationName, true); } // Fall back to user animation (by ID) else if (windUpAnimationId >= 0) { diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index 6485b9b81..aebfd4255 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -2,6 +2,9 @@ package kamkeel.npcs.util; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; +import noppes.npcs.LogWriter; +import noppes.npcs.controllers.AnimationController; +import noppes.npcs.controllers.data.Animation; import java.util.LinkedHashMap; import java.util.Map; @@ -34,5 +37,20 @@ public class Register { } } - // TODO ADD ANIMATION REGISTER + public static class Animations extends Register { + private final Class modClass; + private final String animationsPath; + + public Animations(Class modClass, String animationsPath, String namespace) { + super("ability", namespace); + this.animationsPath = animationsPath; + this.modClass = modClass; + } + + public void register() throws Exception { + for (Map.Entry> entry : entries.entrySet()) { + AnimationController.Instance.loadBuiltInAnimation(modClass, animationsPath, entry.getKey()); + } + } + } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index ef799961d..32cfc51b2 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -1003,7 +1003,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi Animation animation = null; // Check for built-in animation (by name) first if (windUpAnimationName != null && !windUpAnimationName.isEmpty()) { - animation = (Animation) AnimationController.Instance.get(windUpAnimationName); + animation = (Animation) AnimationController.Instance.get(windUpAnimationName, true); } // Fall back to user animation (by ID) else if (windUpAnimationId >= 0) { diff --git a/src/main/java/noppes/npcs/controllers/AnimationController.java b/src/main/java/noppes/npcs/controllers/AnimationController.java index c6837b6c7..b4203b452 100644 --- a/src/main/java/noppes/npcs/controllers/AnimationController.java +++ b/src/main/java/noppes/npcs/controllers/AnimationController.java @@ -120,12 +120,9 @@ public class AnimationController implements IAnimationHandler { } } - /** - * Load a single built-in animation by name from assets folder. - */ - private void loadBuiltInAnimation(String animName) throws Exception { - String resourcePath = BUILTIN_ANIMATIONS_PATH + "/" + animName + ".json"; - try (InputStream stream = CustomNpcs.class.getResourceAsStream(resourcePath)) { + public void loadBuiltInAnimation(Class modClass, String path, String animName) throws Exception { + String resourcePath = path + "/" + animName + ".json"; + try (InputStream stream = modClass.getResourceAsStream(resourcePath)) { if (stream == null) { return; } @@ -150,6 +147,13 @@ public class AnimationController implements IAnimationHandler { } } + /** + * Load a single built-in animation by name from assets folder. + */ + private void loadBuiltInAnimation(String animName) throws Exception { + loadBuiltInAnimation(CustomNpcs.class, BUILTIN_ANIMATIONS_PATH, animName); + } + private void loadAnimations() { animations.clear(); @@ -314,6 +318,10 @@ public class AnimationController implements IAnimationHandler { } public IAnimation get(String name) { + return get(name, false); + } + + public IAnimation get(String name, boolean builtIn) { // Check user/custom animations first (allows overriding built-in) for (Map.Entry entry : animations.entrySet()) { if (entry.getValue().name.equalsIgnoreCase(name)) { @@ -321,10 +329,13 @@ public class AnimationController implements IAnimationHandler { } } // Then check built-in animations - BuiltInAnimation builtIn = builtInAnimations.get(name.toLowerCase()); - if (builtIn != null) { - return builtIn; + if (builtIn) { + BuiltInAnimation builtInAnim = builtInAnimations.get(name.toLowerCase()); + if (builtInAnim != null) { + return builtInAnim; + } } + return null; } From b254ad0ba9030ae6d712202366520e06434cae9a Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 1 Feb 2026 20:25:16 -0300 Subject: [PATCH 347/358] WIP Animations Register --- src/main/java/kamkeel/npcs/util/Register.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index aebfd4255..d5dd9006f 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -2,7 +2,6 @@ package kamkeel.npcs.util; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; -import noppes.npcs.LogWriter; import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.data.Animation; @@ -12,7 +11,7 @@ import java.util.function.Supplier; public class Register { private final String registryKey; - private final String namespace; + protected final String namespace; protected final Map> entries = new LinkedHashMap<>(); public Register(String registryKey, String namespace) { @@ -41,15 +40,21 @@ public class Register { private final Class modClass; private final String animationsPath; - public Animations(Class modClass, String animationsPath, String namespace) { - super("ability", namespace); + public Animations(Class modClass, String namespace, String animationsPath) { + super("animation", namespace); this.animationsPath = animationsPath; this.modClass = modClass; } public void register() throws Exception { - for (Map.Entry> entry : entries.entrySet()) { - AnimationController.Instance.loadBuiltInAnimation(modClass, animationsPath, entry.getKey()); + try { + + } catch(Exception e) { + String path = "/assets/" + namespace + "/" + animationsPath + "/"; + for (Map.Entry> entry : entries.entrySet()) { + String key = entry.getKey().substring(entry.getKey().indexOf(":")); + AnimationController.Instance.loadBuiltInAnimation(modClass, path, key); + } } } } From 9d7ad714a13b419c176371a30dfd992f43d50a43 Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 1 Feb 2026 20:27:21 -0300 Subject: [PATCH 348/358] WIP Animations Register --- src/main/java/kamkeel/npcs/util/Register.java | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index d5dd9006f..0c6cb8ee8 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -2,6 +2,7 @@ package kamkeel.npcs.util; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.AbilityController; +import noppes.npcs.LogWriter; import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.data.Animation; @@ -40,21 +41,19 @@ public class Register { private final Class modClass; private final String animationsPath; - public Animations(Class modClass, String namespace, String animationsPath) { - super("animation", namespace); + public Animations(Class modClass, String animationsPath, String namespace) { + super("ability", namespace); this.animationsPath = animationsPath; this.modClass = modClass; } - public void register() throws Exception { + public void register() { try { - - } catch(Exception e) { - String path = "/assets/" + namespace + "/" + animationsPath + "/"; for (Map.Entry> entry : entries.entrySet()) { - String key = entry.getKey().substring(entry.getKey().indexOf(":")); - AnimationController.Instance.loadBuiltInAnimation(modClass, path, key); + AnimationController.Instance.loadBuiltInAnimation(modClass, animationsPath, entry.getKey()); } + } catch (Exception e) { + LogWriter.error("Error scanning built-in animations folder", e); } } } From 0883d7fb9498c841cac8cc19b27d2eb0d018ac2a Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 1 Feb 2026 20:39:15 -0300 Subject: [PATCH 349/358] Cast register animation to builtin --- src/main/java/kamkeel/npcs/util/Register.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index 0c6cb8ee8..91d284e21 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -5,6 +5,7 @@ import kamkeel.npcs.controllers.data.ability.AbilityController; import noppes.npcs.LogWriter; import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.BuiltInAnimation; import java.util.LinkedHashMap; import java.util.Map; @@ -47,6 +48,11 @@ public class Register { this.modClass = modClass; } + @Override + public Animation register(String factoryName, Supplier factory) { + return (BuiltInAnimation) super.register(factoryName, factory); + } + public void register() { try { for (Map.Entry> entry : entries.entrySet()) { From 8ae3bcff1c9923bbbf61b958faf66686198e93bb Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 1 Feb 2026 20:44:23 -0300 Subject: [PATCH 350/358] nevermind --- src/main/java/kamkeel/npcs/util/Register.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index 91d284e21..26fda3f3d 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -50,7 +50,7 @@ public class Register { @Override public Animation register(String factoryName, Supplier factory) { - return (BuiltInAnimation) super.register(factoryName, factory); + return super.register(factoryName, factory); } public void register() { From bc88c8898203000e8e36de316d432bd26631472b Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 1 Feb 2026 20:49:34 -0300 Subject: [PATCH 351/358] Changed animation name reading logic --- src/main/java/kamkeel/npcs/util/Register.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index 26fda3f3d..30016fb52 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -55,8 +55,10 @@ public class Register { public void register() { try { + String path = "/assets/" + namespace + "/" + animationsPath; for (Map.Entry> entry : entries.entrySet()) { - AnimationController.Instance.loadBuiltInAnimation(modClass, animationsPath, entry.getKey()); + String name = entry.getKey().substring(entry.getKey().indexOf(":")); + AnimationController.Instance.loadBuiltInAnimation(modClass, path, name); } } catch (Exception e) { LogWriter.error("Error scanning built-in animations folder", e); From 0369b6de834e462f0c09ef2be8ce9ebd28af347c Mon Sep 17 00:00:00 2001 From: Vidal Date: Sun, 1 Feb 2026 20:53:12 -0300 Subject: [PATCH 352/358] oopsie >< --- src/main/java/kamkeel/npcs/util/Register.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kamkeel/npcs/util/Register.java b/src/main/java/kamkeel/npcs/util/Register.java index 30016fb52..0f1400ab3 100644 --- a/src/main/java/kamkeel/npcs/util/Register.java +++ b/src/main/java/kamkeel/npcs/util/Register.java @@ -57,7 +57,7 @@ public class Register { try { String path = "/assets/" + namespace + "/" + animationsPath; for (Map.Entry> entry : entries.entrySet()) { - String name = entry.getKey().substring(entry.getKey().indexOf(":")); + String name = entry.getKey().substring(entry.getKey().indexOf(":") + 1); AnimationController.Instance.loadBuiltInAnimation(modClass, path, name); } } catch (Exception e) { From b74149d98de62a37937adc6d3b0025a7caf4bc87 Mon Sep 17 00:00:00 2001 From: Vidal Date: Mon, 2 Feb 2026 00:52:43 -0300 Subject: [PATCH 353/358] Added Heavy Hit and Cutter animations --- .../data/ability/type/AbilityCutter.java | 2 + .../data/ability/type/AbilityHeavyHit.java | 9 +- .../animations/Ability_Cutter_Active.json | 86 ++ .../animations/Ability_Cutter_Windup.json | 156 ++ .../animations/Ability_HeavyHit_Active.json | 77 + .../animations/Ability_HeavyHit_Windup.json | 1274 +++++++++++++++++ 6 files changed, 1600 insertions(+), 4 deletions(-) create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_Cutter_Active.json create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_Cutter_Windup.json create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Active.json create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Windup.json diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index ef2d2f687..3ba6c3079 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -61,6 +61,8 @@ public class AbilityCutter extends Ability implements IAbilityCutter { this.telegraphType = TelegraphType.CONE; this.windUpSound = "random.bow"; this.activeSound = "random.break"; + this.windUpAnimationName = "Ability_Cutter_Windup"; + this.activeAnimationName = "Ability_Cutter_Active"; } @Override diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index 7d0c217c0..3419492c3 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -37,6 +37,8 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { this.showTelegraph = false; this.windUpSound = "random.anvil_use"; this.activeSound = "random.anvil_land"; + this.windUpAnimationName = "Ability_HeavyHit_Windup"; + this.activeAnimationName = "Ability_HeavyHit_Active"; } @Override @@ -75,14 +77,13 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { if (wasHit) { applyEffects(target); } - - // Heavy Hit is instant - signalCompletion(); } @Override public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, World world, int tick) { - // Heavy Hit is instant, no active tick needed + // Just enough delay to see the active animation + if (tick == 10) + signalCompletion(); } @Override diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Active.json new file mode 100644 index 000000000..1fdf06c24 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Active.json @@ -0,0 +1,86 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 18.0f, + "Rotation0": 88.0f, + "Pivot0": -10.0f, + "Pivot1": 0.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 107.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 107.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 6.0f, + "Rotation0": 82.0f, + "Pivot0": 3.0f, + "Pivot1": 3.0f, + "Rotation2": 88.0f + }, + { + "Pivot2": 5.0f, + "Part": "RIGHT_LEG", + "Rotation1": 107.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_LEG", + "Rotation1": 56.0f, + "Rotation0": 0.0f, + "Pivot0": -6.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 20 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 14, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Cutter_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Windup.json new file mode 100644 index 000000000..153d90f12 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_Cutter_Windup.json @@ -0,0 +1,156 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "FrameParts": [ + { + "Pivot2": 1.0f, + "Part": "LEFT_ARM", + "Rotation1": -31.0f, + "Rotation0": -94.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": -3.0f, + "Part": "HEAD", + "Rotation1": -31.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "BODY", + "Rotation1": -31.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": -75.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": -50.0f, + "Rotation0": -145.0f, + "Pivot0": 2.0f, + "Pivot1": 3.0f, + "Rotation2": 88.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 18.0f, + "Rotation0": 12.0f, + "Pivot0": -3.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Duration": 20 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 18.0f, + "Rotation0": 88.0f, + "Pivot0": -10.0f, + "Pivot1": 0.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 107.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 107.0f, + "Rotation0": -25.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "RIGHT_ARM", + "Rotation1": 6.0f, + "Rotation0": 82.0f, + "Pivot0": 3.0f, + "Pivot1": 3.0f, + "Rotation2": 88.0f + }, + { + "Pivot2": 5.0f, + "Part": "RIGHT_LEG", + "Rotation1": 107.0f, + "Rotation0": 6.0f, + "Pivot0": -2.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_LEG", + "Rotation1": 56.0f, + "Rotation0": 0.0f, + "Pivot0": -6.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 13, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_Cutter_Windup", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Active.json new file mode 100644 index 000000000..fa6b44543 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Active.json @@ -0,0 +1,77 @@ +{ + "Loop": 0, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": -25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 6.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -63.0f, + "Rotation0": -31.0f, + "Pivot0": -2.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 20 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 19, + "Smooth": 0b, + "ID": 16, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_HeavyHit_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Windup.json new file mode 100644 index 000000000..15d49bce6 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_HeavyHit_Windup.json @@ -0,0 +1,1274 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.8f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 31.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -37.0f, + "Rotation0": -37.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 6.0f + } + ], + "Smooth": 0b, + "Duration": 10 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.1f, + "FrameParts": [ + { + "Pivot2": 3.0f, + "Part": "RIGHT_ARM", + "Rotation1": 40.0f, + "Rotation0": -107.0f, + "Pivot0": 2.0f, + "Pivot1": 1.0f, + "Rotation2": -37.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": -1.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 37.0f, + "Rotation0": 25.0f, + "Pivot0": -2.0f, + "Pivot1": -1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "BODY", + "Rotation1": 18.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": -40.0f, + "Rotation0": -40.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -90.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 90.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_LEG", + "Rotation1": -31.0f, + "Rotation0": 31.0f, + "Pivot0": 1.0f, + "Pivot1": 0.0f, + "Rotation2": -18.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": -25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_LEG", + "Rotation1": 6.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 4.0f, + "Part": "LEFT_ARM", + "Rotation1": -63.0f, + "Rotation0": -31.0f, + "Pivot0": -2.0f, + "Pivot1": 1.0f, + "Rotation2": -6.0f + } + ], + "Smooth": 0b, + "Duration": 2 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 19, + "Smooth": 0b, + "ID": 15, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_HeavyHit_Windup", + "WhileStanding": 1b +} From bcc9d790d606a76242ddc2a7816aee8445de7178 Mon Sep 17 00:00:00 2001 From: Vidal Date: Mon, 2 Feb 2026 03:00:35 -0300 Subject: [PATCH 354/358] Added Disc Dual animation --- .../data/ability/type/AbilityDiscDual.java | 6 +- .../animations/Ability_DiscDual_Active.json | 392 ++++++++++++++++++ .../animations/Ability_DiscDual_Windup.json | 293 +++++++++++++ 3 files changed, 688 insertions(+), 3 deletions(-) create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Active.json create mode 100644 src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Windup.json diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java index 0fc3ab061..1495c49a0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDiscDual.java @@ -49,7 +49,7 @@ public class AbilityDiscDual extends Ability { }; private EnergyLifespanData lifespanData = new EnergyLifespanData(35.0f, 200); private EnergyAnchorData[] anchorData = new EnergyAnchorData[]{ - new EnergyAnchorData(AnchorPoint.RIGHT_HAND), new EnergyAnchorData(AnchorPoint.LEFT_HAND) + new EnergyAnchorData(AnchorPoint.LEFT_HAND), new EnergyAnchorData(AnchorPoint.RIGHT_HAND) }; // Transient state for disc entity (used during windup charging) @@ -68,8 +68,8 @@ public class AbilityDiscDual extends Ability { this.telegraphType = TelegraphType.CIRCLE; this.showTelegraph = true; // Default built-in animations - this.windUpAnimationName = "Ability_BeamDual_Windup"; - this.activeAnimationName = "Ability_BeamDual_Active"; + this.windUpAnimationName = "Ability_DiscDual_Windup"; + this.activeAnimationName = "Ability_DiscDual_Active"; } @Override diff --git a/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Active.json b/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Active.json new file mode 100644 index 000000000..8fbaa73da --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Active.json @@ -0,0 +1,392 @@ +{ + "Loop": 5, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": -37.0f, + "Rotation0": 37.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 56.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -37.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -25.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": 120.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.5f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": -37.0f, + "Rotation0": 37.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 56.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -37.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -25.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": 120.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 4 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": -37.0f, + "Rotation0": 37.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 56.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -37.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -25.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": 120.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 4 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -145.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 75.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 37.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -12.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 2 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -145.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 75.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 37.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -12.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 4 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": 2.0f, + "Part": "BODY", + "Rotation1": -25.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": -145.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 75.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -25.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -12.0f + }, + { + "Pivot2": 2.0f, + "Part": "LEFT_ARM", + "Rotation1": 37.0f, + "Rotation0": -90.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": -12.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 6.0f + }, + { + "Pivot2": 2.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": -12.0f, + "Pivot0": -1.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 4 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 0, + "Smooth": 0b, + "ID": 18, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_DiscDual_Active", + "WhileStanding": 1b +} diff --git a/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Windup.json b/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Windup.json new file mode 100644 index 000000000..080306c95 --- /dev/null +++ b/src/main/resources/assets/customnpcs/animations/Ability_DiscDual_Windup.json @@ -0,0 +1,293 @@ +{ + "Loop": -1, + "Speed": 1.0f, + "Frames": [ + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.5f, + "FrameParts": [ + { + "Pivot2": -8.0f, + "Part": "BODY", + "Rotation1": 25.0f, + "Rotation0": 31.0f, + "Pivot0": -2.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 50.0f, + "Pivot0": 0.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -2.0f, + "Part": "LEFT_LEG", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -31.0f + }, + { + "Pivot2": -8.0f, + "Part": "LEFT_ARM", + "Rotation1": 31.0f, + "Rotation0": -56.0f, + "Pivot0": -2.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -3.0f, + "Part": "RIGHT_LEG", + "Rotation1": 25.0f, + "Rotation0": 18.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 360.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -8.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": -2.0f, + "Pivot1": 2.0f, + "Rotation2": -12.0f + } + ], + "Smooth": 0b, + "Duration": 10 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 0.35f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 50.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 50.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 170.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 75.0f, + "Rotation0": -107.0f, + "Pivot0": -6.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -6.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 50.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": -4.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 360.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 46 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 1.0f, + "FrameParts": [ + { + "Pivot2": 0.0f, + "Part": "BODY", + "Rotation1": 50.0f, + "Rotation0": -6.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 5.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 50.0f, + "Pivot0": 2.0f, + "Pivot1": 0.0f, + "Rotation2": 170.0f + }, + { + "Pivot2": -3.0f, + "Part": "LEFT_ARM", + "Rotation1": 75.0f, + "Rotation0": -107.0f, + "Pivot0": -6.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -4.0f, + "Part": "LEFT_LEG", + "Rotation1": -6.0f, + "Rotation0": 12.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "RIGHT_LEG", + "Rotation1": 50.0f, + "Rotation0": 18.0f, + "Pivot0": -3.0f, + "Pivot1": -4.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "FULL_MODEL", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": 0.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 0.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 2b, + "Duration": 1 + }, + { + "ColorMarker": 16777215, + "Comment": "", + "Speed": 2.0f, + "FrameParts": [ + { + "Pivot2": -5.0f, + "Part": "BODY", + "Rotation1": -37.0f, + "Rotation0": 37.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -7.0f, + "Part": "RIGHT_ARM", + "Rotation1": 0.0f, + "Rotation0": 25.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 56.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_LEG", + "Rotation1": -37.0f, + "Rotation0": 31.0f, + "Pivot0": 0.0f, + "Pivot1": 0.0f, + "Rotation2": -25.0f + }, + { + "Pivot2": 0.0f, + "Part": "LEFT_ARM", + "Rotation1": 0.0f, + "Rotation0": 120.0f, + "Pivot0": 0.0f, + "Pivot1": 1.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -1.0f, + "Part": "RIGHT_LEG", + "Rotation1": -18.0f, + "Rotation0": 12.0f, + "Pivot0": -1.0f, + "Pivot1": 0.0f, + "Rotation2": 0.0f + }, + { + "Pivot2": -6.0f, + "Part": "HEAD", + "Rotation1": 0.0f, + "Rotation0": 18.0f, + "Pivot0": 3.0f, + "Pivot1": 2.0f, + "Rotation2": 0.0f + } + ], + "Smooth": 0b, + "Duration": 3 + } + ], + "CurrentFrameTime": 0, + "CurrentFrame": 1, + "Smooth": 0b, + "ID": 17, + "WhileAttacking": 1b, + "WhileWalking": 1b, + "Name": "Ability_DiscDual_Windup", + "WhileStanding": 1b +} From 335ccd260bf6eed3b5f96e0bf53e2e01758b44b7 Mon Sep 17 00:00:00 2001 From: Vidal Date: Mon, 2 Feb 2026 04:48:43 -0300 Subject: [PATCH 355/358] Updated translation --- src/main/resources/assets/customnpcs/lang/en_US.lang | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index ab6ee1904..0737e9e55 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1612,6 +1612,9 @@ ability.piercing=Piercing # Orb Settings ability.homing=Homing ability.explosive=Explosive +ability.firingSpeed=Firing Speed +ability.amount=Amount +ability.dualCharge=Dual Charging # Vortex Settings ability.pullRadius=Radius @@ -1729,9 +1732,6 @@ ability.manager.title=Ability Manager # Explosive Hover Text ability.explosive.noBlockDamage=Explosions do not destroy blocks -# Additional Orb Settings -ability.explRadius=Expl Radius - # Telegraph Colors (for abilities that support separate colors) ability.windUpColor=Wind Up Color ability.activeColor=Active Color @@ -1768,6 +1768,7 @@ ability.hover.piercing=Hits multiple targets instead of stopping on first hit ability.hover.lightning=Add electrical effect to visuals ability.hover.outerGlow=Enable outer glow layer around projectile ability.hover.anchorPoint=Where on the NPC the ability spawns from +ability.hover.dualCharge=Whether or not should create two anchor points ability.hover.damage=Base damage dealt to targets ability.hover.speed=Movement speed of projectile ability.hover.lifetime=Maximum ticks before despawning From 3651643f26d2cc2e997541f4f564b2bbd19b83f8 Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 2 Feb 2026 14:59:55 -0500 Subject: [PATCH 356/358] Fix GL Issues? --- src/api | 2 +- .../npcs/client/renderer/RenderAbilityProjectile.java | 5 ++--- .../npcs/client/renderer/blocks/BlockBeamRenderer.java | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/api b/src/api index bf650319d..06c9daf81 160000 --- a/src/api +++ b/src/api @@ -1 +1 @@ -Subproject commit bf650319da422790bff294d570dd879143fe8d4c +Subproject commit 06c9daf81be8df0138a992ca6bcabc05e37189cb diff --git a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java index cce9bbe58..2ba840aec 100644 --- a/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/client/renderer/RenderAbilityProjectile.java @@ -31,6 +31,7 @@ public abstract class RenderAbilityProjectile extends Render { */ protected void setupRenderState() { GL11.glPushMatrix(); + GL11.glPushAttrib(GL11.GL_ALL_ATTRIB_BITS); GL11.glDisable(GL11.GL_CULL_FACE); GL11.glEnable(GL12.GL_RESCALE_NORMAL); GL11.glEnable(GL11.GL_ALPHA_TEST); @@ -48,9 +49,7 @@ public abstract class RenderAbilityProjectile extends Render { * Restore GL state after rendering. */ protected void restoreRenderState() { - GL11.glEnable(GL11.GL_TEXTURE_2D); - GL11.glEnable(GL11.GL_LIGHTING); - GL11.glEnable(GL11.GL_CULL_FACE); + GL11.glPopAttrib(); GL11.glPopMatrix(); } diff --git a/src/main/java/noppes/npcs/client/renderer/blocks/BlockBeamRenderer.java b/src/main/java/noppes/npcs/client/renderer/blocks/BlockBeamRenderer.java index 64b37340f..7f9fdd19b 100644 --- a/src/main/java/noppes/npcs/client/renderer/blocks/BlockBeamRenderer.java +++ b/src/main/java/noppes/npcs/client/renderer/blocks/BlockBeamRenderer.java @@ -26,8 +26,8 @@ public class BlockBeamRenderer extends BlockRendererInterface { public void renderTileEntityAt(TileEntity var1, double var2, double var4, double var6, float var8) { TileVariant tile = (TileVariant) var1; - GL11.glDisable(GL12.GL_RESCALE_NORMAL); GL11.glPushMatrix(); + GL11.glDisable(GL12.GL_RESCALE_NORMAL); GL11.glTranslatef((float) var2 + 0.5f, (float) var4 + 1.5f, (float) var6 + 0.5f); //GL11.glScalef(1.2f, 1.1f, 1.2f); GL11.glRotatef(180, 0, 0, 1); From 0ff501bb167713f7dcf52df1bb5fa3e68578a781 Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 2 Feb 2026 16:17:37 -0500 Subject: [PATCH 357/358] Declarative ability GUI: replace 18 SubGui classes with FieldDef system All ability GUI tabs (General, Target, Effects, Type, Visual) are now fully declarative via FieldDef definitions. Deleted 18 SubGuiAbility* classes (~3100 lines) and rewrote SubGuiAbilityConfig to use GuiScrollWindow for every tab with a unified field renderer. - Add FieldDef, FieldType, TabTarget, ColumnHint, TabDef gui framework - Add getFieldDefinitions() to all 18 ability subclasses - Add getBaseFieldDefinitions() to Ability.java for shared tabs - Merge COLOR/EFFECTS into SUB_GUI with convenience factories - Net reduction: ~3100 lines removed Co-Authored-By: Claude Opus 4.5 --- .../controllers/data/ability/Ability.java | 89 ++ .../data/ability/gui/ColumnHint.java | 7 + .../data/ability/gui/FieldDef.java | 287 +++++ .../data/ability/gui/FieldType.java | 11 + .../controllers/data/ability/gui/TabDef.java | 28 + .../data/ability/gui/TabTarget.java | 10 + .../data/ability/type/AbilityCharge.java | 23 +- .../data/ability/type/AbilityCutter.java | 35 +- .../data/ability/type/AbilityDash.java | 24 +- .../data/ability/type/AbilityDisc.java | 66 +- .../data/ability/type/AbilityEnergyBeam.java | 64 +- .../data/ability/type/AbilityGuard.java | 40 +- .../data/ability/type/AbilityHazard.java | 38 +- .../data/ability/type/AbilityHeal.java | 32 +- .../data/ability/type/AbilityHeavyHit.java | 26 +- .../data/ability/type/AbilityLaserShot.java | 55 +- .../data/ability/type/AbilityOrb.java | 62 +- .../data/ability/type/AbilityProjectile.java | 34 +- .../data/ability/type/AbilityShockwave.java | 24 +- .../data/ability/type/AbilitySlam.java | 24 +- .../data/ability/type/AbilitySweeper.java | 45 +- .../data/ability/type/AbilityTeleport.java | 41 +- .../data/ability/type/AbilityTrap.java | 32 +- .../data/ability/type/AbilityVortex.java | 32 +- .../gui/advanced/SubGuiAbilityConfig.java | 1145 +++++------------ .../advanced/ability/SubGuiAbilityCharge.java | 92 -- .../advanced/ability/SubGuiAbilityCutter.java | 141 -- .../advanced/ability/SubGuiAbilityDash.java | 65 - .../advanced/ability/SubGuiAbilityDisc.java | 323 ----- .../ability/SubGuiAbilityEnergyBeam.java | 312 ----- .../advanced/ability/SubGuiAbilityGuard.java | 143 -- .../advanced/ability/SubGuiAbilityHazard.java | 159 --- .../advanced/ability/SubGuiAbilityHeal.java | 106 -- .../ability/SubGuiAbilityHeavyHit.java | 76 -- .../ability/SubGuiAbilityLaserShot.java | 268 ---- .../advanced/ability/SubGuiAbilityOrb.java | 294 ----- .../ability/SubGuiAbilityProjectile.java | 145 --- .../ability/SubGuiAbilityShockwave.java | 95 -- .../advanced/ability/SubGuiAbilitySlam.java | 98 -- .../ability/SubGuiAbilitySweeper.java | 219 ---- .../ability/SubGuiAbilityTeleport.java | 165 --- .../advanced/ability/SubGuiAbilityTrap.java | 126 -- .../advanced/ability/SubGuiAbilityVortex.java | 129 -- 43 files changed, 1232 insertions(+), 3998 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/gui/ColumnHint.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDef.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldType.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabDef.java create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabTarget.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java delete mode 100644 src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index b67ed95ac..b368d6267 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -23,7 +23,11 @@ import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; + import java.util.ArrayList; +import java.util.Collections; import java.util.List; import net.minecraft.entity.Entity; @@ -300,6 +304,91 @@ public abstract class Ability implements IAbility { return true; } + /** + * Returns the declarative field definitions for this ability's GUI. + * Override in subclasses to declare type-specific and visual fields. + * The GUI rendering engine in SubGuiAbilityConfig uses this list to + * automatically build the Type and Visual tabs. + */ + public List getFieldDefinitions() { + return Collections.emptyList(); + } + + /** + * Returns field definitions for all base Ability fields (General, Target, Effects tabs). + */ + @SideOnly(Side.CLIENT) + public List getBaseFieldDefinitions() { + List defs = new ArrayList<>(); + + // General tab + defs.add(FieldDef.stringField("gui.name", this::getName, this::setName) + .tab(TabTarget.GENERAL)); + defs.add(FieldDef.intField("ability.weight", this::getWeight, this::setWeight) + .tab(TabTarget.GENERAL).range(1, 1000)); + defs.add(FieldDef.enumField("ability.lockMove", LockMovementType.class, + this::getLockMovement, this::setLockMovement) + .tab(TabTarget.GENERAL).hover("ability.hover.lockMove")); + defs.add(FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible) + .tab(TabTarget.GENERAL).hover("ability.hover.interruptible")); + defs.add(FieldDef.intField("ability.dazed", this::getDazedTicks, this::setDazedTicks) + .tab(TabTarget.GENERAL).range(0, 1000) + .visibleWhen(this::isInterruptible)); + defs.add(FieldDef.intField("ability.windup", this::getWindUpTicks, this::setWindUpTicks) + .tab(TabTarget.GENERAL).range(0, 1000)); + defs.add(FieldDef.intField("ability.cooldown", this::getCooldownTicks, this::setCooldownTicks) + .tab(TabTarget.GENERAL).range(0, 10000)); + defs.add(FieldDef.boolField("gui.enabled", this::isEnabled, this::setEnabled) + .tab(TabTarget.GENERAL)); + + // Target tab + defs.add(FieldDef.intField("ability.minRange", () -> (int) getMinRange(), v -> setMinRange(v)) + .tab(TabTarget.TARGET).range(0, 100)); + defs.add(FieldDef.intField("ability.maxRange", () -> (int) getMaxRange(), v -> setMaxRange(v)) + .tab(TabTarget.TARGET).range(1, 100)); + if (!isTargetingModeLocked()) { + defs.add(FieldDef.enumField("ability.targeting", TargetingMode.class, + this::getTargetingMode, this::setTargetingMode) + .tab(TabTarget.TARGET).hover("ability.hover.targeting")); + } + + // Effects tab + defs.add(FieldDef.soundSubGui("ability.windUpSound", this::getWindUpSound, this::setWindUpSound) + .tab(TabTarget.EFFECTS)); + defs.add(FieldDef.soundSubGui("ability.activeSound", this::getActiveSound, this::setActiveSound) + .tab(TabTarget.EFFECTS)); + defs.add(FieldDef.animSubGui("ability.windUpAnim", + this::getWindUpAnimationId, this::setWindUpAnimationId, + this::getWindUpAnimationName, this::setWindUpAnimationName) + .tab(TabTarget.EFFECTS)); + defs.add(FieldDef.animSubGui("ability.activeAnim", + this::getActiveAnimationId, this::setActiveAnimationId, + this::getActiveAnimationName, this::setActiveAnimationName) + .tab(TabTarget.EFFECTS)); + + TelegraphType tType = getTelegraphType(); + if (tType != null && tType != TelegraphType.NONE) { + defs.add(FieldDef.boolField("ability.showTelegraph", this::isShowTelegraph, this::setShowTelegraph) + .tab(TabTarget.EFFECTS).hover("ability.hover.showTelegraph")); + defs.add(FieldDef.colorSubGui("ability.windUpColor", this::getWindUpColor, this::setWindUpColor) + .tab(TabTarget.EFFECTS).visibleWhen(this::isShowTelegraph)); + defs.add(FieldDef.colorSubGui("ability.activeColor", this::getActiveColor, this::setActiveColor) + .tab(TabTarget.EFFECTS).visibleWhen(this::isShowTelegraph)); + } + + return defs; + } + + /** + * Returns ALL field definitions: base (General/Target/Effects) + type-specific (Type/Visual). + */ + @SideOnly(Side.CLIENT) + public final List getAllFieldDefinitions() { + List all = new ArrayList<>(getBaseFieldDefinitions()); + all.addAll(getFieldDefinitions()); + return all; + } + /** * Returns true if this ability type has custom settings that need a Type-specific tab. * Override to return true if the ability has settings beyond the base Ability fields. diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/ColumnHint.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/ColumnHint.java new file mode 100644 index 000000000..a1b5dd270 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/ColumnHint.java @@ -0,0 +1,7 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +public enum ColumnHint { + FULL, + LEFT, + RIGHT +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDef.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDef.java new file mode 100644 index 000000000..25ec6cb66 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDef.java @@ -0,0 +1,287 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import noppes.npcs.client.gui.SubGuiColorSelector; +import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityEffects; +import noppes.npcs.client.gui.select.GuiAnimationSelection; +import noppes.npcs.client.gui.select.GuiSoundSelection; +import noppes.npcs.client.gui.util.SubGuiInterface; + +import java.util.List; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * Declarative field definition for ability GUI rendering. + * Built via static factories and chaining methods. + */ +public class FieldDef { + + private final String label; + private final FieldType type; + private TabTarget tab = TabTarget.TYPE; + private String customTabName = null; + private ColumnHint column = ColumnHint.FULL; + private Supplier getter; + private Consumer setter; + private BooleanSupplier visibleWhen = () -> true; + private String tooltip = null; + private String hoverText = null; + + // Numeric range + private float min = Float.MIN_VALUE; + private float max = Float.MAX_VALUE; + + // Enum support + private Class> enumClass; + private String[] stringEnumValues; + private Supplier stringEnumGetter; + private Consumer stringEnumSetter; + + // Sub-gui support + private Supplier subGuiFactory; + private Consumer subGuiResultHandler; + + // Button display + private Supplier buttonLabelSupplier; + private Supplier buttonTextColorSupplier; + private Runnable clearAction; + + private FieldDef(String label, FieldType type) { + this.label = label; + this.type = type; + } + + // ═══════════════════════════════════════════════════════════════════ + // STATIC FACTORIES + // ═══════════════════════════════════════════════════════════════════ + + public static FieldDef floatField(String label, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.FLOAT); + def.getter = () -> getter.get(); + def.setter = v -> setter.accept(((Number) v).floatValue()); + return def; + } + + public static FieldDef intField(String label, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.INT); + def.getter = () -> getter.get(); + def.setter = v -> setter.accept(((Number) v).intValue()); + return def; + } + + public static FieldDef boolField(String label, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.BOOLEAN); + def.getter = () -> getter.get(); + def.setter = v -> setter.accept((Boolean) v); + return def; + } + + @SuppressWarnings("unchecked") + public static > FieldDef enumField(String label, Class enumClass, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.ENUM); + def.enumClass = enumClass; + def.getter = () -> getter.get(); + def.setter = v -> setter.accept((E) v); + return def; + } + + public static FieldDef stringEnumField(String label, String[] values, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.STRING_ENUM); + def.stringEnumValues = values; + def.stringEnumGetter = getter; + def.stringEnumSetter = setter; + return def; + } + + public static FieldDef stringField(String label, Supplier getter, Consumer setter) { + FieldDef def = new FieldDef(label, FieldType.STRING); + def.getter = () -> getter.get(); + def.setter = v -> setter.accept((String) v); + return def; + } + + public static FieldDef subGuiField(String label, Supplier factory, Consumer resultHandler) { + FieldDef def = new FieldDef(label, FieldType.SUB_GUI); + def.subGuiFactory = factory; + def.subGuiResultHandler = resultHandler; + return def; + } + + // ═══════════════════════════════════════════════════════════════════ + // CONVENIENCE FACTORIES (SUB_GUI wrappers) + // ═══════════════════════════════════════════════════════════════════ + + public static FieldDef colorSubGui(String label, Supplier getter, Consumer setter) { + return subGuiField(label, + () -> new SubGuiColorSelector(getter.get()), + gui -> setter.accept(((SubGuiColorSelector) gui).color & 0x00FFFFFF)) + .buttonLabel(() -> String.format("%06X", getter.get() & 0xFFFFFF)) + .buttonTextColor(() -> getter.get() & 0xFFFFFF); + } + + public static FieldDef effectsSubGui(String label, Supplier> getter, Consumer> setter) { + return subGuiField(label, + () -> new SubGuiAbilityEffects(getter.get()), + gui -> { + List r = ((SubGuiAbilityEffects) gui).getResult(); + if (r != null) setter.accept(r); + }); + } + + public static FieldDef soundSubGui(String label, Supplier getter, Consumer setter) { + return subGuiField(label, + () -> new GuiSoundSelection(getter.get()), + gui -> { + GuiSoundSelection s = (GuiSoundSelection) gui; + if (s.selectedResource != null) setter.accept(s.selectedResource.toString()); + }) + .buttonLabel(() -> { + String s = getter.get(); + return s == null || s.isEmpty() ? "gui.none" : truncateEnd(s, 10); + }) + .clearable(() -> setter.accept("")); + } + + public static FieldDef animSubGui(String label, + Supplier idGetter, Consumer idSetter, + Supplier nameGetter, Consumer nameSetter) { + return subGuiField(label, + () -> new GuiAnimationSelection(idGetter.get(), nameGetter.get()), + gui -> { + GuiAnimationSelection sel = (GuiAnimationSelection) gui; + if (sel.isBuiltInSelected()) { + nameSetter.accept(sel.selectedBuiltInName); + idSetter.accept(-1); + } else { + idSetter.accept(sel.selectedAnimationId); + nameSetter.accept(""); + } + }) + .buttonLabel(() -> { + String name = nameGetter.get(); + if (name != null && !name.isEmpty()) return truncateEnd(name, 8); + int id = idGetter.get(); + return id >= 0 ? "ID: " + id : "gui.none"; + }) + .clearable(() -> { idSetter.accept(-1); nameSetter.accept(""); }); + } + + // ═══════════════════════════════════════════════════════════════════ + // CHAINING METHODS + // ═══════════════════════════════════════════════════════════════════ + + public FieldDef tab(TabTarget tab) { + this.tab = tab; + return this; + } + + public FieldDef customTab(String tabName) { + this.tab = TabTarget.CUSTOM; + this.customTabName = tabName; + return this; + } + + public FieldDef range(float min, float max) { + this.min = min; + this.max = max; + return this; + } + + public FieldDef column(ColumnHint column) { + this.column = column; + return this; + } + + public FieldDef visibleWhen(BooleanSupplier condition) { + this.visibleWhen = condition; + return this; + } + + public FieldDef tooltip(String tooltip) { + this.tooltip = tooltip; + return this; + } + + public FieldDef hover(String hoverText) { + this.hoverText = hoverText; + return this; + } + + public FieldDef buttonLabel(Supplier supplier) { + this.buttonLabelSupplier = supplier; + return this; + } + + public FieldDef buttonTextColor(Supplier supplier) { + this.buttonTextColorSupplier = supplier; + return this; + } + + public FieldDef clearable(Runnable action) { + this.clearAction = action; + return this; + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS + // ═══════════════════════════════════════════════════════════════════ + + public String getLabel() { return label; } + public FieldType getType() { return type; } + public TabTarget getTab() { return tab; } + public String getCustomTabName() { return customTabName; } + public ColumnHint getColumn() { return column; } + public boolean isVisible() { return visibleWhen.getAsBoolean(); } + public String getTooltip() { return tooltip; } + public String getHoverText() { return hoverText; } + public float getMin() { return min; } + public float getMax() { return max; } + public boolean hasRange() { return min != Float.MIN_VALUE || max != Float.MAX_VALUE; } + public Class> getEnumClass() { return enumClass; } + public String[] getStringEnumValues() { return stringEnumValues; } + public Supplier getSubGuiFactory() { return subGuiFactory; } + public Consumer getSubGuiResultHandler() { return subGuiResultHandler; } + public boolean hasClearAction() { return clearAction != null; } + public Runnable getClearAction() { return clearAction; } + + public Object getValue() { + return getter != null ? getter.get() : null; + } + + public void setValue(Object value) { + if (setter != null) setter.accept(value); + } + + public String getStringEnumValue() { + return stringEnumGetter != null ? stringEnumGetter.get() : null; + } + + public void setStringEnumValue(String value) { + if (stringEnumSetter != null) stringEnumSetter.accept(value); + } + + public String getButtonLabel() { + if (buttonLabelSupplier != null) return buttonLabelSupplier.get(); + return label; + } + + public Integer getButtonTextColor() { + return buttonTextColorSupplier != null ? buttonTextColorSupplier.get() : null; + } + + // ═══════════════════════════════════════════════════════════════════ + // HELPERS + // ═══════════════════════════════════════════════════════════════════ + + static String truncateEnd(String str, int maxLen) { + if (str == null) return ""; + if (str.length() <= maxLen) return str; + int lastSlash = str.lastIndexOf('/'); + if (lastSlash >= 0 && str.length() - lastSlash <= maxLen) { + return "..." + str.substring(lastSlash); + } + return "..." + str.substring(str.length() - maxLen + 3); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldType.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldType.java new file mode 100644 index 000000000..b908d9b88 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldType.java @@ -0,0 +1,11 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +public enum FieldType { + FLOAT, + INT, + BOOLEAN, + ENUM, + STRING_ENUM, + SUB_GUI, + STRING +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabDef.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabDef.java new file mode 100644 index 000000000..64d1501fd --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabDef.java @@ -0,0 +1,28 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +/** + * Simple data class representing a tab in the ability config GUI. + * Built automatically from the FieldDef list. + */ +public class TabDef { + + public final String name; + public final TabTarget target; + public int index; + + public TabDef(String name, TabTarget target, int index) { + this.name = name; + this.target = target; + this.index = index; + } + + /** + * Returns a unique key combining target and custom name for deduplication. + */ + public String getKey() { + if (target == TabTarget.CUSTOM && name != null) { + return "CUSTOM:" + name; + } + return target.name(); + } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabTarget.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabTarget.java new file mode 100644 index 000000000..aba49646b --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabTarget.java @@ -0,0 +1,10 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +public enum TabTarget { + GENERAL, + TYPE, + TARGET, + EFFECTS, + VISUAL, + CUSTOM +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 25941e0a5..8241fab86 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -14,13 +14,13 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityCharge; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityCharge; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -63,12 +63,6 @@ public class AbilityCharge extends Ability implements IAbilityCharge { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityCharge(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -405,4 +399,15 @@ public class AbilityCharge extends Ability implements IAbilityCharge { public void setHitWidth(float hitWidth) { this.hitWidth = hitWidth; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("stats.speed", this::getChargeSpeed, this::setChargeSpeed), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.floatField("ability.hitWidth", this::getHitWidth, this::setHitWidth), + FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index 9db9d7700..29ae4aa22 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -1,7 +1,5 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; @@ -11,13 +9,14 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityCutter; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilityCutter; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; + +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -68,12 +67,6 @@ public class AbilityCutter extends Ability implements IAbilityCutter { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityCutter(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -311,4 +304,24 @@ public class AbilityCutter extends Ability implements IAbilityCutter { public float getCurrentRotation() { return currentRotation; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("gui.range", this::getRange, this::setRange), + FieldDef.floatField("ability.arcAngle", this::getArcAngle, this::setArcAngle), + FieldDef.floatField("ability.innerRadius", this::getInnerRadius, this::setInnerRadius), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.floatField("ability.sweepSpeed", this::getSweepSpeed, this::setSweepSpeed), + FieldDef.enumField("ability.sweepMode", SweepMode.class, this::getSweepModeEnum, this::setSweepModeEnum) + .hover("ability.hover.sweepMode"), + FieldDef.boolField("ability.piercing", this::isPiercing, this::setPiercing) + .hover("ability.hover.piercing"), + FieldDef.intField("ability.duration", this::getSpinDurationTicks, this::setSpinDurationTicks) + .range(1, 1000) + .visibleWhen(() -> this.getSweepModeEnum() == SweepMode.SPIN), + FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 054176a7b..3684a8b73 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -10,13 +10,14 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDash; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityDash; +import java.util.Arrays; +import java.util.List; import java.util.Random; /** @@ -104,13 +105,6 @@ public class AbilityDash extends Ability implements IAbilityDash { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityDash(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -349,4 +343,14 @@ public class AbilityDash extends Ability implements IAbilityDash { public DashDirection getChosenDirection() { return chosenDirection; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.enumField("ability.mode", DashMode.class, this::getDashModeEnum, this::setDashModeEnum) + .hover("ability.hover.dashMode"), + FieldDef.floatField("ability.dashDist", this::getDashDistance, this::setDashDistance), + FieldDef.floatField("ability.dashSpeed", this::getDashSpeed, this::setDashSpeed) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index 3ef2e164d..fb2f94646 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -17,10 +17,12 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityDisc; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityDisc; + +import java.util.Arrays; +import java.util.List; import noppes.npcs.entity.EntityNPCInterface; /** @@ -69,12 +71,6 @@ public class AbilityDisc extends Ability implements IAbilityDisc { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityDisc(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -279,4 +275,56 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public int getPreviewActiveDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed), + FieldDef.floatField("ability.discRadius", this::getDiscRadius, this::setDiscRadius), + FieldDef.floatField("ability.discThickness", this::getDiscThickness, this::setDiscThickness), + FieldDef.boolField("ability.homing", this::isHoming, this::setHoming) + .hover("ability.hover.homing"), + FieldDef.floatField("ability.homingStr", this::getHomingStrength, this::setHomingStrength) + .visibleWhen(this::isHoming), + FieldDef.boolField("ability.boomerang", this::isBoomerang, this::setBoomerang) + .hover("ability.hover.boomerang"), + FieldDef.intField("ability.boomerangDelay", this::getBoomerangDelay, this::setBoomerangDelay) + .visibleWhen(this::isBoomerang), + FieldDef.boolField("ability.explosive", this::isExplosive, this::setExplosive) + .hover("ability.hover.explosive"), + FieldDef.floatField("ability.explosionRad", this::getExplosionRadius, this::setExplosionRadius) + .visibleWhen(this::isExplosive), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.floatField("ability.maxDist", this::getMaxDistance, this::setMaxDistance), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime), + FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects), + // Visual tab + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) + .tab(TabTarget.VISUAL), + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) + .tab(TabTarget.VISUAL), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab(TabTarget.VISUAL), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .tab(TabTarget.VISUAL), + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .tab(TabTarget.VISUAL) + .visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .tab(TabTarget.VISUAL).range(0, 1) + .visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed) + .tab(TabTarget.VISUAL), + FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) + .tab(TabTarget.VISUAL), + FieldDef.floatField("ability.lightningDensity", this::getLightningDensity, this::setLightningDensity) + .tab(TabTarget.VISUAL).range(0.01f, 5.0f) + .visibleWhen(this::hasLightningEffect), + FieldDef.floatField("ability.lightningRadius", this::getLightningRadius, this::setLightningRadius) + .tab(TabTarget.VISUAL).range(0.1f, 10.0f) + .visibleWhen(this::hasLightningEffect) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 53377161a..89fe6acfc 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -17,10 +17,12 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityEnergyBeam; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityEnergyBeam; + +import java.util.Arrays; +import java.util.List; import noppes.npcs.entity.EntityNPCInterface; /** @@ -66,12 +68,6 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityEnergyBeam(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -282,4 +278,54 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { public int getPreviewActiveDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed), + FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth), + FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize), + FieldDef.boolField("ability.homing", this::isHoming, this::setHoming) + .hover("ability.hover.homing"), + FieldDef.floatField("ability.homingStr", this::getHomingStrength, this::setHomingStrength) + .visibleWhen(this::isHoming), + FieldDef.floatField("ability.homingRange", this::getHomingRange, this::setHomingRange) + .visibleWhen(this::isHoming), + FieldDef.floatField("ability.maxDist", this::getMaxDistance, this::setMaxDistance), + FieldDef.boolField("ability.explosive", this::isExplosive, this::setExplosive) + .hover("ability.hover.explosive"), + FieldDef.floatField("ability.explosionRad", this::getExplosionRadius, this::setExplosionRadius) + .visibleWhen(this::isExplosive), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime), + FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects), + // Visual tab + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) + .tab(TabTarget.VISUAL), + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) + .tab(TabTarget.VISUAL), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab(TabTarget.VISUAL), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .tab(TabTarget.VISUAL), + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .tab(TabTarget.VISUAL) + .visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .tab(TabTarget.VISUAL).range(0, 1) + .visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed) + .tab(TabTarget.VISUAL), + FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) + .tab(TabTarget.VISUAL), + FieldDef.floatField("ability.lightningDensity", this::getLightningDensity, this::setLightningDensity) + .tab(TabTarget.VISUAL).range(0.01f, 5.0f) + .visibleWhen(this::hasLightningEffect), + FieldDef.floatField("ability.lightningRadius", this::getLightningRadius, this::setLightningRadius) + .tab(TabTarget.VISUAL).range(0.1f, 10.0f) + .visibleWhen(this::hasLightningEffect) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index 92c7ca58e..bfa7b0f34 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -1,7 +1,5 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; @@ -10,13 +8,15 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityGuard; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilityGuard; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; + +import java.util.Arrays; +import java.util.List; import java.util.Random; /** @@ -67,13 +67,6 @@ public class AbilityGuard extends Ability implements IAbilityGuard { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityGuard(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -290,4 +283,27 @@ public class AbilityGuard extends Ability implements IAbilityGuard { public void setCounterAnimationId(int counterAnimationId) { this.counterAnimationId = counterAnimationId; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks) + .range(1, 1000), + FieldDef.floatField("ability.dmgReduce", this::getDamageReduction, this::setDamageReduction), + FieldDef.boolField("ability.canCounter", this::isCanCounter, this::setCanCounter) + .hover("ability.hover.canCounter"), + FieldDef.enumField("ability.counterType", CounterType.class, this::getCounterTypeEnum, this::setCounterTypeEnum) + .hover("ability.hover.counterType") + .visibleWhen(this::isCanCounter), + FieldDef.floatField("ability.counterValue", this::getCounterValue, this::setCounterValue) + .visibleWhen(this::isCanCounter), + FieldDef.floatField("ability.counterChance", this::getCounterChance, this::setCounterChance) + .visibleWhen(this::isCanCounter), + FieldDef.stringField("ability.counterSound", this::getCounterSound, this::setCounterSound) + .visibleWhen(this::isCanCounter), + FieldDef.intField("ability.counterAnim", this::getCounterAnimationId, this::setCounterAnimationId) + .visibleWhen(this::isCanCounter), + FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index 61841ded4..cd5194b29 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -1,7 +1,5 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; @@ -14,13 +12,14 @@ import net.minecraft.potion.PotionEffect; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHazard; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilityHazard; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; + +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Random; @@ -95,13 +94,6 @@ public class AbilityHazard extends Ability implements IAbilityHazard { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityHazard(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -616,4 +608,26 @@ public class AbilityHazard extends Ability implements IAbilityHazard { public double getZoneZ() { return zoneZ; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks) + .range(1, 2000), + FieldDef.enumField("ability.shape", HazardShape.class, this::getShapeEnum, this::setShapeEnum) + .hover("ability.hover.shape"), + FieldDef.enumField("ability.placement", PlacementMode.class, this::getPlacementEnum, this::setPlacementEnum) + .hover("ability.hover.placement"), + FieldDef.floatField("ability.radius", this::getRadius, this::setRadius), + FieldDef.floatField("ability.innerRadius", this::getInnerRadius, this::setInnerRadius), + FieldDef.floatField("ability.dmgPerSec", this::getDamagePerSecond, this::setDamagePerSecond), + FieldDef.intField("ability.dmgInterval", this::getDamageInterval, this::setDamageInterval), + FieldDef.intField("ability.slownessLvl", this::getSlownessLevel, this::setSlownessLevel), + FieldDef.intField("ability.debuffDur", this::getDebuffDuration, this::setDebuffDuration), + FieldDef.intField("ability.poisonLvl", this::getPoisonLevel, this::setPoisonLevel), + FieldDef.boolField("ability.affectsCaster", this::isAffectsCaster, this::setAffectsCaster) + .hover("ability.hover.affectsCaster"), + FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index 406be2f3a..543a26821 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -1,7 +1,5 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; @@ -11,14 +9,14 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeal; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityHeal; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; /** @@ -63,12 +61,6 @@ public class AbilityHeal extends Ability implements IAbilityHeal { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityHeal(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -289,4 +281,22 @@ public class AbilityHeal extends Ability implements IAbilityHeal { public void setInstantHeal(boolean instantHeal) { this.instantHeal = instantHeal; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.boolField("dialog.instant", this::isInstantHeal, this::setInstantHeal) + .hover("ability.hover.instant"), + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks) + .range(1, 1000) + .visibleWhen(() -> !this.isInstantHeal()), + FieldDef.floatField("ability.healAmount", this::getHealAmount, this::setHealAmount), + FieldDef.floatField("ability.healPercent", this::getHealPercent, this::setHealPercent), + FieldDef.floatField("ability.healRadius", this::getHealRadius, this::setHealRadius), + FieldDef.boolField("ability.healSelf", this::isHealSelf, this::setHealSelf) + .hover("ability.hover.healSelf"), + FieldDef.boolField("ability.healAllies", this::isHealAllies, this::setHealAllies) + .hover("ability.hover.healAllies") + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index 4ed4a0e78..a86662304 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -1,7 +1,5 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; @@ -9,12 +7,14 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityHeavyHit; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityHeavyHit; import noppes.npcs.entity.EntityNPCInterface; +import java.util.Arrays; +import java.util.List; + /** * Heavy Hit ability: Single-target melee attack with optional stun. * Deals high damage to one target and can stun them. @@ -44,13 +44,6 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityHeavyHit(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -113,4 +106,13 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { public void setKnockback(float knockback) { this.knockback = knockback; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index 3b45894b4..4d3ce9fbe 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -6,6 +6,8 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; @@ -16,10 +18,10 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityLaserShot; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.api.ability.type.IAbilityLaserShot; + +import java.util.Arrays; +import java.util.List; import noppes.npcs.entity.EntityNPCInterface; /** @@ -63,12 +65,6 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityLaserShot(this, callback); - } - @Override public boolean isTargetingModeLocked() { return false; @@ -229,4 +225,45 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { public boolean spawnPreviewDuringWindup() { return false; // Laser has no charging, spawns at active phase } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("ability.laserWidth", this::getLaserWidth, this::setLaserWidth), + FieldDef.floatField("ability.expansionSpeed", this::getExpansionSpeed, this::setExpansionSpeed), + FieldDef.intField("ability.lingerTicks", this::getLingerTicks, this::setLingerTicks), + FieldDef.floatField("ability.maxDist", this::getMaxDistance, this::setMaxDistance), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime), + FieldDef.boolField("ability.explosive", this::isExplosive, this::setExplosive) + .hover("ability.hover.explosive"), + FieldDef.floatField("ability.explosionRad", this::getExplosionRadius, this::setExplosionRadius) + .visibleWhen(this::isExplosive), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.floatField("ability.knockbackUp", this::getKnockbackUp, this::setKnockbackUp), + FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects), + // Visual tab + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) + .tab(TabTarget.VISUAL), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab(TabTarget.VISUAL), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .tab(TabTarget.VISUAL), + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .tab(TabTarget.VISUAL) + .visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .tab(TabTarget.VISUAL).range(0, 1) + .visibleWhen(this::isOuterColorEnabled), + FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) + .tab(TabTarget.VISUAL), + FieldDef.floatField("ability.lightningDensity", this::getLightningDensity, this::setLightningDensity) + .tab(TabTarget.VISUAL).range(0.01f, 5.0f) + .visibleWhen(this::hasLightningEffect), + FieldDef.floatField("ability.lightningRadius", this::getLightningRadius, this::setLightningRadius) + .tab(TabTarget.VISUAL).range(0.1f, 10.0f) + .visibleWhen(this::hasLightningEffect) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index 78936adbf..f4c156f49 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -17,10 +17,12 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityOrb; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityOrb; + +import java.util.Arrays; +import java.util.List; import noppes.npcs.entity.EntityNPCInterface; /** @@ -65,12 +67,6 @@ public class AbilityOrb extends Ability implements IAbilityOrb { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityOrb(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -418,4 +414,52 @@ public class AbilityOrb extends Ability implements IAbilityOrb { public int getPreviewActiveDuration() { return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed), + FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize), + FieldDef.floatField("ability.maxDist", this::getMaxDistance, this::setMaxDistance), + FieldDef.boolField("ability.homing", this::isHoming, this::setHoming) + .hover("ability.hover.homing"), + FieldDef.floatField("ability.homingStr", this::getHomingStrength, this::setHomingStrength) + .visibleWhen(this::isHoming), + FieldDef.boolField("ability.explosive", this::isExplosive, this::setExplosive) + .hover("ability.hover.explosive"), + FieldDef.floatField("ability.explosionRad", this::getExplosionRadius, this::setExplosionRadius) + .visibleWhen(this::isExplosive), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime), + FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects), + // Visual tab + FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) + .tab(TabTarget.VISUAL), + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) + .tab(TabTarget.VISUAL), + FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed) + .tab(TabTarget.VISUAL), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .tab(TabTarget.VISUAL), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .tab(TabTarget.VISUAL).range(0, 1) + .visibleWhen(this::isOuterColorEnabled), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab(TabTarget.VISUAL) + .visibleWhen(this::isOuterColorEnabled), + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .tab(TabTarget.VISUAL) + .visibleWhen(this::isOuterColorEnabled), + FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) + .tab(TabTarget.VISUAL), + FieldDef.floatField("ability.lightningDensity", this::getLightningDensity, this::setLightningDensity) + .tab(TabTarget.VISUAL).range(0.01f, 5.0f) + .visibleWhen(this::hasLightningEffect), + FieldDef.floatField("ability.lightningRadius", this::getLightningRadius, this::setLightningRadius) + .tab(TabTarget.VISUAL).range(0.1f, 10.0f) + .visibleWhen(this::hasLightningEffect) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index 6b81fabe7..03b815441 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -1,7 +1,5 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; @@ -10,13 +8,14 @@ import net.minecraft.entity.Entity; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityProjectile; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilityProjectile; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; + +import java.util.Arrays; import java.util.List; /** @@ -54,12 +53,6 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityProjectile(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -258,4 +251,23 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { public void setHomingStrength(float homingStrength) { this.homingStrength = homingStrength; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.stringEnumField("ability.projType", new String[]{"fireball", "arrow", "magic"}, this::getProjectileType, this::setProjectileType), + FieldDef.boolField("ability.explosive", this::isExplosive, this::setExplosive) + .hover("ability.hover.explosive"), + FieldDef.floatField("ability.explosionRad", this::getExplosionRadius, this::setExplosionRadius) + .visibleWhen(this::isExplosive), + FieldDef.boolField("ability.homing", this::isHoming, this::setHoming) + .hover("ability.hover.homing"), + FieldDef.floatField("ability.homingStr", this::getHomingStrength, this::setHomingStrength) + .visibleWhen(this::isHoming), + FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index 006084f5e..83af93df7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -1,7 +1,5 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; @@ -11,13 +9,13 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityShockwave; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityShockwave; +import java.util.Arrays; import java.util.List; /** @@ -49,12 +47,6 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityShockwave(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -175,4 +167,14 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { this.maxTargets = maxTargets; } + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("ability.pushRadius", this::getPushRadius, this::setPushRadius), + FieldDef.floatField("ability.pushStrength", this::getPushStrength, this::setPushStrength), + FieldDef.intField("ability.maxTargets", this::getMaxTargets, this::setMaxTargets), + FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index b3c800d81..b26e203c1 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -13,14 +13,14 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilitySlam; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilitySlam; import somehussar.gui.annotationHandling.GuiEditable; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; +import java.util.Arrays; import java.util.List; /** @@ -71,12 +71,6 @@ public class AbilitySlam extends Ability implements IAbilitySlam { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilitySlam(this, callback); - } - /** * Slam can be AOE_SELF (jump in place) or AOE_TARGET (leap to target). */ @@ -573,4 +567,16 @@ public class AbilitySlam extends Ability implements IAbilitySlam { public void setLeapHeight(float leapHeight) { this.leapHeight = leapHeight; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("ability.radius", this::getRadius, this::setRadius), + FieldDef.floatField("ability.knockback", this::getKnockbackStrength, this::setKnockbackStrength), + FieldDef.floatField("ability.leapSpeed", this::getLeapSpeed, this::setLeapSpeed), + FieldDef.floatField("ability.leapHeight", this::getLeapHeight, this::setLeapHeight), + FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index 976d9e44e..ef00df35e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -1,11 +1,11 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; @@ -14,10 +14,10 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; import noppes.npcs.LogWriter; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilitySweeper; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.api.ability.type.IAbilitySweeper; + +import java.util.Arrays; +import java.util.List; import noppes.npcs.entity.EntityNPCInterface; /** @@ -66,12 +66,6 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilitySweeper(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -205,4 +199,33 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { public void setOuterColorWidth(float outerColorWidth) { colorData.outerColorWidth = outerColorWidth; } public boolean isOuterColorEnabled() { return colorData.outerColorEnabled; } public void setOuterColorEnabled(boolean outerColorEnabled) { colorData.outerColorEnabled = outerColorEnabled; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + // Type tab + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.intField("ability.dmgInterval", this::getDamageInterval, this::setDamageInterval), + FieldDef.floatField("ability.beamLength", this::getBeamLength, this::setBeamLength), + FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth), + FieldDef.floatField("ability.beamHeight", this::getBeamHeight, this::setBeamHeight), + FieldDef.floatField("ability.sweepSpeed", this::getSweepSpeed, this::setSweepSpeed), + FieldDef.intField("ability.rotations", this::getNumberOfRotations, this::setNumberOfRotations), + FieldDef.boolField("ability.piercing", this::isPiercing, this::setPiercing) + .hover("ability.hover.piercing"), + FieldDef.boolField("ability.lockTarget", this::isLockOnTarget, this::setLockOnTarget) + .hover("ability.hover.lockTarget"), + FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects), + // Visual tab + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) + .tab(TabTarget.VISUAL), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .tab(TabTarget.VISUAL), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) + .tab(TabTarget.VISUAL), + FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) + .tab(TabTarget.VISUAL) + .visibleWhen(this::isOuterColorEnabled) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index 1fef00a55..284bdec03 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -1,7 +1,5 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; @@ -14,13 +12,14 @@ import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.MathHelper; import net.minecraft.util.Vec3; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityTeleport; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilityTeleport; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; + +import java.util.Arrays; import java.util.List; import java.util.Random; @@ -77,13 +76,6 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityTeleport(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -534,4 +526,29 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { public void setDamageRadius(float damageRadius) { this.damageRadius = damageRadius; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.enumField("ability.mode", TeleportMode.class, this::getModeEnum, this::setModeEnum), + FieldDef.floatField("ability.blinkRadius", this::getBlinkRadius, this::setBlinkRadius) + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK || this.getModeEnum() == TeleportMode.SINGLE), + FieldDef.intField("ability.blinkCount", this::getBlinkCount, this::setBlinkCount) + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK), + FieldDef.intField("ability.blinkDelay", this::getBlinkDelayTicks, this::setBlinkDelayTicks) + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK), + FieldDef.floatField("ability.behindDistance", this::getBehindDistance, this::setBehindDistance) + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BEHIND), + FieldDef.boolField("ability.lineOfSight", this::isRequireLineOfSight, this::setRequireLineOfSight) + .hover("ability.hover.lineOfSight") + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK || this.getModeEnum() == TeleportMode.SINGLE), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("ability.dmgRadius", this::getDamageRadius, this::setDamageRadius), + FieldDef.boolField("ability.dmgAtStart", this::isDamageAtStart, this::setDamageAtStart) + .hover("ability.hover.dmgAtStart"), + FieldDef.boolField("ability.dmgAtEnd", this::isDamageAtEnd, this::setDamageAtEnd) + .hover("ability.hover.dmgAtEnd"), + FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 6bf69a60b..2cb7a8a26 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -1,7 +1,5 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; @@ -12,13 +10,14 @@ import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; import net.minecraft.util.DamageSource; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityTrap; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilityTrap; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; + +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Random; @@ -78,12 +77,6 @@ public class AbilityTrap extends Ability implements IAbilityTrap { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui(IAbilityConfigCallback callback) { - return new SubGuiAbilityTrap(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -464,4 +457,21 @@ public class AbilityTrap extends Ability implements IAbilityTrap { public boolean isArmed() { return armed; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks) + .range(1, 2000), + FieldDef.enumField("ability.placement", TrapPlacement.class, this::getPlacementEnum, this::setPlacementEnum) + .hover("ability.hover.placement"), + FieldDef.floatField("ability.triggerRad", this::getTriggerRadius, this::setTriggerRadius), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("ability.dmgRadius", this::getDamageRadius, this::setDamageRadius), + FieldDef.intField("ability.armTime", this::getArmTime, this::setArmTime), + FieldDef.intField("ability.maxTriggers", this::getMaxTriggers, this::setMaxTriggers), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index 1bcfb7cbc..61e246bc9 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -1,7 +1,5 @@ package kamkeel.npcs.controllers.data.ability.type; -import cpw.mods.fml.relauncher.Side; -import cpw.mods.fml.relauncher.SideOnly; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; @@ -10,13 +8,13 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.AxisAlignedBB; import net.minecraft.world.World; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityVortex; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityVortex; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -67,13 +65,6 @@ public class AbilityVortex extends Ability implements IAbilityVortex { return true; } - @Override - @SideOnly(Side.CLIENT) - public SubGuiAbilityConfig createConfigGui( - IAbilityConfigCallback callback) { - return new SubGuiAbilityVortex(this, callback); - } - @Override public boolean isTargetingModeLocked() { return true; @@ -309,4 +300,21 @@ public class AbilityVortex extends Ability implements IAbilityVortex { public void setPullDamage(float pullDamage) { this.pullDamage = pullDamage; } + + @Override + public List getFieldDefinitions() { + return Arrays.asList( + FieldDef.floatField("ability.pullRadius", this::getPullRadius, this::setPullRadius), + FieldDef.floatField("ability.pullStrength", this::getPullStrength, this::setPullStrength), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), + FieldDef.boolField("ability.aoe", this::isAoe, this::setAoe) + .hover("ability.hover.aoe"), + FieldDef.intField("ability.maxTargets", this::getMaxTargets, this::setMaxTargets), + FieldDef.boolField("ability.dmgOnPull", this::isDamageOnPull, this::setDamageOnPull) + .hover("ability.hover.dmgOnPull"), + FieldDef.floatField("ability.pullDamage", this::getPullDamage, this::setPullDamage), + FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + ); + } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index 684138891..9c862e3b0 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -2,171 +2,95 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.Condition; -import kamkeel.npcs.controllers.data.ability.LockMovementType; -import kamkeel.npcs.controllers.data.ability.TargetingMode; -import kamkeel.npcs.controllers.data.telegraph.TelegraphType; -import noppes.npcs.controllers.data.Animation; -import noppes.npcs.controllers.data.Frame; -import noppes.npcs.controllers.AnimationController; +import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.FieldType; +import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import net.minecraft.client.gui.GuiButton; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.StatCollector; -import noppes.npcs.client.gui.SubGuiColorSelector; -import noppes.npcs.client.gui.select.GuiAnimationSelection; -import noppes.npcs.client.gui.select.GuiSoundSelection; import noppes.npcs.client.gui.util.GuiMenuTopButton; import noppes.npcs.client.gui.util.GuiNpcButton; import noppes.npcs.client.gui.util.GuiNpcLabel; import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.GuiScrollWindow; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.client.gui.util.ISubGuiListener; import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.client.gui.util.SubGuiInterface; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; -/** - * Base SubGui for editing ability configuration with tabbed interface. - * Uses GuiMenuTopButton for navigation tabs. - *

- * Tabs: - * - General: Name, Weight, Lock Movement, Interruptible, Timing - * - Type: Type-specific settings per Ability (override in subclasses) - * - Target: Min/Max Range, Targeting Mode, Conditions (up to 3) - * - Effects: Sounds, Animations, Telegraph settings, Colors - *

- * Subclasses should override initTypeTab() and handleTypeButton()/handleTypeTextField() - * to provide type-specific settings UI. - */ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldListener, ISubGuiListener { - // Tab constants - protected static final int TAB_GENERAL = 0; - protected static final int TAB_TYPE = 1; - protected static final int TAB_TARGET = 2; - protected static final int TAB_EFFECTS = 3; - protected static final int TAB_VISUAL = 4; + private static final int DECLARATIVE_ID_START = 1000; + private static final int CLEAR_ID_START = 2000; - // Core references - protected final Ability ability; - protected final IAbilityConfigCallback callback; - protected int activeTab = TAB_GENERAL; + private static final int TAB_GENERAL = 0; + private static final int TAB_TYPE = 1; + private static final int TAB_TARGET = 2; + private static final int TAB_EFFECTS = 3; + private static final int TAB_VISUAL = 4; - // ═══════════════════════════════════════════════════════════════════════════ - // CACHED VALUES - General Tab - // ═══════════════════════════════════════════════════════════════════════════ - protected String name; - protected boolean enabled; - protected int weight; - protected LockMovementType lockMovement; - protected boolean interruptible; + private final Ability ability; + private final IAbilityConfigCallback callback; + private int activeTab = TAB_GENERAL; - // Timing - protected int cooldownTicks; - protected int windUpTicks; - protected int dazedTicks; - protected boolean syncWindupWithAnimation = true; // Sync windup ticks with animation duration + private List fieldDefs; + private boolean hasTypeFields; + private boolean hasVisualFields; - // ═══════════════════════════════════════════════════════════════════════════ - // CACHED VALUES - Target Tab - // ═══════════════════════════════════════════════════════════════════════════ - protected float minRange; - protected float maxRange; - protected TargetingMode targetingMode; - protected List conditions; + // Conditions are complex (list with add/edit/remove) - cached separately + private List conditions; + private int editingConditionIndex = -1; - // ═══════════════════════════════════════════════════════════════════════════ - // CACHED VALUES - Effects Tab - // ═══════════════════════════════════════════════════════════════════════════ - protected String windUpSound; - protected String activeSound; - protected int windUpAnimationId; - protected int activeAnimationId; - protected String windUpAnimationName = null; - protected String activeAnimationName = null; - - // Telegraph - protected boolean showTelegraph; - protected int windUpColor; - protected int activeColor; - - // Editing state - protected int editingColorId = 0; - protected int editingSoundId = 0; - protected int editingAnimationId = 0; - protected int editingConditionIndex = -1; - - // ═══════════════════════════════════════════════════════════════════════════ - // ABILITY TYPE FEATURES - // ═══════════════════════════════════════════════════════════════════════════ - protected final boolean supportsTelegraph; - protected final boolean targetingModeLocked; - protected final boolean hasTypeSettings; - protected final boolean hasVisualSettings; + // Event routing + private final Map buttonFieldMap = new HashMap<>(); + private final Map textFieldMap = new HashMap<>(); + private final Map clearFieldMap = new HashMap<>(); + private FieldDef activeSubGuiField = null; public SubGuiAbilityConfig(Ability ability, IAbilityConfigCallback callback) { this.ability = ability; this.callback = callback; - - // Cache values from ability - General - this.name = ability.getName() != null ? ability.getName() : ""; - this.enabled = ability.isEnabled(); - this.weight = ability.getWeight(); - this.lockMovement = ability.getLockMovement(); - this.interruptible = ability.isInterruptible(); - - // Timing - this.cooldownTicks = ability.getCooldownTicks(); - this.windUpTicks = ability.getWindUpTicks(); - this.dazedTicks = ability.getDazedTicks(); - - // Target - this.minRange = ability.getMinRange(); - this.maxRange = ability.getMaxRange(); - this.targetingMode = ability.getTargetingMode(); this.conditions = new ArrayList<>(ability.getConditions()); - // Effects - this.windUpSound = ability.getWindUpSound(); - this.activeSound = ability.getActiveSound(); - // Animation: check name first (built-in), then ID (user) - this.windUpAnimationName = ability.getWindUpAnimationName(); - this.activeAnimationName = ability.getActiveAnimationName(); - this.windUpAnimationId = ability.getWindUpAnimationId(); - this.activeAnimationId = ability.getActiveAnimationId(); - - // Telegraph - this.showTelegraph = ability.isShowTelegraph(); - this.windUpColor = ability.getWindUpColor(); - this.activeColor = ability.getActiveColor(); - - // Determine ability type features - TelegraphType defaultType = ability.getTelegraphType(); - this.supportsTelegraph = defaultType != null && defaultType != TelegraphType.NONE; - this.targetingModeLocked = ability.isTargetingModeLocked(); - this.hasTypeSettings = ability.hasTypeSettings(); - this.hasVisualSettings = hasVisualSettings(); + this.fieldDefs = ability.getAllFieldDefinitions(); + this.hasTypeFields = hasFieldsForTab(TabTarget.TYPE); + this.hasVisualFields = hasFieldsForTab(TabTarget.VISUAL); setBackground("menubg.png", 217); xSize = 356; ySize = 200; } + private boolean hasFieldsForTab(TabTarget target) { + for (FieldDef def : fieldDefs) { + if (def.getTab() == target) return true; + } + return false; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // INIT GUI + // ═══════════════════════════════════════════════════════════════════════════ + @Override public void initGui() { super.initGui(); + buttonFieldMap.clear(); + textFieldMap.clear(); + clearFieldMap.clear(); - // ═══════════════════════════════════════════════════════════════════════ - // TOP TABS using GuiMenuTopButton - // ═══════════════════════════════════════════════════════════════════════ + // Tab buttons GuiMenuTopButton generalTab = new GuiMenuTopButton(90, guiLeft + 4, guiTop - 17, "menu.general"); generalTab.active = (activeTab == TAB_GENERAL); addTopButton(generalTab); - GuiMenuTopButton lastTab = generalTab; - if (hasTypeSettings) { + if (hasTypeFields) { GuiMenuTopButton typeTab = new GuiMenuTopButton(91, lastTab, "gui.type"); typeTab.active = (activeTab == TAB_TYPE); addTopButton(typeTab); @@ -183,277 +107,197 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi addTopButton(effectsTab); lastTab = effectsTab; - if (hasVisualSettings) { + if (hasVisualFields) { GuiMenuTopButton visualTab = new GuiMenuTopButton(94, lastTab, "ability.tab.visual"); visualTab.active = (activeTab == TAB_VISUAL); addTopButton(visualTab); } - // Close button (X) in top menu bar - use -1000 to avoid any ID conflicts GuiMenuTopButton closeBtn = new GuiMenuTopButton(-1000, guiLeft + xSize - 22, guiTop - 17, "X"); addTopButton(closeBtn); - int contentY = guiTop + 5; + // Scroll window for tab content + int swX = guiLeft + 4; + int swY = guiTop + 5; + int swW = xSize - 8; + int swH = ySize - 10; - // ═══════════════════════════════════════════════════════════════════════ - // TAB CONTENT - // ═══════════════════════════════════════════════════════════════════════ - switch (activeTab) { - case TAB_GENERAL: - initGeneralTab(contentY); - break; - case TAB_TYPE: - initTypeTab(contentY); - break; - case TAB_TARGET: - initTargetTab(contentY); - break; - case TAB_EFFECTS: - initEffectsTab(contentY); - break; - case TAB_VISUAL: - initVisualTab(contentY); - break; - } + GuiScrollWindow sw = new GuiScrollWindow(this, swX, swY, swW, swH, 0); - // ═══════════════════════════════════════════════════════════════════════ - // BOTTOM ROW - Enabled toggle (only on General tab) - // ═══════════════════════════════════════════════════════════════════════ + TabTarget tabTarget = tabTargetForIndex(activeTab); + int y = 5; + int idCounter = DECLARATIVE_ID_START; + int clearCounter = CLEAR_ID_START; + + // General tab: show type label at top if (activeTab == TAB_GENERAL) { - addLabel(new GuiNpcLabel(99, "gui.enabled", guiLeft + 8, guiTop + ySize - 18)); - GuiNpcButton enabledBtn = new GuiNpcButton(2, guiLeft + 55, guiTop + ySize - 24, 40, 20, new String[]{"gui.no", "gui.yes"}, enabled ? 1 : 0); - enabledBtn.setTextColor(enabled ? 0x00FF00 : 0xFF0000); - addButton(enabledBtn); + sw.addLabel(new GuiNpcLabel(1, "gui.type", 5, y + 5)); + sw.addLabel(new GuiNpcLabel(2, ability.getTypeId(), 50, y + 5)); + y += 24; + } + + // Render declarative fields for this tab + List tabFields = getVisibleFieldsForTab(tabTarget); + for (FieldDef def : tabFields) { + int widgetId = idCounter++; + int clearId = clearCounter++; + y = renderField(sw, def, widgetId, clearId, y); + } + + // Target tab: append conditions list + if (activeTab == TAB_TARGET) { + y = renderConditions(sw, y); + } + + sw.maxScrollY = Math.max(y - swH, 0); + addScrollableGui(0, sw); + } + + private TabTarget tabTargetForIndex(int tab) { + switch (tab) { + case TAB_GENERAL: return TabTarget.GENERAL; + case TAB_TYPE: return TabTarget.TYPE; + case TAB_TARGET: return TabTarget.TARGET; + case TAB_EFFECTS: return TabTarget.EFFECTS; + case TAB_VISUAL: return TabTarget.VISUAL; + default: return TabTarget.TYPE; } } - // ═══════════════════════════════════════════════════════════════════════════ - // GENERAL TAB - // ═══════════════════════════════════════════════════════════════════════════ - - private void initGeneralTab(int startY) { - int y = startY; - int col1LabelX = guiLeft + 8; - int col1FieldX = guiLeft + 70; - int col2LabelX = guiLeft + 180; - int col2FieldX = guiLeft + 248; - - // Row 1: Type (read-only) + Name - addLabel(new GuiNpcLabel(1, "gui.type", col1LabelX, y + 5)); - addLabel(new GuiNpcLabel(2, ability.getTypeId(), col1FieldX, y + 5)); - - addLabel(new GuiNpcLabel(3, "gui.name", col2LabelX, y + 5)); - GuiNpcTextField nameField = new GuiNpcTextField(1, this, fontRendererObj, col2FieldX, y, 100, 20, name); - addTextField(nameField); - - y += 24; - - // Row 2: Weight + Lock Movement - addLabel(new GuiNpcLabel(4, "ability.weight", col1LabelX, y + 5)); - GuiNpcTextField weightField = new GuiNpcTextField(3, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(weight)); - weightField.setIntegersOnly(); - weightField.setMinMaxDefault(1, 1000, 10); - addTextField(weightField); - - addLabel(new GuiNpcLabel(5, "ability.lockMove", col2LabelX, y + 5)); - GuiNpcButton lockMoveBtn = new GuiNpcButton(16, col2FieldX, y, 60, 20, LockMovementType.getDisplayKeys(), lockMovement.ordinal()); - lockMoveBtn.setHoverText("ability.hover.lockMove"); - addButton(lockMoveBtn); - - y += 24; - - // Row 3: Interruptible + Dazed Ticks (only if interruptible) - addLabel(new GuiNpcLabel(6, "ability.interruptible", col1LabelX, y + 5)); - GuiNpcButton interruptBtn = new GuiNpcButton(14, guiLeft + 85, y, 40, 20, new String[]{"gui.no", "gui.yes"}, interruptible ? 1 : 0); - interruptBtn.setHoverText("ability.hover.interruptible"); - addButton(interruptBtn); - - // Only show dazed ticks if interruptible is enabled - if (interruptible) { - addLabel(new GuiNpcLabel(12, "ability.dazed", col2LabelX, y + 5)); - GuiNpcTextField dazedField = new GuiNpcTextField(13, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(dazedTicks)); - dazedField.setIntegersOnly(); - dazedField.setMinMaxDefault(0, 1000, 80); - addTextField(dazedField); + private List getVisibleFieldsForTab(TabTarget target) { + List result = new ArrayList<>(); + for (FieldDef def : fieldDefs) { + if (def.getTab() == target && def.isVisible()) { + result.add(def); + } } + return result; + } - y += 28; + // ═══════════════════════════════════════════════════════════════════════════ + // FIELD RENDERING + // ═══════════════════════════════════════════════════════════════════════════ - // Separator - Timing section header - addLabel(new GuiNpcLabel(7, "ability.timing", col1LabelX, y)); + @SuppressWarnings({"unchecked", "rawtypes"}) + private int renderField(GuiScrollWindow sw, FieldDef def, int widgetId, int clearId, int y) { + int labelX = 5; + int fieldX = 100; + int fieldW = 60; + + sw.addLabel(new GuiNpcLabel(widgetId, def.getLabel(), labelX, y + 5)); + + switch (def.getType()) { + case FLOAT: { + float fVal = def.getValue() instanceof Number ? ((Number) def.getValue()).floatValue() : 0f; + GuiNpcTextField tf = new GuiNpcTextField(widgetId, this, fontRendererObj, fieldX, y, fieldW, 20, String.format("%.2f", fVal)); + tf.setFloatsOnly(); + if (def.hasRange()) tf.setMinMaxDefaultFloat(def.getMin(), def.getMax(), fVal); + if (def.getTooltip() != null) tf.setHoverText(def.getTooltip()); + sw.addTextField(tf); + textFieldMap.put(widgetId, def); + break; + } + case INT: { + int iVal = def.getValue() instanceof Number ? ((Number) def.getValue()).intValue() : 0; + GuiNpcTextField tf = new GuiNpcTextField(widgetId, this, fontRendererObj, fieldX, y, fieldW, 20, String.valueOf(iVal)); + tf.setIntegersOnly(); + if (def.hasRange()) tf.setMinMaxDefault((int) def.getMin(), (int) def.getMax(), iVal); + if (def.getTooltip() != null) tf.setHoverText(def.getTooltip()); + sw.addTextField(tf); + textFieldMap.put(widgetId, def); + break; + } + case STRING: { + String sVal = def.getValue() != null ? def.getValue().toString() : ""; + GuiNpcTextField tf = new GuiNpcTextField(widgetId, this, fontRendererObj, fieldX, y, fieldW + 40, 20, sVal); + if (def.getTooltip() != null) tf.setHoverText(def.getTooltip()); + sw.addTextField(tf); + textFieldMap.put(widgetId, def); + break; + } + case BOOLEAN: { + boolean bVal = def.getValue() instanceof Boolean ? (Boolean) def.getValue() : false; + GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, 40, 20, new String[]{"gui.no", "gui.yes"}, bVal ? 1 : 0); + if (def.getHoverText() != null) btn.setHoverText(def.getHoverText()); + sw.addButton(btn); + buttonFieldMap.put(widgetId, def); + break; + } + case ENUM: { + Class> enumClass = def.getEnumClass(); + if (enumClass != null) { + Enum[] constants = enumClass.getEnumConstants(); + String[] names = new String[constants.length]; + for (int i = 0; i < constants.length; i++) names[i] = constants[i].toString(); + int selected = def.getValue() instanceof Enum ? ((Enum) def.getValue()).ordinal() : 0; + GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, 80, 20, names, selected); + if (def.getHoverText() != null) btn.setHoverText(def.getHoverText()); + sw.addButton(btn); + buttonFieldMap.put(widgetId, def); + } + break; + } + case STRING_ENUM: { + String[] values = def.getStringEnumValues(); + if (values != null && values.length > 0) { + String curVal = def.getStringEnumValue(); + int selected = 0; + for (int i = 0; i < values.length; i++) { + if (values[i].equals(curVal)) { selected = i; break; } + } + GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, 80, 20, values, selected); + if (def.getHoverText() != null) btn.setHoverText(def.getHoverText()); + sw.addButton(btn); + buttonFieldMap.put(widgetId, def); + } + break; + } + case SUB_GUI: { + String btnText = def.getButtonLabel(); + int btnW = def.hasClearAction() ? 60 : 80; + GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, btnW, 20, btnText); + Integer textColor = def.getButtonTextColor(); + if (textColor != null) btn.setTextColor(textColor); + if (def.getHoverText() != null) btn.setHoverText(def.getHoverText()); + sw.addButton(btn); + buttonFieldMap.put(widgetId, def); + + if (def.hasClearAction()) { + GuiNpcButton clearBtn = new GuiNpcButton(clearId, fieldX + btnW + 5, y, 20, 20, "X"); + sw.addButton(clearBtn); + clearFieldMap.put(clearId, def); + } + break; + } + } + return y + 24; + } + + // ═══════════════════════════════════════════════════════════════════════════ + // CONDITIONS (Target tab special handling) + // ═══════════════════════════════════════════════════════════════════════════ + + private int renderConditions(GuiScrollWindow sw, int y) { + int x = 5; + + sw.addLabel(new GuiNpcLabel(50, "ability.conditions", x, y)); y += 14; - // Check if windup animation is selected (either by name or ID) - boolean hasWindupAnimation = windUpAnimationId >= 0 || (windUpAnimationName != null && !windUpAnimationName.isEmpty()); - - // Row 4: Windup Ticks + Cooldown Ticks - addLabel(new GuiNpcLabel(10, "ability.windup", col1LabelX, y + 5)); - - // Only show windup textbox if sync is off or no animation selected - if (!syncWindupWithAnimation || !hasWindupAnimation) { - GuiNpcTextField windupField = new GuiNpcTextField(11, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf(windUpTicks)); - windupField.setIntegersOnly(); - windupField.setMinMaxDefault(0, 1000, 20); - addTextField(windupField); - } else { - // Show calculated value as label when synced - addLabel(new GuiNpcLabel(11, String.valueOf(windUpTicks), col1FieldX + 5, y + 5)); - } - - // Sync toggle button - only show if windup animation is selected - if (hasWindupAnimation) { - String syncLabel = syncWindupWithAnimation ? "ability.syncOn" : "ability.syncOff"; - GuiNpcButton syncBtn = new GuiNpcButton(17, guiLeft + 115, y, 50, 20, syncLabel); - syncBtn.setHoverText("ability.hover.sync"); - addButton(syncBtn); - } - - addLabel(new GuiNpcLabel(13, "ability.cooldown", col2LabelX, y + 5)); - GuiNpcTextField cooldownField = new GuiNpcTextField(10, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf(cooldownTicks)); - cooldownField.setIntegersOnly(); - cooldownField.setMinMaxDefault(0, 10000, 0); - addTextField(cooldownField); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // TYPE TAB - Override in subclasses for type-specific settings - // ═══════════════════════════════════════════════════════════════════════════ - - /** - * Override this method to render type-specific settings in the Type tab. - * Use the helper methods createFloatField(), createIntField(), etc. - * Button/TextField IDs should start at 100. - * - * @param startY The Y position to start rendering from - */ - protected void initTypeTab(int startY) { - // Default: show message that no type settings are available - addLabel(new GuiNpcLabel(100, "ability.noTypeSettings", guiLeft + 8, startY + 5)); - } - - /** - * Override this method to handle button clicks for type-specific buttons. - * Button IDs 100+ are reserved for type-specific use. - */ - protected void handleTypeButton(int id, GuiNpcButton button) { - // Default: no-op - } - - /** - * Override this method to handle text field changes for type-specific fields. - * Field IDs 100+ are reserved for type-specific use. - */ - protected void handleTypeTextField(int id, GuiNpcTextField field) { - // Default: no-op - } - - // ═══════════════════════════════════════════════════════════════════════════ - // VISUAL TAB - Override in subclasses for visual settings (colors, etc.) - // ═══════════════════════════════════════════════════════════════════════════ - - /** - * Override to return true if this ability has visual settings (colors, glow, etc.). - * When true, a Visual tab will be shown in the GUI. - */ - protected boolean hasVisualSettings() { - return false; - } - - /** - * Override this method to render visual settings in the Visual tab. - * Use button/text field IDs starting at 200 for visual settings. - * - * @param startY The Y position to start rendering from - */ - protected void initVisualTab(int startY) { - // Default: show message that no visual settings are available - addLabel(new GuiNpcLabel(200, "ability.noVisualSettings", guiLeft + 8, startY + 5)); - } - - /** - * Override this method to handle button clicks for visual-specific buttons. - * Button IDs 200+ are reserved for visual-specific use. - */ - protected void handleVisualButton(int id, GuiNpcButton button) { - // Default: no-op - } - - /** - * Override this method to handle text field changes for visual-specific fields. - * Field IDs 200+ are reserved for visual-specific use. - */ - protected void handleVisualTextField(int id, GuiNpcTextField field) { - // Default: no-op - } - - // ═══════════════════════════════════════════════════════════════════════════ - // TARGET TAB - // ═══════════════════════════════════════════════════════════════════════════ - - private void initTargetTab(int startY) { - int y = startY; - int col1LabelX = guiLeft + 8; - int col1FieldX = guiLeft + 70; - int col2LabelX = guiLeft + 180; - int col2FieldX = guiLeft + 248; - - // Row 1: Min Range + Max Range - addLabel(new GuiNpcLabel(20, "ability.minRange", col1LabelX, y + 5)); - GuiNpcTextField minRangeField = new GuiNpcTextField(5, this, fontRendererObj, col1FieldX, y, 40, 20, String.valueOf((int) minRange)); - minRangeField.setIntegersOnly(); - minRangeField.setMinMaxDefault(0, 100, 0); - addTextField(minRangeField); - - addLabel(new GuiNpcLabel(21, "ability.maxRange", col2LabelX, y + 5)); - GuiNpcTextField maxRangeField = new GuiNpcTextField(6, this, fontRendererObj, col2FieldX, y, 40, 20, String.valueOf((int) maxRange)); - maxRangeField.setIntegersOnly(); - maxRangeField.setMinMaxDefault(1, 100, 20); - addTextField(maxRangeField); - - y += 24; - - // Row 2: Targeting Mode (if not locked) - addLabel(new GuiNpcLabel(22, "ability.targeting", col1LabelX, y + 5)); - if (!targetingModeLocked) { - String[] targetingModes = getAvailableTargetingModes(); - int selectedIndex = getTargetingModeIndex(targetingMode); - GuiNpcButton targetBtn = new GuiNpcButton(4, col1FieldX, y, 90, 20, targetingModes, selectedIndex); - targetBtn.setHoverText("ability.hover.targeting"); - addButton(targetBtn); - } else { - String modeName = "ability.target." + targetingMode.name().toLowerCase(); - addLabel(new GuiNpcLabel(23, modeName, col1FieldX, y + 5)); - } - - y += 28; - - // ───────────────────────────────────────────────────────────────────── - // CONDITIONS SECTION (up to 3) - // ───────────────────────────────────────────────────────────────────── - addLabel(new GuiNpcLabel(24, "ability.conditions", col1LabelX, y)); - y += 14; - - // Display existing conditions (similar to Marks pattern) for (int i = 0; i < conditions.size() && i < 3; i++) { Condition cond = conditions.get(i); String condName = getConditionDisplayName(cond); - - // Condition type button - addButton(new GuiNpcButton(50 + i * 10, col1LabelX, y, 140, 20, condName)); - - // Edit button - addButton(new GuiNpcButton(51 + i * 10, col1LabelX + 145, y, 40, 20, "gui.edit")); - - // Delete button - addButton(new GuiNpcButton(52 + i * 10, col1LabelX + 190, y, 20, 20, "X")); - + sw.addButton(new GuiNpcButton(50 + i * 10, x, y, 140, 20, condName)); + sw.addButton(new GuiNpcButton(51 + i * 10, x + 145, y, 40, 20, "gui.edit")); + sw.addButton(new GuiNpcButton(52 + i * 10, x + 190, y, 20, 20, "X")); y += 22; } - // Add button (if less than 3 conditions) if (conditions.size() < 3) { - addButton(new GuiNpcButton(80, col1LabelX, y, 50, 20, "gui.add")); + sw.addButton(new GuiNpcButton(80, x, y, 50, 20, "gui.add")); + y += 24; } + + return y; } private String getConditionDisplayName(Condition cond) { @@ -469,310 +313,102 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi case "target_hp_below": return StatCollector.translateToLocal("condition.target_hp_below") + " " + (int) (getConditionThreshold(cond) * 100) + "%"; case "hit_count": - Condition.ConditionHitCount hitCount = (Condition.ConditionHitCount) cond; - return StatCollector.translateToLocal("condition.hit_count") + ": " + hitCount.getRequiredHits() + "/" + hitCount.getWithinTicks() + "t"; + Condition.ConditionHitCount hc = (Condition.ConditionHitCount) cond; + return StatCollector.translateToLocal("condition.hit_count") + ": " + hc.getRequiredHits() + "/" + hc.getWithinTicks() + "t"; default: return typeId; } } private float getConditionThreshold(Condition cond) { - // Read threshold from NBT since inner classes don't expose it NBTTagCompound nbt = cond.writeNBT(); return nbt.hasKey("threshold") ? nbt.getFloat("threshold") : 0.5f; } - private String[] getAvailableTargetingModes() { - TargetingMode[] modes = ability.getAllowedTargetingModes(); - if (modes == null) { - modes = TargetingMode.values(); - } - String[] result = new String[modes.length]; - for (int i = 0; i < modes.length; i++) { - result[i] = "ability.target." + modes[i].name().toLowerCase(); - } - return result; - } - - private int getTargetingModeIndex(TargetingMode mode) { - TargetingMode[] modes = ability.getAllowedTargetingModes(); - if (modes == null) { - return mode.ordinal(); - } - for (int i = 0; i < modes.length; i++) { - if (modes[i] == mode) return i; - } - return 0; - } - - // ═══════════════════════════════════════════════════════════════════════════ - // EFFECTS TAB - // ═══════════════════════════════════════════════════════════════════════════ - - private void initEffectsTab(int startY) { - int y = startY; - int col1LabelX = guiLeft + 8; - int col1FieldX = guiLeft + 85; - int col1ClearX = guiLeft + 160; - int col2LabelX = guiLeft + 185; - int col2FieldX = guiLeft + 255; - - // ───────────────────────────────────────────────────────────────────── - // SOUNDS SECTION - // ───────────────────────────────────────────────────────────────────── - addLabel(new GuiNpcLabel(30, "advanced.sounds", col1LabelX, y)); - y += 14; - - // Wind Up Sound - addLabel(new GuiNpcLabel(31, "ability.windUpSound", col1LabelX, y + 5)); - String windUpSoundDisplay = windUpSound != null && !windUpSound.isEmpty() ? truncateString(windUpSound, 10) : StatCollector.translateToLocal("gui.none"); - addButton(new GuiNpcButton(30, col1FieldX, y, 70, 20, windUpSoundDisplay)); - addButton(new GuiNpcButton(35, col1ClearX, y, 20, 20, "X")); - - // Active Sound - addLabel(new GuiNpcLabel(32, "ability.activeSound", col2LabelX, y + 5)); - String activeSoundDisplay = activeSound != null && !activeSound.isEmpty() ? truncateString(activeSound, 10) : StatCollector.translateToLocal("gui.none"); - addButton(new GuiNpcButton(31, col2FieldX, y, 70, 20, activeSoundDisplay)); - addButton(new GuiNpcButton(36, col2FieldX + 75, y, 20, 20, "X")); - - y += 26; - - // ───────────────────────────────────────────────────────────────────── - // ANIMATIONS SECTION - // ───────────────────────────────────────────────────────────────────── - addLabel(new GuiNpcLabel(33, "menu.animations", col1LabelX, y)); - y += 14; - - // Wind Up Animation - addLabel(new GuiNpcLabel(34, "ability.windUpAnim", col1LabelX, y + 5)); - String windUpAnimName = getAnimationName(windUpAnimationId, true); - addButton(new GuiNpcButton(32, col1FieldX, y, 70, 20, windUpAnimName)); - addButton(new GuiNpcButton(37, col1ClearX, y, 20, 20, "X")); - - // Active Animation - addLabel(new GuiNpcLabel(35, "ability.activeAnim", col2LabelX, y + 5)); - String activeAnimName = getAnimationName(activeAnimationId, false); - addButton(new GuiNpcButton(33, col2FieldX, y, 70, 20, activeAnimName)); - addButton(new GuiNpcButton(38, col2FieldX + 75, y, 20, 20, "X")); - - y += 26; - - // ───────────────────────────────────────────────────────────────────── - // TELEGRAPH SECTION (only if supported) - // ───────────────────────────────────────────────────────────────────── - if (supportsTelegraph) { - addLabel(new GuiNpcLabel(40, "ability.telegraph", col1LabelX, y)); - y += 14; - - // Show Telegraph - addLabel(new GuiNpcLabel(41, "ability.showTelegraph", col1LabelX, y + 5)); - GuiNpcButton telegraphBtn = new GuiNpcButton(20, guiLeft + 95, y, 40, 20, new String[]{"gui.no", "gui.yes"}, showTelegraph ? 1 : 0); - telegraphBtn.setHoverText("ability.hover.showTelegraph"); - addButton(telegraphBtn); - - // Telegraph Type (separate column) - String typeKey = ability.getTelegraphType().name().toLowerCase(); - addLabel(new GuiNpcLabel(42, "ability.telegraphType", guiLeft + 200, y + 5)); - String telegraphLangKey = typeKey.equals("none") ? "gui.none" : "telegraph." + typeKey; - addLabel(new GuiNpcLabel(43, telegraphLangKey, guiLeft + 280, y + 5)); - - y += 24; - - // Colors - only show when telegraph is enabled - if (showTelegraph) { - addLabel(new GuiNpcLabel(44, "ability.windUpColor", col1LabelX, y + 5)); - String windUpHex = String.format("%06X", windUpColor & 0xFFFFFF); - GuiNpcButton windUpColorBtn = new GuiNpcButton(22, guiLeft + 95, y, 55, 20, windUpHex); - windUpColorBtn.setTextColor(windUpColor & 0xFFFFFF); - addButton(windUpColorBtn); - - addLabel(new GuiNpcLabel(45, "ability.activeColor", guiLeft + 200, y + 5)); - String activeHex = String.format("%06X", activeColor & 0xFFFFFF); - GuiNpcButton activeColorBtn = new GuiNpcButton(24, guiLeft + 280, y, 55, 20, activeHex); - activeColorBtn.setTextColor(activeColor & 0xFFFFFF); - addButton(activeColorBtn); - } - } - } - - private String truncateString(String str, int maxLen) { - if (str == null) return ""; - if (str.length() <= maxLen) return str; - int lastSlash = str.lastIndexOf('/'); - if (lastSlash >= 0 && str.length() - lastSlash <= maxLen) { - return "..." + str.substring(lastSlash); - } - return "..." + str.substring(str.length() - maxLen + 3); - } - - private String getAnimationName(int animId, boolean isWindUp) { - String animName = isWindUp ? windUpAnimationName : activeAnimationName; - // Check if we have a built-in animation (by name) - if (animName != null && !animName.isEmpty()) { - return truncateString(animName, 8); - } - // Check if we have a user animation (by ID) - if (animId >= 0) { - return "ID: " + animId; - } - return StatCollector.translateToLocal("gui.none"); - } - // ═══════════════════════════════════════════════════════════════════════════ // BUTTON EVENTS // ═══════════════════════════════════════════════════════════════════════════ @Override + @SuppressWarnings({"unchecked", "rawtypes"}) public void buttonEvent(GuiButton guibutton) { int id = guibutton.id; // Tab switching - if (id == 90) { - activeTab = TAB_GENERAL; - initGui(); - return; - } - if (id == 91 && hasTypeSettings) { - activeTab = TAB_TYPE; - initGui(); - return; - } - if (id == 92) { - activeTab = TAB_TARGET; - initGui(); - return; - } - if (id == 93) { - activeTab = TAB_EFFECTS; - initGui(); - return; - } - if (id == 94 && hasVisualSettings) { - activeTab = TAB_VISUAL; - initGui(); - return; - } + if (id == 90) { activeTab = TAB_GENERAL; initGui(); return; } + if (id == 91) { activeTab = TAB_TYPE; initGui(); return; } + if (id == 92) { activeTab = TAB_TARGET; initGui(); return; } + if (id == 93) { activeTab = TAB_EFFECTS; initGui(); return; } + if (id == 94) { activeTab = TAB_VISUAL; initGui(); return; } + if (id == -1000) { close(); return; } - // Enabled button (bottom left) - if (id == 2) { - enabled = ((GuiNpcButton) guibutton).getValue() == 1; - initGui(); - } - - // General tab buttons - else if (id == 14) { - interruptible = ((GuiNpcButton) guibutton).getValue() == 1; - initGui(); // Refresh to show/hide dazed ticks field - } else if (id == 16) { - lockMovement = LockMovementType.fromOrdinal(((GuiNpcButton) guibutton).getValue()); - } else if (id == 17) { - // Toggle sync on/off - syncWindupWithAnimation = !syncWindupWithAnimation; - if (syncWindupWithAnimation) { - // Calculate windup ticks from animation when turning sync on - calculateWindupFromAnimation(); - } - initGui(); - } - - // Target tab - Targeting mode - else if (id == 4) { - TargetingMode[] modes = ability.getAllowedTargetingModes(); - int idx = ((GuiNpcButton) guibutton).getValue(); - if (modes != null && idx < modes.length) { - targetingMode = modes[idx]; - } else { - targetingMode = TargetingMode.values()[idx]; - } - } - - // Close button (X in top menu) - use -1000 to avoid any ID conflicts - else if (id == -1000) { - close(); - return; - } - - // Target tab - Condition buttons (50-79) - else if (id >= 50 && id < 80) { + // Condition buttons (50-80) + if (id >= 50 && id < 80) { int condIndex = (id - 50) / 10; int action = (id - 50) % 10; - if (action == 0 || action == 1) { - // Click on condition name or Edit button - open edit subgui if (condIndex < conditions.size()) { editingConditionIndex = condIndex; setSubGui(new SubGuiConditionEdit(conditions.get(condIndex))); } } else if (action == 2) { - // Delete button if (condIndex < conditions.size()) { conditions.remove(condIndex); initGui(); } } + return; } - // Add condition button - else if (id == 80) { + if (id == 80) { if (conditions.size() < 3) { editingConditionIndex = conditions.size(); setSubGui(new SubGuiConditionEdit(null)); } + return; } - // Effects tab - Telegraph - else if (id == 20) { - showTelegraph = ((GuiNpcButton) guibutton).getValue() == 1; - initGui(); - } else if (id == 22) { - editingColorId = 22; - setSubGui(new SubGuiColorSelector(windUpColor)); - } else if (id == 24) { - editingColorId = 24; - setSubGui(new SubGuiColorSelector(activeColor)); + // Clear buttons + if (id >= CLEAR_ID_START) { + FieldDef def = clearFieldMap.get(id); + if (def != null && def.hasClearAction()) { + def.getClearAction().run(); + initGui(); + } + return; } - // Effects tab - Sound buttons - else if (id == 30) { - editingSoundId = 30; - setSubGui(new GuiSoundSelection(windUpSound)); - } else if (id == 31) { - editingSoundId = 31; - setSubGui(new GuiSoundSelection(activeSound)); - } - // Sound clear buttons - else if (id == 35) { - windUpSound = ""; - initGui(); - } else if (id == 36) { - activeSound = ""; - initGui(); - } - // Animation buttons - else if (id == 32) { - editingAnimationId = 32; - setSubGui(new GuiAnimationSelection(windUpAnimationId, windUpAnimationName)); - } else if (id == 33) { - editingAnimationId = 33; - setSubGui(new GuiAnimationSelection(activeAnimationId, activeAnimationName)); - } - // Animation clear buttons - else if (id == 37) { - windUpAnimationId = -1; - windUpAnimationName = ""; - initGui(); - } else if (id == 38) { - activeAnimationId = -1; - activeAnimationName = ""; - initGui(); - } + // Declarative field buttons + if (id >= DECLARATIVE_ID_START) { + FieldDef def = buttonFieldMap.get(id); + if (def == null) return; - // Visual-specific buttons (200+) - delegate to subclass - else if (id >= 200) { - handleVisualButton(id, (GuiNpcButton) guibutton); - } - // Type-specific buttons (100-199) - delegate to subclass - else if (id >= 100) { - handleTypeButton(id, (GuiNpcButton) guibutton); + switch (def.getType()) { + case BOOLEAN: + def.setValue(((GuiNpcButton) guibutton).getValue() == 1); + initGui(); + break; + case ENUM: { + int idx = ((GuiNpcButton) guibutton).getValue(); + Class> ec = def.getEnumClass(); + if (ec != null) { + Enum[] constants = ec.getEnumConstants(); + if (idx >= 0 && idx < constants.length) def.setValue(constants[idx]); + } + initGui(); + break; + } + case STRING_ENUM: { + int idx = ((GuiNpcButton) guibutton).getValue(); + String[] values = def.getStringEnumValues(); + if (values != null && idx >= 0 && idx < values.length) def.setStringEnumValue(values[idx]); + initGui(); + break; + } + case SUB_GUI: + activeSubGuiField = def; + if (def.getSubGuiFactory() != null) setSubGui(def.getSubGuiFactory().get()); + break; + } } } @@ -783,94 +419,41 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi @Override public void unFocused(GuiNpcTextField textField) { int id = textField.id; + if (id < DECLARATIVE_ID_START) return; - // General tab - if (id == 1) { - name = textField.getText(); - } else if (id == 3) { - weight = textField.getInteger(); - } + FieldDef def = textFieldMap.get(id); + if (def == null) return; - // Target tab - else if (id == 5) { - minRange = textField.getInteger(); - } else if (id == 6) { - maxRange = textField.getInteger(); - } - - // Timing (General tab) - else if (id == 10) { - cooldownTicks = textField.getInteger(); - } else if (id == 11) { - windUpTicks = textField.getInteger(); - } else if (id == 13) { - dazedTicks = textField.getInteger(); - } - - // Visual-specific fields (200+) - delegate to subclass - else if (id >= 200) { - handleVisualTextField(id, textField); - } - // Type-specific fields (100-199) - delegate to subclass - else if (id >= 100) { - handleTypeTextField(id, textField); + switch (def.getType()) { + case FLOAT: + try { def.setValue(Float.parseFloat(textField.getText())); } catch (NumberFormatException ignored) {} + break; + case INT: + try { def.setValue(Integer.parseInt(textField.getText())); } catch (NumberFormatException ignored) {} + break; + case STRING: + def.setValue(textField.getText()); + break; } } // ═══════════════════════════════════════════════════════════════════════════ - // SUBGUI CLOSED HANDLER + // SUB GUI CLOSED // ═══════════════════════════════════════════════════════════════════════════ @Override public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiColorSelector) { - SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; - int rgb = colorSelector.color & 0x00FFFFFF; - if (editingColorId == 22) { - windUpColor = 0x80000000 | rgb; - } else if (editingColorId == 24) { - activeColor = 0xC0000000 | rgb; + if (activeSubGuiField != null) { + FieldDef def = activeSubGuiField; + activeSubGuiField = null; + if (def.getSubGuiResultHandler() != null) { + def.getSubGuiResultHandler().accept(subgui); } - editingColorId = 0; initGui(); - } else if (subgui instanceof GuiSoundSelection) { - GuiSoundSelection soundSelector = (GuiSoundSelection) subgui; - if (soundSelector.selectedResource != null) { - String sound = soundSelector.selectedResource.toString(); - if (editingSoundId == 30) { - windUpSound = sound; - } else if (editingSoundId == 31) { - activeSound = sound; - } - } - editingSoundId = 0; - initGui(); - } else if (subgui instanceof GuiAnimationSelection) { - GuiAnimationSelection animSelector = (GuiAnimationSelection) subgui; - if (editingAnimationId == 32) { - if (animSelector.isBuiltInSelected()) { - // Built-in animation selected - use name, clear ID - windUpAnimationName = animSelector.selectedBuiltInName; - windUpAnimationId = -1; - } else { - // User animation selected - use ID, clear name - windUpAnimationId = animSelector.selectedAnimationId; - windUpAnimationName = ""; - } - } else if (editingAnimationId == 33) { - if (animSelector.isBuiltInSelected()) { - // Built-in animation selected - use name, clear ID - activeAnimationName = animSelector.selectedBuiltInName; - activeAnimationId = -1; - } else { - // User animation selected - use ID, clear name - activeAnimationId = animSelector.selectedAnimationId; - activeAnimationName = ""; - } - } - editingAnimationId = 0; - initGui(); - } else if (subgui instanceof SubGuiConditionEdit) { + return; + } + + if (subgui instanceof SubGuiConditionEdit) { SubGuiConditionEdit condEdit = (SubGuiConditionEdit) subgui; Condition result = condEdit.getResult(); if (result != null && editingConditionIndex >= 0) { @@ -886,178 +469,36 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi } // ═══════════════════════════════════════════════════════════════════════════ - // CLOSE - Save before closing + // CLOSE / APPLY // ═══════════════════════════════════════════════════════════════════════════ @Override public void close() { - // Apply all cached values to the ability before closing applyToAbility(); - - // Notify callback that ability was saved callback.onAbilitySaved(ability); - - // Close the GUI super.close(); } - // ═══════════════════════════════════════════════════════════════════════════ - // APPLY TO ABILITY - // ═══════════════════════════════════════════════════════════════════════════ - protected void applyToAbility() { - // General - ability.setName(name); - ability.setEnabled(enabled); - ability.setWeight(weight); - ability.setLockMovement(lockMovement); - ability.setInterruptible(interruptible); - - // If sync is enabled and we have a windup animation, recalculate before saving - boolean hasWindupAnimation = windUpAnimationId >= 0 || (windUpAnimationName != null && !windUpAnimationName.isEmpty()); - if (syncWindupWithAnimation && hasWindupAnimation) { - calculateWindupFromAnimation(); - } - - // Timing - ability.setCooldownTicks(cooldownTicks); - ability.setWindUpTicks(windUpTicks); - ability.setDazedTicks(dazedTicks); - - // Target - ability.setMinRange(minRange); - ability.setMaxRange(maxRange); - ability.setTargetingMode(targetingMode); - - // Conditions + // Conditions are the only cached values - write them back ability.getConditions().clear(); - for (Condition c : conditions) { - ability.addCondition(c); - } - - // Effects - ability.setWindUpSound(windUpSound); - ability.setActiveSound(activeSound); - ability.setWindUpAnimationId(windUpAnimationId); - ability.setActiveAnimationId(activeAnimationId); - ability.setWindUpAnimationName(windUpAnimationName != null ? windUpAnimationName : ""); - ability.setActiveAnimationName(activeAnimationName != null ? activeAnimationName : ""); - - // Telegraph - ability.setShowTelegraph(showTelegraph); - ability.setWindUpColor(windUpColor); - ability.setActiveColor(activeColor); - - // Type-specific values are applied directly in handleTypeTextField and handleTypeButton + for (Condition c : conditions) ability.addCondition(c); + // All other fields write directly to ability via FieldDef lambdas } - /** - * Called when an ability is loaded from saved presets. - */ public void loadAbility(Ability loadedAbility) { if (loadedAbility == null) return; - - // Copy all values from loaded ability to current ability NBTTagCompound nbt = loadedAbility.writeNBT(); - // Keep the current typeId since we can't change types mid-edit nbt.setString("typeId", ability.getTypeId()); ability.readNBT(nbt); - // Update cached values - this.name = ability.getName() != null ? ability.getName() : ""; - this.enabled = ability.isEnabled(); - this.weight = ability.getWeight(); - this.lockMovement = ability.getLockMovement(); - this.interruptible = ability.isInterruptible(); - - this.cooldownTicks = ability.getCooldownTicks(); - this.windUpTicks = ability.getWindUpTicks(); - this.dazedTicks = ability.getDazedTicks(); - - this.minRange = ability.getMinRange(); - this.maxRange = ability.getMaxRange(); - this.targetingMode = ability.getTargetingMode(); this.conditions = new ArrayList<>(ability.getConditions()); - - this.showTelegraph = ability.isShowTelegraph(); - this.windUpColor = ability.getWindUpColor(); - this.activeColor = ability.getActiveColor(); - + this.fieldDefs = ability.getAllFieldDefinitions(); + this.hasTypeFields = hasFieldsForTab(TabTarget.TYPE); + this.hasVisualFields = hasFieldsForTab(TabTarget.VISUAL); initGui(); } - // ═══════════════════════════════════════════════════════════════════════════ - // SYNC ANIMATION - // ═══════════════════════════════════════════════════════════════════════════ - - /** - * Calculates windup ticks from the selected windup animation's total duration. - */ - private void calculateWindupFromAnimation() { - if (AnimationController.Instance == null) { - return; - } - - Animation animation = null; - // Check for built-in animation (by name) first - if (windUpAnimationName != null && !windUpAnimationName.isEmpty()) { - animation = (Animation) AnimationController.Instance.get(windUpAnimationName); - } - // Fall back to user animation (by ID) - else if (windUpAnimationId >= 0) { - animation = (Animation) AnimationController.Instance.get(windUpAnimationId); - } - - if (animation == null || animation.frames.isEmpty()) { - return; - } - - // Calculate total duration by summing all frame durations - int totalDuration = 0; - for (Frame frame : animation.frames) { - totalDuration += frame.getDuration(); - } - - // Update the cached value and refresh GUI - windUpTicks = totalDuration; - initGui(); - } - - // ═══════════════════════════════════════════════════════════════════════════ - // HELPER METHODS FOR SUBCLASSES - // ═══════════════════════════════════════════════════════════════════════════ - - /** - * Creates a float text field for type-specific settings. - */ - protected GuiNpcTextField createFloatField(int id, int x, int y, int width, float value) { - GuiNpcTextField field = new GuiNpcTextField(id, this, fontRendererObj, x, y, width, 20, String.format("%.1f", value)); - return field; - } - - /** - * Creates an integer text field for type-specific settings. - */ - protected GuiNpcTextField createIntField(int id, int x, int y, int width, int value) { - GuiNpcTextField field = new GuiNpcTextField(id, this, fontRendererObj, x, y, width, 20, String.valueOf(value)); - field.setIntegersOnly(); - return field; - } - - /** - * Parses a float from a text field with a fallback default value. - */ - protected float parseFloat(GuiNpcTextField field, float defaultValue) { - try { - return Float.parseFloat(field.getText()); - } catch (NumberFormatException e) { - return defaultValue; - } - } - - /** - * Gets the ability being edited. - */ protected Ability getAbility() { return ability; } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java deleted file mode 100644 index ea35797c4..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCharge.java +++ /dev/null @@ -1,92 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.AbilityEffect; -import kamkeel.npcs.controllers.data.ability.type.AbilityCharge; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.util.SubGuiInterface; - -import java.util.List; - -/** - * GUI for configuring Charge ability type-specific settings. - */ -public class SubGuiAbilityCharge extends SubGuiAbilityConfig { - - private final AbilityCharge charge; - - public SubGuiAbilityCharge(AbilityCharge ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.charge = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, charge.getDamage())); - - addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, charge.getChargeSpeed())); - - y += 24; - - // Row 2: Knockback - addLabel(new GuiNpcLabel(102, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, charge.getKnockback())); - - y += 24; - - // Row 3: Hit Width + Effects - addLabel(new GuiNpcLabel(104, "ability.hitWidth", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, charge.getHitWidth())); - - addButton(new GuiNpcButton(150, col2LabelX, y, 80, 20, "ability.effects")); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - if (id == 150) { - setSubGui(new SubGuiAbilityEffects(charge.getEffects())); - } - } - - @Override - public void subGuiClosed(SubGuiInterface subgui) { - super.subGuiClosed(subgui); - if (subgui instanceof SubGuiAbilityEffects) { - SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; - List result = effectsGui.getResult(); - if (result != null) { - charge.setEffects(result); - } - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - charge.setDamage(parseFloat(field, charge.getDamage())); - break; - case 101: - charge.setChargeSpeed(parseFloat(field, charge.getChargeSpeed())); - break; - case 102: - charge.setKnockback(parseFloat(field, charge.getKnockback())); - break; - case 104: - charge.setHitWidth(parseFloat(field, charge.getHitWidth())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java deleted file mode 100644 index a1539536d..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityCutter.java +++ /dev/null @@ -1,141 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.AbilityEffect; -import kamkeel.npcs.controllers.data.ability.type.AbilityCutter; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.util.SubGuiInterface; - -import java.util.List; - -/** - * GUI for configuring Cutter ability type-specific settings. - */ -public class SubGuiAbilityCutter extends SubGuiAbilityConfig { - - private final AbilityCutter cutter; - - public SubGuiAbilityCutter(AbilityCutter ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.cutter = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Range - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, cutter.getDamage())); - - addLabel(new GuiNpcLabel(101, "gui.range", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, cutter.getRange())); - - y += 24; - - // Row 2: Arc Angle + Inner Radius - addLabel(new GuiNpcLabel(102, "ability.arcAngle", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, cutter.getArcAngle())); - - addLabel(new GuiNpcLabel(103, "ability.innerRadius", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, cutter.getInnerRadius())); - - y += 24; - - // Row 3: Knockback + Sweep Speed - addLabel(new GuiNpcLabel(104, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, cutter.getKnockback())); - - addLabel(new GuiNpcLabel(105, "ability.sweepSpeed", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, cutter.getSweepSpeed())); - - y += 24; - - // Row 4: Sweep Mode + Piercing - addLabel(new GuiNpcLabel(106, "ability.sweepMode", labelX, y + 5)); - String[] sweepModes = {"Swipe", "Spin"}; - GuiNpcButton sweepModeBtn = new GuiNpcButton(106, fieldX, y, 70, 20, sweepModes, cutter.getSweepModeEnum().ordinal()); - sweepModeBtn.setHoverText("ability.hover.sweepMode"); - addButton(sweepModeBtn); - - addLabel(new GuiNpcLabel(107, "ability.piercing", col2LabelX, y + 5)); - GuiNpcButton piercingBtn = new GuiNpcButton(107, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, cutter.isPiercing() ? 1 : 0); - piercingBtn.setHoverText("ability.hover.piercing"); - addButton(piercingBtn); - - y += 24; - - // Row 5: Spin Duration (only for SPIN mode) + Effects button - if (cutter.getSweepModeEnum() == AbilityCutter.SweepMode.SPIN) { - addLabel(new GuiNpcLabel(108, "ability.duration", labelX, y + 5)); - GuiNpcTextField durationField = createIntField(108, fieldX, y, 50, cutter.getSpinDurationTicks()); - durationField.setMinMaxDefault(1, 1000, 60); - addTextField(durationField); - } - - addButton(new GuiNpcButton(150, col2LabelX, y, 80, 20, "ability.effects")); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 106: - cutter.setSweepModeEnum(AbilityCutter.SweepMode.values()[value]); - initGui(); // Refresh to show/hide duration field - break; - case 107: - cutter.setPiercing(value == 1); - break; - case 150: - setSubGui(new SubGuiAbilityEffects(cutter.getEffects())); - break; - } - } - - @Override - public void subGuiClosed(SubGuiInterface subgui) { - super.subGuiClosed(subgui); - if (subgui instanceof SubGuiAbilityEffects) { - SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; - List result = effectsGui.getResult(); - if (result != null) { - cutter.setEffects(result); - } - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - cutter.setDamage(parseFloat(field, cutter.getDamage())); - break; - case 101: - cutter.setRange(parseFloat(field, cutter.getRange())); - break; - case 102: - cutter.setArcAngle(parseFloat(field, cutter.getArcAngle())); - break; - case 103: - cutter.setInnerRadius(parseFloat(field, cutter.getInnerRadius())); - break; - case 104: - cutter.setKnockback(parseFloat(field, cutter.getKnockback())); - break; - case 105: - cutter.setSweepSpeed(parseFloat(field, cutter.getSweepSpeed())); - break; - case 108: - cutter.setSpinDurationTicks(field.getInteger()); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java deleted file mode 100644 index 9d7bd63b6..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDash.java +++ /dev/null @@ -1,65 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityDash; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; - -/** - * GUI for configuring Dash ability type-specific settings. - */ -public class SubGuiAbilityDash extends SubGuiAbilityConfig { - - private final AbilityDash dash; - - public SubGuiAbilityDash(AbilityDash ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.dash = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Dash Mode - addLabel(new GuiNpcLabel(100, "ability.mode", labelX, y + 5)); - String[] dashModes = {"Aggressive", "Defensive"}; - GuiNpcButton modeBtn = new GuiNpcButton(100, fieldX, y, 80, 20, dashModes, dash.getDashModeEnum().ordinal()); - modeBtn.setHoverText("ability.hover.dashMode"); - addButton(modeBtn); - - y += 24; - - // Row 2: Dash Distance + Dash Speed - addLabel(new GuiNpcLabel(101, "ability.dashDist", labelX, y + 5)); - addTextField(createFloatField(101, fieldX, y, 50, dash.getDashDistance())); - - addLabel(new GuiNpcLabel(102, "ability.dashSpeed", col2LabelX, y + 5)); - addTextField(createFloatField(102, col2FieldX, y, 50, dash.getDashSpeed())); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - if (id == 100) { - dash.setDashModeEnum(AbilityDash.DashMode.values()[button.getValue()]); - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 101: - dash.setDashDistance(parseFloat(field, dash.getDashDistance())); - break; - case 102: - dash.setDashSpeed(parseFloat(field, dash.getDashSpeed())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java deleted file mode 100644 index 5faf662b0..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityDisc.java +++ /dev/null @@ -1,323 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.AbilityEffect; -import kamkeel.npcs.controllers.data.ability.AnchorPoint; -import kamkeel.npcs.controllers.data.ability.type.AbilityDisc; -import noppes.npcs.client.gui.SubGuiColorSelector; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.util.SubGuiInterface; - -import java.util.List; - -/** - * GUI for configuring Disc ability type-specific settings. - */ -public class SubGuiAbilityDisc extends SubGuiAbilityConfig { - - private final AbilityDisc disc; - private int editingVisualColorId = 0; - - public SubGuiAbilityDisc(AbilityDisc ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.disc = ability; - } - - @Override - protected boolean hasVisualSettings() { - return true; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, disc.getDamage())); - - addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, disc.getSpeed())); - - y += 24; - - // Row 2: Disc Radius + Disc Thickness - addLabel(new GuiNpcLabel(102, "ability.discRadius", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, disc.getDiscRadius())); - - addLabel(new GuiNpcLabel(103, "ability.discThickness", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, disc.getDiscThickness())); - - y += 24; - - // Row 3: Homing + Homing Strength (only show strength if homing) - addLabel(new GuiNpcLabel(104, "ability.homing", labelX, y + 5)); - GuiNpcButton homingBtn = new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isHoming() ? 1 : 0); - homingBtn.setHoverText("ability.hover.homing"); - addButton(homingBtn); - - if (disc.isHoming()) { - addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, disc.getHomingStrength())); - } - - y += 24; - - // Row 4: Boomerang + Boomerang Delay (only show delay if boomerang) - addLabel(new GuiNpcLabel(106, "ability.boomerang", labelX, y + 5)); - GuiNpcButton boomerangBtn = new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isBoomerang() ? 1 : 0); - boomerangBtn.setHoverText("ability.hover.boomerang"); - addButton(boomerangBtn); - - if (disc.isBoomerang()) { - addLabel(new GuiNpcLabel(107, "ability.boomerangDelay", col2LabelX, y + 5)); - addTextField(createIntField(107, col2FieldX, y, 50, disc.getBoomerangDelay())); - } - - y += 24; - - // Row 5: Explosive + Explosion Radius (only show radius if explosive) - addLabel(new GuiNpcLabel(108, "ability.explosive", labelX, y + 5)); - GuiNpcButton explosiveBtn = new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isExplosive() ? 1 : 0); - explosiveBtn.setHoverText("ability.hover.explosive"); - addButton(explosiveBtn); - - if (disc.isExplosive()) { - addLabel(new GuiNpcLabel(109, "ability.explosionRad", col2LabelX, y + 5)); - addTextField(createFloatField(109, col2FieldX, y, 50, disc.getExplosionRadius())); - } - - y += 24; - - // Row 6: Knockback + Max Distance - addLabel(new GuiNpcLabel(110, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(110, fieldX, y, 50, disc.getKnockback())); - - addLabel(new GuiNpcLabel(111, "ability.maxDist", col2LabelX, y + 5)); - addTextField(createFloatField(111, col2FieldX, y, 50, disc.getMaxDistance())); - - y += 24; - - // Row 7: Max Lifetime + Effects button - addLabel(new GuiNpcLabel(112, "ability.lifetime", labelX, y + 5)); - addTextField(createIntField(112, fieldX, y, 50, disc.getMaxLifetime())); - - addButton(new GuiNpcButton(150, col2LabelX, y, 80, 20, "ability.effects")); - } - - @Override - protected void initVisualTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 180; - int col2FieldX = guiLeft + 260; - - // Row 1: Anchor Point - addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); - GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), disc.getAnchorPointEnum().getId()); - anchorBtn.setHoverText("ability.hover.anchorPoint"); - addButton(anchorBtn); - - y += 24; - - // Row 2: Inner Color + Outer Color - addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); - String innerHex = String.format("%06X", disc.getInnerColor() & 0xFFFFFF); - GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); - innerColorBtn.setTextColor(disc.getInnerColor() & 0xFFFFFF); - addButton(innerColorBtn); - - addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); - String outerHex = String.format("%06X", disc.getOuterColor() & 0xFFFFFF); - GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); - outerColorBtn.setTextColor(disc.getOuterColor() & 0xFFFFFF); - outerColorBtn.setEnabled(disc.isOuterColorEnabled()); - addButton(outerColorBtn); - - y += 24; - - // Row 2: Outer Color Enabled + Outer Color Width - addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); - addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.isOuterColorEnabled() ? 1 : 0)); - - addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); - GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, disc.getOuterColorWidth()); - widthField.setEnabled(disc.isOuterColorEnabled()); - addTextField(widthField); - - y += 24; - - // Row 3: Outer Color Alpha - addLabel(new GuiNpcLabel(204, "ability.outerAlpha", labelX, y + 5)); - GuiNpcTextField alphaField = createFloatField(204, fieldX, y, 55, disc.getOuterColorAlpha()); - alphaField.setEnabled(disc.isOuterColorEnabled()); - alphaField.setMinMaxDefaultFloat(0, 1, 0.5f); - addTextField(alphaField); - - y += 24; - - // Row 4: Rotation Speed - addLabel(new GuiNpcLabel(205, "ability.rotationSpeed", labelX, y + 5)); - addTextField(createFloatField(205, fieldX, y, 50, disc.getRotationSpeed())); - - y += 24; - - // Row 5: Lightning Effect Enabled - addLabel(new GuiNpcLabel(206, "ability.lightning", labelX, y + 5)); - GuiNpcButton lightningBtn = new GuiNpcButton(206, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, disc.hasLightningEffect() ? 1 : 0); - lightningBtn.setHoverText("ability.hover.lightning"); - addButton(lightningBtn); - - // Only show Lightning settings if Lightning is enabled - if (disc.hasLightningEffect()) { - y += 24; - - // Row 6: Density + Radius - addLabel(new GuiNpcLabel(207, "ability.lightningDensity", labelX, y + 5)); - GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(disc.getLightningDensity())); - densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); - addTextField(densityField); - - addLabel(new GuiNpcLabel(208, "ability.lightningRadius", col2LabelX, y + 5)); - GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(disc.getLightningRadius())); - radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); - addTextField(radiusField); - } - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 104: - disc.setHoming(value == 1); - initGui(); - break; - case 106: - disc.setBoomerang(value == 1); - initGui(); - break; - case 108: - disc.setExplosive(value == 1); - initGui(); - break; - case 150: - setSubGui(new SubGuiAbilityEffects(disc.getEffects())); - break; - } - } - - @Override - protected void handleVisualButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 200: - editingVisualColorId = 200; - setSubGui(new SubGuiColorSelector(disc.getInnerColor())); - break; - case 201: - editingVisualColorId = 201; - setSubGui(new SubGuiColorSelector(disc.getOuterColor())); - break; - case 202: - disc.setOuterColorEnabled(value == 1); - initGui(); - break; - case 206: - disc.setLightningEffect(value == 1); - initGui(); - break; - case 210: - disc.setAnchorPointEnum(AnchorPoint.fromId(value)); - break; - } - } - - @Override - public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { - SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; - int rgb = colorSelector.color & 0x00FFFFFF; - if (editingVisualColorId == 200) { - disc.setInnerColor(rgb); - } else if (editingVisualColorId == 201) { - disc.setOuterColor(rgb); - } - editingVisualColorId = 0; - initGui(); - } else if (subgui instanceof SubGuiAbilityEffects) { - SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; - List result = effectsGui.getResult(); - if (result != null) { - disc.setEffects(result); - } - } else { - super.subGuiClosed(subgui); - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - disc.setDamage(parseFloat(field, disc.getDamage())); - break; - case 101: - disc.setSpeed(parseFloat(field, disc.getSpeed())); - break; - case 102: - disc.setDiscRadius(parseFloat(field, disc.getDiscRadius())); - break; - case 103: - disc.setDiscThickness(parseFloat(field, disc.getDiscThickness())); - break; - case 105: - disc.setHomingStrength(parseFloat(field, disc.getHomingStrength())); - break; - case 107: - disc.setBoomerangDelay(field.getInteger()); - break; - case 109: - disc.setExplosionRadius(parseFloat(field, disc.getExplosionRadius())); - break; - case 110: - disc.setKnockback(parseFloat(field, disc.getKnockback())); - break; - case 111: - disc.setMaxDistance(parseFloat(field, disc.getMaxDistance())); - break; - case 112: - disc.setMaxLifetime(field.getInteger()); - break; - } - } - - @Override - protected void handleVisualTextField(int id, GuiNpcTextField field) { - switch (id) { - case 203: - disc.setOuterColorWidth(parseFloat(field, disc.getOuterColorWidth())); - break; - case 204: - disc.setOuterColorAlpha(parseFloat(field, disc.getOuterColorAlpha())); - break; - case 205: - disc.setRotationSpeed(parseFloat(field, disc.getRotationSpeed())); - break; - case 207: - disc.setLightningDensity(parseFloat(field, disc.getLightningDensity())); - break; - case 208: - disc.setLightningRadius(parseFloat(field, disc.getLightningRadius())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java deleted file mode 100644 index 49ab42e5d..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityEnergyBeam.java +++ /dev/null @@ -1,312 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.AbilityEffect; -import kamkeel.npcs.controllers.data.ability.AnchorPoint; -import kamkeel.npcs.controllers.data.ability.type.AbilityEnergyBeam; -import noppes.npcs.client.gui.SubGuiColorSelector; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.util.SubGuiInterface; - -import java.util.List; - -/** - * GUI for configuring Energy Beam ability type-specific settings. - * Energy Beam is a homing head with trailing path (DBZ-style). - */ -public class SubGuiAbilityEnergyBeam extends SubGuiAbilityConfig { - - private final AbilityEnergyBeam beam; - private int editingVisualColorId = 0; - - public SubGuiAbilityEnergyBeam(AbilityEnergyBeam ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.beam = ability; - } - - @Override - protected boolean hasVisualSettings() { - return true; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, beam.getDamage())); - - addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, beam.getSpeed())); - - y += 24; - - // Row 2: Beam Width + Head Size - addLabel(new GuiNpcLabel(102, "ability.beamWidth", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, beam.getBeamWidth())); - - addLabel(new GuiNpcLabel(103, "ability.headSize", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, beam.getHeadSize())); - - y += 24; - - // Row 3: Homing + Homing Strength (only show strength if homing) - addLabel(new GuiNpcLabel(104, "ability.homing", labelX, y + 5)); - GuiNpcButton homingBtn = new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isHoming() ? 1 : 0); - homingBtn.setHoverText("ability.hover.homing"); - addButton(homingBtn); - - if (beam.isHoming()) { - addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, beam.getHomingStrength())); - } - - y += 24; - - // Row 4: Max Distance (+ Homing Range only if homing) - if (beam.isHoming()) { - addLabel(new GuiNpcLabel(106, "ability.homingRange", labelX, y + 5)); - addTextField(createFloatField(106, fieldX, y, 50, beam.getHomingRange())); - } - - addLabel(new GuiNpcLabel(107, "ability.maxDist", beam.isHoming() ? col2LabelX : labelX, y + 5)); - addTextField(createFloatField(107, beam.isHoming() ? col2FieldX : fieldX, y, 50, beam.getMaxDistance())); - - y += 24; - - // Row 5: Explosive + Explosion Radius (only show radius if explosive) - addLabel(new GuiNpcLabel(108, "ability.explosive", labelX, y + 5)); - GuiNpcButton explosiveBtn = new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isExplosive() ? 1 : 0); - explosiveBtn.setHoverText("ability.hover.explosive"); - addButton(explosiveBtn); - - if (beam.isExplosive()) { - addLabel(new GuiNpcLabel(109, "ability.explosionRad", col2LabelX, y + 5)); - addTextField(createFloatField(109, col2FieldX, y, 50, beam.getExplosionRadius())); - } - - y += 24; - - // Row 6: Knockback + Max Lifetime - addLabel(new GuiNpcLabel(110, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(110, fieldX, y, 50, beam.getKnockback())); - - addLabel(new GuiNpcLabel(111, "ability.lifetime", col2LabelX, y + 5)); - addTextField(createIntField(111, col2FieldX, y, 50, beam.getMaxLifetime())); - - y += 24; - - // Row 7: Effects button - addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); - } - - @Override - protected void initVisualTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 180; - int col2FieldX = guiLeft + 260; - - // Row 1: Anchor Point - addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); - GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), beam.getAnchorPointEnum().getId()); - anchorBtn.setHoverText("ability.hover.anchorPoint"); - addButton(anchorBtn); - - y += 24; - - // Row 2: Inner Color + Outer Color - addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); - String innerHex = String.format("%06X", beam.getInnerColor() & 0xFFFFFF); - GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); - innerColorBtn.setTextColor(beam.getInnerColor() & 0xFFFFFF); - addButton(innerColorBtn); - - addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); - String outerHex = String.format("%06X", beam.getOuterColor() & 0xFFFFFF); - GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); - outerColorBtn.setTextColor(beam.getOuterColor() & 0xFFFFFF); - outerColorBtn.setEnabled(beam.isOuterColorEnabled()); - addButton(outerColorBtn); - - y += 24; - - // Row 2: Outer Color Enabled + Outer Color Width - addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); - addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.isOuterColorEnabled() ? 1 : 0)); - - addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); - GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, beam.getOuterColorWidth()); - widthField.setEnabled(beam.isOuterColorEnabled()); - addTextField(widthField); - - y += 24; - - // Row 3: Rotation Speed - addLabel(new GuiNpcLabel(204, "ability.outerAlpha", labelX, y + 5)); - GuiNpcTextField alphaField = createFloatField(204, fieldX, y, 55, beam.getOuterColorAlpha()); - alphaField.setEnabled(beam.isOuterColorEnabled()); - alphaField.setMinMaxDefaultFloat(0, 1, 0.5f); - addTextField(alphaField); - - addLabel(new GuiNpcLabel(205, "ability.rotationSpeed", col2LabelX, y + 5)); - addTextField(createFloatField(205, col2FieldX, y, 50, beam.getRotationSpeed())); - - y += 24; - - // Row 4: Lightning Effect Enabled (only affects head) - addLabel(new GuiNpcLabel(206, "ability.lightning", labelX, y + 5)); - GuiNpcButton lightningBtn = new GuiNpcButton(206, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, beam.hasLightningEffect() ? 1 : 0); - lightningBtn.setHoverText("ability.hover.lightning"); - addButton(lightningBtn); - - // Only show Lightning settings if Lightning is enabled - if (beam.hasLightningEffect()) { - y += 24; - - // Row 5: Density + Radius - addLabel(new GuiNpcLabel(207, "ability.lightningDensity", labelX, y + 5)); - GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(beam.getLightningDensity())); - densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); - addTextField(densityField); - - addLabel(new GuiNpcLabel(208, "ability.lightningRadius", col2LabelX, y + 5)); - GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(beam.getLightningRadius())); - radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); - addTextField(radiusField); - } - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 104: - beam.setHoming(value == 1); - initGui(); - break; - case 108: - beam.setExplosive(value == 1); - initGui(); - break; - case 150: - setSubGui(new SubGuiAbilityEffects(beam.getEffects())); - break; - } - } - - @Override - protected void handleVisualButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 200: - editingVisualColorId = 200; - setSubGui(new SubGuiColorSelector(beam.getInnerColor())); - break; - case 201: - editingVisualColorId = 201; - setSubGui(new SubGuiColorSelector(beam.getOuterColor())); - break; - case 202: - beam.setOuterColorEnabled(value == 1); - initGui(); - break; - case 206: - beam.setLightningEffect(value == 1); - initGui(); - break; - case 210: - beam.setAnchorPointEnum(AnchorPoint.fromId(value)); - break; - } - } - - @Override - public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { - SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; - int rgb = colorSelector.color & 0x00FFFFFF; - if (editingVisualColorId == 200) { - beam.setInnerColor(rgb); - } else if (editingVisualColorId == 201) { - beam.setOuterColor(rgb); - } - editingVisualColorId = 0; - initGui(); - } else if (subgui instanceof SubGuiAbilityEffects) { - SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; - List result = effectsGui.getResult(); - if (result != null) { - beam.setEffects(result); - } - } else { - super.subGuiClosed(subgui); - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - beam.setDamage(parseFloat(field, beam.getDamage())); - break; - case 101: - beam.setSpeed(parseFloat(field, beam.getSpeed())); - break; - case 102: - beam.setBeamWidth(parseFloat(field, beam.getBeamWidth())); - break; - case 103: - beam.setHeadSize(parseFloat(field, beam.getHeadSize())); - break; - case 105: - beam.setHomingStrength(parseFloat(field, beam.getHomingStrength())); - break; - case 106: - beam.setHomingRange(parseFloat(field, beam.getHomingRange())); - break; - case 107: - beam.setMaxDistance(parseFloat(field, beam.getMaxDistance())); - break; - case 109: - beam.setExplosionRadius(parseFloat(field, beam.getExplosionRadius())); - break; - case 110: - beam.setKnockback(parseFloat(field, beam.getKnockback())); - break; - case 111: - beam.setMaxLifetime(field.getInteger()); - break; - } - } - - @Override - protected void handleVisualTextField(int id, GuiNpcTextField field) { - switch (id) { - case 203: - beam.setOuterColorWidth(parseFloat(field, beam.getOuterColorWidth())); - break; - case 204: - beam.setOuterColorAlpha(parseFloat(field, beam.getOuterColorAlpha())); - break; - case 205: - beam.setRotationSpeed(parseFloat(field, beam.getRotationSpeed())); - break; - case 207: - beam.setLightningDensity(parseFloat(field, beam.getLightningDensity())); - break; - case 208: - beam.setLightningRadius(parseFloat(field, beam.getLightningRadius())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java deleted file mode 100644 index 0d2ec667b..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityGuard.java +++ /dev/null @@ -1,143 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.AbilityEffect; -import kamkeel.npcs.controllers.data.ability.type.AbilityGuard; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.util.SubGuiInterface; - -import java.util.List; - -/** - * GUI for configuring Guard ability type-specific settings. - */ -public class SubGuiAbilityGuard extends SubGuiAbilityConfig { - - private final AbilityGuard guard; - - public SubGuiAbilityGuard(AbilityGuard ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.guard = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 105; - int col2LabelX = guiLeft + 165; - int col2FieldX = guiLeft + 225; - - // Row 1: Duration + Damage Reduction - addLabel(new GuiNpcLabel(99, "ability.duration", labelX, y + 5)); - GuiNpcTextField durationField = createIntField(99, fieldX, y, 50, guard.getDurationTicks()); - durationField.setMinMaxDefault(1, 1000, 60); - addTextField(durationField); - - addLabel(new GuiNpcLabel(100, "ability.dmgReduce", col2LabelX, y + 5)); - addTextField(createFloatField(100, col2FieldX, y, 50, guard.getDamageReduction())); - - y += 24; - - // Row 2: Can Counter + Counter Type - addLabel(new GuiNpcLabel(101, "ability.canCounter", labelX, y + 5)); - GuiNpcButton canCounterBtn = new GuiNpcButton(101, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, guard.isCanCounter() ? 1 : 0); - canCounterBtn.setHoverText("ability.hover.canCounter"); - addButton(canCounterBtn); - - addLabel(new GuiNpcLabel(102, "ability.counterType", col2LabelX, y + 5)); - String[] types = {"Flat", "Percent"}; - GuiNpcButton counterTypeButton = new GuiNpcButton(102, col2FieldX, y, 60, 20, types, guard.getCounterTypeEnum().ordinal()); - counterTypeButton.setHoverText("ability.hover.counterType"); - counterTypeButton.setEnabled(guard.isCanCounter()); - addButton(counterTypeButton); - - y += 24; - - // Row 3: Counter Value + Counter Chance - addLabel(new GuiNpcLabel(103, "ability.counterValue", labelX, y + 5)); - GuiNpcTextField counterValueField = createFloatField(103, fieldX, y, 50, guard.getCounterValue()); - counterValueField.setEnabled(guard.isCanCounter()); - addTextField(counterValueField); - - addLabel(new GuiNpcLabel(104, "ability.counterChance", col2LabelX, y + 5)); - GuiNpcTextField counterChanceField = createFloatField(104, col2FieldX, y, 50, guard.getCounterChance()); - counterChanceField.setEnabled(guard.isCanCounter()); - addTextField(counterChanceField); - - y += 24; - - // Row 4: Counter Sound + Counter Animation - addLabel(new GuiNpcLabel(105, "ability.counterSound", labelX, y + 5)); - GuiNpcTextField counterSoundField = new GuiNpcTextField(105, this, fontRendererObj, fieldX, y, 100, 20, guard.getCounterSound()); - counterSoundField.setEnabled(guard.isCanCounter()); - addTextField(counterSoundField); - - addLabel(new GuiNpcLabel(106, "ability.counterAnim", col2LabelX, y + 5)); - GuiNpcTextField counterAnimField = createIntField(106, col2FieldX, y, 50, guard.getCounterAnimationId()); - counterAnimField.setEnabled(guard.isCanCounter()); - addTextField(counterAnimField); - - y += 24; - - // Row 5: Effects button (applies on counter) - GuiNpcButton effectsBtn = new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects"); - effectsBtn.setEnabled(guard.isCanCounter()); - addButton(effectsBtn); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - switch (id) { - case 101: - guard.setCanCounter(button.getValue() == 1); - initGui(); - break; - case 102: - guard.setCounterTypeEnum(AbilityGuard.CounterType.values()[button.getValue()]); - break; - case 150: - setSubGui(new SubGuiAbilityEffects(guard.getEffects())); - break; - } - } - - @Override - public void subGuiClosed(SubGuiInterface subgui) { - super.subGuiClosed(subgui); - if (subgui instanceof SubGuiAbilityEffects) { - SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; - List result = effectsGui.getResult(); - if (result != null) { - guard.setEffects(result); - } - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 99: - guard.setDurationTicks(field.getInteger()); - break; - case 100: - guard.setDamageReduction(parseFloat(field, guard.getDamageReduction())); - break; - case 103: - guard.setCounterValue(parseFloat(field, guard.getCounterValue())); - break; - case 104: - guard.setCounterChance(parseFloat(field, guard.getCounterChance())); - break; - case 105: - guard.setCounterSound(field.getText()); - break; - case 106: - guard.setCounterAnimationId(field.getInteger()); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java deleted file mode 100644 index 5b8be7976..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHazard.java +++ /dev/null @@ -1,159 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.AbilityEffect; -import kamkeel.npcs.controllers.data.ability.type.AbilityHazard; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.util.SubGuiInterface; - -import java.util.List; - -/** - * GUI for configuring Hazard ability type-specific settings. - */ -public class SubGuiAbilityHazard extends SubGuiAbilityConfig { - - private final AbilityHazard hazard; - - public SubGuiAbilityHazard(AbilityHazard ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.hazard = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Duration + Shape - addLabel(new GuiNpcLabel(99, "ability.duration", labelX, y + 5)); - GuiNpcTextField durationField = createIntField(99, fieldX, y, 50, hazard.getDurationTicks()); - durationField.setMinMaxDefault(1, 2000, 100); - addTextField(durationField); - - addLabel(new GuiNpcLabel(100, "ability.shape", col2LabelX, y + 5)); - String[] shapes = {"Circle", "Ring", "Cone"}; - GuiNpcButton shapeBtn = new GuiNpcButton(100, col2FieldX, y, 55, 20, shapes, hazard.getShapeEnum().ordinal()); - shapeBtn.setHoverText("ability.hover.shape"); - addButton(shapeBtn); - - y += 24; - - // Row 2: Placement - addLabel(new GuiNpcLabel(101, "ability.placement", labelX, y + 5)); - String[] placements = {"Caster", "Target", "FollowCast", "FollowTgt"}; - GuiNpcButton placementBtn = new GuiNpcButton(101, fieldX, y, 70, 20, placements, hazard.getPlacementEnum().ordinal()); - placementBtn.setHoverText("ability.hover.placement"); - addButton(placementBtn); - - y += 24; - - // Row 2: Radius + Inner Radius - addLabel(new GuiNpcLabel(102, "ability.radius", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, hazard.getRadius())); - - addLabel(new GuiNpcLabel(103, "ability.innerRadius", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, hazard.getInnerRadius())); - - y += 24; - - // Row 3: Damage Per Second + Damage Interval - addLabel(new GuiNpcLabel(104, "ability.dmgPerSec", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, hazard.getDamagePerSecond())); - - addLabel(new GuiNpcLabel(105, "ability.dmgInterval", col2LabelX, y + 5)); - addTextField(createIntField(105, col2FieldX, y, 50, hazard.getDamageInterval())); - - y += 24; - - // Row 4: Slowness Level + Debuff Duration - addLabel(new GuiNpcLabel(106, "ability.slownessLvl", labelX, y + 5)); - addTextField(createIntField(106, fieldX, y, 50, hazard.getSlownessLevel())); - - addLabel(new GuiNpcLabel(107, "ability.debuffDur", col2LabelX, y + 5)); - addTextField(createIntField(107, col2FieldX, y, 50, hazard.getDebuffDuration())); - - y += 24; - - // Row 5: Poison Level + Affects Caster - addLabel(new GuiNpcLabel(108, "ability.poisonLvl", labelX, y + 5)); - addTextField(createIntField(108, fieldX, y, 50, hazard.getPoisonLevel())); - - addLabel(new GuiNpcLabel(109, "ability.affectsCaster", col2LabelX, y + 5)); - GuiNpcButton affectsCasterBtn = new GuiNpcButton(109, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, hazard.isAffectsCaster() ? 1 : 0); - affectsCasterBtn.setHoverText("ability.hover.affectsCaster"); - addButton(affectsCasterBtn); - - y += 24; - - // Row 6: Effects button - addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 100: - hazard.setShapeEnum(AbilityHazard.HazardShape.values()[value]); - break; - case 101: - hazard.setPlacementEnum(AbilityHazard.PlacementMode.values()[value]); - break; - case 109: - hazard.setAffectsCaster(value == 1); - break; - case 150: - setSubGui(new SubGuiAbilityEffects(hazard.getEffects())); - break; - } - } - - @Override - public void subGuiClosed(SubGuiInterface subgui) { - super.subGuiClosed(subgui); - if (subgui instanceof SubGuiAbilityEffects) { - SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; - List result = effectsGui.getResult(); - if (result != null) { - hazard.setEffects(result); - } - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 99: - hazard.setDurationTicks(field.getInteger()); - break; - case 102: - hazard.setRadius(parseFloat(field, hazard.getRadius())); - break; - case 103: - hazard.setInnerRadius(parseFloat(field, hazard.getInnerRadius())); - break; - case 104: - hazard.setDamagePerSecond(parseFloat(field, hazard.getDamagePerSecond())); - break; - case 105: - hazard.setDamageInterval(field.getInteger()); - break; - case 106: - hazard.setSlownessLevel(field.getInteger()); - break; - case 107: - hazard.setDebuffDuration(field.getInteger()); - break; - case 108: - hazard.setPoisonLevel(field.getInteger()); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java deleted file mode 100644 index 490b4773a..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeal.java +++ /dev/null @@ -1,106 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.type.AbilityHeal; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; - -/** - * GUI for configuring Heal ability type-specific settings. - */ -public class SubGuiAbilityHeal extends SubGuiAbilityConfig { - - private final AbilityHeal heal; - - public SubGuiAbilityHeal(AbilityHeal ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.heal = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Instant Heal + Duration (only shown if not instant) - addLabel(new GuiNpcLabel(103, "dialog.instant", labelX, y + 5)); - GuiNpcButton instantBtn = new GuiNpcButton(103, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isInstantHeal() ? 1 : 0); - instantBtn.setHoverText("ability.hover.instant"); - addButton(instantBtn); - - if (!heal.isInstantHeal()) { - addLabel(new GuiNpcLabel(99, "ability.duration", col2LabelX, y + 5)); - GuiNpcTextField durationField = createIntField(99, col2FieldX, y, 50, heal.getDurationTicks()); - durationField.setMinMaxDefault(1, 1000, 60); - addTextField(durationField); - } - - y += 24; - - // Row 2: Heal Amount + Heal Percent - addLabel(new GuiNpcLabel(100, "ability.healAmount", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, heal.getHealAmount())); - - addLabel(new GuiNpcLabel(101, "ability.healPercent", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, heal.getHealPercent())); - - y += 24; - - // Row 3: Heal Radius - addLabel(new GuiNpcLabel(102, "ability.healRadius", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, heal.getHealRadius())); - - y += 24; - - // Row 4: Heal Self + Heal Allies - addLabel(new GuiNpcLabel(104, "ability.healSelf", labelX, y + 5)); - GuiNpcButton healSelfBtn = new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isHealSelf() ? 1 : 0); - healSelfBtn.setHoverText("ability.hover.healSelf"); - addButton(healSelfBtn); - - addLabel(new GuiNpcLabel(105, "ability.healAllies", col2LabelX, y + 5)); - GuiNpcButton healAlliesBtn = new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, heal.isHealAllies() ? 1 : 0); - healAlliesBtn.setHoverText("ability.hover.healAllies"); - addButton(healAlliesBtn); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 103: - heal.setInstantHeal(value == 1); - initGui(); // Refresh to show/hide duration field - break; - case 104: - heal.setHealSelf(value == 1); - break; - case 105: - heal.setHealAllies(value == 1); - break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 99: - heal.setDurationTicks(field.getInteger()); - break; - case 100: - heal.setHealAmount(parseFloat(field, heal.getHealAmount())); - break; - case 101: - heal.setHealPercent(parseFloat(field, heal.getHealPercent())); - break; - case 102: - heal.setHealRadius(parseFloat(field, heal.getHealRadius())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java deleted file mode 100644 index 897a4af1f..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityHeavyHit.java +++ /dev/null @@ -1,76 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.AbilityEffect; -import kamkeel.npcs.controllers.data.ability.type.AbilityHeavyHit; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.util.SubGuiInterface; - -import java.util.List; - -/** - * GUI for configuring Heavy Hit ability type-specific settings. - */ -public class SubGuiAbilityHeavyHit extends SubGuiAbilityConfig { - - private final AbilityHeavyHit heavyHit; - - public SubGuiAbilityHeavyHit(AbilityHeavyHit ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.heavyHit = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - - // Row 1: Damage + Knockback - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, heavyHit.getDamage())); - - addLabel(new GuiNpcLabel(102, "ability.knockback", col2LabelX, y + 5)); - addTextField(createFloatField(102, guiLeft + 205, y, 50, heavyHit.getKnockback())); - - y += 24; - - // Row 2: Effects button - addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - if (id == 150) { - setSubGui(new SubGuiAbilityEffects(heavyHit.getEffects())); - } - } - - @Override - public void subGuiClosed(SubGuiInterface subgui) { - super.subGuiClosed(subgui); - if (subgui instanceof SubGuiAbilityEffects) { - SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; - List result = effectsGui.getResult(); - if (result != null) { - heavyHit.setEffects(result); - } - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - heavyHit.setDamage(parseFloat(field, heavyHit.getDamage())); - break; - case 102: - heavyHit.setKnockback(parseFloat(field, heavyHit.getKnockback())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java deleted file mode 100644 index e5184a3bb..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityLaserShot.java +++ /dev/null @@ -1,268 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.AbilityEffect; -import kamkeel.npcs.controllers.data.ability.type.AbilityLaserShot; -import noppes.npcs.client.gui.SubGuiColorSelector; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.util.SubGuiInterface; - -import java.util.List; - -/** - * GUI for configuring Laser Shot ability type-specific settings. - */ -public class SubGuiAbilityLaserShot extends SubGuiAbilityConfig { - - private final AbilityLaserShot laser; - private int editingVisualColorId = 0; - - public SubGuiAbilityLaserShot(AbilityLaserShot ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.laser = ability; - } - - @Override - protected boolean hasVisualSettings() { - return true; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Laser Width - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, laser.getDamage())); - - addLabel(new GuiNpcLabel(101, "ability.laserWidth", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, laser.getLaserWidth())); - - y += 24; - - // Row 2: Expansion Speed + Linger Ticks - addLabel(new GuiNpcLabel(102, "ability.expansionSpeed", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, laser.getExpansionSpeed())); - - addLabel(new GuiNpcLabel(103, "ability.lingerTicks", col2LabelX, y + 5)); - addTextField(createIntField(103, col2FieldX, y, 50, laser.getLingerTicks())); - - y += 24; - - // Row 3: Max Distance + Max Lifetime - addLabel(new GuiNpcLabel(104, "ability.maxDist", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, laser.getMaxDistance())); - - addLabel(new GuiNpcLabel(105, "ability.lifetime", col2LabelX, y + 5)); - addTextField(createIntField(105, col2FieldX, y, 50, laser.getMaxLifetime())); - - y += 24; - - // Row 4: Explosive + Explosion Radius (only show radius if explosive) - addLabel(new GuiNpcLabel(106, "ability.explosive", labelX, y + 5)); - GuiNpcButton explosiveBtn = new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, laser.isExplosive() ? 1 : 0); - explosiveBtn.setHoverText("ability.hover.explosive"); - addButton(explosiveBtn); - - if (laser.isExplosive()) { - addLabel(new GuiNpcLabel(107, "ability.explosionRad", col2LabelX, y + 5)); - addTextField(createFloatField(107, col2FieldX, y, 50, laser.getExplosionRadius())); - } - - y += 24; - - // Row 5: Knockback + Knockback Up - addLabel(new GuiNpcLabel(108, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(108, fieldX, y, 50, laser.getKnockback())); - - addLabel(new GuiNpcLabel(109, "ability.knockbackUp", col2LabelX, y + 5)); - addTextField(createFloatField(109, col2FieldX, y, 50, laser.getKnockbackUp())); - - y += 24; - - // Row 6: Effects button - addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); - } - - @Override - protected void initVisualTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 180; - int col2FieldX = guiLeft + 260; - - // Row 1: Inner Color + Outer Color - addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); - String innerHex = String.format("%06X", laser.getInnerColor() & 0xFFFFFF); - GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); - innerColorBtn.setTextColor(laser.getInnerColor() & 0xFFFFFF); - addButton(innerColorBtn); - - addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); - String outerHex = String.format("%06X", laser.getOuterColor() & 0xFFFFFF); - GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); - outerColorBtn.setTextColor(laser.getOuterColor() & 0xFFFFFF); - outerColorBtn.setEnabled(laser.isOuterColorEnabled()); - addButton(outerColorBtn); - - y += 24; - - // Row 2: Outer Color Enabled + Outer Color Width - addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); - addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, laser.isOuterColorEnabled() ? 1 : 0)); - - addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); - GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, laser.getOuterColorWidth()); - widthField.setEnabled(laser.isOuterColorEnabled()); - addTextField(widthField); - - y += 24; - - // Row 3: Lightning Effect Enabled - addLabel(new GuiNpcLabel(204, "ability.outerAlpha", labelX, y + 5)); - GuiNpcTextField alphaField = createFloatField(204, fieldX, y, 55, laser.getOuterColorAlpha()); - alphaField.setEnabled(laser.isOuterColorEnabled()); - alphaField.setMinMaxDefaultFloat(0, 1, 0.5f); - addTextField(alphaField); - - addLabel(new GuiNpcLabel(205, "ability.lightning", col2LabelX, y + 5)); - GuiNpcButton lightningBtn = new GuiNpcButton(205, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, laser.hasLightningEffect() ? 1 : 0); - lightningBtn.setHoverText("ability.hover.lightning"); - addButton(lightningBtn); - - // Only show Lightning settings if Lightning is enabled - if (laser.hasLightningEffect()) { - y += 24; - - // Row 4: Density + Radius - addLabel(new GuiNpcLabel(206, "ability.lightningDensity", labelX, y + 5)); - GuiNpcTextField densityField = new GuiNpcTextField(206, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(laser.getLightningDensity())); - densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); - addTextField(densityField); - - addLabel(new GuiNpcLabel(207, "ability.lightningRadius", col2LabelX, y + 5)); - GuiNpcTextField radiusField = new GuiNpcTextField(207, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(laser.getLightningRadius())); - radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); - addTextField(radiusField); - } - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 106: - laser.setExplosive(value == 1); - initGui(); - break; - case 150: - setSubGui(new SubGuiAbilityEffects(laser.getEffects())); - break; - } - } - - @Override - protected void handleVisualButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 200: - editingVisualColorId = 200; - setSubGui(new SubGuiColorSelector(laser.getInnerColor())); - break; - case 201: - editingVisualColorId = 201; - setSubGui(new SubGuiColorSelector(laser.getOuterColor())); - break; - case 202: - laser.setOuterColorEnabled(value == 1); - initGui(); - break; - case 205: - laser.setLightningEffect(value == 1); - initGui(); - break; - } - } - - @Override - public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { - SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; - int rgb = colorSelector.color & 0x00FFFFFF; - if (editingVisualColorId == 200) { - laser.setInnerColor(rgb); - } else if (editingVisualColorId == 201) { - laser.setOuterColor(rgb); - } - editingVisualColorId = 0; - initGui(); - } else if (subgui instanceof SubGuiAbilityEffects) { - SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; - List result = effectsGui.getResult(); - if (result != null) { - laser.setEffects(result); - } - } else { - super.subGuiClosed(subgui); - } - } - - @Override - protected void handleVisualTextField(int id, GuiNpcTextField field) { - switch (id) { - case 203: - laser.setOuterColorWidth(parseFloat(field, laser.getOuterColorWidth())); - break; - case 204: - laser.setOuterColorAlpha(parseFloat(field, laser.getOuterColorAlpha())); - break; - case 206: - laser.setLightningDensity(parseFloat(field, laser.getLightningDensity())); - break; - case 207: - laser.setLightningRadius(parseFloat(field, laser.getLightningRadius())); - break; - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - laser.setDamage(parseFloat(field, laser.getDamage())); - break; - case 101: - laser.setLaserWidth(parseFloat(field, laser.getLaserWidth())); - break; - case 102: - laser.setExpansionSpeed(parseFloat(field, laser.getExpansionSpeed())); - break; - case 103: - laser.setLingerTicks(field.getInteger()); - break; - case 104: - laser.setMaxDistance(parseFloat(field, laser.getMaxDistance())); - break; - case 105: - laser.setMaxLifetime(field.getInteger()); - break; - case 107: - laser.setExplosionRadius(parseFloat(field, laser.getExplosionRadius())); - break; - case 108: - laser.setKnockback(parseFloat(field, laser.getKnockback())); - break; - case 109: - laser.setKnockbackUp(parseFloat(field, laser.getKnockbackUp())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java deleted file mode 100644 index 7cc0f9f2f..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityOrb.java +++ /dev/null @@ -1,294 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.AbilityEffect; -import kamkeel.npcs.controllers.data.ability.AnchorPoint; -import kamkeel.npcs.controllers.data.ability.type.AbilityOrb; -import noppes.npcs.client.gui.SubGuiColorSelector; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.util.SubGuiInterface; - -import java.util.List; - -/** - * GUI for configuring Orb ability type-specific settings. - */ -public class SubGuiAbilityOrb extends SubGuiAbilityConfig { - - private final AbilityOrb orb; - private int editingVisualColorId = 0; - - public SubGuiAbilityOrb(AbilityOrb ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.orb = ability; - } - - @Override - protected boolean hasVisualSettings() { - return true; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, orb.getDamage())); - - addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, orb.getOrbSpeed())); - - y += 24; - - // Row 2: Orb Size + Max Distance - addLabel(new GuiNpcLabel(102, "stats.size", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, orb.getOrbSize())); - - addLabel(new GuiNpcLabel(103, "ability.maxDist", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, orb.getMaxDistance())); - - y += 24; - - // Row 3: Homing + Homing Strength (only show strength if homing) - addLabel(new GuiNpcLabel(104, "ability.homing", labelX, y + 5)); - GuiNpcButton homingBtn = new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isHoming() ? 1 : 0); - homingBtn.setHoverText("ability.hover.homing"); - addButton(homingBtn); - - if (orb.isHoming()) { - addLabel(new GuiNpcLabel(105, "ability.homingStr", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, orb.getHomingStrength())); - } - - y += 24; - - // Row 4: Explosive + Explosion Radius (only show radius if explosive) - addLabel(new GuiNpcLabel(106, "ability.explosive", labelX, y + 5)); - GuiNpcButton explosiveBtn = new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isExplosive() ? 1 : 0); - explosiveBtn.setHoverText("ability.hover.explosive"); - addButton(explosiveBtn); - - if (orb.isExplosive()) { - addLabel(new GuiNpcLabel(107, "ability.explosionRad", col2LabelX, y + 5)); - addTextField(createFloatField(107, col2FieldX, y, 50, orb.getExplosionRadius())); - } - - y += 24; - - // Row 5: Knockback + Max Lifetime - addLabel(new GuiNpcLabel(108, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(108, fieldX, y, 50, orb.getKnockback())); - - addLabel(new GuiNpcLabel(109, "ability.lifetime", col2LabelX, y + 5)); - addTextField(createIntField(109, col2FieldX, y, 50, orb.getMaxLifetime())); - - y += 24; - - // Row 6: Effects button - addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); - } - - @Override - protected void initVisualTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 180; - int col2FieldX = guiLeft + 260; - - // Row 1: Anchor Point - addLabel(new GuiNpcLabel(210, "ability.anchorPoint", labelX, y + 5)); - GuiNpcButton anchorBtn = new GuiNpcButton(210, fieldX, y, 80, 20, AnchorPoint.getDisplayNames(), orb.getAnchorPointEnum().getId()); - anchorBtn.setHoverText("ability.hover.anchorPoint"); - addButton(anchorBtn); - - y += 24; - - // Row 2: Inner Color + Rotation Speed - addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); - String innerHex = String.format("%06X", orb.getInnerColor() & 0xFFFFFF); - GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); - innerColorBtn.setTextColor(orb.getInnerColor() & 0xFFFFFF); - addButton(innerColorBtn); - - addLabel(new GuiNpcLabel(205, "ability.rotationSpeed", col2LabelX, y + 5)); - addTextField(createFloatField(205, col2FieldX, y, 55, orb.getRotationSpeed())); - - y += 24; - - // Row 3: Outer Glow Enabled - addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); - addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.isOuterColorEnabled() ? 1 : 0)); - - // Only show Outer Color and Width if Outer Glow is enabled - if (orb.isOuterColorEnabled()) { - addLabel(new GuiNpcLabel(204, "ability.outerAlpha", col2LabelX, y + 5)); - GuiNpcTextField alphaField = createFloatField(204, col2FieldX, y, 55, orb.getOuterColorAlpha()); - alphaField.setEnabled(orb.isOuterColorEnabled()); - alphaField.setMinMaxDefaultFloat(0, 1, 0.5f); - addTextField(alphaField); - - y += 24; - - // Row 4: Outer Color + Outer Width - addLabel(new GuiNpcLabel(201, "ability.outerColor", labelX, y + 5)); - String outerHex = String.format("%06X", orb.getOuterColor() & 0xFFFFFF); - GuiNpcButton outerColorBtn = new GuiNpcButton(201, fieldX, y, 55, 20, outerHex); - outerColorBtn.setTextColor(orb.getOuterColor() & 0xFFFFFF); - addButton(outerColorBtn); - - addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); - addTextField(createFloatField(203, col2FieldX, y, 55, orb.getOuterColorWidth())); - } - - y += 24; - - // Row 5: Lightning Effect Enabled - addLabel(new GuiNpcLabel(206, "ability.lightning", labelX, y + 5)); - GuiNpcButton lightningBtn = new GuiNpcButton(206, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, orb.hasLightningEffect() ? 1 : 0); - lightningBtn.setHoverText("ability.hover.lightning"); - addButton(lightningBtn); - - // Only show Lightning settings if Lightning is enabled - if (orb.hasLightningEffect()) { - y += 24; - - // Row 61: Density + Radius - addLabel(new GuiNpcLabel(207, "ability.lightningDensity", labelX, y + 5)); - GuiNpcTextField densityField = new GuiNpcTextField(207, this, fontRendererObj, fieldX, y, 55, 18, String.valueOf(orb.getLightningDensity())); - densityField.setMinMaxDefaultFloat(0.01f, 5.0f, 0.15f); - addTextField(densityField); - - addLabel(new GuiNpcLabel(208, "ability.lightningRadius", col2LabelX, y + 5)); - GuiNpcTextField radiusField = new GuiNpcTextField(208, this, fontRendererObj, col2FieldX, y, 55, 18, String.valueOf(orb.getLightningRadius())); - radiusField.setMinMaxDefaultFloat(0.1f, 10.0f, 0.5f); - addTextField(radiusField); - } - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 104: - orb.setHoming(value == 1); - initGui(); - break; - case 106: - orb.setExplosive(value == 1); - initGui(); - break; - case 150: - setSubGui(new SubGuiAbilityEffects(orb.getEffects())); - break; - } - } - - @Override - protected void handleVisualButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 200: - editingVisualColorId = 200; - setSubGui(new SubGuiColorSelector(orb.getInnerColor())); - break; - case 201: - editingVisualColorId = 201; - setSubGui(new SubGuiColorSelector(orb.getOuterColor())); - break; - case 202: - orb.setOuterColorEnabled(value == 1); - initGui(); - break; - case 206: - orb.setLightningEffect(value == 1); - initGui(); - break; - case 210: - orb.setAnchorPointEnum(AnchorPoint.fromId(value)); - break; - } - } - - @Override - public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { - SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; - int rgb = colorSelector.color & 0x00FFFFFF; - if (editingVisualColorId == 200) { - orb.setInnerColor(rgb); - } else if (editingVisualColorId == 201) { - orb.setOuterColor(rgb); - } - editingVisualColorId = 0; - initGui(); - } else if (subgui instanceof SubGuiAbilityEffects) { - SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; - List result = effectsGui.getResult(); - if (result != null) { - orb.setEffects(result); - } - } else { - super.subGuiClosed(subgui); - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - orb.setDamage(parseFloat(field, orb.getDamage())); - break; - case 101: - orb.setOrbSpeed(parseFloat(field, orb.getOrbSpeed())); - break; - case 102: - orb.setOrbSize(parseFloat(field, orb.getOrbSize())); - break; - case 103: - orb.setMaxDistance(parseFloat(field, orb.getMaxDistance())); - break; - case 105: - orb.setHomingStrength(parseFloat(field, orb.getHomingStrength())); - break; - case 107: - orb.setExplosionRadius(parseFloat(field, orb.getExplosionRadius())); - break; - case 108: - orb.setKnockback(parseFloat(field, orb.getKnockback())); - break; - case 109: - orb.setMaxLifetime(field.getInteger()); - break; - } - } - - @Override - protected void handleVisualTextField(int id, GuiNpcTextField field) { - switch (id) { - case 203: - orb.setOuterColorWidth(parseFloat(field, orb.getOuterColorWidth())); - break; - case 204: - orb.setOuterColorAlpha(parseFloat(field, orb.getOuterColorAlpha())); - break; - case 205: - orb.setRotationSpeed(parseFloat(field, orb.getRotationSpeed())); - break; - case 207: - orb.setLightningDensity(parseFloat(field, orb.getLightningDensity())); - break; - case 208: - orb.setLightningRadius(parseFloat(field, orb.getLightningRadius())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java deleted file mode 100644 index fe66fa242..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityProjectile.java +++ /dev/null @@ -1,145 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.AbilityEffect; -import kamkeel.npcs.controllers.data.ability.type.AbilityProjectile; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.util.SubGuiInterface; - -import java.util.List; - -/** - * GUI for configuring Projectile ability type-specific settings. - */ -public class SubGuiAbilityProjectile extends SubGuiAbilityConfig { - - private final AbilityProjectile projectile; - - public SubGuiAbilityProjectile(AbilityProjectile ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.projectile = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Speed - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, projectile.getDamage())); - - addLabel(new GuiNpcLabel(101, "stats.speed", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, projectile.getSpeed())); - - y += 24; - - // Row 2: Knockback + Projectile Type - addLabel(new GuiNpcLabel(102, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, projectile.getKnockback())); - - addLabel(new GuiNpcLabel(103, "ability.projType", col2LabelX, y + 5)); - String[] projTypes = {"fireball", "arrow", "magic"}; - int typeIdx = 0; - String currType = projectile.getProjectileType(); - for (int i = 0; i < projTypes.length; i++) { - if (projTypes[i].equals(currType)) { - typeIdx = i; - break; - } - } - addButton(new GuiNpcButton(103, col2FieldX, y, 60, 20, projTypes, typeIdx)); - - y += 24; - - // Row 3: Explosive + Explosion Radius (only show radius if explosive) - addLabel(new GuiNpcLabel(104, "ability.explosive", labelX, y + 5)); - GuiNpcButton explosiveBtn = new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, projectile.isExplosive() ? 1 : 0); - explosiveBtn.setHoverText("ability.hover.explosive"); - addButton(explosiveBtn); - - if (projectile.isExplosive()) { - addLabel(new GuiNpcLabel(105, "ability.explosionRad", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, projectile.getExplosionRadius())); - } - - y += 24; - - // Row 4: Homing + Homing Strength (only show strength if homing) - addLabel(new GuiNpcLabel(106, "ability.homing", labelX, y + 5)); - GuiNpcButton homingBtn = new GuiNpcButton(106, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, projectile.isHoming() ? 1 : 0); - homingBtn.setHoverText("ability.hover.homing"); - addButton(homingBtn); - - if (projectile.isHoming()) { - addLabel(new GuiNpcLabel(107, "ability.homingStr", col2LabelX, y + 5)); - addTextField(createFloatField(107, col2FieldX, y, 50, projectile.getHomingStrength())); - } - - y += 24; - - // Row 5: Effects button - addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 103: - String[] projTypes = {"fireball", "arrow", "magic"}; - projectile.setProjectileType(projTypes[value]); - break; - case 104: - projectile.setExplosive(value == 1); - initGui(); - break; - case 106: - projectile.setHoming(value == 1); - initGui(); - break; - case 150: - setSubGui(new SubGuiAbilityEffects(projectile.getEffects())); - break; - } - } - - @Override - public void subGuiClosed(SubGuiInterface subgui) { - super.subGuiClosed(subgui); - if (subgui instanceof SubGuiAbilityEffects) { - SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; - List result = effectsGui.getResult(); - if (result != null) { - projectile.setEffects(result); - } - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - projectile.setDamage(parseFloat(field, projectile.getDamage())); - break; - case 101: - projectile.setSpeed(parseFloat(field, projectile.getSpeed())); - break; - case 102: - projectile.setKnockback(parseFloat(field, projectile.getKnockback())); - break; - case 105: - projectile.setExplosionRadius(parseFloat(field, projectile.getExplosionRadius())); - break; - case 107: - projectile.setHomingStrength(parseFloat(field, projectile.getHomingStrength())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java deleted file mode 100644 index 48c2c5295..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityShockwave.java +++ /dev/null @@ -1,95 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.AbilityEffect; -import kamkeel.npcs.controllers.data.ability.type.AbilityShockwave; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.util.SubGuiInterface; - -import java.util.List; - -/** - * GUI for configuring Shockwave ability type-specific settings. - */ -public class SubGuiAbilityShockwave extends SubGuiAbilityConfig { - - private final AbilityShockwave shockwave; - - public SubGuiAbilityShockwave(AbilityShockwave ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.shockwave = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Push Radius - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, shockwave.getDamage())); - - addLabel(new GuiNpcLabel(101, "ability.pushRadius", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, shockwave.getPushRadius())); - - y += 24; - - // Row 2: Push Strength - addLabel(new GuiNpcLabel(102, "ability.pushStrength", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, shockwave.getPushStrength())); - - y += 24; - - // Row 3: Max Targets + Effects button - addLabel(new GuiNpcLabel(104, "ability.maxTargets", labelX, y + 5)); - addTextField(createIntField(104, fieldX, y, 50, shockwave.getMaxTargets())); - - addButton(new GuiNpcButton(150, col2LabelX, y, 80, 20, "ability.effects")); - - y += 24; - addLabel(new GuiNpcLabel(105, "ability.shockwaveFalloff", labelX, y + 5)); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - if (id == 150) { - setSubGui(new SubGuiAbilityEffects(shockwave.getEffects())); - } - } - - @Override - public void subGuiClosed(SubGuiInterface subgui) { - super.subGuiClosed(subgui); - if (subgui instanceof SubGuiAbilityEffects) { - SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; - List result = effectsGui.getResult(); - if (result != null) { - shockwave.setEffects(result); - } - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - shockwave.setDamage(parseFloat(field, shockwave.getDamage())); - break; - case 101: - shockwave.setPushRadius(parseFloat(field, shockwave.getPushRadius())); - break; - case 102: - shockwave.setPushStrength(parseFloat(field, shockwave.getPushStrength())); - break; - case 104: - shockwave.setMaxTargets(field.getInteger()); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java deleted file mode 100644 index 4e020829b..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySlam.java +++ /dev/null @@ -1,98 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.AbilityEffect; -import kamkeel.npcs.controllers.data.ability.type.AbilitySlam; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.util.SubGuiInterface; - -import java.util.List; - -/** - * GUI for configuring Slam ability type-specific settings. - */ -public class SubGuiAbilitySlam extends SubGuiAbilityConfig { - - private final AbilitySlam slam; - - public SubGuiAbilitySlam(AbilitySlam ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.slam = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Radius - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, slam.getDamage())); - - addLabel(new GuiNpcLabel(101, "ability.radius", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, slam.getRadius())); - - y += 24; - - // Row 2: Knockback + Leap Speed - addLabel(new GuiNpcLabel(102, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, slam.getKnockbackStrength())); - - addLabel(new GuiNpcLabel(103, "ability.leapSpeed", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, slam.getLeapSpeed())); - - y += 24; - - // Row 3: Leap Height + Effects - addLabel(new GuiNpcLabel(104, "ability.leapHeight", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, slam.getLeapHeight())); - - addButton(new GuiNpcButton(150, col2LabelX, y, 80, 20, "ability.effects")); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - if (id == 150) { - setSubGui(new SubGuiAbilityEffects(slam.getEffects())); - } - } - - @Override - public void subGuiClosed(SubGuiInterface subgui) { - super.subGuiClosed(subgui); - if (subgui instanceof SubGuiAbilityEffects) { - SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; - List result = effectsGui.getResult(); - if (result != null) { - slam.setEffects(result); - } - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - slam.setDamage(parseFloat(field, slam.getDamage())); - break; - case 101: - slam.setRadius(parseFloat(field, slam.getRadius())); - break; - case 102: - slam.setKnockbackStrength(parseFloat(field, slam.getKnockbackStrength())); - break; - case 103: - slam.setLeapSpeed(parseFloat(field, slam.getLeapSpeed())); - break; - case 104: - slam.setLeapHeight(parseFloat(field, slam.getLeapHeight())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java deleted file mode 100644 index 1561b408a..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilitySweeper.java +++ /dev/null @@ -1,219 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.AbilityEffect; -import kamkeel.npcs.controllers.data.ability.type.AbilitySweeper; -import noppes.npcs.client.gui.SubGuiColorSelector; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.util.SubGuiInterface; - -import java.util.List; - -/** - * GUI for configuring Sweeper ability type-specific settings. - * Sweeper is a low rotating beam that players can jump over. - */ -public class SubGuiAbilitySweeper extends SubGuiAbilityConfig { - - private final AbilitySweeper sweeper; - private int editingVisualColorId = 0; - - public SubGuiAbilitySweeper(AbilitySweeper ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.sweeper = ability; - } - - @Override - protected boolean hasVisualSettings() { - return true; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Damage + Damage Interval - addLabel(new GuiNpcLabel(100, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, sweeper.getDamage())); - - addLabel(new GuiNpcLabel(101, "ability.dmgInterval", col2LabelX, y + 5)); - addTextField(createIntField(101, col2FieldX, y, 50, sweeper.getDamageInterval())); - - y += 24; - - // Row 2: Beam Length + Beam Width - addLabel(new GuiNpcLabel(102, "ability.beamLength", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, sweeper.getBeamLength())); - - addLabel(new GuiNpcLabel(103, "ability.beamWidth", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, sweeper.getBeamWidth())); - - y += 24; - - // Row 3: Beam Height + Sweep Speed - addLabel(new GuiNpcLabel(104, "ability.beamHeight", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, sweeper.getBeamHeight())); - - addLabel(new GuiNpcLabel(105, "ability.sweepSpeed", col2LabelX, y + 5)); - addTextField(createFloatField(105, col2FieldX, y, 50, sweeper.getSweepSpeed())); - - y += 24; - - // Row 4: Number of Rotations + Piercing - addLabel(new GuiNpcLabel(106, "ability.rotations", labelX, y + 5)); - addTextField(createIntField(106, fieldX, y, 50, sweeper.getNumberOfRotations())); - - addLabel(new GuiNpcLabel(107, "ability.piercing", col2LabelX, y + 5)); - GuiNpcButton piercingBtn = new GuiNpcButton(107, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, sweeper.isPiercing() ? 1 : 0); - piercingBtn.setHoverText("ability.hover.piercing"); - addButton(piercingBtn); - - y += 24; - - // Row 5: Lock On Target - addLabel(new GuiNpcLabel(108, "ability.lockTarget", labelX, y + 5)); - GuiNpcButton lockTargetBtn = new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, sweeper.isLockOnTarget() ? 1 : 0); - lockTargetBtn.setHoverText("ability.hover.lockTarget"); - addButton(lockTargetBtn); - - y += 24; - - // Row 6: Effects button - addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); - } - - @Override - protected void initVisualTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 180; - int col2FieldX = guiLeft + 260; - - // Row 1: Inner Color + Outer Color - addLabel(new GuiNpcLabel(200, "ability.innerColor", labelX, y + 5)); - String innerHex = String.format("%06X", sweeper.getInnerColor() & 0xFFFFFF); - GuiNpcButton innerColorBtn = new GuiNpcButton(200, fieldX, y, 55, 20, innerHex); - innerColorBtn.setTextColor(sweeper.getInnerColor() & 0xFFFFFF); - addButton(innerColorBtn); - - addLabel(new GuiNpcLabel(201, "ability.outerColor", col2LabelX, y + 5)); - String outerHex = String.format("%06X", sweeper.getOuterColor() & 0xFFFFFF); - GuiNpcButton outerColorBtn = new GuiNpcButton(201, col2FieldX, y, 55, 20, outerHex); - outerColorBtn.setTextColor(sweeper.getOuterColor() & 0xFFFFFF); - outerColorBtn.setEnabled(sweeper.isOuterColorEnabled()); - addButton(outerColorBtn); - - y += 24; - - // Row 2: Outer Color Enabled + Outer Color Width - addLabel(new GuiNpcLabel(202, "ability.outerEnabled", labelX, y + 5)); - addButton(new GuiNpcButton(202, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, sweeper.isOuterColorEnabled() ? 1 : 0)); - - addLabel(new GuiNpcLabel(203, "ability.outerWidth", col2LabelX, y + 5)); - GuiNpcTextField widthField = createFloatField(203, col2FieldX, y, 55, sweeper.getOuterColorWidth()); - widthField.setEnabled(sweeper.isOuterColorEnabled()); - addTextField(widthField); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 107: - sweeper.setPiercing(value == 1); - break; - case 108: - sweeper.setLockOnTarget(value == 1); - break; - case 150: - setSubGui(new SubGuiAbilityEffects(sweeper.getEffects())); - break; - } - } - - @Override - protected void handleVisualButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 200: - editingVisualColorId = 200; - setSubGui(new SubGuiColorSelector(sweeper.getInnerColor())); - break; - case 201: - editingVisualColorId = 201; - setSubGui(new SubGuiColorSelector(sweeper.getOuterColor())); - break; - case 202: - sweeper.setOuterColorEnabled(value == 1); - initGui(); - break; - } - } - - @Override - public void subGuiClosed(SubGuiInterface subgui) { - if (subgui instanceof SubGuiColorSelector && editingVisualColorId != 0) { - SubGuiColorSelector colorSelector = (SubGuiColorSelector) subgui; - int rgb = colorSelector.color & 0x00FFFFFF; - if (editingVisualColorId == 200) { - sweeper.setInnerColor(rgb); - } else if (editingVisualColorId == 201) { - sweeper.setOuterColor(rgb); - } - editingVisualColorId = 0; - initGui(); - } else if (subgui instanceof SubGuiAbilityEffects) { - SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; - List result = effectsGui.getResult(); - if (result != null) { - sweeper.setEffects(result); - } - } else { - super.subGuiClosed(subgui); - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - sweeper.setDamage(parseFloat(field, sweeper.getDamage())); - break; - case 101: - sweeper.setDamageInterval(field.getInteger()); - break; - case 102: - sweeper.setBeamLength(parseFloat(field, sweeper.getBeamLength())); - break; - case 103: - sweeper.setBeamWidth(parseFloat(field, sweeper.getBeamWidth())); - break; - case 104: - sweeper.setBeamHeight(parseFloat(field, sweeper.getBeamHeight())); - break; - case 105: - sweeper.setSweepSpeed(parseFloat(field, sweeper.getSweepSpeed())); - break; - case 106: - sweeper.setNumberOfRotations(field.getInteger()); - break; - } - } - - @Override - protected void handleVisualTextField(int id, GuiNpcTextField field) { - switch (id) { - case 203: - sweeper.setOuterColorWidth(parseFloat(field, sweeper.getOuterColorWidth())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java deleted file mode 100644 index 4b23b0ca0..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTeleport.java +++ /dev/null @@ -1,165 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.AbilityEffect; -import kamkeel.npcs.controllers.data.ability.type.AbilityTeleport; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.util.SubGuiInterface; - -import java.util.List; - -/** - * GUI for configuring Teleport ability type-specific settings. - */ -public class SubGuiAbilityTeleport extends SubGuiAbilityConfig { - - private final AbilityTeleport teleport; - - public SubGuiAbilityTeleport(AbilityTeleport ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.teleport = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Mode - addLabel(new GuiNpcLabel(100, "ability.mode", labelX, y + 5)); - String[] modes = {"Blink", "Behind", "Single"}; - addButton(new GuiNpcButton(100, fieldX, y, 80, 20, modes, teleport.getModeEnum().ordinal())); - - y += 24; - - switch (teleport.getModeEnum()) { - case BLINK: - // Row 2: Blink Radius + Blink Count - addLabel(new GuiNpcLabel(101, "ability.blinkRadius", labelX, y + 5)); - addTextField(createFloatField(101, fieldX, y, 50, teleport.getBlinkRadius())); - - addLabel(new GuiNpcLabel(102, "ability.blinkCount", col2LabelX, y + 5)); - addTextField(createIntField(102, col2FieldX, y, 50, teleport.getBlinkCount())); - - y += 24; - - // Row 3: Blink Delay + Line of Sight - addLabel(new GuiNpcLabel(103, "ability.blinkDelay", labelX, y + 5)); - addTextField(createIntField(103, fieldX, y, 50, teleport.getBlinkDelayTicks())); - - addLabel(new GuiNpcLabel(105, "ability.lineOfSight", col2LabelX, y + 5)); - GuiNpcButton losBtn1 = new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isRequireLineOfSight() ? 1 : 0); - losBtn1.setHoverText("ability.hover.lineOfSight"); - addButton(losBtn1); - y += 24; - break; - case SINGLE: - // Row 2: Blink Radius + Line of Sight - addLabel(new GuiNpcLabel(101, "ability.blinkRadius", labelX, y + 5)); - addTextField(createFloatField(101, fieldX, y, 50, teleport.getBlinkRadius())); - - addLabel(new GuiNpcLabel(105, "ability.lineOfSight", col2LabelX, y + 5)); - GuiNpcButton losBtn2 = new GuiNpcButton(105, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isRequireLineOfSight() ? 1 : 0); - losBtn2.setHoverText("ability.hover.lineOfSight"); - addButton(losBtn2); - y += 24; - break; - case BEHIND: - // Row 2: Behind Distance - addLabel(new GuiNpcLabel(104, "ability.behindDistance", labelX, y + 5)); - addTextField(createFloatField(104, fieldX, y, 50, teleport.getBehindDistance())); - y += 24; - break; - } - - // Damage + Damage Radius - addLabel(new GuiNpcLabel(106, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(106, fieldX, y, 50, teleport.getDamage())); - - addLabel(new GuiNpcLabel(107, "ability.dmgRadius", col2LabelX, y + 5)); - addTextField(createFloatField(107, col2FieldX, y, 50, teleport.getDamageRadius())); - - y += 24; - - // Damage at Start/End - addLabel(new GuiNpcLabel(108, "ability.dmgAtStart", labelX, y + 5)); - GuiNpcButton dmgStartBtn = new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isDamageAtStart() ? 1 : 0); - dmgStartBtn.setHoverText("ability.hover.dmgAtStart"); - addButton(dmgStartBtn); - - addLabel(new GuiNpcLabel(109, "ability.dmgAtEnd", col2LabelX, y + 5)); - GuiNpcButton dmgEndBtn = new GuiNpcButton(109, col2FieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, teleport.isDamageAtEnd() ? 1 : 0); - dmgEndBtn.setHoverText("ability.hover.dmgAtEnd"); - addButton(dmgEndBtn); - - y += 24; - - // Effects button - addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 100: - teleport.setModeEnum(AbilityTeleport.TeleportMode.values()[value]); - initGui(); - break; - case 105: - teleport.setRequireLineOfSight(value == 1); - break; - case 108: - teleport.setDamageAtStart(value == 1); - break; - case 109: - teleport.setDamageAtEnd(value == 1); - break; - case 150: - setSubGui(new SubGuiAbilityEffects(teleport.getEffects())); - break; - } - } - - @Override - public void subGuiClosed(SubGuiInterface subgui) { - super.subGuiClosed(subgui); - if (subgui instanceof SubGuiAbilityEffects) { - SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; - List result = effectsGui.getResult(); - if (result != null) { - teleport.setEffects(result); - } - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 101: - teleport.setBlinkRadius(parseFloat(field, teleport.getBlinkRadius())); - break; - case 102: - teleport.setBlinkCount(field.getInteger()); - break; - case 103: - teleport.setBlinkDelayTicks(field.getInteger()); - break; - case 104: - teleport.setBehindDistance(parseFloat(field, teleport.getBehindDistance())); - break; - case 106: - teleport.setDamage(parseFloat(field, teleport.getDamage())); - break; - case 107: - teleport.setDamageRadius(parseFloat(field, teleport.getDamageRadius())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java deleted file mode 100644 index 5c9c52558..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityTrap.java +++ /dev/null @@ -1,126 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.AbilityEffect; -import kamkeel.npcs.controllers.data.ability.type.AbilityTrap; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.util.SubGuiInterface; - -import java.util.List; - -/** - * GUI for configuring Trap ability type-specific settings. - */ -public class SubGuiAbilityTrap extends SubGuiAbilityConfig { - - private final AbilityTrap trap; - - public SubGuiAbilityTrap(AbilityTrap ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.trap = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Duration + Placement - addLabel(new GuiNpcLabel(99, "ability.duration", labelX, y + 5)); - GuiNpcTextField durationField = createIntField(99, fieldX, y, 50, trap.getDurationTicks()); - durationField.setMinMaxDefault(1, 2000, 200); - addTextField(durationField); - - addLabel(new GuiNpcLabel(100, "ability.placement", col2LabelX, y + 5)); - String[] placements = {"Caster", "Target", "Ahead"}; - GuiNpcButton placementBtn = new GuiNpcButton(100, col2FieldX, y, 55, 20, placements, trap.getPlacementEnum().ordinal()); - placementBtn.setHoverText("ability.hover.placement"); - addButton(placementBtn); - - y += 24; - - // Row 2: Trigger Radius - addLabel(new GuiNpcLabel(101, "ability.triggerRad", labelX, y + 5)); - addTextField(createFloatField(101, fieldX, y, 50, trap.getTriggerRadius())); - - y += 24; - - // Row 2: Damage + Damage Radius - addLabel(new GuiNpcLabel(102, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, trap.getDamage())); - - addLabel(new GuiNpcLabel(103, "ability.dmgRadius", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, trap.getDamageRadius())); - - y += 24; - - // Row 3: Arm Time + Max Triggers - addLabel(new GuiNpcLabel(104, "ability.armTime", labelX, y + 5)); - addTextField(createIntField(104, fieldX, y, 50, trap.getArmTime())); - - addLabel(new GuiNpcLabel(105, "ability.maxTriggers", col2LabelX, y + 5)); - addTextField(createIntField(105, col2FieldX, y, 50, trap.getMaxTriggers())); - - y += 24; - - // Row 4: Knockback + Effects button - addLabel(new GuiNpcLabel(108, "ability.knockback", labelX, y + 5)); - addTextField(createFloatField(108, fieldX, y, 50, trap.getKnockback())); - - addButton(new GuiNpcButton(150, col2LabelX, y, 80, 20, "ability.effects")); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - if (id == 100) { - trap.setPlacementEnum(AbilityTrap.TrapPlacement.values()[button.getValue()]); - } else if (id == 150) { - setSubGui(new SubGuiAbilityEffects(trap.getEffects())); - } - } - - @Override - public void subGuiClosed(SubGuiInterface subgui) { - super.subGuiClosed(subgui); - if (subgui instanceof SubGuiAbilityEffects) { - SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; - List result = effectsGui.getResult(); - if (result != null) { - trap.setEffects(result); - } - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 99: - trap.setDurationTicks(field.getInteger()); - break; - case 101: - trap.setTriggerRadius(parseFloat(field, trap.getTriggerRadius())); - break; - case 102: - trap.setDamage(parseFloat(field, trap.getDamage())); - break; - case 103: - trap.setDamageRadius(parseFloat(field, trap.getDamageRadius())); - break; - case 104: - trap.setArmTime(field.getInteger()); - break; - case 105: - trap.setMaxTriggers(field.getInteger()); - break; - case 108: - trap.setKnockback(parseFloat(field, trap.getKnockback())); - break; - } - } -} diff --git a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java b/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java deleted file mode 100644 index b56828470..000000000 --- a/src/main/java/noppes/npcs/client/gui/advanced/ability/SubGuiAbilityVortex.java +++ /dev/null @@ -1,129 +0,0 @@ -package noppes.npcs.client.gui.advanced.ability; - -import kamkeel.npcs.controllers.data.ability.AbilityEffect; -import kamkeel.npcs.controllers.data.ability.type.AbilityVortex; -import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; -import noppes.npcs.client.gui.util.GuiNpcButton; -import noppes.npcs.client.gui.util.GuiNpcLabel; -import noppes.npcs.client.gui.util.GuiNpcTextField; -import noppes.npcs.client.gui.util.IAbilityConfigCallback; -import noppes.npcs.client.gui.util.SubGuiInterface; - -import java.util.List; - -/** - * GUI for configuring Vortex ability type-specific settings. - */ -public class SubGuiAbilityVortex extends SubGuiAbilityConfig { - - private final AbilityVortex vortex; - - public SubGuiAbilityVortex(AbilityVortex ability, IAbilityConfigCallback callback) { - super(ability, callback); - this.vortex = ability; - } - - @Override - protected void initTypeTab(int startY) { - int y = startY; - int labelX = guiLeft + 8; - int fieldX = guiLeft + 85; - int col2LabelX = guiLeft + 145; - int col2FieldX = guiLeft + 205; - - // Row 1: Pull Radius + Pull Strength - addLabel(new GuiNpcLabel(100, "ability.pullRadius", labelX, y + 5)); - addTextField(createFloatField(100, fieldX, y, 50, vortex.getPullRadius())); - - addLabel(new GuiNpcLabel(101, "ability.pullStrength", col2LabelX, y + 5)); - addTextField(createFloatField(101, col2FieldX, y, 50, vortex.getPullStrength())); - - y += 24; - - // Row 2: Damage + Knockback - addLabel(new GuiNpcLabel(102, "enchantment.damage", labelX, y + 5)); - addTextField(createFloatField(102, fieldX, y, 50, vortex.getDamage())); - - addLabel(new GuiNpcLabel(103, "ability.knockback", col2LabelX, y + 5)); - addTextField(createFloatField(103, col2FieldX, y, 50, vortex.getKnockback())); - - y += 24; - - // Row 3: AOE + Max Targets - addLabel(new GuiNpcLabel(104, "ability.aoe", labelX, y + 5)); - GuiNpcButton aoeBtn = new GuiNpcButton(104, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, vortex.isAoe() ? 1 : 0); - aoeBtn.setHoverText("ability.hover.aoe"); - addButton(aoeBtn); - - addLabel(new GuiNpcLabel(105, "ability.maxTargets", col2LabelX, y + 5)); - addTextField(createIntField(105, col2FieldX, y, 50, vortex.getMaxTargets())); - - y += 24; - - // Row 4: Effects button - addButton(new GuiNpcButton(150, labelX, y, 80, 20, "ability.effects")); - - y += 24; - - // Row 5: Damage on Pull + Pull Damage - addLabel(new GuiNpcLabel(108, "ability.dmgOnPull", labelX, y + 5)); - GuiNpcButton dmgOnPullBtn = new GuiNpcButton(108, fieldX, y, 50, 20, new String[]{"gui.no", "gui.yes"}, vortex.isDamageOnPull() ? 1 : 0); - dmgOnPullBtn.setHoverText("ability.hover.dmgOnPull"); - addButton(dmgOnPullBtn); - - addLabel(new GuiNpcLabel(109, "ability.pullDamage", col2LabelX, y + 5)); - addTextField(createFloatField(109, col2FieldX, y, 50, vortex.getPullDamage())); - } - - @Override - protected void handleTypeButton(int id, GuiNpcButton button) { - int value = button.getValue(); - switch (id) { - case 104: - vortex.setAoe(value == 1); - break; - case 108: - vortex.setDamageOnPull(value == 1); - break; - case 150: - setSubGui(new SubGuiAbilityEffects(vortex.getEffects())); - break; - } - } - - @Override - public void subGuiClosed(SubGuiInterface subgui) { - super.subGuiClosed(subgui); - if (subgui instanceof SubGuiAbilityEffects) { - SubGuiAbilityEffects effectsGui = (SubGuiAbilityEffects) subgui; - List result = effectsGui.getResult(); - if (result != null) { - vortex.setEffects(result); - } - } - } - - @Override - protected void handleTypeTextField(int id, GuiNpcTextField field) { - switch (id) { - case 100: - vortex.setPullRadius(parseFloat(field, vortex.getPullRadius())); - break; - case 101: - vortex.setPullStrength(parseFloat(field, vortex.getPullStrength())); - break; - case 102: - vortex.setDamage(parseFloat(field, vortex.getDamage())); - break; - case 103: - vortex.setKnockback(parseFloat(field, vortex.getKnockback())); - break; - case 105: - vortex.setMaxTargets(field.getInteger()); - break; - case 109: - vortex.setPullDamage(parseFloat(field, vortex.getPullDamage())); - break; - } - } -} From 911f1854004ec38f9e93d345e48b150e23aafd3f Mon Sep 17 00:00:00 2001 From: Kam Date: Mon, 2 Feb 2026 18:26:52 -0500 Subject: [PATCH 358/358] Fields Builder for Ability GUIs --- .../controllers/data/ability/Ability.java | 105 ++++- .../data/ability/AbilityEffect.java | 7 + .../controllers/data/ability/AnchorPoint.java | 13 + .../data/ability/TargetingMode.java | 13 +- .../data/ability/gui/FieldDef.java | 52 +-- .../ability/gui/FieldDefinitionsBuilder.java | 359 ++++++++++++++++++ .../data/ability/gui/FieldType.java | 5 +- .../data/ability/gui/TabTarget.java | 1 - .../data/ability/type/AbilityCharge.java | 11 +- .../data/ability/type/AbilityCutter.java | 31 +- .../data/ability/type/AbilityDash.java | 18 +- .../data/ability/type/AbilityDisc.java | 76 ++-- .../data/ability/type/AbilityEnergyBeam.java | 71 ++-- .../data/ability/type/AbilityGuard.java | 40 +- .../data/ability/type/AbilityHazard.java | 48 ++- .../data/ability/type/AbilityHeal.java | 15 +- .../data/ability/type/AbilityHeavyHit.java | 7 +- .../data/ability/type/AbilityLaserShot.java | 55 ++- .../data/ability/type/AbilityOrb.java | 67 ++-- .../data/ability/type/AbilityProjectile.java | 23 +- .../data/ability/type/AbilityShockwave.java | 8 +- .../data/ability/type/AbilitySlam.java | 12 +- .../data/ability/type/AbilitySweeper.java | 34 +- .../data/ability/type/AbilityTeleport.java | 32 +- .../data/ability/type/AbilityTrap.java | 28 +- .../data/ability/type/AbilityVortex.java | 24 +- .../gui/advanced/SubGuiAbilityConfig.java | 358 +++++++++-------- .../npcs/client/gui/util/GuiNpcButton.java | 16 + .../assets/customnpcs/lang/en_US.lang | 299 +++++++++------ 29 files changed, 1223 insertions(+), 605 deletions(-) create mode 100644 src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDefinitionsBuilder.java diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java index b368d6267..0696674f4 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/Ability.java @@ -19,10 +19,12 @@ import noppes.npcs.client.gui.advanced.SubGuiAbilityConfig; import noppes.npcs.client.gui.util.IAbilityConfigCallback; import noppes.npcs.controllers.AnimationController; import noppes.npcs.controllers.data.Animation; +import noppes.npcs.controllers.data.Frame; import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.scripted.NpcAPI; import noppes.npcs.scripted.event.AbilityEvent; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; import kamkeel.npcs.controllers.data.ability.gui.TabTarget; @@ -60,6 +62,7 @@ public abstract class Ability implements IAbility { // Timing (ticks) protected int cooldownTicks = 0; // Added to global cooldown after ability completes protected int windUpTicks = 20; + protected boolean syncWindupWithAnimation = true; protected int dazedTicks = 80; // Only used when interrupted during WINDUP (if interruptible) // Interruption @@ -325,49 +328,70 @@ public abstract class Ability implements IAbility { defs.add(FieldDef.stringField("gui.name", this::getName, this::setName) .tab(TabTarget.GENERAL)); defs.add(FieldDef.intField("ability.weight", this::getWeight, this::setWeight) - .tab(TabTarget.GENERAL).range(1, 1000)); - defs.add(FieldDef.enumField("ability.lockMove", LockMovementType.class, - this::getLockMovement, this::setLockMovement) + .tab(TabTarget.GENERAL).range(1, 1000).column(ColumnHint.LEFT)); + defs.add(FieldDef.boolField("gui.enabled", this::isEnabled, this::setEnabled) + .tab(TabTarget.GENERAL).column(ColumnHint.RIGHT)); + defs.add(FieldDef.section("ability.section.timing").tab(TabTarget.GENERAL)); + defs.add(FieldDef.intField("ability.windUpTicks", this::getRawWindUpTicks, this::setWindUpTicks) + .tab(TabTarget.GENERAL).range(0, 1000).column(ColumnHint.LEFT) + .visibleWhen(() -> !isSyncWindupWithAnimation())); + defs.add(FieldDef.labelField("ability.windUpTicks", () -> getWindUpTicks() + "t") + .tab(TabTarget.GENERAL).column(ColumnHint.LEFT) + .visibleWhen(this::isSyncWindupWithAnimation)); + defs.add(FieldDef.boolField("ability.syncWindup", this::isSyncWindupWithAnimation, this::setSyncWindupWithAnimation) + .tab(TabTarget.GENERAL).hover("ability.hover.sync").column(ColumnHint.RIGHT) + .visibleWhen(this::hasWindUpAnimation)); + defs.add(FieldDef.intField("ability.cooldownTicks", this::getCooldownTicks, this::setCooldownTicks) + .tab(TabTarget.GENERAL).range(0, 10000)); + defs.add(FieldDef.section("ability.section.movement").tab(TabTarget.GENERAL)); + defs.add(FieldDef.stringEnumField("ability.lockMovement", LockMovementType.getDisplayKeys(), + () -> this.getLockMovement().getDisplayKey(), + v -> { + String[] keys = LockMovementType.getDisplayKeys(); + for (int i = 0; i < keys.length; i++) { + if (keys[i].equals(v)) { this.setLockMovement(LockMovementType.fromOrdinal(i)); break; } + } + }) .tab(TabTarget.GENERAL).hover("ability.hover.lockMove")); defs.add(FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible) - .tab(TabTarget.GENERAL).hover("ability.hover.interruptible")); - defs.add(FieldDef.intField("ability.dazed", this::getDazedTicks, this::setDazedTicks) + .tab(TabTarget.GENERAL).hover("ability.hover.interruptible").column(ColumnHint.LEFT)); + defs.add(FieldDef.intField("ability.dazedTicks", this::getDazedTicks, this::setDazedTicks) .tab(TabTarget.GENERAL).range(0, 1000) - .visibleWhen(this::isInterruptible)); - defs.add(FieldDef.intField("ability.windup", this::getWindUpTicks, this::setWindUpTicks) - .tab(TabTarget.GENERAL).range(0, 1000)); - defs.add(FieldDef.intField("ability.cooldown", this::getCooldownTicks, this::setCooldownTicks) - .tab(TabTarget.GENERAL).range(0, 10000)); - defs.add(FieldDef.boolField("gui.enabled", this::isEnabled, this::setEnabled) - .tab(TabTarget.GENERAL)); + .visibleWhen(this::isInterruptible).column(ColumnHint.RIGHT)); // Target tab defs.add(FieldDef.intField("ability.minRange", () -> (int) getMinRange(), v -> setMinRange(v)) - .tab(TabTarget.TARGET).range(0, 100)); + .tab(TabTarget.TARGET).range(0, 100).column(ColumnHint.LEFT)); defs.add(FieldDef.intField("ability.maxRange", () -> (int) getMaxRange(), v -> setMaxRange(v)) - .tab(TabTarget.TARGET).range(1, 100)); + .tab(TabTarget.TARGET).range(1, 100).column(ColumnHint.RIGHT)); if (!isTargetingModeLocked()) { - defs.add(FieldDef.enumField("ability.targeting", TargetingMode.class, + defs.add(FieldDef.enumField("ability.targetingMode", TargetingMode.class, this::getTargetingMode, this::setTargetingMode) .tab(TabTarget.TARGET).hover("ability.hover.targeting")); } - // Effects tab + // Effects tab - Sounds + defs.add(FieldDef.section("ability.section.sounds").tab(TabTarget.EFFECTS)); defs.add(FieldDef.soundSubGui("ability.windUpSound", this::getWindUpSound, this::setWindUpSound) .tab(TabTarget.EFFECTS)); defs.add(FieldDef.soundSubGui("ability.activeSound", this::getActiveSound, this::setActiveSound) .tab(TabTarget.EFFECTS)); - defs.add(FieldDef.animSubGui("ability.windUpAnim", + // Effects tab - Animations + defs.add(FieldDef.section("ability.section.animations").tab(TabTarget.EFFECTS)); + defs.add(FieldDef.animSubGui("ability.windUpAnimation", this::getWindUpAnimationId, this::setWindUpAnimationId, this::getWindUpAnimationName, this::setWindUpAnimationName) .tab(TabTarget.EFFECTS)); - defs.add(FieldDef.animSubGui("ability.activeAnim", + defs.add(FieldDef.animSubGui("ability.activeAnimation", this::getActiveAnimationId, this::setActiveAnimationId, this::getActiveAnimationName, this::setActiveAnimationName) .tab(TabTarget.EFFECTS)); + // Effects tab - Telegraph TelegraphType tType = getTelegraphType(); if (tType != null && tType != TelegraphType.NONE) { + defs.add(FieldDef.section("ability.section.telegraph").tab(TabTarget.EFFECTS) + .tooltip("telegraph." + tType.name().toLowerCase())); defs.add(FieldDef.boolField("ability.showTelegraph", this::isShowTelegraph, this::setShowTelegraph) .tab(TabTarget.EFFECTS).hover("ability.hover.showTelegraph")); defs.add(FieldDef.colorSubGui("ability.windUpColor", this::getWindUpColor, this::setWindUpColor) @@ -847,6 +871,7 @@ public abstract class Ability implements IAbility { nbt.setFloat("maxRange", maxRange); nbt.setInteger("cooldown", cooldownTicks); nbt.setInteger("windUp", windUpTicks); + nbt.setBoolean("syncWindup", syncWindupWithAnimation); nbt.setInteger("recovery", dazedTicks); nbt.setBoolean("interruptible", interruptible); nbt.setInteger("lockMovement", lockMovement.ordinal()); @@ -894,6 +919,7 @@ public abstract class Ability implements IAbility { maxRange = nbt.getFloat("maxRange"); cooldownTicks = nbt.getInteger("cooldown"); windUpTicks = nbt.getInteger("windUp"); + syncWindupWithAnimation = !nbt.hasKey("syncWindup") || nbt.getBoolean("syncWindup"); dazedTicks = nbt.getInteger("recovery"); interruptible = nbt.getBoolean("interruptible"); lockMovement = LockMovementType.fromOrdinal(nbt.getInteger("lockMovement")); @@ -1036,6 +1062,10 @@ public abstract class Ability implements IAbility { } public int getWindUpTicks() { + return calculateWindupFromAnimation(); + } + + public int getRawWindUpTicks() { return windUpTicks; } @@ -1043,6 +1073,45 @@ public abstract class Ability implements IAbility { this.windUpTicks = Math.max(0, windUpTicks); } + public boolean isSyncWindupWithAnimation() { + return syncWindupWithAnimation; + } + + public boolean hasWindUpAnimation() { + return (windUpAnimationName != null && !windUpAnimationName.isEmpty()) || windUpAnimationId >= 0; + } + + public void setSyncWindupWithAnimation(boolean syncWindupWithAnimation) { + this.syncWindupWithAnimation = syncWindupWithAnimation; + } + + private int calculateWindupFromAnimation() { + if (!syncWindupWithAnimation) { + return windUpTicks; + } + + if (AnimationController.Instance == null) { + return windUpTicks; + } + + Animation animation = null; + if (windUpAnimationName != null && !windUpAnimationName.isEmpty()) { + animation = (Animation) AnimationController.Instance.get(windUpAnimationName); + } else if (windUpAnimationId >= 0) { + animation = (Animation) AnimationController.Instance.get(windUpAnimationId); + } + + if (animation == null || animation.frames.isEmpty()) { + return windUpTicks; + } + + int totalDuration = 0; + for (Frame frame : animation.frames) { + totalDuration += frame.getDuration(); + } + return totalDuration; + } + public int getDazedTicks() { return dazedTicks; } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java index 47e03f10d..57f0cbf17 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AbilityEffect.java @@ -38,6 +38,13 @@ public class AbilityEffect { return potionId; } + public static String[] getLangKeys() { + EffectType[] types = values(); + String[] keys = new String[types.length]; + for (int i = 0; i < types.length; i++) keys[i] = types[i].langKey; + return keys; + } + public static EffectType fromOrdinal(int ordinal) { EffectType[] values = values(); if (ordinal >= 0 && ordinal < values.length) { diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java index 9bf408c27..1bd99d4f8 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/AnchorPoint.java @@ -52,4 +52,17 @@ public enum AnchorPoint { } return names; } + + @Override + public String toString() { + switch (this) { + case FRONT: return "ability.anchor.front"; + case CENTER: return "ability.anchor.center"; + case RIGHT_HAND: return "ability.anchor.rightHand"; + case LEFT_HAND: return "ability.anchor.leftHand"; + case ABOVE_HEAD: return "ability.anchor.aboveHead"; + case CHEST: return "ability.anchor.chest"; + default: return name(); + } + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java b/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java index bd1bb6a47..9bdbdac6f 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/TargetingMode.java @@ -22,5 +22,16 @@ public enum TargetingMode { /** * AOE centered on target */ - AOE_TARGET + AOE_TARGET; + + @Override + public String toString() { + switch (this) { + case AGGRO_TARGET: return "ability.target.aggro_target"; + case SELF: return "ability.target.self"; + case AOE_SELF: return "ability.target.aoe_self"; + case AOE_TARGET: return "ability.target.aoe_target"; + default: return name(); + } + } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDef.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDef.java index 25ec6cb66..28b211c3e 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDef.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDef.java @@ -2,7 +2,6 @@ package kamkeel.npcs.controllers.data.ability.gui; import kamkeel.npcs.controllers.data.ability.AbilityEffect; import noppes.npcs.client.gui.SubGuiColorSelector; -import noppes.npcs.client.gui.advanced.ability.SubGuiAbilityEffects; import noppes.npcs.client.gui.select.GuiAnimationSelection; import noppes.npcs.client.gui.select.GuiSoundSelection; import noppes.npcs.client.gui.util.SubGuiInterface; @@ -26,6 +25,7 @@ public class FieldDef { private Supplier getter; private Consumer setter; private BooleanSupplier visibleWhen = () -> true; + private BooleanSupplier enabledWhen = () -> true; private String tooltip = null; private String hoverText = null; @@ -57,6 +57,10 @@ public class FieldDef { // STATIC FACTORIES // ═══════════════════════════════════════════════════════════════════ + public static FieldDef section(String label) { + return new FieldDef(label, FieldType.SECTION_HEADER); + } + public static FieldDef floatField(String label, Supplier getter, Consumer setter) { FieldDef def = new FieldDef(label, FieldType.FLOAT); def.getter = () -> getter.get(); @@ -102,6 +106,20 @@ public class FieldDef { return def; } + public static FieldDef labelField(String label, Supplier textSupplier) { + FieldDef def = new FieldDef(label, FieldType.LABEL); + def.getter = () -> textSupplier.get(); + return def; + } + + @SuppressWarnings("unchecked") + public static FieldDef effectsListField(String label, Supplier> getter, Consumer> setter) { + FieldDef def = new FieldDef(label, FieldType.EFFECTS_LIST); + def.getter = () -> getter.get(); + def.setter = v -> setter.accept((List) v); + return def; + } + public static FieldDef subGuiField(String label, Supplier factory, Consumer resultHandler) { FieldDef def = new FieldDef(label, FieldType.SUB_GUI); def.subGuiFactory = factory; @@ -121,15 +139,6 @@ public class FieldDef { .buttonTextColor(() -> getter.get() & 0xFFFFFF); } - public static FieldDef effectsSubGui(String label, Supplier> getter, Consumer> setter) { - return subGuiField(label, - () -> new SubGuiAbilityEffects(getter.get()), - gui -> { - List r = ((SubGuiAbilityEffects) gui).getResult(); - if (r != null) setter.accept(r); - }); - } - public static FieldDef soundSubGui(String label, Supplier getter, Consumer setter) { return subGuiField(label, () -> new GuiSoundSelection(getter.get()), @@ -139,7 +148,7 @@ public class FieldDef { }) .buttonLabel(() -> { String s = getter.get(); - return s == null || s.isEmpty() ? "gui.none" : truncateEnd(s, 10); + return s == null || s.isEmpty() ? "gui.none" : s; }) .clearable(() -> setter.accept("")); } @@ -161,7 +170,7 @@ public class FieldDef { }) .buttonLabel(() -> { String name = nameGetter.get(); - if (name != null && !name.isEmpty()) return truncateEnd(name, 8); + if (name != null && !name.isEmpty()) return name; int id = idGetter.get(); return id >= 0 ? "ID: " + id : "gui.none"; }) @@ -199,6 +208,11 @@ public class FieldDef { return this; } + public FieldDef enabledWhen(BooleanSupplier condition) { + this.enabledWhen = condition; + return this; + } + public FieldDef tooltip(String tooltip) { this.tooltip = tooltip; return this; @@ -234,6 +248,7 @@ public class FieldDef { public String getCustomTabName() { return customTabName; } public ColumnHint getColumn() { return column; } public boolean isVisible() { return visibleWhen.getAsBoolean(); } + public boolean isEnabled() { return enabledWhen.getAsBoolean(); } public String getTooltip() { return tooltip; } public String getHoverText() { return hoverText; } public float getMin() { return min; } @@ -271,17 +286,4 @@ public class FieldDef { return buttonTextColorSupplier != null ? buttonTextColorSupplier.get() : null; } - // ═══════════════════════════════════════════════════════════════════ - // HELPERS - // ═══════════════════════════════════════════════════════════════════ - - static String truncateEnd(String str, int maxLen) { - if (str == null) return ""; - if (str.length() <= maxLen) return str; - int lastSlash = str.lastIndexOf('/'); - if (lastSlash >= 0 && str.length() - lastSlash <= maxLen) { - return "..." + str.substring(lastSlash); - } - return "..." + str.substring(str.length() - maxLen + 3); - } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDefinitionsBuilder.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDefinitionsBuilder.java new file mode 100644 index 000000000..1ba3f2694 --- /dev/null +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldDefinitionsBuilder.java @@ -0,0 +1,359 @@ +package kamkeel.npcs.controllers.data.ability.gui; + +import kamkeel.npcs.controllers.data.ability.AbilityEffect; +import net.minecraft.client.gui.FontRenderer; +import net.minecraft.client.gui.GuiScreen; +import net.minecraft.util.StatCollector; +import noppes.npcs.client.gui.util.GuiNpcButton; +import noppes.npcs.client.gui.util.GuiNpcButtonYesNo; +import noppes.npcs.client.gui.util.GuiNpcLabel; +import noppes.npcs.client.gui.util.GuiNpcTextField; +import noppes.npcs.client.gui.util.GuiScrollWindow; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Reusable builder that converts a list of {@link FieldDef} into positioned + * GUI widgets inside a {@link GuiScrollWindow}. + */ +public class FieldDefinitionsBuilder { + + // Layout defaults + private int contentRight = 330; + private int colLLabel = 5; + private int colLField = 75; + private int colLWidth = 70; + private int colRLabel = 178; + private int colRField = 248; + private int colRWidth = 70; + private int rowHeight = 24; + private int labelPadding = 8; + + // Dependencies + private final GuiScreen parent; + private final GuiScrollWindow sw; + private final FontRenderer fontRenderer; + + // ID counters + private int widgetId; + private int clearId; + private int labelId; + + // Starting Y + private int startY = 5; + + // Output maps + private final Map buttonFieldMap = new HashMap<>(); + private final Map textFieldMap = new HashMap<>(); + private final Map clearFieldMap = new HashMap<>(); + + // Effects list metadata: widgetId -> [effectIndex, action] + // action: 0=type, 1=duration(text), 2=amp, 3=delete, 4=add + private final Map effectWidgetMeta = new HashMap<>(); + + public FieldDefinitionsBuilder(GuiScreen parent, GuiScrollWindow sw, FontRenderer fontRenderer) { + this.parent = parent; + this.sw = sw; + this.fontRenderer = fontRenderer; + } + + // ═══════════════════════════════════════════════════════════════════ + // CONFIG CHAINING + // ═══════════════════════════════════════════════════════════════════ + + public FieldDefinitionsBuilder contentRight(int v) { this.contentRight = v; return this; } + public FieldDefinitionsBuilder startIds(int widget, int clear, int label) { + this.widgetId = widget; this.clearId = clear; this.labelId = label; return this; + } + public FieldDefinitionsBuilder startY(int y) { this.startY = y; return this; } + public FieldDefinitionsBuilder rowHeight(int h) { this.rowHeight = h; return this; } + + // ═══════════════════════════════════════════════════════════════════ + // BUILD + // ═══════════════════════════════════════════════════════════════════ + + /** + * Processes all fields and adds widgets to the scroll window. + * @return the final Y position after all fields + */ + public int build(List fields) { + buttonFieldMap.clear(); + textFieldMap.clear(); + clearFieldMap.clear(); + effectWidgetMeta.clear(); + + int y = startY; + + for (int i = 0; i < fields.size(); i++) { + FieldDef def = fields.get(i); + + // Section header + if (def.getType() == FieldType.SECTION_HEADER) { + y += 3; + String sectionText = StatCollector.translateToLocal(def.getLabel()); + if (def.getTooltip() != null && !def.getTooltip().isEmpty()) { + sectionText += " (" + StatCollector.translateToLocal(def.getTooltip()) + ")"; + } + sw.addLabel(new GuiNpcLabel(labelId++, sectionText, colLLabel, y + 2, 0xFFFF55)); + y += 15; + continue; + } + + // Effects list renders its own multi-row block + if (def.getType() == FieldType.EFFECTS_LIST) { + y = renderEffectsList(def, y); + continue; + } + + // Two-column pair: LEFT followed by RIGHT + if (def.getColumn() == ColumnHint.LEFT + && i + 1 < fields.size() + && fields.get(i + 1).getColumn() == ColumnHint.RIGHT + && fields.get(i + 1).getType() != FieldType.SECTION_HEADER) { + renderFieldAt(def, colLLabel, colLField, colLWidth, y); + + i++; + FieldDef rightDef = fields.get(i); + renderFieldAt(rightDef, colRLabel, colRField, colRWidth, y); + + y += rowHeight; + } else { + // Full-width: compute fieldX from label pixel width + int fieldW = getFullFieldWidth(def); + String translated = StatCollector.translateToLocal(def.getLabel()); + int labelW = fontRenderer.getStringWidth(translated); + int fieldX = colLLabel + labelW + labelPadding; + + renderFieldAt(def, colLLabel, fieldX, fieldW, y); + y += rowHeight; + } + } + + return y; + } + + // ═══════════════════════════════════════════════════════════════════ + // FIELD RENDERING + // ═══════════════════════════════════════════════════════════════════ + + @SuppressWarnings({"unchecked", "rawtypes"}) + private void renderFieldAt(FieldDef def, int labelX, int fieldX, int fieldW, int y) { + sw.addLabel(new GuiNpcLabel(labelId++, def.getLabel(), labelX, y + 5, 0xFFFFFF)); + + String hover = def.getHoverText() != null ? def.getHoverText() : def.getTooltip(); + + switch (def.getType()) { + case FLOAT: { + float fVal = def.getValue() instanceof Number ? ((Number) def.getValue()).floatValue() : 0f; + GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, String.valueOf(fVal)); + tf.setFloatsOnly(); + if (def.hasRange()) tf.setMinMaxDefaultFloat(def.getMin(), def.getMax(), fVal); + if (!def.isEnabled()) tf.setEnabled(false); + if (hover != null) tf.setHoverText(hover); + sw.addTextField(tf); + textFieldMap.put(widgetId, def); + widgetId++; + clearId++; + break; + } + case INT: { + int iVal = def.getValue() instanceof Number ? ((Number) def.getValue()).intValue() : 0; + GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, String.valueOf(iVal)); + tf.setIntegersOnly(); + if (def.hasRange()) tf.setMinMaxDefault((int) def.getMin(), (int) def.getMax(), iVal); + if (!def.isEnabled()) tf.setEnabled(false); + if (hover != null) tf.setHoverText(hover); + sw.addTextField(tf); + textFieldMap.put(widgetId, def); + widgetId++; + clearId++; + break; + } + case STRING: { + String sVal = def.getValue() != null ? def.getValue().toString() : ""; + GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, sVal); + if (!def.isEnabled()) tf.setEnabled(false); + if (hover != null) tf.setHoverText(hover); + sw.addTextField(tf); + textFieldMap.put(widgetId, def); + widgetId++; + clearId++; + break; + } + case LABEL: { + String text = def.getValue() != null ? def.getValue().toString() : ""; + sw.addLabel(new GuiNpcLabel(labelId++, text, fieldX, y + 5, 0xAAAAAA)); + widgetId++; + clearId++; + break; + } + case BOOLEAN: { + boolean bVal = def.getValue() instanceof Boolean ? (Boolean) def.getValue() : false; + GuiNpcButtonYesNo btn = new GuiNpcButtonYesNo(widgetId, fieldX, y, fieldW, 20, bVal); + if ("gui.enabled".equals(def.getLabel()) && def.getTab() == TabTarget.GENERAL) { + btn.setTextColor(bVal ? 0x00FF00 : 0xFF0000); + } + if (!def.isEnabled()) btn.setEnabled(false); + if (hover != null) btn.setHoverText(hover); + sw.addButton(btn); + buttonFieldMap.put(widgetId, def); + widgetId++; + clearId++; + break; + } + case ENUM: { + Class> enumClass = def.getEnumClass(); + if (enumClass != null) { + Enum[] constants = enumClass.getEnumConstants(); + String[] names = new String[constants.length]; + for (int i = 0; i < constants.length; i++) names[i] = constants[i].toString(); + int selected = def.getValue() instanceof Enum ? ((Enum) def.getValue()).ordinal() : 0; + GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, fieldW, 20, names, selected); + if (!def.isEnabled()) btn.setEnabled(false); + if (hover != null) btn.setHoverText(hover); + sw.addButton(btn); + buttonFieldMap.put(widgetId, def); + } + widgetId++; + clearId++; + break; + } + case STRING_ENUM: { + String[] values = def.getStringEnumValues(); + if (values != null && values.length > 0) { + String curVal = def.getStringEnumValue(); + int selected = 0; + for (int i = 0; i < values.length; i++) { + if (values[i].equals(curVal)) { selected = i; break; } + } + GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, fieldW, 20, values, selected); + if (!def.isEnabled()) btn.setEnabled(false); + if (hover != null) btn.setHoverText(hover); + sw.addButton(btn); + buttonFieldMap.put(widgetId, def); + } + widgetId++; + clearId++; + break; + } + case SUB_GUI: { + String btnText = def.getButtonLabel(); + int btnW; + if (def.hasClearAction()) { + btnW = contentRight - 22 - fieldX; + GuiNpcButton clearBtn = new GuiNpcButton(clearId, contentRight - 20, y, 20, 20, "X"); + sw.addButton(clearBtn); + clearFieldMap.put(clearId, def); + } else { + btnW = contentRight - fieldX; + } + GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, btnW, 20, btnText); + Integer textColor = def.getButtonTextColor(); + if (textColor != null) btn.setTextColor(textColor); + if (!def.isEnabled()) btn.setEnabled(false); + if (hover != null) btn.setHoverText(hover); + sw.addButton(btn); + buttonFieldMap.put(widgetId, def); + widgetId++; + clearId++; + break; + } + } + } + + // ═══════════════════════════════════════════════════════════════════ + // EFFECTS LIST RENDERING + // ═══════════════════════════════════════════════════════════════════ + + @SuppressWarnings("unchecked") + private int renderEffectsList(FieldDef def, int y) { + // Section header + y += 3; + sw.addLabel(new GuiNpcLabel(labelId++, def.getLabel(), colLLabel, y + 2, 0xFFFF55)); + y += 15; + + List effects = (List) def.getValue(); + if (effects == null) effects = new ArrayList<>(); + + String[] typeNames = AbilityEffect.EffectType.getLangKeys(); + String[] ampValues = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"}; + + for (int e = 0; e < effects.size() && e < 5; e++) { + AbilityEffect effect = effects.get(e); + + // Type selector button + int typeIdx = effect.getType().ordinal(); + GuiNpcButton typeBtn = new GuiNpcButton(widgetId, colLLabel, y, 100, 20, typeNames, typeIdx); + sw.addButton(typeBtn); + buttonFieldMap.put(widgetId, def); + effectWidgetMeta.put(widgetId, new int[]{e, 0}); + widgetId++; + + // Duration text field + GuiNpcTextField durField = new GuiNpcTextField(widgetId, parent, fontRenderer, + colLLabel + 104, y, 50, 20, String.valueOf(effect.getDurationTicks())); + durField.setIntegersOnly(); + durField.setMinMaxDefault(1, 12000, 60); + sw.addTextField(durField); + textFieldMap.put(widgetId, def); + effectWidgetMeta.put(widgetId, new int[]{e, 1}); + widgetId++; + + // Amplifier selector (0-10) + GuiNpcButton ampBtn = new GuiNpcButton(widgetId, colLLabel + 158, y, 40, 20, ampValues, effect.getAmplifier()); + sw.addButton(ampBtn); + buttonFieldMap.put(widgetId, def); + effectWidgetMeta.put(widgetId, new int[]{e, 2}); + widgetId++; + + // Delete button + GuiNpcButton delBtn = new GuiNpcButton(clearId, colLLabel + 202, y, 20, 20, "X"); + sw.addButton(delBtn); + clearFieldMap.put(clearId, def); + effectWidgetMeta.put(clearId, new int[]{e, 3}); + clearId++; + + y += rowHeight; + } + + // Add button (if < 5 effects) + if (effects.size() < 5) { + GuiNpcButton addBtn = new GuiNpcButton(widgetId, colLLabel, y, 50, 20, "gui.add"); + sw.addButton(addBtn); + buttonFieldMap.put(widgetId, def); + effectWidgetMeta.put(widgetId, new int[]{effects.size(), 4}); + widgetId++; + clearId++; + y += rowHeight; + } + + return y; + } + + private int getFullFieldWidth(FieldDef def) { + switch (def.getType()) { + case FLOAT: + case INT: return 60; + case STRING: return 200; + case BOOLEAN: return 50; + case LABEL: return 60; + case ENUM: + case STRING_ENUM: return 120; + case SUB_GUI: return def.hasClearAction() ? 175 : 200; + default: return 80; + } + } + + // ═══════════════════════════════════════════════════════════════════ + // GETTERS + // ═══════════════════════════════════════════════════════════════════ + + public Map getButtonFieldMap() { return buttonFieldMap; } + public Map getTextFieldMap() { return textFieldMap; } + public Map getClearFieldMap() { return clearFieldMap; } + public Map getEffectWidgetMeta() { return effectWidgetMeta; } + public int getNextLabelId() { return labelId; } +} diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldType.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldType.java index b908d9b88..019b24bdf 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldType.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/FieldType.java @@ -7,5 +7,8 @@ public enum FieldType { ENUM, STRING_ENUM, SUB_GUI, - STRING + STRING, + SECTION_HEADER, + LABEL, + EFFECTS_LIST } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabTarget.java b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabTarget.java index aba49646b..978f15224 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabTarget.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/gui/TabTarget.java @@ -5,6 +5,5 @@ public enum TabTarget { TYPE, TARGET, EFFECTS, - VISUAL, CUSTOM } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java index 8241fab86..48ac3c374 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCharge.java @@ -16,6 +16,7 @@ import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityCharge; @@ -403,11 +404,11 @@ public class AbilityCharge extends Ability implements IAbilityCharge { @Override public List getFieldDefinitions() { return Arrays.asList( - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("stats.speed", this::getChargeSpeed, this::setChargeSpeed), - FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), - FieldDef.floatField("ability.hitWidth", this::getHitWidth, this::setHitWidth), - FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("ability.chargeSpeed", this::getChargeSpeed, this::setChargeSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.LEFT), + FieldDef.floatField("ability.hitWidth", this::getHitWidth, this::setHitWidth).column(ColumnHint.RIGHT), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java index 29ae4aa22..7e265b7d1 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityCutter.java @@ -13,6 +13,7 @@ import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilityCutter; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; import kamkeel.npcs.controllers.data.ability.gui.TabTarget; @@ -30,7 +31,16 @@ public class AbilityCutter extends Ability implements IAbilityCutter { public enum SweepMode { SWIPE, - SPIN + SPIN; + + @Override + public String toString() { + switch (this) { + case SWIPE: return "ability.sweep.swipe"; + case SPIN: return "ability.sweep.spin"; + default: return name(); + } + } } private float arcAngle = 90.0f; @@ -308,20 +318,21 @@ public class AbilityCutter extends Ability implements IAbilityCutter { @Override public List getFieldDefinitions() { return Arrays.asList( - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("gui.range", this::getRange, this::setRange), - FieldDef.floatField("ability.arcAngle", this::getArcAngle, this::setArcAngle), - FieldDef.floatField("ability.innerRadius", this::getInnerRadius, this::setInnerRadius), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("gui.range", this::getRange, this::setRange).column(ColumnHint.RIGHT), FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), - FieldDef.floatField("ability.sweepSpeed", this::getSweepSpeed, this::setSweepSpeed), + FieldDef.section("ability.section.sweep"), FieldDef.enumField("ability.sweepMode", SweepMode.class, this::getSweepModeEnum, this::setSweepModeEnum) .hover("ability.hover.sweepMode"), - FieldDef.boolField("ability.piercing", this::isPiercing, this::setPiercing) - .hover("ability.hover.piercing"), - FieldDef.intField("ability.duration", this::getSpinDurationTicks, this::setSpinDurationTicks) + FieldDef.floatField("ability.arcAngle", this::getArcAngle, this::setArcAngle).column(ColumnHint.LEFT), + FieldDef.floatField("ability.innerRadius", this::getInnerRadius, this::setInnerRadius).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.sweepSpeed", this::getSweepSpeed, this::setSweepSpeed), + FieldDef.intField("ability.spinDuration", this::getSpinDurationTicks, this::setSpinDurationTicks) .range(1, 1000) .visibleWhen(() -> this.getSweepModeEnum() == SweepMode.SPIN), - FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + FieldDef.boolField("ability.piercing", this::isPiercing, this::setPiercing) + .hover("ability.hover.piercing"), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java index 3684a8b73..307f8aed2 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDash.java @@ -12,6 +12,7 @@ import net.minecraft.util.Vec3; import net.minecraft.world.World; import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityDash; @@ -31,7 +32,16 @@ public class AbilityDash extends Ability implements IAbilityDash { */ public enum DashMode { AGGRESSIVE, - DEFENSIVE + DEFENSIVE; + + @Override + public String toString() { + switch (this) { + case AGGRESSIVE: return "ability.dash.aggressive"; + case DEFENSIVE: return "ability.dash.defensive"; + default: return name(); + } + } } /** @@ -347,10 +357,10 @@ public class AbilityDash extends Ability implements IAbilityDash { @Override public List getFieldDefinitions() { return Arrays.asList( - FieldDef.enumField("ability.mode", DashMode.class, this::getDashModeEnum, this::setDashModeEnum) + FieldDef.enumField("ability.dashMode", DashMode.class, this::getDashModeEnum, this::setDashModeEnum) .hover("ability.hover.dashMode"), - FieldDef.floatField("ability.dashDist", this::getDashDistance, this::setDashDistance), - FieldDef.floatField("ability.dashSpeed", this::getDashSpeed, this::setDashSpeed) + FieldDef.floatField("ability.dashDistance", this::getDashDistance, this::setDashDistance).column(ColumnHint.LEFT), + FieldDef.floatField("ability.dashSpeed", this::getDashSpeed, this::setDashSpeed).column(ColumnHint.RIGHT) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java index fb2f94646..f08092bd9 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityDisc.java @@ -17,8 +17,8 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; import net.minecraft.world.World; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; -import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityDisc; import java.util.Arrays; @@ -280,51 +280,53 @@ public class AbilityDisc extends Ability implements IAbilityDisc { public List getFieldDefinitions() { return Arrays.asList( // Type tab - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed), - FieldDef.floatField("ability.discRadius", this::getDiscRadius, this::setDiscRadius), - FieldDef.floatField("ability.discThickness", this::getDiscThickness, this::setDiscThickness), - FieldDef.boolField("ability.homing", this::isHoming, this::setHoming) - .hover("ability.hover.homing"), - FieldDef.floatField("ability.homingStr", this::getHomingStrength, this::setHomingStrength) - .visibleWhen(this::isHoming), - FieldDef.boolField("ability.boomerang", this::isBoomerang, this::setBoomerang) - .hover("ability.hover.boomerang"), - FieldDef.intField("ability.boomerangDelay", this::getBoomerangDelay, this::setBoomerangDelay) - .visibleWhen(this::isBoomerang), - FieldDef.boolField("ability.explosive", this::isExplosive, this::setExplosive) - .hover("ability.hover.explosive"), - FieldDef.floatField("ability.explosionRad", this::getExplosionRadius, this::setExplosionRadius) - .visibleWhen(this::isExplosive), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed).column(ColumnHint.RIGHT), FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), - FieldDef.floatField("ability.maxDist", this::getMaxDistance, this::setMaxDistance), - FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime), - FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects), + FieldDef.section("ability.section.disc"), + FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius).column(ColumnHint.LEFT), + FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming) + .hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength) + .visibleWhen(this::isHoming), + FieldDef.section("ability.section.boomerang"), + FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang) + .hover("ability.hover.boomerang"), + FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay) + .visibleWhen(this::isBoomerang), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive) + .hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .visibleWhen(this::isExplosive), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects), // Visual tab FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) - .tab(TabTarget.VISUAL), + .customTab("ability.tab.visual"), + FieldDef.section("ability.section.colors").customTab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) - .tab(TabTarget.VISUAL), - FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) - .tab(TabTarget.VISUAL), + .customTab("ability.tab.visual"), FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) - .tab(TabTarget.VISUAL), + .customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) - .tab(TabTarget.VISUAL) - .visibleWhen(this::isOuterColorEnabled), + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled).column(ColumnHint.LEFT), FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) - .tab(TabTarget.VISUAL).range(0, 1) - .visibleWhen(this::isOuterColorEnabled), + .customTab("ability.tab.visual").range(0, 1).visibleWhen(this::isOuterColorEnabled).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.effects").customTab("ability.tab.visual"), FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed) - .tab(TabTarget.VISUAL), + .customTab("ability.tab.visual"), FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) - .tab(TabTarget.VISUAL), - FieldDef.floatField("ability.lightningDensity", this::getLightningDensity, this::setLightningDensity) - .tab(TabTarget.VISUAL).range(0.01f, 5.0f) - .visibleWhen(this::hasLightningEffect), - FieldDef.floatField("ability.lightningRadius", this::getLightningRadius, this::setLightningRadius) - .tab(TabTarget.VISUAL).range(0.1f, 10.0f) - .visibleWhen(this::hasLightningEffect) + .customTab("ability.tab.visual"), + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .customTab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .customTab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.RIGHT) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java index 89fe6acfc..c47f6e0f0 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityEnergyBeam.java @@ -17,8 +17,8 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; import net.minecraft.world.World; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; -import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityEnergyBeam; import java.util.Arrays; @@ -283,49 +283,50 @@ public class AbilityEnergyBeam extends Ability implements IAbilityEnergyBeam { public List getFieldDefinitions() { return Arrays.asList( // Type tab - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed), - FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth), - FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize), - FieldDef.boolField("ability.homing", this::isHoming, this::setHoming) - .hover("ability.hover.homing"), - FieldDef.floatField("ability.homingStr", this::getHomingStrength, this::setHomingStrength) - .visibleWhen(this::isHoming), - FieldDef.floatField("ability.homingRange", this::getHomingRange, this::setHomingRange) - .visibleWhen(this::isHoming), - FieldDef.floatField("ability.maxDist", this::getMaxDistance, this::setMaxDistance), - FieldDef.boolField("ability.explosive", this::isExplosive, this::setExplosive) - .hover("ability.hover.explosive"), - FieldDef.floatField("ability.explosionRad", this::getExplosionRadius, this::setExplosionRadius) - .visibleWhen(this::isExplosive), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed).column(ColumnHint.RIGHT), FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), - FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime), - FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects), + FieldDef.section("ability.section.beam"), + FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth).column(ColumnHint.LEFT), + FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming) + .hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength) + .visibleWhen(this::isHoming).column(ColumnHint.LEFT), + FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange) + .visibleWhen(this::isHoming).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive) + .hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius) + .visibleWhen(this::isExplosive), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects), // Visual tab FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) - .tab(TabTarget.VISUAL), + .customTab("ability.tab.visual"), + FieldDef.section("ability.section.colors").customTab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) - .tab(TabTarget.VISUAL), - FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) - .tab(TabTarget.VISUAL), + .customTab("ability.tab.visual"), FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) - .tab(TabTarget.VISUAL), + .customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) - .tab(TabTarget.VISUAL) - .visibleWhen(this::isOuterColorEnabled), + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled).column(ColumnHint.LEFT), FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) - .tab(TabTarget.VISUAL).range(0, 1) - .visibleWhen(this::isOuterColorEnabled), + .customTab("ability.tab.visual").range(0, 1).visibleWhen(this::isOuterColorEnabled).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.effects").customTab("ability.tab.visual"), FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed) - .tab(TabTarget.VISUAL), + .customTab("ability.tab.visual"), FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) - .tab(TabTarget.VISUAL), - FieldDef.floatField("ability.lightningDensity", this::getLightningDensity, this::setLightningDensity) - .tab(TabTarget.VISUAL).range(0.01f, 5.0f) - .visibleWhen(this::hasLightningEffect), - FieldDef.floatField("ability.lightningRadius", this::getLightningRadius, this::setLightningRadius) - .tab(TabTarget.VISUAL).range(0.1f, 10.0f) - .visibleWhen(this::hasLightningEffect) + .customTab("ability.tab.visual"), + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .customTab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .customTab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.RIGHT) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java index bfa7b0f34..bdf95f882 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityGuard.java @@ -12,6 +12,7 @@ import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilityGuard; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; import kamkeel.npcs.controllers.data.ability.gui.TabTarget; @@ -44,7 +45,16 @@ public class AbilityGuard extends Ability implements IAbilityGuard { public enum CounterType { FLAT, - PERCENT + PERCENT; + + @Override + public String toString() { + switch (this) { + case FLAT: return "ability.counter.flat"; + case PERCENT: return "ability.counter.percent"; + default: return name(); + } + } } public AbilityGuard() { @@ -287,23 +297,21 @@ public class AbilityGuard extends Ability implements IAbilityGuard { @Override public List getFieldDefinitions() { return Arrays.asList( - FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks) - .range(1, 1000), - FieldDef.floatField("ability.dmgReduce", this::getDamageReduction, this::setDamageReduction), - FieldDef.boolField("ability.canCounter", this::isCanCounter, this::setCanCounter) - .hover("ability.hover.canCounter"), - FieldDef.enumField("ability.counterType", CounterType.class, this::getCounterTypeEnum, this::setCounterTypeEnum) - .hover("ability.hover.counterType") + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 1000), + FieldDef.floatField("ability.damageReduction", this::getDamageReduction, this::setDamageReduction), + FieldDef.section("ability.section.counter"), + FieldDef.boolField("gui.enabled", this::isCanCounter, this::setCanCounter).hover("ability.hover.canCounter"), + FieldDef.enumField("gui.type", CounterType.class, this::getCounterTypeEnum, this::setCounterTypeEnum) + .hover("ability.hover.counterType").visibleWhen(this::isCanCounter), + FieldDef.floatField("gui.value", this::getCounterValue, this::setCounterValue) + .visibleWhen(this::isCanCounter).column(ColumnHint.LEFT), + FieldDef.floatField("gui.chance", this::getCounterChance, this::setCounterChance) + .visibleWhen(this::isCanCounter).column(ColumnHint.RIGHT), + FieldDef.stringField("gui.sound", this::getCounterSound, this::setCounterSound) .visibleWhen(this::isCanCounter), - FieldDef.floatField("ability.counterValue", this::getCounterValue, this::setCounterValue) + FieldDef.intField("gui.animation", this::getCounterAnimationId, this::setCounterAnimationId) .visibleWhen(this::isCanCounter), - FieldDef.floatField("ability.counterChance", this::getCounterChance, this::setCounterChance) - .visibleWhen(this::isCanCounter), - FieldDef.stringField("ability.counterSound", this::getCounterSound, this::setCounterSound) - .visibleWhen(this::isCanCounter), - FieldDef.intField("ability.counterAnim", this::getCounterAnimationId, this::setCounterAnimationId) - .visibleWhen(this::isCanCounter), - FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java index cd5194b29..11e37d68a 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHazard.java @@ -16,6 +16,7 @@ import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilityHazard; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; import kamkeel.npcs.controllers.data.ability.gui.TabTarget; @@ -34,14 +35,35 @@ public class AbilityHazard extends Ability implements IAbilityHazard { public enum HazardShape { CIRCLE, RING, - CONE + CONE; + + @Override + public String toString() { + switch (this) { + case CIRCLE: return "ability.shape.circle"; + case RING: return "ability.shape.ring"; + case CONE: return "ability.shape.cone"; + default: return name(); + } + } } public enum PlacementMode { AT_CASTER, AT_TARGET, FOLLOW_CASTER, - FOLLOW_TARGET + FOLLOW_TARGET; + + @Override + public String toString() { + switch (this) { + case AT_CASTER: return "ability.placement.atCaster"; + case AT_TARGET: return "ability.placement.atTarget"; + case FOLLOW_CASTER: return "ability.placement.followCaster"; + case FOLLOW_TARGET: return "ability.placement.followTarget"; + default: return name(); + } + } } private int durationTicks = 100; @@ -612,22 +634,24 @@ public class AbilityHazard extends Ability implements IAbilityHazard { @Override public List getFieldDefinitions() { return Arrays.asList( - FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks) - .range(1, 2000), + FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000), FieldDef.enumField("ability.shape", HazardShape.class, this::getShapeEnum, this::setShapeEnum) .hover("ability.hover.shape"), FieldDef.enumField("ability.placement", PlacementMode.class, this::getPlacementEnum, this::setPlacementEnum) .hover("ability.hover.placement"), - FieldDef.floatField("ability.radius", this::getRadius, this::setRadius), - FieldDef.floatField("ability.innerRadius", this::getInnerRadius, this::setInnerRadius), - FieldDef.floatField("ability.dmgPerSec", this::getDamagePerSecond, this::setDamagePerSecond), - FieldDef.intField("ability.dmgInterval", this::getDamageInterval, this::setDamageInterval), - FieldDef.intField("ability.slownessLvl", this::getSlownessLevel, this::setSlownessLevel), - FieldDef.intField("ability.debuffDur", this::getDebuffDuration, this::setDebuffDuration), - FieldDef.intField("ability.poisonLvl", this::getPoisonLevel, this::setPoisonLevel), + FieldDef.section("ability.section.area"), + FieldDef.floatField("gui.radius", this::getRadius, this::setRadius).column(ColumnHint.LEFT), + FieldDef.floatField("ability.innerRadius", this::getInnerRadius, this::setInnerRadius).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.damage"), + FieldDef.floatField("ability.damagePerSecond", this::getDamagePerSecond, this::setDamagePerSecond).column(ColumnHint.LEFT), + FieldDef.intField("ability.damageInterval", this::getDamageInterval, this::setDamageInterval).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.debuffs"), + FieldDef.intField("ability.slownessLevel", this::getSlownessLevel, this::setSlownessLevel).column(ColumnHint.LEFT), + FieldDef.intField("ability.debuffDuration", this::getDebuffDuration, this::setDebuffDuration).column(ColumnHint.RIGHT), + FieldDef.intField("ability.poisonLevel", this::getPoisonLevel, this::setPoisonLevel), FieldDef.boolField("ability.affectsCaster", this::isAffectsCaster, this::setAffectsCaster) .hover("ability.hover.affectsCaster"), - FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java index 543a26821..764bef280 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeal.java @@ -11,6 +11,7 @@ import net.minecraft.util.AxisAlignedBB; import net.minecraft.world.World; import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityHeal; @@ -285,18 +286,18 @@ public class AbilityHeal extends Ability implements IAbilityHeal { @Override public List getFieldDefinitions() { return Arrays.asList( - FieldDef.boolField("dialog.instant", this::isInstantHeal, this::setInstantHeal) + FieldDef.boolField("ability.instantHeal", this::isInstantHeal, this::setInstantHeal) .hover("ability.hover.instant"), FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks) - .range(1, 1000) - .visibleWhen(() -> !this.isInstantHeal()), - FieldDef.floatField("ability.healAmount", this::getHealAmount, this::setHealAmount), - FieldDef.floatField("ability.healPercent", this::getHealPercent, this::setHealPercent), + .range(1, 1000).visibleWhen(() -> !this.isInstantHeal()), + FieldDef.section("ability.section.healing"), + FieldDef.floatField("ability.healAmount", this::getHealAmount, this::setHealAmount).column(ColumnHint.LEFT), + FieldDef.floatField("ability.healPercent", this::getHealPercent, this::setHealPercent).column(ColumnHint.RIGHT), FieldDef.floatField("ability.healRadius", this::getHealRadius, this::setHealRadius), FieldDef.boolField("ability.healSelf", this::isHealSelf, this::setHealSelf) - .hover("ability.hover.healSelf"), + .hover("ability.hover.healSelf").column(ColumnHint.LEFT), FieldDef.boolField("ability.healAllies", this::isHealAllies, this::setHealAllies) - .hover("ability.hover.healAllies") + .hover("ability.hover.healAllies").column(ColumnHint.RIGHT) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java index a86662304..0eca4f4c7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityHeavyHit.java @@ -7,6 +7,7 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType; import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.world.World; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityHeavyHit; @@ -110,9 +111,9 @@ public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit { @Override public List getFieldDefinitions() { return Arrays.asList( - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), - FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.RIGHT), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java index 4d3ce9fbe..bc33f689b 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityLaserShot.java @@ -6,8 +6,8 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; -import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import kamkeel.npcs.controllers.data.ability.data.EnergyCombatData; import kamkeel.npcs.controllers.data.ability.data.EnergyLifespanData; import kamkeel.npcs.controllers.data.ability.data.EnergyLightningData; @@ -231,39 +231,34 @@ public class AbilityLaserShot extends Ability implements IAbilityLaserShot { return Arrays.asList( // Type tab FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("ability.laserWidth", this::getLaserWidth, this::setLaserWidth), - FieldDef.floatField("ability.expansionSpeed", this::getExpansionSpeed, this::setExpansionSpeed), - FieldDef.intField("ability.lingerTicks", this::getLingerTicks, this::setLingerTicks), - FieldDef.floatField("ability.maxDist", this::getMaxDistance, this::setMaxDistance), - FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime), - FieldDef.boolField("ability.explosive", this::isExplosive, this::setExplosive) - .hover("ability.hover.explosive"), - FieldDef.floatField("ability.explosionRad", this::getExplosionRadius, this::setExplosionRadius) - .visibleWhen(this::isExplosive), - FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), - FieldDef.floatField("ability.knockbackUp", this::getKnockbackUp, this::setKnockbackUp), - FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockbackUp", this::getKnockbackUp, this::setKnockbackUp).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.beam"), + FieldDef.floatField("ability.laserWidth", this::getLaserWidth, this::setLaserWidth).column(ColumnHint.LEFT), + FieldDef.floatField("ability.expansionSpeed", this::getExpansionSpeed, this::setExpansionSpeed).column(ColumnHint.RIGHT), + FieldDef.intField("ability.lingerTicks", this::getLingerTicks, this::setLingerTicks).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects), // Visual tab - FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) - .tab(TabTarget.VISUAL), + FieldDef.section("ability.section.colors").customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).customTab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).customTab("ability.tab.visual"), FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) - .tab(TabTarget.VISUAL), - FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) - .tab(TabTarget.VISUAL), + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) - .tab(TabTarget.VISUAL) - .visibleWhen(this::isOuterColorEnabled), + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled).column(ColumnHint.LEFT), FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) - .tab(TabTarget.VISUAL).range(0, 1) - .visibleWhen(this::isOuterColorEnabled), - FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) - .tab(TabTarget.VISUAL), - FieldDef.floatField("ability.lightningDensity", this::getLightningDensity, this::setLightningDensity) - .tab(TabTarget.VISUAL).range(0.01f, 5.0f) - .visibleWhen(this::hasLightningEffect), - FieldDef.floatField("ability.lightningRadius", this::getLightningRadius, this::setLightningRadius) - .tab(TabTarget.VISUAL).range(0.1f, 10.0f) - .visibleWhen(this::hasLightningEffect) + .customTab("ability.tab.visual").range(0, 1).visibleWhen(this::isOuterColorEnabled).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.effects").customTab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect).customTab("ability.tab.visual"), + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .customTab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .customTab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.RIGHT) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java index f4c156f49..66ebdf2c7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityOrb.java @@ -17,8 +17,8 @@ import net.minecraft.entity.EntityLivingBase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.util.Vec3; import net.minecraft.world.World; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; -import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityOrb; import java.util.Arrays; @@ -419,47 +419,38 @@ public class AbilityOrb extends Ability implements IAbilityOrb { public List getFieldDefinitions() { return Arrays.asList( // Type tab - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed), - FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize), - FieldDef.floatField("ability.maxDist", this::getMaxDistance, this::setMaxDistance), - FieldDef.boolField("ability.homing", this::isHoming, this::setHoming) - .hover("ability.hover.homing"), - FieldDef.floatField("ability.homingStr", this::getHomingStrength, this::setHomingStrength) - .visibleWhen(this::isHoming), - FieldDef.boolField("ability.explosive", this::isExplosive, this::setExplosive) - .hover("ability.hover.explosive"), - FieldDef.floatField("ability.explosionRad", this::getExplosionRadius, this::setExplosionRadius) - .visibleWhen(this::isExplosive), - FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), - FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime), - FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed).column(ColumnHint.RIGHT), + FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.RIGHT), + FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).column(ColumnHint.LEFT), + FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects), // Visual tab FieldDef.enumField("ability.anchorPoint", AnchorPoint.class, this::getAnchorPointEnum, this::setAnchorPointEnum) - .tab(TabTarget.VISUAL), - FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) - .tab(TabTarget.VISUAL), - FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed) - .tab(TabTarget.VISUAL), - FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) - .tab(TabTarget.VISUAL), - FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) - .tab(TabTarget.VISUAL).range(0, 1) - .visibleWhen(this::isOuterColorEnabled), + .customTab("ability.tab.visual"), + FieldDef.section("ability.section.colors").customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor).customTab("ability.tab.visual"), + FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled).customTab("ability.tab.visual"), FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) - .tab(TabTarget.VISUAL) - .visibleWhen(this::isOuterColorEnabled), + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) - .tab(TabTarget.VISUAL) - .visibleWhen(this::isOuterColorEnabled), - FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect) - .tab(TabTarget.VISUAL), - FieldDef.floatField("ability.lightningDensity", this::getLightningDensity, this::setLightningDensity) - .tab(TabTarget.VISUAL).range(0.01f, 5.0f) - .visibleWhen(this::hasLightningEffect), - FieldDef.floatField("ability.lightningRadius", this::getLightningRadius, this::setLightningRadius) - .tab(TabTarget.VISUAL).range(0.1f, 10.0f) - .visibleWhen(this::hasLightningEffect) + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled).column(ColumnHint.LEFT), + FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha) + .customTab("ability.tab.visual").range(0, 1).visibleWhen(this::isOuterColorEnabled).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.effects").customTab("ability.tab.visual"), + FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed).customTab("ability.tab.visual"), + FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect).customTab("ability.tab.visual"), + FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity) + .customTab("ability.tab.visual").range(0.01f, 5.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius) + .customTab("ability.tab.visual").range(0.1f, 10.0f).visibleWhen(this::hasLightningEffect).column(ColumnHint.RIGHT) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java index 03b815441..902c9a194 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityProjectile.java @@ -12,6 +12,7 @@ import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilityProjectile; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; import kamkeel.npcs.controllers.data.ability.gui.TabTarget; @@ -255,19 +256,17 @@ public class AbilityProjectile extends Ability implements IAbilityProjectile { @Override public List getFieldDefinitions() { return Arrays.asList( - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed).column(ColumnHint.RIGHT), FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), - FieldDef.stringEnumField("ability.projType", new String[]{"fireball", "arrow", "magic"}, this::getProjectileType, this::setProjectileType), - FieldDef.boolField("ability.explosive", this::isExplosive, this::setExplosive) - .hover("ability.hover.explosive"), - FieldDef.floatField("ability.explosionRad", this::getExplosionRadius, this::setExplosionRadius) - .visibleWhen(this::isExplosive), - FieldDef.boolField("ability.homing", this::isHoming, this::setHoming) - .hover("ability.hover.homing"), - FieldDef.floatField("ability.homingStr", this::getHomingStrength, this::setHomingStrength) - .visibleWhen(this::isHoming), - FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + FieldDef.stringEnumField("ability.projectileType", new String[]{"fireball", "arrow", "magic"}, this::getProjectileType, this::setProjectileType), + FieldDef.section("ability.section.homing"), + FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"), + FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming), + FieldDef.section("ability.section.explosive"), + FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"), + FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java index 83af93df7..768496ec7 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityShockwave.java @@ -11,6 +11,7 @@ import net.minecraft.util.DamageSource; import net.minecraft.world.World; import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityShockwave; @@ -171,10 +172,11 @@ public class AbilityShockwave extends Ability implements IAbilityShockwave { public List getFieldDefinitions() { return Arrays.asList( FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("ability.pushRadius", this::getPushRadius, this::setPushRadius), - FieldDef.floatField("ability.pushStrength", this::getPushStrength, this::setPushStrength), + FieldDef.section("ability.section.push"), + FieldDef.floatField("gui.radius", this::getPushRadius, this::setPushRadius).column(ColumnHint.LEFT), + FieldDef.floatField("gui.strength", this::getPushStrength, this::setPushStrength).column(ColumnHint.RIGHT), FieldDef.intField("ability.maxTargets", this::getMaxTargets, this::setMaxTargets), - FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java index b26e203c1..d5a2599fd 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySlam.java @@ -18,6 +18,7 @@ import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilitySlam; import somehussar.gui.annotationHandling.GuiEditable; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import java.util.Arrays; @@ -572,11 +573,12 @@ public class AbilitySlam extends Ability implements IAbilitySlam { public List getFieldDefinitions() { return Arrays.asList( FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("ability.radius", this::getRadius, this::setRadius), - FieldDef.floatField("ability.knockback", this::getKnockbackStrength, this::setKnockbackStrength), - FieldDef.floatField("ability.leapSpeed", this::getLeapSpeed, this::setLeapSpeed), - FieldDef.floatField("ability.leapHeight", this::getLeapHeight, this::setLeapHeight), - FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + FieldDef.floatField("gui.radius", this::getRadius, this::setRadius).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockback", this::getKnockbackStrength, this::setKnockbackStrength).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.leap"), + FieldDef.floatField("gui.speed", this::getLeapSpeed, this::setLeapSpeed).column(ColumnHint.LEFT), + FieldDef.floatField("gui.height", this::getLeapHeight, this::setLeapHeight).column(ColumnHint.RIGHT), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java index ef00df35e..783f0c1f6 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilitySweeper.java @@ -4,8 +4,8 @@ import kamkeel.npcs.controllers.data.ability.Ability; import kamkeel.npcs.controllers.data.ability.LockMovementType; import kamkeel.npcs.controllers.data.ability.TargetingMode; import kamkeel.npcs.controllers.data.ability.data.EnergyColorData; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; -import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import kamkeel.npcs.controllers.data.telegraph.Telegraph; import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance; import kamkeel.npcs.controllers.data.telegraph.TelegraphType; @@ -203,29 +203,29 @@ public class AbilitySweeper extends Ability implements IAbilitySweeper { @Override public List getFieldDefinitions() { return Arrays.asList( - // Type tab - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.intField("ability.dmgInterval", this::getDamageInterval, this::setDamageInterval), - FieldDef.floatField("ability.beamLength", this::getBeamLength, this::setBeamLength), - FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth), - FieldDef.floatField("ability.beamHeight", this::getBeamHeight, this::setBeamHeight), - FieldDef.floatField("ability.sweepSpeed", this::getSweepSpeed, this::setSweepSpeed), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.intField("ability.damageInterval", this::getDamageInterval, this::setDamageInterval).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.beam"), + FieldDef.floatField("gui.length", this::getBeamLength, this::setBeamLength).column(ColumnHint.LEFT), + FieldDef.floatField("gui.width", this::getBeamWidth, this::setBeamWidth).column(ColumnHint.RIGHT), + FieldDef.floatField("gui.height", this::getBeamHeight, this::setBeamHeight).column(ColumnHint.LEFT), + FieldDef.floatField("ability.sweepSpeed", this::getSweepSpeed, this::setSweepSpeed).column(ColumnHint.RIGHT), FieldDef.intField("ability.rotations", this::getNumberOfRotations, this::setNumberOfRotations), FieldDef.boolField("ability.piercing", this::isPiercing, this::setPiercing) - .hover("ability.hover.piercing"), + .hover("ability.hover.piercing").column(ColumnHint.LEFT), FieldDef.boolField("ability.lockTarget", this::isLockOnTarget, this::setLockOnTarget) - .hover("ability.hover.lockTarget"), - FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects), + .hover("ability.hover.lockTarget").column(ColumnHint.RIGHT), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects), // Visual tab + FieldDef.section("ability.section.colors").customTab("ability.tab.visual"), FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor) - .tab(TabTarget.VISUAL), - FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) - .tab(TabTarget.VISUAL), + .customTab("ability.tab.visual"), FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled) - .tab(TabTarget.VISUAL), + .customTab("ability.tab.visual"), + FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor) + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled), FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth) - .tab(TabTarget.VISUAL) - .visibleWhen(this::isOuterColorEnabled) + .customTab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java index 284bdec03..28ad00cd2 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTeleport.java @@ -16,6 +16,7 @@ import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilityTeleport; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; import kamkeel.npcs.controllers.data.ability.gui.TabTarget; @@ -34,7 +35,17 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { public enum TeleportMode { BLINK, BEHIND, - SINGLE + SINGLE; + + @Override + public String toString() { + switch (this) { + case BLINK: return "ability.teleport.blink"; + case BEHIND: return "ability.teleport.behind"; + case SINGLE: return "ability.teleport.single"; + default: return name(); + } + } } private static final Random RANDOM = new Random(); @@ -534,21 +545,22 @@ public class AbilityTeleport extends Ability implements IAbilityTeleport { FieldDef.floatField("ability.blinkRadius", this::getBlinkRadius, this::setBlinkRadius) .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK || this.getModeEnum() == TeleportMode.SINGLE), FieldDef.intField("ability.blinkCount", this::getBlinkCount, this::setBlinkCount) - .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK), + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK).column(ColumnHint.LEFT), FieldDef.intField("ability.blinkDelay", this::getBlinkDelayTicks, this::setBlinkDelayTicks) - .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK), + .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK).column(ColumnHint.RIGHT), FieldDef.floatField("ability.behindDistance", this::getBehindDistance, this::setBehindDistance) .visibleWhen(() -> this.getModeEnum() == TeleportMode.BEHIND), FieldDef.boolField("ability.lineOfSight", this::isRequireLineOfSight, this::setRequireLineOfSight) .hover("ability.hover.lineOfSight") .visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK || this.getModeEnum() == TeleportMode.SINGLE), - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("ability.dmgRadius", this::getDamageRadius, this::setDamageRadius), - FieldDef.boolField("ability.dmgAtStart", this::isDamageAtStart, this::setDamageAtStart) - .hover("ability.hover.dmgAtStart"), - FieldDef.boolField("ability.dmgAtEnd", this::isDamageAtEnd, this::setDamageAtEnd) - .hover("ability.hover.dmgAtEnd"), - FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + FieldDef.section("ability.section.damage"), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getDamageRadius, this::setDamageRadius).column(ColumnHint.RIGHT), + FieldDef.boolField("ability.damageAtStart", this::isDamageAtStart, this::setDamageAtStart) + .hover("ability.hover.dmgAtStart").column(ColumnHint.LEFT), + FieldDef.boolField("ability.damageAtEnd", this::isDamageAtEnd, this::setDamageAtEnd) + .hover("ability.hover.dmgAtEnd").column(ColumnHint.RIGHT), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java index 2cb7a8a26..e27e28740 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityTrap.java @@ -14,6 +14,7 @@ import noppes.npcs.entity.EntityNPCInterface; import noppes.npcs.api.ability.type.IAbilityTrap; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; import kamkeel.npcs.controllers.data.ability.gui.TabTarget; @@ -33,7 +34,17 @@ public class AbilityTrap extends Ability implements IAbilityTrap { public enum TrapPlacement { AT_CASTER, AT_TARGET, - AHEAD_OF_CASTER + AHEAD_OF_CASTER; + + @Override + public String toString() { + switch (this) { + case AT_CASTER: return "ability.trapPlace.atCaster"; + case AT_TARGET: return "ability.trapPlace.atTarget"; + case AHEAD_OF_CASTER: return "ability.trapPlace.aheadOfCaster"; + default: return name(); + } + } } private int durationTicks = 200; @@ -465,13 +476,16 @@ public class AbilityTrap extends Ability implements IAbilityTrap { .range(1, 2000), FieldDef.enumField("ability.placement", TrapPlacement.class, this::getPlacementEnum, this::setPlacementEnum) .hover("ability.hover.placement"), - FieldDef.floatField("ability.triggerRad", this::getTriggerRadius, this::setTriggerRadius), - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("ability.dmgRadius", this::getDamageRadius, this::setDamageRadius), - FieldDef.intField("ability.armTime", this::getArmTime, this::setArmTime), - FieldDef.intField("ability.maxTriggers", this::getMaxTriggers, this::setMaxTriggers), + FieldDef.section("ability.section.trigger"), + FieldDef.floatField("gui.radius", this::getTriggerRadius, this::setTriggerRadius).column(ColumnHint.LEFT), + FieldDef.intField("ability.armTime", this::getArmTime, this::setArmTime).column(ColumnHint.RIGHT), + FieldDef.intField("ability.maxTriggers", this::getMaxTriggers, this::setMaxTriggers).column(ColumnHint.LEFT), + FieldDef.intField("ability.triggerCooldown", this::getTriggerCooldown, this::setTriggerCooldown).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.damage"), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("gui.radius", this::getDamageRadius, this::setDamageRadius).column(ColumnHint.RIGHT), FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), - FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) ); } } diff --git a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java index 61e246bc9..3304cad74 100644 --- a/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java +++ b/src/main/java/kamkeel/npcs/controllers/data/ability/type/AbilityVortex.java @@ -10,6 +10,7 @@ import net.minecraft.util.AxisAlignedBB; import net.minecraft.world.World; import noppes.npcs.entity.EntityNPCInterface; +import kamkeel.npcs.controllers.data.ability.gui.ColumnHint; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import noppes.npcs.api.ability.type.IAbilityVortex; @@ -304,17 +305,22 @@ public class AbilityVortex extends Ability implements IAbilityVortex { @Override public List getFieldDefinitions() { return Arrays.asList( - FieldDef.floatField("ability.pullRadius", this::getPullRadius, this::setPullRadius), - FieldDef.floatField("ability.pullStrength", this::getPullStrength, this::setPullStrength), - FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage), - FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback), - FieldDef.boolField("ability.aoe", this::isAoe, this::setAoe) + FieldDef.floatField("ability.pullRadius", this::getPullRadius, this::setPullRadius).column(ColumnHint.LEFT), + FieldDef.floatField("ability.pullStrength", this::getPullStrength, this::setPullStrength).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.damage"), + FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage).column(ColumnHint.LEFT), + FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback).column(ColumnHint.RIGHT), + FieldDef.section("ability.section.aoe"), + FieldDef.boolField("gui.enabled", this::isAoe, this::setAoe) .hover("ability.hover.aoe"), - FieldDef.intField("ability.maxTargets", this::getMaxTargets, this::setMaxTargets), - FieldDef.boolField("ability.dmgOnPull", this::isDamageOnPull, this::setDamageOnPull) + FieldDef.intField("ability.maxTargets", this::getMaxTargets, this::setMaxTargets) + .visibleWhen(this::isAoe), + FieldDef.section("ability.section.pullDamage"), + FieldDef.boolField("gui.enabled", this::isDamageOnPull, this::setDamageOnPull) .hover("ability.hover.dmgOnPull"), - FieldDef.floatField("ability.pullDamage", this::getPullDamage, this::setPullDamage), - FieldDef.effectsSubGui("ability.effects", this::getEffects, this::setEffects) + FieldDef.floatField("enchantment.damage", this::getPullDamage, this::setPullDamage) + .visibleWhen(this::isDamageOnPull), + FieldDef.effectsListField("ability.effects", this::getEffects, this::setEffects) ); } } diff --git a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java index 9c862e3b0..04fa3f7f9 100644 --- a/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java +++ b/src/main/java/noppes/npcs/client/gui/advanced/SubGuiAbilityConfig.java @@ -1,8 +1,10 @@ package noppes.npcs.client.gui.advanced; import kamkeel.npcs.controllers.data.ability.Ability; +import kamkeel.npcs.controllers.data.ability.AbilityEffect; import kamkeel.npcs.controllers.data.ability.Condition; import kamkeel.npcs.controllers.data.ability.gui.FieldDef; +import kamkeel.npcs.controllers.data.ability.gui.FieldDefinitionsBuilder; import kamkeel.npcs.controllers.data.ability.gui.FieldType; import kamkeel.npcs.controllers.data.ability.gui.TabTarget; import net.minecraft.client.gui.GuiButton; @@ -19,58 +21,62 @@ import noppes.npcs.client.gui.util.ITextfieldListener; import noppes.npcs.client.gui.util.SubGuiInterface; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldListener, ISubGuiListener { private static final int DECLARATIVE_ID_START = 1000; private static final int CLEAR_ID_START = 2000; + private static final int LABEL_ID_START = 3000; + // Fixed tab indices private static final int TAB_GENERAL = 0; private static final int TAB_TYPE = 1; private static final int TAB_TARGET = 2; private static final int TAB_EFFECTS = 3; - private static final int TAB_VISUAL = 4; + // Custom tabs start at index 4 + + private static final int L_LABEL_X = 5; + private static final int ROW_H = 24; private final Ability ability; private final IAbilityConfigCallback callback; private int activeTab = TAB_GENERAL; private List fieldDefs; - private boolean hasTypeFields; - private boolean hasVisualFields; + private List customTabNames; - // Conditions are complex (list with add/edit/remove) - cached separately private List conditions; private int editingConditionIndex = -1; - // Event routing - private final Map buttonFieldMap = new HashMap<>(); - private final Map textFieldMap = new HashMap<>(); - private final Map clearFieldMap = new HashMap<>(); + private FieldDefinitionsBuilder builder; private FieldDef activeSubGuiField = null; + // Scroll position preservation per tab + private float[] tabScrollY; + public SubGuiAbilityConfig(Ability ability, IAbilityConfigCallback callback) { this.ability = ability; this.callback = callback; this.conditions = new ArrayList<>(ability.getConditions()); this.fieldDefs = ability.getAllFieldDefinitions(); - this.hasTypeFields = hasFieldsForTab(TabTarget.TYPE); - this.hasVisualFields = hasFieldsForTab(TabTarget.VISUAL); + discoverCustomTabs(); setBackground("menubg.png", 217); xSize = 356; ySize = 200; } - private boolean hasFieldsForTab(TabTarget target) { + private void discoverCustomTabs() { + customTabNames = new ArrayList<>(); for (FieldDef def : fieldDefs) { - if (def.getTab() == target) return true; + if (def.getTab() == TabTarget.CUSTOM && def.getCustomTabName() != null + && !customTabNames.contains(def.getCustomTabName())) { + customTabNames.add(def.getCustomTabName()); + } } - return false; + tabScrollY = new float[4 + customTabNames.size()]; } // ═══════════════════════════════════════════════════════════════════════════ @@ -79,23 +85,24 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi @Override public void initGui() { - super.initGui(); - buttonFieldMap.clear(); - textFieldMap.clear(); - clearFieldMap.clear(); + // Save scroll position before super clears scroll windows + GuiScrollWindow oldSw = getScrollableGui(0); + if (oldSw != null && activeTab < tabScrollY.length) { + tabScrollY[activeTab] = oldSw.nextScrollY; + } - // Tab buttons + super.initGui(); + + // Tab buttons — always show General, Type, Target, Effects GuiMenuTopButton generalTab = new GuiMenuTopButton(90, guiLeft + 4, guiTop - 17, "menu.general"); generalTab.active = (activeTab == TAB_GENERAL); addTopButton(generalTab); GuiMenuTopButton lastTab = generalTab; - if (hasTypeFields) { - GuiMenuTopButton typeTab = new GuiMenuTopButton(91, lastTab, "gui.type"); - typeTab.active = (activeTab == TAB_TYPE); - addTopButton(typeTab); - lastTab = typeTab; - } + GuiMenuTopButton typeTab = new GuiMenuTopButton(91, lastTab, "gui.type"); + typeTab.active = (activeTab == TAB_TYPE); + addTopButton(typeTab); + lastTab = typeTab; GuiMenuTopButton targetTab = new GuiMenuTopButton(92, lastTab, "script.target"); targetTab.active = (activeTab == TAB_TARGET); @@ -107,194 +114,117 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi addTopButton(effectsTab); lastTab = effectsTab; - if (hasVisualFields) { - GuiMenuTopButton visualTab = new GuiMenuTopButton(94, lastTab, "ability.tab.visual"); - visualTab.active = (activeTab == TAB_VISUAL); - addTopButton(visualTab); + // Custom tabs (Visual, etc.) + for (int i = 0; i < customTabNames.size(); i++) { + GuiMenuTopButton ct = new GuiMenuTopButton(94 + i, lastTab, customTabNames.get(i)); + ct.active = (activeTab == 4 + i); + addTopButton(ct); + lastTab = ct; } GuiMenuTopButton closeBtn = new GuiMenuTopButton(-1000, guiLeft + xSize - 22, guiTop - 17, "X"); addTopButton(closeBtn); - // Scroll window for tab content + // Scroll window int swX = guiLeft + 4; int swY = guiTop + 5; int swW = xSize - 8; int swH = ySize - 10; GuiScrollWindow sw = new GuiScrollWindow(this, swX, swY, swW, swH, 0); + sw.backgroundColor = 0x88000000; + addScrollableGui(0, sw); - TabTarget tabTarget = tabTargetForIndex(activeTab); int y = 5; - int idCounter = DECLARATIVE_ID_START; - int clearCounter = CLEAR_ID_START; + int labelCounter = LABEL_ID_START; - // General tab: show type label at top + // General tab: type label at top if (activeTab == TAB_GENERAL) { - sw.addLabel(new GuiNpcLabel(1, "gui.type", 5, y + 5)); - sw.addLabel(new GuiNpcLabel(2, ability.getTypeId(), 50, y + 5)); - y += 24; + sw.addLabel(new GuiNpcLabel(labelCounter++, "gui.type", 5, y + 5, 0xFFFFFF)); + sw.addLabel(new GuiNpcLabel(labelCounter++, ability.getTypeId(), 55, y + 5, 0xFFFFFF)); + y += ROW_H; } - // Render declarative fields for this tab - List tabFields = getVisibleFieldsForTab(tabTarget); - for (FieldDef def : tabFields) { - int widgetId = idCounter++; - int clearId = clearCounter++; - y = renderField(sw, def, widgetId, clearId, y); - } + // Build declarative fields using the builder + List tabFields = getVisibleFieldsForTab(activeTab); + builder = new FieldDefinitionsBuilder(this, sw, fontRendererObj) + .startIds(DECLARATIVE_ID_START, CLEAR_ID_START, labelCounter) + .startY(y); - // Target tab: append conditions list + y = builder.build(tabFields); + + // Conditions on target tab if (activeTab == TAB_TARGET) { - y = renderConditions(sw, y); + y = renderConditions(sw, y, builder.getNextLabelId()); } sw.maxScrollY = Math.max(y - swH, 0); - addScrollableGui(0, sw); - } - private TabTarget tabTargetForIndex(int tab) { - switch (tab) { - case TAB_GENERAL: return TabTarget.GENERAL; - case TAB_TYPE: return TabTarget.TYPE; - case TAB_TARGET: return TabTarget.TARGET; - case TAB_EFFECTS: return TabTarget.EFFECTS; - case TAB_VISUAL: return TabTarget.VISUAL; - default: return TabTarget.TYPE; + // Restore scroll position + if (activeTab < tabScrollY.length) { + float restored = Math.min(tabScrollY[activeTab], sw.maxScrollY); + sw.nextScrollY = restored; + sw.scrollY = restored; } } - private List getVisibleFieldsForTab(TabTarget target) { + private List getVisibleFieldsForTab(int tabIndex) { List result = new ArrayList<>(); - for (FieldDef def : fieldDefs) { - if (def.getTab() == target && def.isVisible()) { - result.add(def); + + if (tabIndex >= 4) { + // Custom tab — match by custom tab name + int customIndex = tabIndex - 4; + if (customIndex >= 0 && customIndex < customTabNames.size()) { + String tabName = customTabNames.get(customIndex); + for (FieldDef def : fieldDefs) { + if (def.getTab() == TabTarget.CUSTOM + && tabName.equals(def.getCustomTabName()) + && def.isVisible()) { + result.add(def); + } + } + } + } else { + // Fixed tab + TabTarget target; + switch (tabIndex) { + case TAB_GENERAL: target = TabTarget.GENERAL; break; + case TAB_TYPE: target = TabTarget.TYPE; break; + case TAB_TARGET: target = TabTarget.TARGET; break; + case TAB_EFFECTS: target = TabTarget.EFFECTS; break; + default: target = TabTarget.TYPE; break; + } + for (FieldDef def : fieldDefs) { + if (def.getTab() == target && def.isVisible()) { + result.add(def); + } } } + return result; } // ═══════════════════════════════════════════════════════════════════════════ - // FIELD RENDERING + // CONDITIONS (Target tab) // ═══════════════════════════════════════════════════════════════════════════ - @SuppressWarnings({"unchecked", "rawtypes"}) - private int renderField(GuiScrollWindow sw, FieldDef def, int widgetId, int clearId, int y) { - int labelX = 5; - int fieldX = 100; - int fieldW = 60; - - sw.addLabel(new GuiNpcLabel(widgetId, def.getLabel(), labelX, y + 5)); - - switch (def.getType()) { - case FLOAT: { - float fVal = def.getValue() instanceof Number ? ((Number) def.getValue()).floatValue() : 0f; - GuiNpcTextField tf = new GuiNpcTextField(widgetId, this, fontRendererObj, fieldX, y, fieldW, 20, String.format("%.2f", fVal)); - tf.setFloatsOnly(); - if (def.hasRange()) tf.setMinMaxDefaultFloat(def.getMin(), def.getMax(), fVal); - if (def.getTooltip() != null) tf.setHoverText(def.getTooltip()); - sw.addTextField(tf); - textFieldMap.put(widgetId, def); - break; - } - case INT: { - int iVal = def.getValue() instanceof Number ? ((Number) def.getValue()).intValue() : 0; - GuiNpcTextField tf = new GuiNpcTextField(widgetId, this, fontRendererObj, fieldX, y, fieldW, 20, String.valueOf(iVal)); - tf.setIntegersOnly(); - if (def.hasRange()) tf.setMinMaxDefault((int) def.getMin(), (int) def.getMax(), iVal); - if (def.getTooltip() != null) tf.setHoverText(def.getTooltip()); - sw.addTextField(tf); - textFieldMap.put(widgetId, def); - break; - } - case STRING: { - String sVal = def.getValue() != null ? def.getValue().toString() : ""; - GuiNpcTextField tf = new GuiNpcTextField(widgetId, this, fontRendererObj, fieldX, y, fieldW + 40, 20, sVal); - if (def.getTooltip() != null) tf.setHoverText(def.getTooltip()); - sw.addTextField(tf); - textFieldMap.put(widgetId, def); - break; - } - case BOOLEAN: { - boolean bVal = def.getValue() instanceof Boolean ? (Boolean) def.getValue() : false; - GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, 40, 20, new String[]{"gui.no", "gui.yes"}, bVal ? 1 : 0); - if (def.getHoverText() != null) btn.setHoverText(def.getHoverText()); - sw.addButton(btn); - buttonFieldMap.put(widgetId, def); - break; - } - case ENUM: { - Class> enumClass = def.getEnumClass(); - if (enumClass != null) { - Enum[] constants = enumClass.getEnumConstants(); - String[] names = new String[constants.length]; - for (int i = 0; i < constants.length; i++) names[i] = constants[i].toString(); - int selected = def.getValue() instanceof Enum ? ((Enum) def.getValue()).ordinal() : 0; - GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, 80, 20, names, selected); - if (def.getHoverText() != null) btn.setHoverText(def.getHoverText()); - sw.addButton(btn); - buttonFieldMap.put(widgetId, def); - } - break; - } - case STRING_ENUM: { - String[] values = def.getStringEnumValues(); - if (values != null && values.length > 0) { - String curVal = def.getStringEnumValue(); - int selected = 0; - for (int i = 0; i < values.length; i++) { - if (values[i].equals(curVal)) { selected = i; break; } - } - GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, 80, 20, values, selected); - if (def.getHoverText() != null) btn.setHoverText(def.getHoverText()); - sw.addButton(btn); - buttonFieldMap.put(widgetId, def); - } - break; - } - case SUB_GUI: { - String btnText = def.getButtonLabel(); - int btnW = def.hasClearAction() ? 60 : 80; - GuiNpcButton btn = new GuiNpcButton(widgetId, fieldX, y, btnW, 20, btnText); - Integer textColor = def.getButtonTextColor(); - if (textColor != null) btn.setTextColor(textColor); - if (def.getHoverText() != null) btn.setHoverText(def.getHoverText()); - sw.addButton(btn); - buttonFieldMap.put(widgetId, def); - - if (def.hasClearAction()) { - GuiNpcButton clearBtn = new GuiNpcButton(clearId, fieldX + btnW + 5, y, 20, 20, "X"); - sw.addButton(clearBtn); - clearFieldMap.put(clearId, def); - } - break; - } - } - return y + 24; - } - - // ═══════════════════════════════════════════════════════════════════════════ - // CONDITIONS (Target tab special handling) - // ═══════════════════════════════════════════════════════════════════════════ - - private int renderConditions(GuiScrollWindow sw, int y) { - int x = 5; - - sw.addLabel(new GuiNpcLabel(50, "ability.conditions", x, y)); - y += 14; + private int renderConditions(GuiScrollWindow sw, int y, int labelCounter) { + y += 3; + sw.addLabel(new GuiNpcLabel(labelCounter, "ability.conditions", L_LABEL_X, y + 2, 0xFFFF55)); + y += 15; for (int i = 0; i < conditions.size() && i < 3; i++) { Condition cond = conditions.get(i); String condName = getConditionDisplayName(cond); - sw.addButton(new GuiNpcButton(50 + i * 10, x, y, 140, 20, condName)); - sw.addButton(new GuiNpcButton(51 + i * 10, x + 145, y, 40, 20, "gui.edit")); - sw.addButton(new GuiNpcButton(52 + i * 10, x + 190, y, 20, 20, "X")); + sw.addButton(new GuiNpcButton(50 + i * 10, L_LABEL_X, y, 140, 20, condName)); + sw.addButton(new GuiNpcButton(51 + i * 10, L_LABEL_X + 145, y, 40, 20, "gui.edit")); + sw.addButton(new GuiNpcButton(52 + i * 10, L_LABEL_X + 190, y, 20, 20, "X")); y += 22; } if (conditions.size() < 3) { - sw.addButton(new GuiNpcButton(80, x, y, 50, 20, "gui.add")); - y += 24; + sw.addButton(new GuiNpcButton(80, L_LABEL_X, y, 50, 20, "gui.add")); + y += ROW_H; } return y; @@ -334,12 +264,17 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi public void buttonEvent(GuiButton guibutton) { int id = guibutton.id; - // Tab switching + // Tab switching — fixed tabs if (id == 90) { activeTab = TAB_GENERAL; initGui(); return; } if (id == 91) { activeTab = TAB_TYPE; initGui(); return; } if (id == 92) { activeTab = TAB_TARGET; initGui(); return; } if (id == 93) { activeTab = TAB_EFFECTS; initGui(); return; } - if (id == 94) { activeTab = TAB_VISUAL; initGui(); return; } + // Custom tabs (94+) + if (id >= 94 && id < 94 + customTabNames.size()) { + activeTab = 4 + (id - 94); + initGui(); + return; + } if (id == -1000) { close(); return; } // Condition buttons (50-80) @@ -367,9 +302,19 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi return; } - // Clear buttons + // Clear buttons (effects delete or SUB_GUI clear) if (id >= CLEAR_ID_START) { - FieldDef def = clearFieldMap.get(id); + // Check effects list delete first + int[] meta = builder.getEffectWidgetMeta().get(id); + if (meta != null && meta[1] == 3) { + FieldDef def = builder.getClearFieldMap().get(id); + if (def != null && def.getType() == FieldType.EFFECTS_LIST) { + handleEffectButton(def, meta, guibutton); + return; + } + } + // SUB_GUI clear + FieldDef def = builder.getClearFieldMap().get(id); if (def != null && def.hasClearAction()) { def.getClearAction().run(); initGui(); @@ -379,9 +324,16 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi // Declarative field buttons if (id >= DECLARATIVE_ID_START) { - FieldDef def = buttonFieldMap.get(id); + FieldDef def = builder.getButtonFieldMap().get(id); if (def == null) return; + // Effects list buttons (type, amp, add) + int[] meta = builder.getEffectWidgetMeta().get(id); + if (meta != null && def.getType() == FieldType.EFFECTS_LIST) { + handleEffectButton(def, meta, guibutton); + return; + } + switch (def.getType()) { case BOOLEAN: def.setValue(((GuiNpcButton) guibutton).getValue() == 1); @@ -412,16 +364,63 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi } } + @SuppressWarnings("unchecked") + private void handleEffectButton(FieldDef def, int[] meta, GuiButton btn) { + List effects = (List) def.getValue(); + if (effects == null) return; + int effectIdx = meta[0]; + int action = meta[1]; + + switch (action) { + case 0: // Type changed + if (effectIdx < effects.size()) { + effects.get(effectIdx).setType(AbilityEffect.EffectType.fromOrdinal(((GuiNpcButton) btn).getValue())); + } + break; + case 2: // Amp changed + if (effectIdx < effects.size()) { + effects.get(effectIdx).setAmplifier(((GuiNpcButton) btn).getValue()); + } + break; + case 3: // Delete + if (effectIdx < effects.size()) { + effects.remove(effectIdx); + initGui(); + } + break; + case 4: // Add + if (effects.size() < 5) { + effects.add(new AbilityEffect(AbilityEffect.EffectType.SLOWNESS, 60, 0)); + initGui(); + } + break; + } + } + // ═══════════════════════════════════════════════════════════════════════════ // TEXT FIELD EVENTS // ═══════════════════════════════════════════════════════════════════════════ @Override + @SuppressWarnings("unchecked") public void unFocused(GuiNpcTextField textField) { int id = textField.id; if (id < DECLARATIVE_ID_START) return; - FieldDef def = textFieldMap.get(id); + // Check effects list duration fields + int[] meta = builder.getEffectWidgetMeta().get(id); + if (meta != null && meta[1] == 1) { + FieldDef def = builder.getTextFieldMap().get(id); + if (def != null && def.getType() == FieldType.EFFECTS_LIST) { + List effects = (List) def.getValue(); + if (effects != null && meta[0] < effects.size()) { + effects.get(meta[0]).setDurationTicks(textField.getInteger()); + } + return; + } + } + + FieldDef def = builder.getTextFieldMap().get(id); if (def == null) return; switch (def.getType()) { @@ -480,10 +479,8 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi } protected void applyToAbility() { - // Conditions are the only cached values - write them back ability.getConditions().clear(); for (Condition c : conditions) ability.addCondition(c); - // All other fields write directly to ability via FieldDef lambdas } public void loadAbility(Ability loadedAbility) { @@ -494,8 +491,7 @@ public class SubGuiAbilityConfig extends SubGuiInterface implements ITextfieldLi this.conditions = new ArrayList<>(ability.getConditions()); this.fieldDefs = ability.getAllFieldDefinitions(); - this.hasTypeFields = hasFieldsForTab(TabTarget.TYPE); - this.hasVisualFields = hasFieldsForTab(TabTarget.VISUAL); + discoverCustomTabs(); initGui(); } diff --git a/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java b/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java index d09bddecf..4ebafd54f 100644 --- a/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java +++ b/src/main/java/noppes/npcs/client/gui/util/GuiNpcButton.java @@ -2,6 +2,7 @@ package noppes.npcs.client.gui.util; import kamkeel.npcs.util.TextSplitter; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.FontRenderer; import net.minecraft.client.gui.GuiButton; import net.minecraft.client.gui.ScaledResolution; import net.minecraft.client.renderer.RenderHelper; @@ -151,8 +152,23 @@ public class GuiNpcButton extends GuiButton { public void drawButton(Minecraft mc, int mouseX, int mouseY) { if (!this.visible) return; + // Auto-truncate display text if wider than button + String original = this.displayString; + FontRenderer fr = mc.fontRenderer; + if (fr != null) { + int maxTextWidth = this.width - 6; + if (fr.getStringWidth(original) > maxTextWidth) { + String ellipsis = "..."; + int targetWidth = maxTextWidth - fr.getStringWidth(ellipsis); + if (targetWidth > 0) { + this.displayString = fr.trimStringToWidth(original, targetWidth) + ellipsis; + } + } + } // First, draw the button normally using the superclass method. super.drawButton(mc, mouseX, mouseY); + // Restore original display string + this.displayString = original; // Then, if an icon texture is set, draw it. if (iconTexture != null) { mc.getTextureManager().bindTexture(iconTexture); diff --git a/src/main/resources/assets/customnpcs/lang/en_US.lang b/src/main/resources/assets/customnpcs/lang/en_US.lang index fef02e070..3706e3fc1 100644 --- a/src/main/resources/assets/customnpcs/lang/en_US.lang +++ b/src/main/resources/assets/customnpcs/lang/en_US.lang @@ -1537,6 +1537,48 @@ ability.target.aggro_target=Target ability.target.self=Self ability.target.aoe_self=AOE Self ability.target.aoe_target=AOE Target +ability.syncWindup=Sync Windup + +# Anchor Point +ability.anchor.front=Front +ability.anchor.center=Center +ability.anchor.rightHand=Right Hand +ability.anchor.leftHand=Left Hand +ability.anchor.aboveHead=Above Head +ability.anchor.chest=Chest + +# Sweep Mode +ability.sweep.swipe=Swipe +ability.sweep.spin=Spin + +# Dash Mode +ability.dash.aggressive=Aggressive +ability.dash.defensive=Defensive + +# Counter Type +ability.counter.flat=Flat +ability.counter.percent=Percent + +# Hazard Shape +ability.shape.circle=Circle +ability.shape.ring=Ring +ability.shape.cone=Cone + +# Placement Mode +ability.placement.atCaster=At Caster +ability.placement.atTarget=At Target +ability.placement.followCaster=Follow Caster +ability.placement.followTarget=Follow Target + +# Teleport Mode +ability.teleport.blink=Blink +ability.teleport.behind=Behind +ability.teleport.single=Single + +# Trap Placement +ability.trapPlace.atCaster=At Caster +ability.trapPlace.atTarget=At Target +ability.trapPlace.aheadOfCaster=Ahead of Caster telegraph.circle=Circle telegraph.ring=Ring @@ -1577,136 +1619,173 @@ ability.cnpc.disc=Disc ability.cnpc.laser_shot=Laser Shot ability.cnpc.sweeper=Sweeper -# Ability GUI - Type Tab +# ═══════════════════════════════════════════════════════════════ +# Ability Config GUI +# ═══════════════════════════════════════════════════════════════ + +# General Tab +ability.weight=Weight +ability.windUpTicks=Wind Up +ability.cooldownTicks=Cooldown +ability.lockMovement=Lock Movement +ability.interruptible=Interruptible +ability.dazedTicks=Dazed Ticks +ability.targetingMode=Targeting + +# Target Tab +ability.minRange=Min Range +ability.maxRange=Max Range +ability.conditions=Conditions + +# Effects Tab +ability.windUpSound=Wind Up Sound +ability.activeSound=Active Sound +ability.windUpAnimation=Wind Up Animation +ability.activeAnimation=Active Animation +ability.showTelegraph=Show Telegraph +ability.windUpColor=Wind Up Color +ability.activeColor=Active Color + +# Section Headers +ability.section.timing=Timing +ability.section.movement=Movement +ability.section.sounds=Sounds +ability.section.animations=Animations +ability.section.telegraph=Telegraph +ability.section.beam=Beam +ability.section.sweep=Sweep +ability.section.push=Push +ability.section.leap=Leap +ability.section.disc=Disc +ability.section.homing=Homing +ability.section.boomerang=Boomerang +ability.section.explosive=Explosive +ability.section.colors=Colors +ability.section.effects=Effects +ability.section.counter=Counter +ability.section.area=Area +ability.section.damage=Damage +ability.section.debuffs=Debuffs +ability.section.healing=Healing +ability.section.trigger=Trigger +ability.section.aoe=AOE +ability.section.pullDamage=Pull Damage + +# Shared Sub-Section Labels (used under section headers) +gui.radius=Radius +gui.thickness=Thickness +gui.rotationSpeed=Rotation Speed +gui.strength=Strength +gui.range=Range +gui.delay=Delay +gui.density=Density +gui.length=Length +gui.width=Width +gui.height=Height +gui.value=Value +gui.chance=Chance +gui.sound=Sound +gui.animation=Animation +gui.duration=Duration +gui.amp=Amp # Common Type Settings ability.radius=Radius ability.knockback=Knockback -ability.knockbackUp=KB Up -ability.maxDist=Max Dist -ability.minDist=Min Dist +ability.knockbackUp=Knockback Up ability.distance=Distance +ability.maxDistance=Max Distance ability.lifetime=Lifetime -ability.stunTicks=Stun -ability.leapHeight=Leap Height +ability.stunTicks=Stun Ticks ability.hitWidth=Hit Width ability.effects=Effects -gui.duration=Duration -gui.amp=Amp +ability.mode=Mode +ability.duration=Duration +ability.anchorPoint=Anchor Point +ability.rotationSpeed=Rotation Speed -# Slam Settings +# Slam ability.leapSpeed=Leap Speed +ability.leapHeight=Leap Height -# Charge Settings -ability.hitRadius=Hit Radius - -# Cutter Settings +# Cutter +ability.range=Range +ability.sweepMode=Sweep Mode +ability.spinDuration=Spin Duration ability.arcAngle=Arc Angle -ability.sweepWaves=Waves -ability.rotSpeed=Rot Speed -ability.piercing=Piercing +ability.innerRadius=Inner Radius -# Orb Settings -ability.homing=Homing -ability.explosive=Explosive +# Projectile +ability.projectileType=Projectile Type -# Vortex Settings -ability.pullRadius=Radius -ability.pullStrength=Strength -ability.pullTo=Pull To -ability.aoe=AOE +# Charge (no unique keys) + +# Dash (no unique keys) + +# Teleport +ability.blinkRadius=Blink Radius +ability.blinkCount=Blink Count +ability.blinkDelay=Blink Delay +ability.behindDistance=Behind Distance +ability.lineOfSight=Line of Sight +ability.damageAtStart=Damage At Start +ability.damageAtEnd=Damage At End + +# Vortex +ability.pullRadius=Pull Radius +ability.pullStrength=Pull Strength ability.maxTargets=Max Targets -# Dash Settings -ability.dash.aggressive=Aggressive -ability.dash.defensive=Defensive -ability.mode=Mode -ability.behindDistance=Behind Dist +# Shockwave +ability.pushStrength=Push Strength -# Teleport Settings -ability.blinkCount=Blinks -ability.blinkDelay=Delay -ability.blinkRadius=Radius -ability.minRadius=Min Rad -ability.dmgRadius=Dmg Radius -ability.lineOfSight=Line Sight -ability.dmgAtStart=Dmg Start +# Guard +ability.damageReduction=Damage Reduction -# Projectile Settings -ability.explRadius=Expl Rad - -# Beam Settings -ability.beamLength=Length -ability.beamWidth=Width -ability.beamHeight=Height -ability.dmgInterval=Interval -ability.sweepAngle=Sweep Ang -ability.sweepSpeed=Sweep Spd -ability.lockTarget=Lock Tgt -ability.rotations=Rotations - -# Guard Settings -ability.dmgReduce=Dmg Reduce -ability.counterDmg=Counter Dmg -ability.canCounter=Counter -ability.counterChance=Counter % -ability.counterType=Counter Type -ability.counterValue=Counter Value -ability.counterSound=Counter Sound -ability.counterAnim=Counter Anim - -# Heal Settings -ability.healAmount=Amount -ability.healPercent=Percent -ability.healRadius=Radius +# Heal +ability.instantHeal=Instant Heal +ability.healAmount=Heal Amount +ability.healPercent=Heal Percent +ability.healRadius=Heal Radius ability.healSelf=Heal Self ability.healAllies=Heal Allies -# Hazard Settings -ability.dmgPerSec=Dmg/Sec -ability.debuffDur=Debuff Dur +# Hazard ability.shape=Shape ability.placement=Placement -ability.affectsCaster=Hit Caster -ability.ignoreInvuln=Ign Invuln -ability.place.caster=At Caster -ability.place.target=At Target -ability.place.followCaster=Follow Caster -ability.place.followTarget=Follow Target -ability.place.ahead=Ahead +ability.damagePerSecond=Damage/Sec +ability.damageInterval=Damage Interval +ability.slownessLevel=Slowness Level +ability.debuffDuration=Debuff Duration +ability.poisonLevel=Poison Level +ability.affectsCaster=Affects Caster -# Trap Settings -ability.triggerRadius=Trig Rad +# Trap ability.armTime=Arm Time -ability.maxTriggers=Max Trig +ability.maxTriggers=Max Triggers +ability.triggerCooldown=Trigger Cooldown -# Disc Settings -ability.discRadius=Disc Rad -ability.discThickness=Thickness -ability.boomerang=Boomerang -ability.boomerangDelay=Return Delay -ability.homingStr=Homing Str -ability.homingRange=Homing Rng -ability.explosionRad=Expl Rad -ability.rotationSpeed=Rot Speed +# Sweeper +ability.sweepSpeed=Sweep Speed +ability.rotations=Rotations +ability.piercing=Piercing +ability.lockTarget=Lock Target + +# Beam / Laser +ability.beamWidth=Beam Width +ability.headSize=Head Size +ability.attachedToOwner=Attached to Owner +ability.laserWidth=Laser Width +ability.expansionSpeed=Expansion Speed +ability.lingerTicks=Linger Ticks + +# Visual Settings ability.innerColor=Inner Color ability.outerColor=Outer Color ability.outerEnabled=Outer Glow ability.outerWidth=Outer Width +ability.outerAlpha=Outer Alpha ability.lightning=Lightning -ability.lightningDensity=Density -ability.lightningRadius=Radius -ability.anchorPoint=Anchor Point -ability.noVisualSettings=No visual settings for this ability type - -# Laser Shot Settings -ability.laserWidth=Laser Width -ability.expansionSpeed=Expansion Spd -ability.lingerTicks=Linger Ticks - -# Energy Beam Settings -ability.headSize=Head Size -ability.attachedToOwner=Attached # Ability Save/Load ability.save.confirm=Save ability preset @@ -1718,25 +1797,9 @@ gui.load=Load # Global Abilities Manager ability.manager.title=Ability Manager -# Explosive Hover Text -ability.explosive.noBlockDamage=Explosions do not destroy blocks - -# Additional Orb Settings -ability.explRadius=Expl Radius - -# Telegraph Colors (for abilities that support separate colors) -ability.windUpColor=Wind Up Color -ability.activeColor=Active Color - -# Shockwave Settings -ability.pushRadius=Push Radius -ability.pushStrength=Push Str -ability.shockwaveFalloff=Damage/knockback drop off with distance - -# Ability Config GUI -ability.typeId=Type ID -ability.conditions=Conditions -ability.noTypeSettings=No type-specific settings +# ═══════════════════════════════════════════════════════════════ +# Ability Hover Tooltips +# ═══════════════════════════════════════════════════════════════ ability.hover.weight=Higher weight = more likely to be chosen when multiple abilities available ability.hover.lockMove=Prevents NPC from moving while ability is charging or active