chore: tony starkified clod

This commit is contained in:
2026-02-15 14:01:45 +01:00
parent 786f259b8e
commit 79665a0acc
6 changed files with 227 additions and 15 deletions
+97 -15
View File
@@ -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."
})
@@ -48,6 +48,66 @@ public class ClodEntityRenderer extends MobRenderer<ClodEntity, ClodModel> {
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
@@ -127,5 +127,14 @@ public class ClodModel extends HumanoidModel<ClodEntity> {
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;
}
}
}
@@ -22,6 +22,9 @@ public class ClodEntity extends PathfinderMob {
private static final EntityDataAccessor<String> STATE =
SynchedEntityData.defineId(ClodEntity.class, EntityDataSerializers.STRING);
private static final EntityDataAccessor<Boolean> FLYING =
SynchedEntityData.defineId(ClodEntity.class, EntityDataSerializers.BOOLEAN);
private final SimpleContainer inventory = new SimpleContainer(36);
public ClodEntity(EntityType<? extends PathfinderMob> 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;
@@ -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);