File size: 22,435 Bytes
ef50209
 
 
 
 
 
97d3c5c
 
e3ce233
ef50209
 
 
 
 
 
 
 
 
997f8a8
e3ce233
ef50209
 
 
 
 
 
 
 
46d457b
c957ec8
ef50209
e3ce233
ef50209
 
 
 
 
 
e3ce233
ef50209
 
 
 
e3ce233
ef50209
 
 
 
 
e3ce233
ef50209
 
 
 
 
 
 
 
 
 
 
 
46d457b
 
ef50209
 
 
 
 
 
e3ce233
ef50209
 
 
 
 
46d457b
 
 
 
 
ef50209
 
 
 
 
 
 
 
 
 
 
 
 
e3ce233
da86b6d
 
c957ec8
e3ce233
c957ec8
 
ef50209
 
 
 
 
 
 
 
 
e3ce233
ef50209
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c957ec8
ef50209
e3ce233
ef50209
e3ce233
ef50209
 
8d19551
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3ce233
46d457b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3ce233
 
 
 
46d457b
e3ce233
 
8d19551
 
e3ce233
 
46d457b
ef50209
76234de
 
8d19551
46d457b
2a80c43
 
46d457b
2a80c43
 
 
ef50209
e3ce233
 
 
 
 
 
8d19551
 
43c35d1
e3ce233
 
 
 
 
 
 
 
ef50209
e3ce233
 
 
 
 
 
 
 
c957ec8
 
 
ef50209
e3ce233
ef50209
 
e3ce233
 
 
 
 
ef50209
 
e3ce233
 
 
 
ef50209
 
e3ce233
ef50209
 
e3ce233
 
 
 
 
 
ef50209
 
 
e3ce233
 
c957ec8
 
 
 
e3ce233
ef50209
 
c957ec8
e3ce233
 
 
 
ef50209
 
 
 
c957ec8
 
e3ce233
ef50209
e3ce233
 
ef50209
 
e3ce233
 
 
 
 
 
ef50209
e3ce233
ef50209
e3ce233
 
ef50209
 
 
 
e3ce233
 
cb6eabe
e3ce233
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76234de
8d19551
 
 
 
 
 
e3ce233
8d19551
76234de
 
 
 
 
 
 
 
e3ce233
502868b
e3ce233
502868b
 
76234de
502868b
76234de
e3ce233
76234de
502868b
8d19551
502868b
8d19551
 
 
502868b
 
 
 
 
 
 
 
 
 
76234de
 
502868b
 
76234de
 
 
 
e3ce233
76234de
 
 
 
 
502868b
76234de
 
 
8d19551
76234de
 
8d19551
 
 
 
 
 
76234de
8d19551
76234de
 
502868b
76234de
502868b
 
 
 
76234de
502868b
 
 
8d19551
 
 
 
502868b
 
 
 
8d19551
502868b
 
 
 
 
 
e3ce233
 
502868b
 
 
e3ce233
 
8d19551
e3ce233
 
 
 
 
 
 
 
 
 
 
 
 
 
ef50209
e3ce233
ef50209
 
e3ce233
 
 
ef50209
e3ce233
 
ef50209
 
 
e3ce233
ef50209
e3ce233
 
 
 
ef50209
 
 
 
e3ce233
 
 
ef50209
 
 
e3ce233
ef50209
 
 
8d19551
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
import express from "express";
import { createServer } from "http";
import { Server } from "socket.io";
import { createServer as createViteServer } from "vite";
import path from "path";
import mineflayer from "mineflayer";
import pkg from "mineflayer-pathfinder";
const { pathfinder, Movements, goals } = pkg;
import { GoogleGenAI, Type } from "@google/genai";

const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, {
  cors: {
    origin: "*",
  },
});

const PORT = Number(process.env.PORT) || 7860;

let bot: mineflayer.Bot | null = null;
let ai: GoogleGenAI | null = null;
let selectedModel: string = "gemini-3-flash-preview";
let apiType: string = "google";
let apiBaseUrl: string = "";
let customModel: string = "";
let maxBlocks: number = 10000;
let globalApiKey: string = "";
let currentEdition: string = "java";
let chatHistory: { role: string, content: string }[] = [];

// Helper to log to frontend
const log = (message: string, type: "info" | "success" | "error" | "chat" = "info") => {
  io.emit("bot-log", { message, type, timestamp: new Date().toISOString() });
  console.log(`[${type.toUpperCase()}] ${message}`);
};

async function startServer() {
  // API Routes
  app.get("/api/status", (req, res) => {
    res.json({ connected: !!bot, username: bot?.username });
  });

  // Socket.io for Bot Control
  io.on("connection", (socket) => {
    log("Frontend connected to control panel");

    socket.on("connect-bot", (config) => {
      const { host, port, version, username, apiKey, model } = config;

      if (bot) {
        bot.quit();
        log("Existing bot disconnected");
      }

      try {
        globalApiKey = apiKey;
        apiType = config.apiType || "google";
        apiBaseUrl = config.apiBaseUrl || "";
        customModel = config.customModel || "";
        maxBlocks = parseInt(config.maxBlocks) || 10000;
        selectedModel = model || "gemini-3-flash-preview";
        currentEdition = config.edition || "java";
        const authType = config.auth || "offline";

        if (apiType === "google") {
          ai = new GoogleGenAI({ apiKey });
        } else {
          ai = null;
        }
        
        bot = mineflayer.createBot({
          host,
          port: parseInt(port) || 25565,
          username,
          version: version || false,
          auth: authType as 'offline' | 'microsoft',
          onMsaCode: (data) => {
            io.emit("bot-msa-code", { code: data.user_code, uri: data.verification_uri });
            log(`Waiting for Microsoft login. Go to ${data.verification_uri} and enter code: ${data.user_code}`, "info");
          }
        });

        bot.loadPlugin(pathfinder);

        bot.on("spawn", () => {
          log(`Bot ${bot?.username} spawned on ${host}:${port}`, "success");
          io.emit("bot-status", { connected: true, username: bot?.username });
        });

        bot.on("chat", async (username, message) => {
          if (username === bot?.username) return;
          log(`${username}: ${message}`, "chat");

          // Проверяем на !build ИЛИ !б
          if (message.startsWith("!build ") || message.startsWith("!б ")) {
            const prompt = message.substring(message.indexOf(" ") + 1);
            await handleBuildRequest(prompt, username, false);
          } else if (message.startsWith("!next ") || message.startsWith("!н ")) {
            const prompt = message.substring(message.indexOf(" ") + 1);
            await handleBuildRequest(prompt, username, true);
          }
        });

        bot.on("error", (err) => log(`Bot error: ${err.message}`, "error"));
        bot.on("kicked", (reason) => {
          log(`Bot kicked: ${reason}`, "error");
          bot = null;
          io.emit("bot-status", { connected: false });
        });

      } catch (error: any) {
        log(`Failed to initialize: ${error.message}`, "error");
      }
    });

    socket.on("disconnect-bot", () => {
      if (bot) {
        bot.quit();
        bot = null;
        log("Bot disconnected manually");
        io.emit("bot-status", { connected: false });
      }
    });
  });

  async function handleBuildRequest(prompt: string, requestingUser: string, isNext: boolean) {
    if (!bot) return;

    log(`AI is thinking about: "${prompt}"...`);
    bot.chat(`Request received from ${requestingUser}, beginning generation...`);

    try {
      function getEntityLookingAt(userEntity: any, entities: any, botEntity: any) {
        if (!userEntity || !userEntity.position) return null;
        const pos = { x: userEntity.position.x, y: userEntity.position.y + 1.62, z: userEntity.position.z };
        const yaw = userEntity.yaw;
        const pitch = userEntity.pitch;
        const dx = -Math.sin(yaw) * Math.cos(pitch);
        const dy = -Math.sin(pitch);
        const dz = -Math.cos(yaw) * Math.cos(pitch);

        let bestEntity = null;
        let bestDistance = Infinity;

        for (const id in entities) {
          const entity = entities[id];
          if (entity === userEntity || entity === botEntity) continue;
          if (!entity.position || !['mob', 'player', 'hostile', 'other'].includes(entity.type)) continue;

          const ex = entity.position.x - pos.x;
          const ey = (entity.position.y + (entity.height || 1) / 2) - pos.y;
          const ez = entity.position.z - pos.z;
          const dist = Math.sqrt(ex*ex + ey*ey + ez*ez);
          
          if (dist > 30) continue;

          const nx = ex / dist;
          const ny = ey / dist;
          const nz = ez / dist;

          const dot = dx*nx + dy*ny + dz*nz;
          if (dot > 0.95 && dist < bestDistance) {
            bestDistance = dist;
            bestEntity = entity;
          }
        }
        return bestEntity;
      }

      const userEntity = bot.players[requestingUser]?.entity;
      const lookingAtEntity = getEntityLookingAt(userEntity, bot.entities, bot.entity);
      
      let lookingAtText = "Nothing";
      if (lookingAtEntity) {
        const name = lookingAtEntity.username || lookingAtEntity.name || lookingAtEntity.displayName || "unknown";
        lookingAtText = `${name} (ID: ${lookingAtEntity.id})`;
      }

      const nearbyEntities = Object.values(bot.entities)
        .filter(e => e !== bot?.entity && e.position && bot!.entity.position.distanceTo(e.position) < 30)
        .filter(e => e.type === 'mob' || e.type === 'player' || e.type === 'hostile' || e.type === 'other')
        .map(e => `${e.username || e.name} (ID: ${e.id}, dist: ${Math.round(bot!.entity.position.distanceTo(e.position))}m)`)
        .slice(0, 15)
        .join(", ") || "No entities nearby";

      const syntaxRules = currentEdition === "bedrock"
        ? `CRITICAL BEDROCK COMMAND SYNTAX:
        - Bedrock DOES NOT support NBT tags in commands.
        - To give items: give ${requestingUser} diamond_sword 1
        - To summon with name: summon armor_stand "Palace" ~2 ~ ~`
        : `CRITICAL 1.21.1 JAVA COMMAND SYNTAX:
        - Items with custom names/enchantments: give ${requestingUser} cobblestone[custom_name=[{"text":"Name","italic":false}],enchantments={channeling:1}]
        - Summoning entities with visible names: summon armor_stand ~2 ~ ~ {CustomName:'{"text":"Palace"}',CustomNameVisible:1b,ArmorItems:[{},{},{},{id:"minecraft:shield",count:1}]}`;

      const equipRules = currentEdition === "bedrock"
        ? `3. EQUIPPING ARMOR/WEAPONS (BEDROCK): Use /replaceitem.
           ["cmd", "replaceitem entity @s slot.armor.head 0 netherite_helmet"]
           ["cmd", "replaceitem entity @s slot.armor.chest 0 netherite_chestplate"]
           ["cmd", "replaceitem entity @s slot.armor.legs 0 netherite_leggings"]
           ["cmd", "replaceitem entity @s slot.armor.feet 0 netherite_boots"]
           ["cmd", "replaceitem entity @s slot.weapon.mainhand 0 netherite_sword"]
           ["cmd", "replaceitem entity @s slot.weapon.offhand 0 shield"]`
        : `3. EQUIPPING ARMOR/WEAPONS (JAVA): To actually wear armor or hold weapons, you MUST use these commands (do not just use /give):
           ["cmd", "item replace entity @s armor.head with netherite_helmet"]
           ["cmd", "item replace entity @s armor.chest with netherite_chestplate"]
           ["cmd", "item replace entity @s armor.legs with netherite_leggings"]
           ["cmd", "item replace entity @s armor.feet with netherite_boots"]
           ["cmd", "item replace entity @s weapon.mainhand with netherite_sword"]
           ["cmd", "item replace entity @s weapon.offhand with shield"]`;

      const systemPrompt = `You are a Minecraft architect, admin, and player. The user "${requestingUser}" wants: "${prompt}". 
        You can build structures, execute commands, move around, or interact with players.
        Coordinates (x, y, z) for blocks and movement are relative to the bot's starting position (0,0,0). 
        For commands (like /summon), use relative coordinates (~ ~ ~) which are relative to the bot!
        Use standard Minecraft ${currentEdition === 'bedrock' ? 'Bedrock' : 'Java 1.21.1'} block names and command syntax.
        
        ENVIRONMENT CONTEXT (What you currently see):
        - Nearby entities: ${nearbyEntities}
        - User "${requestingUser}" is currently looking at: ${lookingAtText}
        - Your position: ${Math.round(bot.entity.position.x)}, ${Math.round(bot.entity.position.y)}, ${Math.round(bot.entity.position.z)}
        
        ${syntaxRules}
        
        IMPORTANT RULES: 
        1. When BUILDING structures, ALWAYS start by teleporting or moving away from the build area so you don't get stuck.
        2. When ATTACKING or FOLLOWING an entity, NEVER use the "move" action to go to their coordinates. ONLY use the "attack" or "follow" action directly. The bot will track them automatically.
        ${equipRules}
        4. PVP DUEL PROTOCOL: If the user asks for a fight, duel, or "PvP mode":
           - Build a fighting ring/arena around your current position (floor at y=-1, walls around).
           - Equip EQUAL armor and weapons to BOTH yourself AND the user.
           - Teleport the user and yourself inside the ring on opposite sides (e.g., ["cmd", "tp @s ~4 ~ ~"] and ["cmd", "tp ${requestingUser} ~-4 ~ ~"]).
           - Set both gamemodes to survival (["cmd", "gamemode survival @s"] and ["cmd", "gamemode survival ${requestingUser}"]).
           - Finally, use the ["attack", "${requestingUser}"] action to start the epic battle!
        
        You can build large structures up to ${maxBlocks} blocks.
        To save tokens, output a JSON object containing an "actions" array.
        Action type 1 (Place Block): [x, y, z, "block_name"]
        Action type 2 (Execute Command): ["cmd", "command_string"]
        Action type 3 (Walk/Parkour): ["move", x, y, z]
        Action type 4 (Teleport): ["tp", x, y, z]
        Action type 5 (Attack Entity): ["attack", "player_name_or_entity_id"]
        Action type 6 (Follow Entity): ["follow", "player_name_or_entity_id"]
        
        Example:
        {
          "actions": [
            ["cmd", "item replace entity @s weapon.mainhand with diamond_sword"],
            ["attack", "PlayerName"]
          ]
        }
        Output ONLY the JSON object.`;

      if (!isNext) {
        chatHistory = [];
      }

      let userMessage = prompt;
      if (isNext) {
         userMessage = `CONTINUE OR MODIFY THE PREVIOUS BUILD. User request: "${prompt}". Output the FULL JSON array of actions to execute next (e.g. replacing blocks, adding new ones, etc). Keep in mind the coordinates of the previous blocks.`;
      }

      chatHistory.push({ role: "user", content: userMessage });

      let responseText = "";

      if (apiType === "openai") {
        const url = apiBaseUrl.replace(/\/+$/, '') + '/chat/completions';
        const messages = [
          { role: "system", content: systemPrompt },
          ...chatHistory
        ];

        const res = await fetch(url, {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "Authorization": `Bearer ${globalApiKey}`
          },
          body: JSON.stringify({
            model: customModel || "gpt-3.5-turbo",
            messages: messages
          })
        });
        
        if (!res.ok) {
          const errText = await res.text();
          throw new Error(`Custom API Error: ${res.status} ${errText}`);
        }
        
        const data = await res.json();
        responseText = data.choices[0].message.content;
      } else {
        if (!ai) throw new Error("Google GenAI not initialized");
        
        const contents = chatHistory.map(msg => ({
          role: msg.role === "assistant" ? "model" : "user",
          parts: [{ text: msg.content }]
        }));

        const response = await ai.models.generateContent({
          model: selectedModel,
          contents: contents,
          config: {
            systemInstruction: systemPrompt,
            responseMimeType: "application/json",
          }
        });
        responseText = response.text;
      }

      chatHistory.push({ role: "assistant", content: responseText });

      // Clean up markdown formatting if present
      responseText = responseText.replace(/```json/g, '').replace(/```/g, '').trim();
      
      // Fix bad control characters (like literal newlines or tabs inside strings) that break JSON.parse
      responseText = responseText.replace(/[\n\r\t]/g, ' ').replace(/[\x00-\x1F\x7F-\x9F]/g, '');
      
      const parsed = JSON.parse(responseText);
      const actions = parsed.actions;
      
      if (!Array.isArray(actions)) throw new Error("Invalid AI response format: missing 'actions' array");

      log(`AI generated ${actions.length} actions. Muting command feedback and starting execution...`, "success");

      // Disable command feedback in Minecraft to prevent chat spam
      bot.chat('/gamerule sendCommandFeedback false');
      bot.chat('/gamerule logAdminCommands false');

      const startPos = bot.entity.position.clone();

      for (const action of actions) {
        if (!bot) break;
        if (!Array.isArray(action)) continue;
        
        try {
           // 100ms delay to speed up building while avoiding anti-spam kicks (10 blocks/sec)
           await new Promise(r => setTimeout(r, 100));
           
           if (action[0] === "cmd" && typeof action[1] === "string") {
             let cmd = action[1];
             if (cmd.startsWith("/")) cmd = cmd.substring(1);
             log(`Executing command: /${cmd}`, "info");
             bot.chat(`/${cmd}`);
           } else if (action[0] === "move") {
             const dx = Number(action[1]) || 0;
             const dy = Number(action[2]) || 0;
             const dz = Number(action[3]) || 0;
             const targetPos = startPos.offset(dx, dy, dz);
             log(`Walking to ~${dx} ~${dy} ~${dz}...`, "info");
             const defaultMove = new Movements(bot);
             bot.pathfinder.setMovements(defaultMove);
             try {
               await bot.pathfinder.goto(new goals.GoalBlock(targetPos.x, targetPos.y, targetPos.z));
             } catch (e) {
               log(`Pathfinding failed or interrupted`, "error");
             }
           } else if (action[0] === "tp") {
             const dx = Number(action[1]) || 0;
             const dy = Number(action[2]) || 0;
             const dz = Number(action[3]) || 0;
             log(`Teleporting to ~${dx} ~${dy} ~${dz}`, "info");
             bot.chat(`/tp @s ~${dx} ~${dy} ~${dz}`);
             await new Promise(r => setTimeout(r, 500)); // Wait for teleport to complete
           } else if (action[0] === "attack") {
             const targetIdentifier = action[1];
             let target = bot.players[targetIdentifier]?.entity || bot.entities[targetIdentifier];
             if (!target) {
               target = Object.values(bot.entities).find(e => e.username === targetIdentifier || e.name === targetIdentifier);
             }
             
             if (target) {
               const targetName = target.username || target.name || targetIdentifier;
               log(`Engaging in combat with ${targetName} to the death!`, "info");
               
               let targetDead = false;
               const deathListener = (entity: any) => {
                 if (entity === target) targetDead = true;
               };
               bot.on('entityDead', deathListener);
               
               const defaultMove = new Movements(bot);
               defaultMove.allowSprinting = true; // Enable sprinting for combat
               bot.pathfinder.setMovements(defaultMove);
               
               let currentTargetEntity = target;
               // GoalFollow with dynamic=true keeps updating the path as the target moves
               bot.pathfinder.setGoal(new goals.GoalFollow(currentTargetEntity, 2), true);
               
               try {
                 // Fight loop: continues until target is dead, logs off, or bot dies
                 while (!targetDead && bot.health > 0) {
                   const freshTarget = bot.entities[currentTargetEntity.id];
                   
                   if (!freshTarget || !freshTarget.isValid) {
                     targetDead = true;
                     break;
                   }

                   if (freshTarget !== currentTargetEntity) {
                     currentTargetEntity = freshTarget;
                     bot.pathfinder.setGoal(new goals.GoalFollow(currentTargetEntity, 2), true);
                   }
                   
                   bot.setControlState('sprint', true); // Force sprint to catch up
                   
                   const dist = bot.entity.position.distanceTo(currentTargetEntity.position);
                   
                   if (dist <= 3.5) {
                     bot.lookAt(currentTargetEntity.position.offset(0, 1.5, 0));
                     bot.attack(currentTargetEntity);
                     await new Promise(r => setTimeout(r, 600)); // Weapon cooldown (approx 0.6s)
                   } else {
                     await new Promise(r => setTimeout(r, 100)); // Wait to get closer
                   }
                 }
               } catch (e) {
                 log(`Combat interrupted`, "error");
               } finally {
                 bot.removeListener('entityDead', deathListener);
                 bot.pathfinder.setGoal(null); // Stop following
                 bot.clearControlStates();
               }
               log(`Finished fighting ${targetName}`, "success");
             } else {
               log(`Cannot find target ${targetIdentifier}`, "error");
             }
           } else if (action[0] === "follow") {
             const targetIdentifier = action[1];
             let target = bot.players[targetIdentifier]?.entity || bot.entities[targetIdentifier];
             if (!target) {
               target = Object.values(bot.entities).find(e => e.username === targetIdentifier || e.name === targetIdentifier);
             }

             if (target) {
               const targetName = target.username || target.name || targetIdentifier;
               log(`Following ${targetName}...`, "info");
               const defaultMove = new Movements(bot);
               defaultMove.allowSprinting = true;
               bot.pathfinder.setMovements(defaultMove);
               
               let currentTargetEntity = target;
               bot.pathfinder.setGoal(new goals.GoalFollow(currentTargetEntity, 3), true);
               
               try {
                 // Follow for 30 seconds or until interrupted
                 let followTicks = 0;
                 while (followTicks < 300 && bot.health > 0) {
                   const freshTarget = bot.entities[currentTargetEntity.id];
                   if (!freshTarget || !freshTarget.isValid) break;

                   if (freshTarget !== currentTargetEntity) {
                     currentTargetEntity = freshTarget;
                     bot.pathfinder.setGoal(new goals.GoalFollow(currentTargetEntity, 3), true);
                   }
                   
                   const dist = bot.entity.position.distanceTo(currentTargetEntity.position);
                   if (dist > 5) bot.setControlState('sprint', true);
                   else bot.setControlState('sprint', false);
                   
                   await new Promise(r => setTimeout(r, 100));
                   followTicks++;
                 }
               } catch (e) {
                 log(`Could not reach ${targetName}`, "error");
               } finally {
                 bot.pathfinder.setGoal(null);
                 bot.clearControlStates();
               }
             } else {
               log(`Cannot find target ${targetIdentifier}`, "error");
             }
           } else if (action.length >= 4) {
             const [x, y, z, rawBlock] = action;
             const targetPos = startPos.offset(x, y, z);
             
             // Ensure block name has minecraft: prefix if it doesn't already
             let blockName = String(rawBlock).toLowerCase();
             if (!blockName.startsWith('minecraft:')) {
               blockName = `minecraft:${blockName}`;
             }

             // @ts-ignore
             bot.chat(`/setblock ${Math.floor(targetPos.x)} ${Math.floor(targetPos.y)} ${Math.floor(targetPos.z)} ${blockName}`);
           }
        } catch (e) {
           log(`Failed to execute action: ${JSON.stringify(action)}`, "error");
        }
      }

      bot.chat("Generation complete.");
      log("Generation complete!", "success");
    } catch (error: any) {
      log(`AI Error: ${error.message}`, "error");
      bot.chat(`Error during generation: ${error.message}`);
    }
  }

  // Vite middleware for development
  if (process.env.NODE_ENV !== "production") {
    const vite = await createViteServer({
      server: { middlewareMode: true },
      appType: "spa",
    });
    app.use(vite.middlewares);
  } else {
    const distPath = path.join(process.cwd(), "dist");
    app.use(express.static(distPath));
    app.get("*", (req, res) => {
      res.sendFile(path.join(distPath, "index.html"));
    });
  }

  httpServer.listen(PORT, "0.0.0.0", () => {
    log(`Server running on http://localhost:${PORT}`, "success");
  });
}

startServer();