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 c6f571b..921497e 100644 Binary files a/minecraft-mcp/src/minecraft_mcp/__pycache__/server.cpython-314.pyc and b/minecraft-mcp/src/minecraft_mcp/__pycache__/server.cpython-314.pyc differ 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);