From 79665a0accef96803adf792725ff65f55fc71d09 Mon Sep 17 00:00:00 2001 From: Martijn Jansen Date: Sun, 15 Feb 2026 14:01:45 +0100 Subject: [PATCH] chore: tony starkified clod --- .../__pycache__/server.cpython-314.pyc | Bin 31383 -> 36675 bytes minecraft-mcp/src/minecraft_mcp/server.py | 112 +++++++++++++++--- .../clod/clod/client/ClodEntityRenderer.java | 60 ++++++++++ .../java/com/clod/clod/client/ClodModel.java | 9 ++ .../java/com/clod/clod/entity/ClodEntity.java | 25 ++++ .../com/clod/clod/server/ClodHttpServer.java | 36 ++++++ 6 files changed, 227 insertions(+), 15 deletions(-) diff --git a/minecraft-mcp/src/minecraft_mcp/__pycache__/server.cpython-314.pyc b/minecraft-mcp/src/minecraft_mcp/__pycache__/server.cpython-314.pyc index c6f571b24260d4fa5c7ff5ac3a0f0550e09bdfa1..921497e9819d7a729ee863d453673aa962842ca8 100644 GIT binary patch delta 11847 zcmb7q3s_s%mG0R`Zy_NeBtVGA5pMw&7;J1}V;ci@2oGa{h2vm|00D|Y!siHMNHg;E z=C;nny~W;1$7#}*_BxHzr(4q2lbfbV>?BUpreJJCRX1^ZeVg0qW57wqGfigZuC*oc z%A?;spY-pw_FjAKwI65g*ZIfy$%U`-@kQ}^EeDVLpHBK3T2IEO#J{2BI1hJ>bG0UO z!P=7>N~%b`GN|n2TztLiPIXY7$pKC3H9-x8vU=^EalyDu?x4IS1r@D+T5NQ&Ch_FA zb=mPQ<-vFzSH=eo2jlFVOSOSSM1w|`T1q%bT5{-ghWX@QG@(S7Mlv6SQVx+!&ZP}n zTtrF&o-m$B$w2c_It6GMqt%iXXn8a~6=(&c(1^C?W}o1L6%{~i*ogz#<% z?||?Cgj<-@F~vH?)+PC0Lhh<1?!SQB7UPl|z@o-g7HyBE(H^Bqu_MN9-vg2@t0b#k zd*S`qd9cRi+Pt6Rdf_cf;|}h$bFm(Rp-Zya-*L~>H2Wia?5|(!d0)zBqX<8miOBn#ixZ4~dHDr?CeQOVIS>U5dLwwuufTKcGzn zN(TRTe@IDL89lc}NAGQ1XL6~FRS+9}T$&C!?MO+Z{uHZ1oy|G4^hC9~xmip)g~MvDnhTu;RI5RmXF;15-l zzw6@B61UR(Iu2R4(nX3>nFNDj6~yqXV$={LTNR^$82PFgZTr#UWhIv z_Ah@@GNkI-w-mEkD3WDZ-`=r`kMD@|C?vb}v3bL#+-!pP4tg$)Tag~WCSAsbK)|IM z)~^s+QnDg$DdXnnKrM`LXd8Ve$2h19aB3J1u}VQWY-rNBRS>t*bVHkN*w~cT7B_5g z$47Fv^+vL5*JL+BcFmgX#`ZwzpUq z4oJSBFW?u-h3>&X&mn1*9>3QkdZnP??d|tkwSm4B%KHL=;IQcP2ZaKa5v%}3PHk8*WKrSmx=L!tx0H?QTRi61i~X( z<6(KPPYQ+=QjcfQE1|2(i#$T~q1RG#LqUjVo+b@mmSfyP5~q&8tg_A8hdVVFXH(k}IALr+z;q|!q+$}x@+bWB3FhNF zZ^j*9NssiL&Z7NS;?i#z5+?SZ7@fDIo^E~go|F8I2r()6PfzbtC%g-p_%A)CPe5dIuqQfx~9Ob(L;G7NHW7Vw6pY zQa5!*=9d*rg8AtM6{8oxNoyB1jE;C_1-(&e=ChQvB28+26W`c&;^xa`lA;5rT=wKg z!``Pby(2!#a^A14=10K?i0%&b`0Q!l>sHcVwb-h_Jw~1-P2DW#Qv7N^-?1DI-k$!8 zHK_ue{E{07*P&V=qFCwvSHvqLSu@sTRd-lo*0djXtHHihd6T2VLJw`rlQ)?o1bs6t z->#D6v9@r_B#m1xDLay5d0pxagwC{BiIr|nxA%Yp^}dRAa%)CmZWF&ftaHn}yI72T zsn*+At!*(K?WDc1{W-VHrP-v4#Id|@4jbCvD(59tM|w;WHz`<}ez|RD+y=B=*>n^( zB`SI`L*Uyd>HkQ#ksko2(m$o^`1Y5gq@~K}Hg*~}KLQg8ubS+~Ni_Cuk|zO^l^X0sK{W3bM|*WQstuV?nT!}>0k z5M7k6WV?=zi?txR6c?G=L|lN1n-Or$D59?wabYUrvMwx_4vRtBC)9Q%e4Mvo&L+(F zfj931uigV;&v;k{6J(h02Bv!q{^Q~>s^X7~iHA|K2q59OW3C<@J-sR9CQd*M^G}E1 zt;f~yZ4*ti=Y`%fq&@t=ne@x{J8oL@PAVt(>q*&@JI|KQCOPMf&S|6bokW3FT~EzF ztGb$6`b^ae$ydwkucp>tYQ1W0nb`R$33-#^+3wlY^>gO+Gv@U-Q}ZSdpWQo~S~6!Y znK75#Ov;*Ee>S0fHmPXNSTt=cn%9|b=X1vFIYZWrA#0(Sv*%8$)93A3lZPiYNTp;P z*E67^JwJ5AV7Y9tKh<|Lz2e;A^Lt;|Io*2qmGlEAwG;ele%_uvsh-eZPs*L_T`FKz z#b%7o8$u3_dc0^F6&2m=lA|7{i=Q23t2DcUE2FHkj=BndnV-b ziOHw49;v);zvKM+7ZO63_*wg&lXp$*Jk8pGKgZ9emd}~Xr_JRz;u0>$rO%sEPA5NF zGP&#QrYHAYHI<)}t|r_8Q`g5wUpHl(7AL!BO^!L8V@Bt=3H;&7y|bqLIbHs=E+1wE zo%M2@aHeT4r(!0jg6gttp#X`r*A+w7Efbeea!bou%6?}q`JJ)oRxTvns^Bz^-^J(O zGI9yUVWV?V$5}QkYB_!3+q(SQ{n6ae;IoFT52ad|{y&@8XdsW02A%p#GN2x-PG~4o zyjVyY4C)t)3`lROwn1I=zcWQs_GMSY5Cj`NKs!T3Z#{BpyOf!X|TyJ znv_VVq+on0X~Urx(L+)f&D)UR-fK);em8uHbzn5#Dfb4f=U z3e}hN)tK^{KpH~D>euX8=xZBEgH!!lMJ~`kPs>2O1B-rLK^jWsuPaqZ8wkX|ZenO* zn8MITFg1hGc7{0&^BF?bAy+X&C&N-EU#4u-rD$H?Od7Vtzh0?r)MI;_zW|F*!Fxkrj>N#xVbNH!evt=Z+g}F;3NU~>X9x}7| zM0znNN4Gp3y5#hY99yDH0tYX}!C&W+Nool7bRlPV{@WY}4(}EC{{a49f`23Yx4=IO z{w9Gd;&$^co*vc|$m>XhmOhx<92$Uvn|FofzTQEvBVK$ESXe#|dcDKqQJ}@oAdDf5 zBis)VPU_mZufDOitF^ARwSG@?S6h8k-JX4I;@_bJ^L5PsJ%H>n1Y8)y(SXW3=oyy0 zy=)UNuuZ&GL>I+?HQGhs&{r*JsXK2^=>=a-7YbO)Z@>mrk2YuP5b9x@VJ$=ZwYE z#^U*cb<@V&`2wJG=kfKP7jhAhGA7-&apwHF#JtFFHZgBe#%b)ofe_w{^}jY2-!?1> zTuaRRPB6}}!5~aV6@C}6jiEswtNr0zG zSUKzwJwtGY@i_9zPm!mO772>)g47@Bvqi64)~v2^^(uao9lHE0eYW_|1*UD9KU$7~(&mn$i$?jQ;0+IlPR}@0VU>EjX?%@%fK{hJZL0E%+3DVtu;uS;)f^ z`m^=<Q8h}RH0R`yQB9;lKpFyxI?i3zSSg^xzle<-&pe+pXE zgnqmHMczQDf5TgOtU|g-n{)-ohM_V)!=hGbAmY!cw9&475#r+s{qjbq{3k%tiTrK6 z7^#kqq(>h65^|nmKst0ADd0kYR1Yw_4gqX#^(-NYRKX9$kdt2X7cgp zk2uVr)Ad_Ym9GPtOX#{S737DgMTuI(*KiE+>j+)k^3TFfj9D|%B1fT7Js)hfDc=C0 zVvt{#NK2}+j-Pv+rheEWj~jbsrYT3q-u?{9J`90 zUc%ukF2t5!M2CE>dDHai?U~9eAXP@_8{0o1?W|F@lBtCqojln}_tZ4<+X?+j&BNtY zAk{2}QR8(036^$TUc{A(Z(-~WfE_Tei!d8-;_m?HpLcpnaoGLMeAUbO=$~q>mDf-O zHl9NfmPsQ+VcDS9|Iz9&v4=nm7OP|8KLA0u)}<@|FA!VER99UqaZCdJi9SV~#?)nZ z!0#qgAMQFsnm;|)Wz2=viL(F>)u%Wj6q=@gxzncnBapkul%ZZlB7-O5JzP^;@5&{q zt9x--ISjx18!{Asfy{g9R~z0_!oE7{yOeihqn(}lX`}aVRddN!@jJAkSx_v3lKoS| z&EF)(r%+!zf*xUxdBvWoT`ixJt#zCKiUPOk+pX3#R&Ry)F2=lv@IFFRAgL`!@d5DN zQ`_6hNYNxFeI4OrygF-qcpF*aPqD&}O+B~oC9?lNFvq9neFr4qaqbZR3PHpjKxh1o zjPzQ2u7W_*g--4N->FP^P2;oi@Sn_<6I1DTze6fNMyj8c&2>f!j|>EPr0f;yXw^Mg z3I!DQFzvnPU8M|rjA`C_4oIm2l;A?`03oVHhG%38Ec{M1d?p$`7Y#4UK#rd8$lxz3 zr{+7Xga!^K#TjBd*kKJ_IA|?0x)NL_xFj@pawW1@S~k~#t<0WwQ+p-IST-qWX=iRm z5=bYzIUU>`x)p#{w;C|DOjSZ$woSC8(;DjJhP54d`N5tI(Qr7f)7r7Li0mv|K4a-@ z=rBixz=mPn^6`jU8MDX;8|+o67fxU{OS_{Je+o=MNe>SOxLh+MHMnvR3W7R zj=@QBtUw6e>Q$#g2`={ct*y#x+1C-3ji* z&fb*@@Ps{@o(bu3EfIrO6bP}2v5IAK9ci(omJGZyzQ=8jCAQdONz0*|bER(r6Yv=} zWpYithRfjGru;}0*PF_gFJBWxQe7Eq+XVVFll83NHUMpR z8v(P*G%+oC$=NO$m?C!*m%`1vlyE4M<2IBiP903wTRNb5B9Gfb3lG}tuz-g{e68Dp zcUrw3W?^ z1&-^;iKVsV(dlf9tccYjB?Xe-<~Zz>j*nC^1*=%>)hi@rFDSd*rPR@NzjHLo5n zWOPsq=gL}S!w#xW)|dmxH$o=Bdl_m0N#kLLyBRuQPxS%(-+;dl{;lw@*eLT6N6GlV zGzL7qf=7^g2E1_g@9Uw@c>Y@dG5$-$3&^PBOoB>>b-_T;GuRbfir(Ppg~Zio=q0nmk%}`oPu1e7ILhupC#;XO_-oR?K8p zT+7`2(4OP<6Tx{y%4GaCgY(?x^JACFe;T@0-aK#2m@{V07_%lTuNc=|w`P7V{mbc- z!C7nZoV9$$T7KRI|nu9)0(&T-y7y}tH}scueJ zH>0b&X-dWDd875Td$Rg0d9rSD(^>o3yUuPs7d*fFeB}$Zmu#2x)BC!nZTBw7__Fx- zI6l#Eo8$F{1qAx}zDAvWGI!QgFsCb+))mm1zR&Tem9)J-$9fOpEP3zUOSrhy)48)+ zVNNbg%LTZhr=RQZ2zBwi&T=b@Q`C`LJg-pQO64?r$gLDkzl+?GgYH{u-ekp-EXN`b z?4rQwa~GAI!VauL_3sicrao(H5@6@vMC4!HnGfUz1!3NSBo zq)DrMnUo@35!WPy$ScZf9=KOEiHK>WNw0i0y%^|A8q#D`UeXFk7m}t#<)xw$pkGTR zO-ahvY)+tm4l*gqpKGgCz`UMpMJy#vuwhxB4D>J5q$y4Q3rz;nU`K}h7X_(6|58p6 zRitSX1X{-E85$WTGE8Ea!Vt;?Nw5wu0}!_T+hd!&cz!C|XC@hJk7g_xgp0tgB?lDU zuEQz7zWXwj>tH17c{+Yy0qn+}_wS?sc3&ZH;iqyA-Al;h^uG^gDoa7c%+qfTI>}Gg zZne^Q+Bfv}jys{r*Y0I-jT_w()&MEV0N_=M>g(95j)|&*5?j1gP!)9E_1~(pBdM;?KQ{W{>r+keB%yb}u>sj_}fhnEIDb~nkF*!MXz>+S5 z&noEmg0>PBN-kee-Va=}_yC~zS%hN}!( z%!Yb#67FD*ai5R>`}o_%9Lc!Mr9YB8!=*2-e?W(~s!-T$m;VvJ(DhP~vC1D6YOz;cdqr-z9KTN*? zzjM5zY-6qf7yN;k2B9?wy79{eh&wz26AONUFf=9%`ue;AOk^RL$t34^Wi_NmtblJ1 zBE{q93uuy4s1N2#Lci|_=s}y{wm&F(dV&IEaE_N?nK0D^M*5cw7wXYquXqH%oe(|j z^MruNY%pr zc6WLP`$j!uA2k3>Mq8y7^~VBUNt> z2*DB2kA<>tD`0uz952UGSBT>6SrEMLSk?Vtqa|q~wK9OGX2t1P#;-U!e?*^bAD!V)zOr7)^pUxpNbA z$f`nE8TCRB3>VMfPyjwN0cFr+@X<)f=l4ki&??Z+*tKAX4|{v0e{jq>zNb1O{!wjB zW5SR}f>Ar{^Y(y6)sV4!Ra`N&?2s3V91Dzy@O28TJe=cOB3&EpeMtz0)()KfA!umH z2cLEfj^X&m;R^<{cHfOPTkfId0aFjTR`ztC*E@)#&EGFMg(j#l=<*&N@QkqW?;LMw zhO<+M3l4<5FvwQEb3$#vpC1(1Sn&j*F@!NM^lf!da0E>%#+F^3j_;?_Q77&Mm&2Tq zm=14uK8wghr@BmE211Iq>2#W|GAP}qto=1pD2+0U32)N1=adH-M z`h{t7Hz3Y!B2HFpMhdIDy3hr7b%_yA$r_iGos#S3&!_x-%706JkH?dgcV!%}TVUkEI9(Wf>i+>P C*G38e delta 7199 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 R38Ha~lVi8A{l9qj{{SXJiA4Ya diff --git a/minecraft-mcp/src/minecraft_mcp/server.py b/minecraft-mcp/src/minecraft_mcp/server.py index f5277af..fa3379a 100644 --- a/minecraft-mcp/src/minecraft_mcp/server.py +++ b/minecraft-mcp/src/minecraft_mcp/server.py @@ -24,6 +24,9 @@ _last_build_time: float = 0.0 _BUILD_SESSION_TIMEOUT = 3.0 # seconds of inactivity before session ends _build_session_origin: tuple[float, float, float] | None = None _watcher_task: asyncio.Task | None = None +_build_bounds: dict | None = None # {"min_x", "max_x", "min_y", "max_y", "min_z", "max_z"} +_build_flying: bool = False # whether current session uses flying mode +_build_fly_xz: tuple[float, float] | None = None # fixed XZ while flying async def _request(method: str, path: str, **kwargs) -> Any: @@ -67,6 +70,10 @@ async def _look_at_pos(x: float, y: float, z: float) -> None: await _request("POST", "/look_at", json={"x": x, "y": y, "z": z}) +async def _set_bot_flying(flying: bool) -> None: + await _request("POST", "/set_flying", json={"flying": flying}) + + async def _get_bot_position() -> tuple[float, float, float] | None: data = await _request("GET", "/status") if isinstance(data, dict) and "position" in data: @@ -78,8 +85,42 @@ async def _get_bot_position() -> tuple[float, float, float] | None: def _compute_viewing_position( build_x: float, build_y: float, build_z: float, bot_x: float, bot_y: float, bot_z: float, + bounds: dict | None = None, ) -> tuple[float, float, float]: - """Compute a spot 5 blocks from the build, on the side the bot is on.""" + """Compute a viewing spot outside the build volume. + + When *bounds* is provided, position 3 blocks outside the nearest edge + of the build footprint (on the side the bot is already on). + Otherwise fall back to 5 blocks from the center point. + """ + if bounds is not None: + min_x = bounds["min_x"] + max_x = bounds["max_x"] + min_z = bounds["min_z"] + max_z = bounds["max_z"] + center_x = (min_x + max_x) / 2.0 + center_z = (min_z + max_z) / 2.0 + dx = bot_x - center_x + dz = bot_z - center_z + if abs(dx) < 0.01 and abs(dz) < 0.01: + dx = 1.0 + # Pick the axis the bot is farthest on + if abs(dx) >= abs(dz): + # Position outside X edge + if dx >= 0: + vx = max_x + 3.5 + else: + vx = min_x - 3.5 + vz = center_z + else: + # Position outside Z edge + vx = center_x + if dz >= 0: + vz = max_z + 3.5 + else: + vz = min_z - 3.5 + return (vx, build_y, vz) + dx = bot_x - build_x dz = bot_z - build_z dist = math.hypot(dx, dz) @@ -90,35 +131,67 @@ def _compute_viewing_position( return (build_x + dx * scale, build_y, build_z + dz * scale) -async def _ensure_build_session(x: float, y: float, z: float) -> None: +async def _ensure_build_session( + x: float, y: float, z: float, + bounds: dict | None = None, + flying: bool = False, +) -> None: """Start or continue a build session, managing state/teleport/look.""" global _build_session_active, _last_build_time, _build_session_origin - global _watcher_task + global _watcher_task, _build_bounds, _build_flying, _build_fly_xz async with _build_session_lock: _last_build_time = time.monotonic() if _build_session_active: - # Session already running — just look at the new target + # Session already running _build_session_origin = (x, y, z) - await _look_at_pos(x, y, z) + if _build_flying and _build_fly_xz is not None: + # Flying mode: keep XZ fixed, only update Y to rise with layers + fx, fz = _build_fly_xz + await _teleport_bot(fx, y, fz) + # Look at the center of the build at the current layer height + if _build_bounds: + cx = (_build_bounds["min_x"] + _build_bounds["max_x"]) / 2.0 + cz = (_build_bounds["min_z"] + _build_bounds["max_z"]) / 2.0 + await _look_at_pos(cx, y, cz) + else: + await _look_at_pos(x, y, z) + else: + await _look_at_pos(x, y, z) return # --- Start a new session --- _build_session_active = True _build_session_origin = (x, y, z) + _build_bounds = bounds + _build_flying = flying await _set_bot_state("building") pos = await _get_bot_position() if pos is not None: bx, by, bz = pos - dist = math.sqrt((bx - x) ** 2 + (by - y) ** 2 + (bz - z) ** 2) - if dist > 16: - vx, vy, vz = _compute_viewing_position(x, y, z, bx, by, bz) - await _teleport_bot(vx, vy, vz) - await _look_at_pos(x, y, z) + if bounds is not None and flying: + # Compute safe position outside build volume + vx, vy, vz = _compute_viewing_position(x, y, z, bx, by, bz, bounds=bounds) + _build_fly_xz = (vx, vz) + await _set_bot_flying(True) + await _teleport_bot(vx, y, vz) + else: + _build_fly_xz = None + dist = math.sqrt((bx - x) ** 2 + (by - y) ** 2 + (bz - z) ** 2) + if dist > 16: + vx, vy, vz = _compute_viewing_position(x, y, z, bx, by, bz) + await _teleport_bot(vx, vy, vz) + + if bounds is not None: + cx = (bounds["min_x"] + bounds["max_x"]) / 2.0 + cz = (bounds["min_z"] + bounds["max_z"]) / 2.0 + await _look_at_pos(cx, y, cz) + else: + await _look_at_pos(x, y, z) # Start the watcher if not already running if _watcher_task is None or _watcher_task.done(): @@ -128,6 +201,7 @@ async def _ensure_build_session(x: float, y: float, z: float) -> None: async def _build_session_watcher() -> None: """Background task that ends the build session after inactivity.""" global _build_session_active, _build_session_origin + global _build_bounds, _build_flying, _build_fly_xz while True: await asyncio.sleep(1.0) @@ -136,9 +210,14 @@ async def _build_session_watcher() -> None: return elapsed = time.monotonic() - _last_build_time if elapsed >= _BUILD_SESSION_TIMEOUT: + if _build_flying: + await _set_bot_flying(False) await _set_bot_state("idle") _build_session_active = False _build_session_origin = None + _build_bounds = None + _build_flying = False + _build_fly_xz = None return @@ -366,12 +445,13 @@ async def build_schematic(name: str, x: int, y: int, z: int) -> str: layers = info.get("layers", []) fmt = info.get("format", "unknown") num_layers = len(layers) + bounds = info.get("bounds") # may be None for empty schematics if num_layers == 0: return _fmt({"ok": True, "message": "Schematic is empty, nothing to build.", "placed": 0}) - # 2. Start build session and announce - await _ensure_build_session(x, y, z) + # 2. Start build session with flying + bounds and announce + await _ensure_build_session(x, y, z, bounds=bounds, flying=True) await _request("POST", "/chat", json={ "message": f"Starting build of {name}! {total_blocks} blocks, {num_layers} layers." }) @@ -384,8 +464,8 @@ async def build_schematic(name: str, x: int, y: int, z: int) -> str: layer_y = layer["y"] block_count = layer["block_count"] - # Keep build session alive and look at current layer - await _ensure_build_session(x, layer_y, z) + # Keep build session alive; flying mode rises with each layer + await _ensure_build_session(x, layer_y, z, bounds=bounds, flying=True) # Check if this layer crosses a milestone threshold progress = (i + 1) / num_layers @@ -408,6 +488,7 @@ async def build_schematic(name: str, x: int, y: int, z: int) -> str: }, timeout=15.0) if isinstance(result, dict) and "error" in result: + await _set_bot_flying(False) await _request("POST", "/chat", json={ "message": f"Build failed at layer {i + 1}/{num_layers}! Error: {result['error']}" }) @@ -426,7 +507,8 @@ async def build_schematic(name: str, x: int, y: int, z: int) -> str: if i < num_layers - 1: await asyncio.sleep(0.4) - # 4. Announce completion + # 4. Land and announce completion + await _set_bot_flying(False) await _request("POST", "/chat", json={ "message": f"Build complete! Placed {total_placed} blocks in {num_layers} layers." }) diff --git a/src/main/java/com/clod/clod/client/ClodEntityRenderer.java b/src/main/java/com/clod/clod/client/ClodEntityRenderer.java index 57e7111..28b35b5 100644 --- a/src/main/java/com/clod/clod/client/ClodEntityRenderer.java +++ b/src/main/java/com/clod/clod/client/ClodEntityRenderer.java @@ -48,6 +48,66 @@ public class ClodEntityRenderer extends MobRenderer { double y = entity.getY(); double z = entity.getZ(); + // Rocket leg particles when flying + if (entity.isClodFlying()) { + float yawRad = entity.yBodyRot * ((float) Math.PI / 180F); + double perpX = -Mth.cos(yawRad); + double perpZ = -Mth.sin(yawRad); + + // Right foot: 0.16 blocks to the right + double rfX = x + perpX * 0.16; + double rfZ = z + perpZ * 0.16; + // Left foot: 0.16 blocks to the left + double lfX = x - perpX * 0.16; + double lfZ = z - perpZ * 0.16; + double footY = y + 0.05; + + // Flame jets: 3 per foot + for (int i = 0; i < 3; i++) { + double velY = -0.15 - entity.getRandom().nextDouble() * 0.10; + entity.level().addParticle(ParticleTypes.FLAME, + rfX + (entity.getRandom().nextDouble() - 0.5) * 0.08, + footY, + rfZ + (entity.getRandom().nextDouble() - 0.5) * 0.08, + 0.0, velY, 0.0); + entity.level().addParticle(ParticleTypes.FLAME, + lfX + (entity.getRandom().nextDouble() - 0.5) * 0.08, + footY, + lfZ + (entity.getRandom().nextDouble() - 0.5) * 0.08, + 0.0, velY, 0.0); + } + + // Smoke puffs: 50% chance per foot + if (entity.getRandom().nextFloat() < 0.5F) { + entity.level().addParticle(ParticleTypes.SMOKE, + rfX, footY - 0.1, rfZ, + 0.0, -0.05, 0.0); + } + if (entity.getRandom().nextFloat() < 0.5F) { + entity.level().addParticle(ParticleTypes.SMOKE, + lfX, footY - 0.1, lfZ, + 0.0, -0.05, 0.0); + } + + // Orange glow: 30% chance per foot + if (entity.getRandom().nextFloat() < 0.3F) { + entity.level().addParticle( + new DustParticleOptions(new Vector3f(1.0F, 0.6F, 0.2F), 0.8F), + rfX + (entity.getRandom().nextDouble() - 0.5) * 0.15, + footY - 0.05, + rfZ + (entity.getRandom().nextDouble() - 0.5) * 0.15, + 0.0, -0.02, 0.0); + } + if (entity.getRandom().nextFloat() < 0.3F) { + entity.level().addParticle( + new DustParticleOptions(new Vector3f(1.0F, 0.6F, 0.2F), 0.8F), + lfX + (entity.getRandom().nextDouble() - 0.5) * 0.15, + footY - 0.05, + lfZ + (entity.getRandom().nextDouble() - 0.5) * 0.15, + 0.0, -0.02, 0.0); + } + } + switch (state) { case "idle": // 10% chance per frame — warm dust drifting upward diff --git a/src/main/java/com/clod/clod/client/ClodModel.java b/src/main/java/com/clod/clod/client/ClodModel.java index d4d9722..ffd34cc 100644 --- a/src/main/java/com/clod/clod/client/ClodModel.java +++ b/src/main/java/com/clod/clod/client/ClodModel.java @@ -127,5 +127,14 @@ public class ClodModel extends HumanoidModel { this.leftArm.zRot = -0.08F - Mth.sin(ageInTicks * 0.067F) * 0.02F; break; } + + // Flying hover pose — overrides leg animation from state switch + if (entity.isClodFlying()) { + float hoverBob = Mth.sin(ageInTicks * 0.1F) * 0.04F; + this.rightLeg.xRot = 0.2F + hoverBob; + this.leftLeg.xRot = 0.2F + hoverBob; + this.rightLeg.zRot = 0.1F; + this.leftLeg.zRot = -0.1F; + } } } diff --git a/src/main/java/com/clod/clod/entity/ClodEntity.java b/src/main/java/com/clod/clod/entity/ClodEntity.java index b4476c5..a385922 100644 --- a/src/main/java/com/clod/clod/entity/ClodEntity.java +++ b/src/main/java/com/clod/clod/entity/ClodEntity.java @@ -22,6 +22,9 @@ public class ClodEntity extends PathfinderMob { private static final EntityDataAccessor STATE = SynchedEntityData.defineId(ClodEntity.class, EntityDataSerializers.STRING); + private static final EntityDataAccessor FLYING = + SynchedEntityData.defineId(ClodEntity.class, EntityDataSerializers.BOOLEAN); + private final SimpleContainer inventory = new SimpleContainer(36); public ClodEntity(EntityType type, Level level) { @@ -45,6 +48,7 @@ public class ClodEntity extends PathfinderMob { protected void defineSynchedData(SynchedEntityData.Builder builder) { super.defineSynchedData(builder); builder.define(STATE, "idle"); + builder.define(FLYING, false); } public String getClodState() { @@ -57,6 +61,27 @@ public class ClodEntity extends PathfinderMob { } } + public boolean isClodFlying() { + return this.entityData.get(FLYING); + } + + public void setClodFlying(boolean flying) { + this.entityData.set(FLYING, flying); + this.setNoGravity(flying); + } + + @Override + public void tick() { + super.tick(); + if (isClodFlying()) { + this.setDeltaMovement(0, 0, 0); + this.fallDistance = 0; + } else if (this.isNoGravity() && "idle".equals(this.getClodState())) { + // Safety reset: if gravity is off but we're idle and not flying, re-enable gravity + this.setNoGravity(false); + } + } + @Override public boolean isInvulnerable() { return true; diff --git a/src/main/java/com/clod/clod/server/ClodHttpServer.java b/src/main/java/com/clod/clod/server/ClodHttpServer.java index 671b6c4..2b59647 100644 --- a/src/main/java/com/clod/clod/server/ClodHttpServer.java +++ b/src/main/java/com/clod/clod/server/ClodHttpServer.java @@ -91,6 +91,8 @@ public class ClodHttpServer { httpServer.createContext("/get_time", this::handleGetTime); httpServer.createContext("/get_weather", this::handleGetWeather); + httpServer.createContext("/set_flying", this::handleSetFlying); + // New POST endpoints httpServer.createContext("/place_structure", this::handlePlaceStructure); httpServer.createContext("/clear_area", this::handleClearArea); @@ -850,6 +852,31 @@ public class ClodHttpServer { runOnServerThread(() -> { bot.setClodState(state); + // Safety: disable flying when going idle + if ("idle".equals(state) && bot.isClodFlying()) { + bot.setClodFlying(false); + } + return null; + }); + sendJson(exchange, 200, okJson()); + } catch (TimeoutException e) { + sendJson(exchange, 504, errorJson("Server thread timeout")); + } catch (Exception e) { + sendJson(exchange, 400, errorJson(e.getMessage())); + } + } + + private void handleSetFlying(HttpExchange exchange) throws IOException { + if (!requirePost(exchange)) return; + try { + ClodEntity bot = requireBot(exchange); + if (bot == null) return; + + JsonObject body = readJsonBody(exchange); + boolean flying = body.get("flying").getAsBoolean(); + + runOnServerThread(() -> { + bot.setClodFlying(flying); return null; }); sendJson(exchange, 200, okJson()); @@ -1302,6 +1329,15 @@ public class ClodHttpServer { size.addProperty("y", maxY - minY + 1); size.addProperty("z", maxZ - minZ + 1); result.add("size", size); + + JsonObject bounds = new JsonObject(); + bounds.addProperty("min_x", minX); + bounds.addProperty("max_x", maxX); + bounds.addProperty("min_y", minY); + bounds.addProperty("max_y", maxY); + bounds.addProperty("min_z", minZ); + bounds.addProperty("max_z", maxZ); + result.add("bounds", bounds); } else { JsonObject size = new JsonObject(); size.addProperty("x", 0);