embark-ai 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +351 -0
- package/botSupervisor.js +237 -0
- package/mc-server/bot/bot.js +1415 -0
- package/mc-server/bot/damagePipeline.js +402 -0
- package/mc-server/bot/engine.js +212 -0
- package/mc-server/bot/entityLiveness.js +121 -0
- package/mc-server/bot/env.js +38 -0
- package/mc-server/bot/environmentPerception.js +384 -0
- package/mc-server/bot/fatalDesyncRecovery.js +42 -0
- package/mc-server/bot/goalRegistry.js +49 -0
- package/mc-server/bot/healthIntegrityWatchdog.js +59 -0
- package/mc-server/bot/llm.js +232 -0
- package/mc-server/bot/locomotionRecovery.js +190 -0
- package/mc-server/bot/logger.js +63 -0
- package/mc-server/bot/memory.js +59 -0
- package/mc-server/bot/movementController.js +110 -0
- package/mc-server/bot/package.json +14 -0
- package/mc-server/bot/positionGuard.js +75 -0
- package/mc-server/bot/recoveryEngine.js +315 -0
- package/mc-server/bot/safeMineflayer.js +129 -0
- package/mc-server/bot/state.js +105 -0
- package/mc-server/bot/tasks.js +939 -0
- package/mc-server/server.properties +74 -0
- package/package.json +44 -0
- package/tui.js +1099 -0
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
// tasks.js — All the `taskXxx` bodies + their helpers, extracted from bot.js.
|
|
2
|
+
//
|
|
3
|
+
// Behaviour-preserving move (Fix 9 Step 2). createTasks(deps) returns the bound
|
|
4
|
+
// task functions; bot.js calls them via `tasks.taskXxx`. The task runner
|
|
5
|
+
// (runTask/replaceTask/cancelCurrentTask), navNear, makeMovements, and the goto
|
|
6
|
+
// lifecycle tracking stay in bot.js — tasks receive navNear as a dependency.
|
|
7
|
+
//
|
|
8
|
+
// Two small non-functional clean-ups vs. the original bodies (forced by the move
|
|
9
|
+
// into a module that has `log` (the logger) in scope):
|
|
10
|
+
// - taskGatherWood / taskBuildHouseSmart: the `findBlock` result was named `log`,
|
|
11
|
+
// shadowing the logger — renamed to `logBlock`.
|
|
12
|
+
// - taskCraftPlanks: the `for (const log of logs)` loop var renamed to `logItem`.
|
|
13
|
+
// Everything else is verbatim.
|
|
14
|
+
|
|
15
|
+
'use strict'
|
|
16
|
+
|
|
17
|
+
const { HOSTILE_MOB_NAMES } = require('./state')
|
|
18
|
+
|
|
19
|
+
module.exports = function createTasks(deps) {
|
|
20
|
+
const {
|
|
21
|
+
bot, log, state, memory, movement, liveness,
|
|
22
|
+
safeChat,
|
|
23
|
+
safeDig, safeCraft, safeEquip, safeConsume, safePlaceBlock, safeAttack,
|
|
24
|
+
navNear, awaitValidPosition,
|
|
25
|
+
isValidVec, isFiniteNum, safeFloor, safeNormalize2D,
|
|
26
|
+
rememberEvent, rememberLocation, recallLocation,
|
|
27
|
+
anger,
|
|
28
|
+
BOT_NAME,
|
|
29
|
+
getLastEnvScan, getEnvPerception, getLocomotionRecovery,
|
|
30
|
+
} = deps
|
|
31
|
+
|
|
32
|
+
// ── Shared item-name constants / helpers ─────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
const LOG_NAMES = ['oak_log','birch_log','spruce_log','jungle_log','acacia_log','dark_oak_log','mangrove_log','cherry_log']
|
|
35
|
+
const PLANK_NAMES = ['oak_planks','birch_planks','spruce_planks','jungle_planks','acacia_planks','dark_oak_planks','mangrove_planks','cherry_planks']
|
|
36
|
+
|
|
37
|
+
const logIds = () => LOG_NAMES.map(n => bot.registry.blocksByName[n]?.id).filter(Boolean)
|
|
38
|
+
const plankIds = () => PLANK_NAMES.map(n => bot.registry.itemsByName[n]?.id).filter(Boolean)
|
|
39
|
+
|
|
40
|
+
function countInInv(names) {
|
|
41
|
+
return bot.inventory.items()
|
|
42
|
+
.filter(i => names.includes(i.name))
|
|
43
|
+
.reduce((s, i) => s + i.count, 0)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function equipBestWeapon() {
|
|
47
|
+
const weapons = [
|
|
48
|
+
'netherite_sword','diamond_sword','iron_sword','stone_sword','wooden_sword',
|
|
49
|
+
'netherite_axe','diamond_axe','iron_axe','stone_axe','wooden_axe',
|
|
50
|
+
]
|
|
51
|
+
for (const w of weapons) {
|
|
52
|
+
const id = bot.registry.itemsByName[w]?.id
|
|
53
|
+
const item = id ? bot.inventory.findInventoryItem(id, null, false) : null
|
|
54
|
+
if (item) { await safeEquip(bot, item, 'hand'); return w }
|
|
55
|
+
}
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function equipBestTool(blockName) {
|
|
60
|
+
// Map block to preferred tool tier (best first)
|
|
61
|
+
const toolPrefs = {
|
|
62
|
+
stone: ['netherite_pickaxe','diamond_pickaxe','iron_pickaxe','stone_pickaxe','wooden_pickaxe'],
|
|
63
|
+
coal_ore: ['netherite_pickaxe','diamond_pickaxe','iron_pickaxe','stone_pickaxe','wooden_pickaxe'],
|
|
64
|
+
iron_ore: ['netherite_pickaxe','diamond_pickaxe','iron_pickaxe','stone_pickaxe'],
|
|
65
|
+
diamond_ore: ['netherite_pickaxe','diamond_pickaxe','iron_pickaxe'],
|
|
66
|
+
gold_ore: ['netherite_pickaxe','diamond_pickaxe','iron_pickaxe'],
|
|
67
|
+
sand: ['netherite_shovel','diamond_shovel','iron_shovel','stone_shovel','wooden_shovel'],
|
|
68
|
+
gravel: ['netherite_shovel','diamond_shovel','iron_shovel','stone_shovel','wooden_shovel'],
|
|
69
|
+
dirt: ['netherite_shovel','diamond_shovel','iron_shovel','stone_shovel','wooden_shovel'],
|
|
70
|
+
}
|
|
71
|
+
const isLog = blockName.endsWith('_log')
|
|
72
|
+
const prefs = isLog
|
|
73
|
+
? ['netherite_axe','diamond_axe','iron_axe','stone_axe','wooden_axe']
|
|
74
|
+
: (toolPrefs[blockName] || [])
|
|
75
|
+
for (const tool of prefs) {
|
|
76
|
+
const id = bot.registry.itemsByName[tool]?.id
|
|
77
|
+
const item = id ? bot.inventory.findInventoryItem(id, null, false) : null
|
|
78
|
+
if (item) {
|
|
79
|
+
try { await safeEquip(bot, item, 'hand'); return tool } catch {}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Crafting helpers ─────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
// Items whose recipes always require sticks (vanilla wooden/stone/iron/gold/diamond/netherite tools)
|
|
88
|
+
const STICK_TOOL_RE = /^(wooden|stone|iron|golden|diamond|netherite)_(pickaxe|sword|axe|shovel|hoe)$/
|
|
89
|
+
|
|
90
|
+
function countItemsByName(name) {
|
|
91
|
+
// Match by name string — more robust across protocol/registry version drift
|
|
92
|
+
return bot.inventory.items()
|
|
93
|
+
.filter(i => i.name === name)
|
|
94
|
+
.reduce((s, i) => s + i.count, 0)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function countMaterialFamily(suffix) {
|
|
98
|
+
// e.g., "_planks" → counts all plank types together
|
|
99
|
+
return bot.inventory.items()
|
|
100
|
+
.filter(i => i.name.endsWith(suffix))
|
|
101
|
+
.reduce((s, i) => s + i.count, 0)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Track items that "successfully" crafted but didn't actually consume materials —
|
|
105
|
+
// likely a mineflayer/server protocol mismatch. Don't keep retrying these.
|
|
106
|
+
const phantomCraftBlocklist = new Map() // itemName -> blockedUntilTimestamp
|
|
107
|
+
const PHANTOM_BLOCK_MS = 5 * 60 * 1000 // block re-attempts for 5 minutes
|
|
108
|
+
|
|
109
|
+
function inventoryHas(predicate) {
|
|
110
|
+
return bot.inventory.items().some(predicate)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Auto-craft sticks if needed. Returns true if sticks are now available.
|
|
114
|
+
async function ensureSticks() {
|
|
115
|
+
const stickId = bot.registry.itemsByName['stick']?.id
|
|
116
|
+
if (!stickId) return false
|
|
117
|
+
if (bot.inventory.findInventoryItem(stickId, null, false)) return true
|
|
118
|
+
|
|
119
|
+
// Need at least 2 planks (any kind). If we don't have planks but have logs, craft planks first.
|
|
120
|
+
const totalPlanks = bot.inventory.items().filter(i => i.name.endsWith('_planks')).reduce((s, i) => s + i.count, 0)
|
|
121
|
+
if (totalPlanks < 2) {
|
|
122
|
+
if (bot.inventory.items().some(i => i.name.endsWith('_log'))) {
|
|
123
|
+
log.info('auto_craft_planks_for_sticks')
|
|
124
|
+
await taskCraftPlanks()
|
|
125
|
+
} else {
|
|
126
|
+
log.warn('cannot_make_sticks', { reason: 'no planks or logs' })
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Sticks: 2 planks → 4 sticks. Inventory crafting (no table required).
|
|
132
|
+
const recipes = bot.recipesFor(stickId, null, 1, null)
|
|
133
|
+
if (!recipes.length) {
|
|
134
|
+
log.warn('no_stick_recipe_available', { invPlanks: totalPlanks })
|
|
135
|
+
return false
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
await safeCraft(bot, recipes[0], 1, null)
|
|
139
|
+
log.info('crafted_sticks_auto', { sticksNow: countItemsByName('stick') })
|
|
140
|
+
return true
|
|
141
|
+
} catch (e) {
|
|
142
|
+
log.error('stick_craft_exception', { message: e.message })
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Determine which ingredients are missing for a recipe (best-effort diagnostic).
|
|
148
|
+
function describeRecipe(itemId) {
|
|
149
|
+
// Search the registry for any recipe producing this item
|
|
150
|
+
try {
|
|
151
|
+
const recipes = bot.registry.recipes?.[itemId]
|
|
152
|
+
if (!recipes || !recipes.length) return null
|
|
153
|
+
return recipes[0] // raw shape varies by mineflayer version, used for logging only
|
|
154
|
+
} catch { return null }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Place a block from inventory in front of the bot (or adjacent if blocked).
|
|
158
|
+
// Returns true on success, false if no spot found or no item.
|
|
159
|
+
async function placeFromInventory(blockName) {
|
|
160
|
+
const normalized = blockName.replace(/ /g, '_').toLowerCase()
|
|
161
|
+
const itemId = bot.registry.itemsByName[normalized]?.id
|
|
162
|
+
if (!itemId) return false
|
|
163
|
+
const item = bot.inventory.findInventoryItem(itemId, null, false)
|
|
164
|
+
if (!item) return false
|
|
165
|
+
|
|
166
|
+
await safeEquip(bot, item, 'hand')
|
|
167
|
+
|
|
168
|
+
// Compute "in front of bot" target — uses yaw to find facing direction
|
|
169
|
+
const yaw = bot.entity.yaw
|
|
170
|
+
const dx = -Math.round(Math.sin(yaw))
|
|
171
|
+
const dz = -Math.round(Math.cos(yaw))
|
|
172
|
+
const here = bot.entity.position.floored()
|
|
173
|
+
|
|
174
|
+
// Try positions in priority order: directly in front (ground level), then sides, then behind
|
|
175
|
+
const offsets = [
|
|
176
|
+
[dx, 0, dz], [dx, -1, dz], // in front, ground or one below
|
|
177
|
+
[dz, 0, -dx], [-dz, 0, dx], // left, right
|
|
178
|
+
[-dx, 0, -dz], // behind (last resort)
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
for (const [ox, oy, oz] of offsets) {
|
|
182
|
+
const targetPos = here.offset(ox, oy, oz)
|
|
183
|
+
const current = bot.blockAt(targetPos)
|
|
184
|
+
if (!current || (current.name !== 'air' && current.name !== 'cave_air' && current.name !== 'grass')) continue
|
|
185
|
+
|
|
186
|
+
// Find a solid reference block adjacent to the target to "click on"
|
|
187
|
+
const adj = [[0,-1,0],[1,0,0],[-1,0,0],[0,0,1],[0,0,-1],[0,1,0]]
|
|
188
|
+
for (const [adx, ady, adz] of adj) {
|
|
189
|
+
const refBlock = bot.blockAt(targetPos.offset(adx, ady, adz))
|
|
190
|
+
if (!refBlock || refBlock.boundingBox !== 'block') continue
|
|
191
|
+
try {
|
|
192
|
+
const faceVec = targetPos.minus(refBlock.position)
|
|
193
|
+
await safePlaceBlock(bot, refBlock, faceVec)
|
|
194
|
+
log.info('placed_block', { block: normalized, pos: { x: targetPos.x, y: targetPos.y, z: targetPos.z } })
|
|
195
|
+
return true
|
|
196
|
+
} catch (e) {
|
|
197
|
+
// Try next reference block
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return false
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Tasks: gather / craft / mine / eat ───────────────────────────────────────
|
|
205
|
+
|
|
206
|
+
async function taskExplore() {
|
|
207
|
+
let p
|
|
208
|
+
try { p = await awaitValidPosition(2000) }
|
|
209
|
+
catch (e) { log.warn('explore_skip_invalid_position', { reason: e.message }); return }
|
|
210
|
+
|
|
211
|
+
const angle = Math.random() * Math.PI * 2
|
|
212
|
+
const dist = 10 + Math.random() * 20 // 10-30 blocks — short enough to reliably reach
|
|
213
|
+
const tx = Math.floor(p.x + Math.sin(angle) * dist)
|
|
214
|
+
const ty = Math.floor(p.y)
|
|
215
|
+
const tz = Math.floor(p.z + Math.cos(angle) * dist)
|
|
216
|
+
if (!isFiniteNum(tx) || !isFiniteNum(ty) || !isFiniteNum(tz)) {
|
|
217
|
+
log.error('explore_target_invalid', { tx, ty, tz })
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
log.debug('explore_target', { from: safeFloor(p), to: { x: tx, y: ty, z: tz } })
|
|
221
|
+
|
|
222
|
+
// Fail fast on complex terrain — try another direction next idle cycle
|
|
223
|
+
movement.setThinkingTimeout(2000)
|
|
224
|
+
try {
|
|
225
|
+
await navNear(tx, ty, tz, 5)
|
|
226
|
+
rememberLocation(memory, 'last_explored', bot.entity.position)
|
|
227
|
+
} finally {
|
|
228
|
+
movement.restoreThinkingTimeout()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function taskGatherWood() {
|
|
233
|
+
const ids = logIds()
|
|
234
|
+
const logBlock = bot.findBlock({ matching: ids, maxDistance: 50 })
|
|
235
|
+
if (!logBlock) { safeChat('No trees in range.'); return }
|
|
236
|
+
console.log(`[${BOT_NAME}] Chopping log at ${logBlock.position}`)
|
|
237
|
+
await navNear(logBlock.position.x, logBlock.position.y, logBlock.position.z, 2)
|
|
238
|
+
const fresh = bot.blockAt(logBlock.position)
|
|
239
|
+
if (fresh && ids.includes(fresh.type) && bot.canDigBlock(fresh)) {
|
|
240
|
+
await safeDig(bot, fresh)
|
|
241
|
+
safeChat('Got wood.')
|
|
242
|
+
rememberEvent(memory, 'gathered_wood', {})
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function taskCraftPlanks() {
|
|
247
|
+
const logs = bot.inventory.items().filter(i => LOG_NAMES.includes(i.name))
|
|
248
|
+
if (!logs.length) { safeChat('No logs to convert.'); return }
|
|
249
|
+
|
|
250
|
+
let totalCrafted = 0
|
|
251
|
+
for (const logItem of logs) {
|
|
252
|
+
const plankName = logItem.name.replace('_log', '_planks')
|
|
253
|
+
const plankId = bot.registry.itemsByName[plankName]?.id
|
|
254
|
+
if (!plankId) continue
|
|
255
|
+
|
|
256
|
+
const recipes = bot.recipesFor(plankId, null, 1, null)
|
|
257
|
+
if (!recipes.length) continue
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
const count = logItem.count
|
|
261
|
+
await safeCraft(bot, recipes[0], count, null)
|
|
262
|
+
totalCrafted += count * 4
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.error(`[${BOT_NAME}] Plank craft failed:`, err.message)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (totalCrafted > 0) {
|
|
269
|
+
safeChat(`Made ${totalCrafted} planks.`)
|
|
270
|
+
rememberEvent(memory, 'crafted_planks', { count: totalCrafted })
|
|
271
|
+
} else {
|
|
272
|
+
safeChat('Could not make planks.')
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function taskGoTo(name) {
|
|
277
|
+
const loc = recallLocation(memory, name)
|
|
278
|
+
if (!loc) { safeChat(`Don't know where "${name}" is.`); return }
|
|
279
|
+
console.log(`[${BOT_NAME}] Going to ${name}`)
|
|
280
|
+
await navNear(loc.pos.x, loc.pos.y, loc.pos.z)
|
|
281
|
+
safeChat(`Reached ${name}.`)
|
|
282
|
+
rememberEvent(memory, 'visited', { name })
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function taskCollectNearby() {
|
|
286
|
+
const pos = bot.entity.position
|
|
287
|
+
const drops = Object.values(bot.entities)
|
|
288
|
+
.filter(e => e.type === 'object' && e.objectType === 'Item' && e.position.distanceTo(pos) < 25)
|
|
289
|
+
.sort((a, b) => a.position.distanceTo(pos) - b.position.distanceTo(pos))
|
|
290
|
+
.slice(0, 10)
|
|
291
|
+
|
|
292
|
+
if (!drops.length) { safeChat('No items nearby.'); return }
|
|
293
|
+
|
|
294
|
+
safeChat(`Collecting ${drops.length} item(s).`)
|
|
295
|
+
for (const e of drops) {
|
|
296
|
+
if (!e.isValid) continue
|
|
297
|
+
try { await navNear(e.position.x, e.position.y, e.position.z, 1) } catch {}
|
|
298
|
+
await new Promise(r => setTimeout(r, 300))
|
|
299
|
+
}
|
|
300
|
+
rememberEvent(memory, 'collected_items', {})
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function taskCraftItem(itemName) {
|
|
304
|
+
const normalized = (itemName || '').replace(/ /g, '_').toLowerCase()
|
|
305
|
+
if (!normalized) { safeChat('Craft what?'); return }
|
|
306
|
+
|
|
307
|
+
const itemData = bot.registry.itemsByName[normalized]
|
|
308
|
+
if (!itemData) { safeChat(`Don't know what "${itemName}" is.`); return }
|
|
309
|
+
|
|
310
|
+
// Skip items that previously phantom-crafted (succeeded silently without consuming materials)
|
|
311
|
+
const blockedUntil = phantomCraftBlocklist.get(normalized)
|
|
312
|
+
if (blockedUntil && Date.now() < blockedUntil) {
|
|
313
|
+
const secsLeft = Math.ceil((blockedUntil - Date.now()) / 1000)
|
|
314
|
+
log.warn('craft_skip_phantom_blocked', { item: normalized, secsLeft })
|
|
315
|
+
safeChat(`Skipping ${itemName} — recipe seems broken.`)
|
|
316
|
+
return
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const beforeCount = countItemsByName(normalized)
|
|
320
|
+
const invSnapshot = bot.inventory.items().map(i => `${i.name}x${i.count}`)
|
|
321
|
+
log.info('craft_start', { item: normalized, before: beforeCount, inventory: invSnapshot })
|
|
322
|
+
|
|
323
|
+
// ── 1. Try inventory crafting (recipes that don't need a table) ────────────
|
|
324
|
+
let recipes = bot.recipesFor(itemData.id, null, 1, null)
|
|
325
|
+
if (recipes.length > 0) {
|
|
326
|
+
try {
|
|
327
|
+
const stickBefore = countItemsByName('stick')
|
|
328
|
+
const planksBefore = countMaterialFamily('_planks')
|
|
329
|
+
await safeCraft(bot, recipes[0], 1, null)
|
|
330
|
+
const after = countItemsByName(normalized)
|
|
331
|
+
const stickAfter = countItemsByName('stick')
|
|
332
|
+
const planksAfter = countMaterialFamily('_planks')
|
|
333
|
+
const consumed = (stickAfter < stickBefore) || (planksAfter < planksBefore)
|
|
334
|
+
if (after > beforeCount && consumed) {
|
|
335
|
+
safeChat(`Crafted ${itemName}.`)
|
|
336
|
+
log.info('craft_success', { item: normalized, table: false, before: beforeCount, after })
|
|
337
|
+
rememberEvent(memory, 'crafted', { item: itemName })
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
if (after > beforeCount && !consumed) {
|
|
341
|
+
log.error('craft_phantom', { item: normalized, table: false, beforeCount, after, stickBefore, stickAfter, planksBefore, planksAfter })
|
|
342
|
+
phantomCraftBlocklist.set(normalized, Date.now() + PHANTOM_BLOCK_MS)
|
|
343
|
+
safeChat(`Recipe broken for ${itemName}.`)
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
log.warn('craft_no_count_change', { item: normalized, table: false })
|
|
347
|
+
} catch (e) {
|
|
348
|
+
log.error('craft_inventory_exception', { item: normalized, message: e.message })
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── 2. Need a crafting table — find or auto-place one ──────────────────────
|
|
353
|
+
const tableId = bot.registry.blocksByName['crafting_table']?.id
|
|
354
|
+
let table = tableId ? bot.findBlock({ matching: [tableId], maxDistance: 20 }) : null
|
|
355
|
+
|
|
356
|
+
if (!table) {
|
|
357
|
+
const tableItemId = bot.registry.itemsByName['crafting_table']?.id
|
|
358
|
+
if (tableItemId && bot.inventory.findInventoryItem(tableItemId, null, false)) {
|
|
359
|
+
log.info('auto_place_table', { reason: `crafting ${normalized}` })
|
|
360
|
+
const placed = await placeFromInventory('crafting_table')
|
|
361
|
+
if (placed) {
|
|
362
|
+
await new Promise(r => setTimeout(r, 300))
|
|
363
|
+
table = tableId ? bot.findBlock({ matching: [tableId], maxDistance: 5 }) : null
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!table) {
|
|
369
|
+
safeChat('No crafting table.')
|
|
370
|
+
log.warn('craft_no_table', { item: normalized, hasTableInInv: inventoryHas(i => i.name === 'crafting_table') })
|
|
371
|
+
return
|
|
372
|
+
}
|
|
373
|
+
await navNear(table.position.x, table.position.y, table.position.z, 2)
|
|
374
|
+
|
|
375
|
+
// ── 3. Try with the table ──────────────────────────────────────────────────
|
|
376
|
+
recipes = bot.recipesFor(itemData.id, null, 1, table)
|
|
377
|
+
|
|
378
|
+
// ── 4. If no recipe is currently makeable and this is a tool, auto-make sticks ──
|
|
379
|
+
if (recipes.length === 0 && STICK_TOOL_RE.test(normalized) && !inventoryHas(i => i.name === 'stick')) {
|
|
380
|
+
log.info('auto_resolve_sticks', { reason: `tool prerequisite for ${normalized}` })
|
|
381
|
+
safeChat('Need sticks first.')
|
|
382
|
+
const ok = await ensureSticks()
|
|
383
|
+
if (ok) {
|
|
384
|
+
recipes = bot.recipesFor(itemData.id, null, 1, table)
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ── 5. Final sanity check ──────────────────────────────────────────────────
|
|
389
|
+
if (recipes.length === 0) {
|
|
390
|
+
const inv = bot.inventory.items().map(i => `${i.name}x${i.count}`).join(', ') || 'empty'
|
|
391
|
+
log.warn('craft_failed_no_recipe', { item: normalized, inventory: inv })
|
|
392
|
+
safeChat(`Can't craft ${itemName}. Have: ${inv.length > 80 ? inv.slice(0, 77) + '...' : inv}`)
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── 6. Craft and verify (also detect phantom: success but materials not consumed) ──
|
|
397
|
+
try {
|
|
398
|
+
const stickBefore = countItemsByName('stick')
|
|
399
|
+
const planksBefore = countMaterialFamily('_planks')
|
|
400
|
+
await safeCraft(bot, recipes[0], 1, table)
|
|
401
|
+
const after = countItemsByName(normalized)
|
|
402
|
+
const stickAfter = countItemsByName('stick')
|
|
403
|
+
const planksAfter = countMaterialFamily('_planks')
|
|
404
|
+
const consumed = (stickAfter < stickBefore) || (planksAfter < planksBefore)
|
|
405
|
+
|
|
406
|
+
if (after > beforeCount && consumed) {
|
|
407
|
+
safeChat(`Crafted ${itemName}.`)
|
|
408
|
+
log.info('craft_success', { item: normalized, table: true, before: beforeCount, after, consumed: { stick: stickBefore - stickAfter, planks: planksBefore - planksAfter } })
|
|
409
|
+
rememberEvent(memory, 'crafted', { item: itemName })
|
|
410
|
+
} else if (after > beforeCount && !consumed) {
|
|
411
|
+
// Phantom craft: server didn't actually accept the recipe (likely protocol/data version drift)
|
|
412
|
+
// Don't keep retrying the same item — block for 5 minutes.
|
|
413
|
+
log.error('craft_phantom', { item: normalized, table: true, beforeCount, after, stickBefore, stickAfter, planksBefore, planksAfter })
|
|
414
|
+
phantomCraftBlocklist.set(normalized, Date.now() + PHANTOM_BLOCK_MS)
|
|
415
|
+
safeChat(`Recipe broken for ${itemName}. Skipping.`)
|
|
416
|
+
} else {
|
|
417
|
+
log.warn('craft_silent_failure', { item: normalized, before: beforeCount, after })
|
|
418
|
+
safeChat(`Crafted ${itemName} but inventory unchanged?`)
|
|
419
|
+
}
|
|
420
|
+
} catch (e) {
|
|
421
|
+
log.error('craft_table_exception', { item: normalized, message: e.message, stack: e.stack?.split('\n')[1]?.trim() })
|
|
422
|
+
safeChat(`Craft error: ${e.message.slice(0, 50)}`)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function taskPlaceBlock(blockName) {
|
|
427
|
+
const normalized = (blockName || '').replace(/ /g, '_').toLowerCase()
|
|
428
|
+
if (!normalized) { safeChat("Place what?"); return }
|
|
429
|
+
if (!bot.registry.itemsByName[normalized]) {
|
|
430
|
+
safeChat(`Don't know what "${blockName}" is.`); return
|
|
431
|
+
}
|
|
432
|
+
const itemId = bot.registry.itemsByName[normalized].id
|
|
433
|
+
if (!bot.inventory.findInventoryItem(itemId, null, false)) {
|
|
434
|
+
safeChat(`No ${blockName} in inventory.`); return
|
|
435
|
+
}
|
|
436
|
+
const ok = await placeFromInventory(normalized)
|
|
437
|
+
if (ok) {
|
|
438
|
+
safeChat(`Placed ${blockName}.`)
|
|
439
|
+
rememberEvent(memory, 'placed', { block: normalized })
|
|
440
|
+
} else {
|
|
441
|
+
safeChat(`No good spot to place ${blockName}.`)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Generic mining — works for stone, ores, sand, etc.
|
|
446
|
+
// Resolves singular → plural (e.g. "stone" finds stone block).
|
|
447
|
+
async function taskMineBlock(blockName, count = 1) {
|
|
448
|
+
const normalized = (blockName || '').replace(/ /g, '_').toLowerCase()
|
|
449
|
+
if (!normalized) { safeChat('Mine what?'); return }
|
|
450
|
+
|
|
451
|
+
// Resolve a few common aliases
|
|
452
|
+
const aliasMap = {
|
|
453
|
+
wood: 'oak_log', tree: 'oak_log', log: 'oak_log',
|
|
454
|
+
stone_block: 'stone',
|
|
455
|
+
iron: 'iron_ore', coal: 'coal_ore', gold: 'gold_ore', diamond: 'diamond_ore',
|
|
456
|
+
}
|
|
457
|
+
const target = aliasMap[normalized] || normalized
|
|
458
|
+
const blockId = bot.registry.blocksByName[target]?.id
|
|
459
|
+
if (!blockId) {
|
|
460
|
+
safeChat(`Don't know how to find "${blockName}".`)
|
|
461
|
+
log.warn('mine_unknown_block', { block: normalized })
|
|
462
|
+
return
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
log.info('mine_start', { block: target, count })
|
|
466
|
+
let mined = 0
|
|
467
|
+
let consecutiveFailures = 0
|
|
468
|
+
while (mined < count && consecutiveFailures < 3) {
|
|
469
|
+
const block = bot.findBlock({ matching: [blockId], maxDistance: 32 })
|
|
470
|
+
if (!block) {
|
|
471
|
+
log.warn('mine_no_block_found', { block: target, mined, requested: count })
|
|
472
|
+
safeChat(mined > 0 ? `Got ${mined}. No more nearby.` : `No ${blockName} nearby.`)
|
|
473
|
+
return
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (!bot.canDigBlock(block)) {
|
|
477
|
+
log.warn('mine_cant_dig', { block: target, hasPickaxe: inventoryHas(i => i.name.includes('pickaxe')) })
|
|
478
|
+
safeChat(`Can't break ${blockName} — wrong tool?`)
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
try {
|
|
483
|
+
await navNear(block.position.x, block.position.y, block.position.z, 2)
|
|
484
|
+
const fresh = bot.blockAt(block.position)
|
|
485
|
+
if (!fresh || fresh.type !== blockId) {
|
|
486
|
+
consecutiveFailures++
|
|
487
|
+
continue // someone else broke it, or it changed
|
|
488
|
+
}
|
|
489
|
+
// Equip the best tool we have
|
|
490
|
+
await equipBestTool(target)
|
|
491
|
+
await safeDig(bot, fresh)
|
|
492
|
+
mined++
|
|
493
|
+
consecutiveFailures = 0
|
|
494
|
+
log.info('mined', { block: target, count: mined })
|
|
495
|
+
} catch (e) {
|
|
496
|
+
consecutiveFailures++
|
|
497
|
+
log.error('mine_exception', { block: target, message: e.message, attempt: consecutiveFailures })
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (mined > 0) {
|
|
502
|
+
safeChat(`Got ${mined} ${blockName}.`)
|
|
503
|
+
rememberEvent(memory, 'mined', { block: target, count: mined })
|
|
504
|
+
} else {
|
|
505
|
+
safeChat(`Couldn't mine ${blockName}.`)
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Eat food when hungry. Picks the best available food (cooked > raw > emergency).
|
|
510
|
+
const FOOD_PRIORITY = [
|
|
511
|
+
'cooked_beef','cooked_porkchop','cooked_chicken','cooked_mutton','cooked_rabbit',
|
|
512
|
+
'cooked_cod','cooked_salmon','baked_potato','bread','pumpkin_pie',
|
|
513
|
+
'beef','porkchop','chicken','mutton','rabbit',
|
|
514
|
+
'apple','carrot','melon_slice','sweet_berries','cookie','golden_apple',
|
|
515
|
+
// Last resort — these have side effects
|
|
516
|
+
'rotten_flesh','spider_eye','poisonous_potato',
|
|
517
|
+
]
|
|
518
|
+
|
|
519
|
+
async function taskEatFood() {
|
|
520
|
+
if (bot.food >= 20) { safeChat('Already full.'); return }
|
|
521
|
+
|
|
522
|
+
const inv = bot.inventory.items()
|
|
523
|
+
let food = null
|
|
524
|
+
for (const fn of FOOD_PRIORITY) {
|
|
525
|
+
food = inv.find(i => i.name === fn)
|
|
526
|
+
if (food) break
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (!food) {
|
|
530
|
+
log.warn('eat_no_food', { hunger: bot.food })
|
|
531
|
+
safeChat('No food.')
|
|
532
|
+
return
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
log.info('eat_start', { food: food.name, hungerBefore: bot.food })
|
|
536
|
+
try {
|
|
537
|
+
await safeEquip(bot, food, 'hand')
|
|
538
|
+
await safeConsume(bot)
|
|
539
|
+
log.info('eat_success', { food: food.name, hungerBefore: bot.food, hungerAfter: bot.food })
|
|
540
|
+
safeChat(`Ate ${food.name.replace(/_/g, ' ')}.`)
|
|
541
|
+
rememberEvent(memory, 'ate', { food: food.name })
|
|
542
|
+
} catch (e) {
|
|
543
|
+
log.error('eat_exception', { food: food.name, message: e.message })
|
|
544
|
+
safeChat(`Couldn't eat: ${e.message.slice(0, 50)}`)
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ── Tasks: combat / survival ─────────────────────────────────────────────────
|
|
549
|
+
|
|
550
|
+
async function taskAttackMobs() {
|
|
551
|
+
const mob = bot.nearestEntity(e =>
|
|
552
|
+
e.name && HOSTILE_MOB_NAMES.has(e.name) && e.position.distanceTo(bot.entity.position) < 20
|
|
553
|
+
)
|
|
554
|
+
if (!mob) { safeChat('No hostile mobs nearby.'); return }
|
|
555
|
+
|
|
556
|
+
const weapon = await equipBestWeapon()
|
|
557
|
+
console.log(`[${BOT_NAME}] Attacking ${mob.name} with ${weapon || 'fists'}`)
|
|
558
|
+
safeChat(`Fighting ${mob.name}.`)
|
|
559
|
+
await safeAttack(bot, mob, { maxMs: 10_000, label: mob.name })
|
|
560
|
+
|
|
561
|
+
if (mob.isValid) safeChat('Got away.')
|
|
562
|
+
else { safeChat(`${mob.name} down.`); rememberEvent(memory, 'killed_mob', { name: mob.name }) }
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
async function taskAttackPlayer(entity, username) {
|
|
566
|
+
if (!entity || !entity.isValid) { safeChat(`${username} is gone.`); return }
|
|
567
|
+
|
|
568
|
+
const weapon = await equipBestWeapon()
|
|
569
|
+
console.log(`[${BOT_NAME}] Attacking player ${username} with ${weapon || 'fists'}`)
|
|
570
|
+
safeChat(`Coming for you, ${username}.`)
|
|
571
|
+
await safeAttack(bot, entity, { maxMs: 10_000, label: username })
|
|
572
|
+
|
|
573
|
+
// Reduce anger after fighting
|
|
574
|
+
const rec = anger.get(username)
|
|
575
|
+
if (rec) rec.level = Math.max(0, rec.level - 3)
|
|
576
|
+
safeChat('We even now.')
|
|
577
|
+
rememberEvent(memory, 'fought_player', { username })
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Escape from being stuck (in a hole, surrounded, etc.)
|
|
581
|
+
// Strategy: try to navigate to a higher Y at our current X/Z (pathfinder will dig
|
|
582
|
+
// or pillar with our scaffolding blocks). If that fails, dig straight up as a last resort.
|
|
583
|
+
async function taskEscape() {
|
|
584
|
+
let p
|
|
585
|
+
try { p = await awaitValidPosition(2000) }
|
|
586
|
+
catch (e) { log.warn('escape_skip_invalid_position', { reason: e.message }); safeChat("Can't escape yet."); return }
|
|
587
|
+
|
|
588
|
+
// Get current perception to classify stuck state and orient escape
|
|
589
|
+
const ep = getEnvPerception()
|
|
590
|
+
const scan = getLastEnvScan() || (ep ? ep.scan() : null)
|
|
591
|
+
const stuckClass = scan?.stuckClass || 'unknown'
|
|
592
|
+
|
|
593
|
+
log.info('escape_start', {
|
|
594
|
+
from: { x: Math.floor(p.x), y: Math.floor(p.y), z: Math.floor(p.z) },
|
|
595
|
+
stuckClass,
|
|
596
|
+
locomotionRisk: scan?.locomotionRisk ?? null,
|
|
597
|
+
hazardSummary: scan?.hazardSummary ?? null,
|
|
598
|
+
})
|
|
599
|
+
safeChat('Stuck — escaping.')
|
|
600
|
+
|
|
601
|
+
// Phase 1: locomotion micro-escape first (fast, no pathfinder)
|
|
602
|
+
const lr = getLocomotionRecovery()
|
|
603
|
+
if (lr && scan?.valid !== false) {
|
|
604
|
+
log.info('escape_attempt', { method: 'locomotion_phase', stuckClass })
|
|
605
|
+
await lr.runHazardEscape(stuckClass, scan, 'task_escape')
|
|
606
|
+
await new Promise(r => setTimeout(r, 300))
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// Re-read position after locomotion phase
|
|
610
|
+
const p2 = bot.entity?.position
|
|
611
|
+
if (!isValidVec(p2)) {
|
|
612
|
+
log.warn('escape_position_invalid_after_locomotion')
|
|
613
|
+
return
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
const startY = Math.floor(p2.y)
|
|
617
|
+
const targetY = Math.min(75, startY + 15)
|
|
618
|
+
|
|
619
|
+
// Phase 2: pathfinder escape — prefer escape-vector direction if known
|
|
620
|
+
const ev = scan?.escapeVector
|
|
621
|
+
const cx = Math.floor(p2.x)
|
|
622
|
+
const cz = Math.floor(p2.z)
|
|
623
|
+
|
|
624
|
+
const attempts = [
|
|
625
|
+
{ x: cx, z: cz, y: targetY }, // straight up
|
|
626
|
+
// Escape-vector-biased targets first
|
|
627
|
+
...(ev ? [
|
|
628
|
+
{ x: cx + ev.dx * 6, z: cz + ev.dz * 6, y: targetY },
|
|
629
|
+
{ x: cx + ev.dx * 4, z: cz + ev.dz * 4, y: startY },
|
|
630
|
+
] : []),
|
|
631
|
+
{ x: cx + 6, z: cz, y: targetY },
|
|
632
|
+
{ x: cx - 6, z: cz, y: targetY },
|
|
633
|
+
{ x: cx, z: cz + 6, y: targetY },
|
|
634
|
+
{ x: cx, z: cz - 6, y: targetY },
|
|
635
|
+
]
|
|
636
|
+
|
|
637
|
+
movement.setThinkingTimeout(5000)
|
|
638
|
+
try {
|
|
639
|
+
for (const a of attempts) {
|
|
640
|
+
if (!isFiniteNum(a.x) || !isFiniteNum(a.y) || !isFiniteNum(a.z)) continue
|
|
641
|
+
try {
|
|
642
|
+
await navNear(Math.floor(a.x), Math.floor(a.y), Math.floor(a.z), 3)
|
|
643
|
+
const newY = Math.floor(bot.entity.position.y)
|
|
644
|
+
log.info('escape_complete', { method: 'pathfinder', reached: a, newY, stuckClass })
|
|
645
|
+
safeChat('Out!')
|
|
646
|
+
return
|
|
647
|
+
} catch (e) {
|
|
648
|
+
log.warn('escape_attempt_failed', { target: a, message: e.message })
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
} finally {
|
|
652
|
+
movement.restoreThinkingTimeout()
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Phase 3: dig up as last resort
|
|
656
|
+
log.warn('escape_falling_back_to_dig_up', { stuckClass })
|
|
657
|
+
await digUp(12)
|
|
658
|
+
log.info('escape_complete', { method: 'dig_up', endY: Math.floor(bot.entity.position.y) })
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Mine the block above the bot's head, jump to fill the new space, repeat.
|
|
662
|
+
// Used when pathfinder can't find a way out.
|
|
663
|
+
async function digUp(maxBlocks = 10) {
|
|
664
|
+
let dug = 0
|
|
665
|
+
for (let i = 0; i < maxBlocks; i++) {
|
|
666
|
+
const headBlock = bot.blockAt(bot.entity.position.offset(0, 2, 0))
|
|
667
|
+
if (!headBlock || ['air', 'cave_air', 'void_air'].includes(headBlock.name)) {
|
|
668
|
+
break // already at open air
|
|
669
|
+
}
|
|
670
|
+
if (!bot.canDigBlock(headBlock)) {
|
|
671
|
+
log.warn('dig_up_blocked', { block: headBlock.name })
|
|
672
|
+
safeChat(`Can't dig ${headBlock.name} — wrong tool?`)
|
|
673
|
+
break
|
|
674
|
+
}
|
|
675
|
+
try {
|
|
676
|
+
await equipBestTool(headBlock.name)
|
|
677
|
+
await safeDig(bot, headBlock)
|
|
678
|
+
dug++
|
|
679
|
+
// Jump up to occupy the new block space
|
|
680
|
+
bot.setControlState('jump', true)
|
|
681
|
+
await new Promise(r => setTimeout(r, 350))
|
|
682
|
+
bot.setControlState('jump', false)
|
|
683
|
+
} catch (e) {
|
|
684
|
+
log.warn('dig_up_step_failed', { message: e.message })
|
|
685
|
+
break
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return dug
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// Step away from an environmental hazard (cactus, lava, fire, etc.).
|
|
692
|
+
// `hazard` is optional — if null, just moves 5 blocks in a random horizontal direction.
|
|
693
|
+
async function taskEvadeHazard(hazard) {
|
|
694
|
+
// Wait for valid position — critical right after teleport / respawn,
|
|
695
|
+
// when bot.entity.position briefly has NaN x/z.
|
|
696
|
+
let pos
|
|
697
|
+
try {
|
|
698
|
+
pos = await awaitValidPosition(2000)
|
|
699
|
+
} catch (e) {
|
|
700
|
+
log.warn('evade_skip_invalid_position', { reason: e.message, hazard: hazard?.name })
|
|
701
|
+
safeChat('Need a moment.')
|
|
702
|
+
return
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Compute escape vector — always via safeNormalize2D so length-0 / NaN
|
|
706
|
+
// can never produce NaN target coords.
|
|
707
|
+
let rawDx, rawDz, label
|
|
708
|
+
const hazardPos = hazard?.block?.position
|
|
709
|
+
if (hazard && isValidVec(hazardPos)) {
|
|
710
|
+
rawDx = pos.x - hazardPos.x
|
|
711
|
+
rawDz = pos.z - hazardPos.z
|
|
712
|
+
label = (hazard.name || 'something').replace(/_/g, ' ')
|
|
713
|
+
} else {
|
|
714
|
+
rawDx = NaN
|
|
715
|
+
rawDz = NaN // forces safeNormalize2D into random fallback
|
|
716
|
+
label = hazard?.name?.replace(/_/g, ' ') || 'something'
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
const { dx, dz, fallback } = safeNormalize2D(rawDx, rawDz)
|
|
720
|
+
if (fallback) log.debug('evade_normalize_fallback', { reason: fallback, hazard: hazard?.name })
|
|
721
|
+
|
|
722
|
+
const tx = Math.floor(pos.x + dx * 5)
|
|
723
|
+
const ty = Math.floor(pos.y)
|
|
724
|
+
const tz = Math.floor(pos.z + dz * 5)
|
|
725
|
+
|
|
726
|
+
// Final safety net: if for any reason we still have NaN, pick a random walkable spot
|
|
727
|
+
if (!isFiniteNum(tx) || !isFiniteNum(ty) || !isFiniteNum(tz)) {
|
|
728
|
+
log.error('evade_target_invalid_after_safety', { tx, ty, tz, hazard: hazard?.name })
|
|
729
|
+
return
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
log.info('evade_start', {
|
|
733
|
+
hazard: hazard?.name || null,
|
|
734
|
+
from: safeFloor(pos),
|
|
735
|
+
to: { x: tx, y: ty, z: tz },
|
|
736
|
+
})
|
|
737
|
+
safeChat(`Ow — ${label}!`)
|
|
738
|
+
|
|
739
|
+
movement.forceStop('evade_start')
|
|
740
|
+
bot.clearControlStates()
|
|
741
|
+
await new Promise(r => setTimeout(r, 100))
|
|
742
|
+
await navNear(tx, ty, tz, 2, movement.PRIORITY.HIGH)
|
|
743
|
+
log.info('evade_complete', { newPos: safeFloor(bot.entity?.position) })
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Run away from any nearby threats
|
|
747
|
+
async function taskFlee() {
|
|
748
|
+
let pos
|
|
749
|
+
try { pos = await awaitValidPosition(2000) }
|
|
750
|
+
catch (e) { log.warn('flee_skip_invalid_position', { reason: e.message }); return }
|
|
751
|
+
|
|
752
|
+
const threats = Object.values(bot.entities)
|
|
753
|
+
.filter(e => e.name && HOSTILE_MOB_NAMES.has(e.name) && isValidVec(e.position) && e.position.distanceTo(pos) < 24)
|
|
754
|
+
|
|
755
|
+
if (threats.length === 0) {
|
|
756
|
+
safeChat('Nothing to run from.')
|
|
757
|
+
log.info('flee_no_threats')
|
|
758
|
+
return
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Direction directly away from the centroid of threats — guarded against NaN.
|
|
762
|
+
let avgX = 0, avgZ = 0
|
|
763
|
+
for (const t of threats) { avgX += t.position.x; avgZ += t.position.z }
|
|
764
|
+
avgX /= threats.length; avgZ /= threats.length
|
|
765
|
+
const { dx, dz, fallback } = safeNormalize2D(pos.x - avgX, pos.z - avgZ)
|
|
766
|
+
if (fallback) log.debug('flee_normalize_fallback', { reason: fallback })
|
|
767
|
+
|
|
768
|
+
const fleeX = Math.floor(pos.x + dx * 30)
|
|
769
|
+
const fleeY = Math.floor(pos.y)
|
|
770
|
+
const fleeZ = Math.floor(pos.z + dz * 30)
|
|
771
|
+
if (!isFiniteNum(fleeX) || !isFiniteNum(fleeY) || !isFiniteNum(fleeZ)) {
|
|
772
|
+
log.error('flee_target_invalid', { fleeX, fleeY, fleeZ })
|
|
773
|
+
return
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
log.info('flee_start', { threats: threats.map(t => t.name), to: { x: fleeX, y: fleeY, z: fleeZ } })
|
|
777
|
+
safeChat('Falling back!')
|
|
778
|
+
try {
|
|
779
|
+
await navNear(fleeX, fleeY, fleeZ, 5)
|
|
780
|
+
log.info('flee_complete', { newPos: safeFloor(bot.entity?.position) })
|
|
781
|
+
} catch (e) {
|
|
782
|
+
log.warn('flee_path_failed', { message: e.message })
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
// Blind survival: sprint away in a random direction using raw control states.
|
|
787
|
+
// Used when position is invalid (NaN) — does NOT use the pathfinder.
|
|
788
|
+
// This breaks the "validator dead-end" failure where the bot stands still dying
|
|
789
|
+
// because all damage events are dropped due to stale position cache (diagnosis F18).
|
|
790
|
+
async function taskBlindSurvival() {
|
|
791
|
+
const ep = getEnvPerception()
|
|
792
|
+
const scan = getLastEnvScan() || (ep ? ep.scan() : null)
|
|
793
|
+
const stuck = scan?.stuckClass || 'unknown'
|
|
794
|
+
log.warn('blind_survival_start', { livenessState: liveness.getState(), stuckClass: stuck, risk: scan?.locomotionRisk ?? null })
|
|
795
|
+
safeChat('Taking hits. Moving.')
|
|
796
|
+
|
|
797
|
+
const lr = getLocomotionRecovery()
|
|
798
|
+
if (lr && scan?.valid !== false) {
|
|
799
|
+
// Use perception-aware escape rather than random yaw
|
|
800
|
+
await lr.runHazardEscape(stuck, scan, 'blind_survival')
|
|
801
|
+
} else {
|
|
802
|
+
// Fallback: random direction sprint
|
|
803
|
+
const angle = Math.random() * Math.PI * 2
|
|
804
|
+
try { bot.entity.yaw = angle } catch {}
|
|
805
|
+
bot.setControlState('forward', true)
|
|
806
|
+
bot.setControlState('sprint', true)
|
|
807
|
+
bot.setControlState('jump', true)
|
|
808
|
+
await new Promise(r => setTimeout(r, 400))
|
|
809
|
+
bot.setControlState('jump', false)
|
|
810
|
+
await new Promise(r => setTimeout(r, 1600))
|
|
811
|
+
bot.clearControlStates()
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
log.info('blind_survival_complete', { livenessState: liveness.getState() })
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ── Tasks: building ──────────────────────────────────────────────────────────
|
|
818
|
+
|
|
819
|
+
async function placeBlockAt(targetPos, blockName) {
|
|
820
|
+
const current = bot.blockAt(targetPos)
|
|
821
|
+
if (current && current.name !== 'air' && current.name !== 'cave_air') return true
|
|
822
|
+
|
|
823
|
+
const itemId = bot.registry.itemsByName[blockName]?.id
|
|
824
|
+
const item = itemId ? bot.inventory.findInventoryItem(itemId, null, false) : null
|
|
825
|
+
if (!item) return false
|
|
826
|
+
|
|
827
|
+
await safeEquip(bot, item, 'hand')
|
|
828
|
+
|
|
829
|
+
const adj = [[0,-1,0],[0,1,0],[1,0,0],[-1,0,0],[0,0,1],[0,0,-1]]
|
|
830
|
+
for (const [dx, dy, dz] of adj) {
|
|
831
|
+
const refBlock = bot.blockAt(targetPos.offset(dx, dy, dz))
|
|
832
|
+
if (!refBlock || refBlock.boundingBox !== 'block') continue
|
|
833
|
+
try {
|
|
834
|
+
await navNear(targetPos.x, targetPos.y, targetPos.z, 4)
|
|
835
|
+
const faceVec = targetPos.minus(refBlock.position)
|
|
836
|
+
await safePlaceBlock(bot, refBlock, faceVec)
|
|
837
|
+
return true
|
|
838
|
+
} catch {}
|
|
839
|
+
}
|
|
840
|
+
return false
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
async function buildHouseStructure() {
|
|
844
|
+
const planks = bot.inventory.items().filter(i => PLANK_NAMES.includes(i.name))
|
|
845
|
+
if (!planks.length) { safeChat('No planks to build with.'); return }
|
|
846
|
+
const plankName = planks[0].name
|
|
847
|
+
|
|
848
|
+
const origin = bot.entity.position.floored().offset(0, 0, -5)
|
|
849
|
+
console.log(`[${BOT_NAME}] Building house at ${origin}`)
|
|
850
|
+
|
|
851
|
+
let outOfBlocks = false
|
|
852
|
+
|
|
853
|
+
// Walls: 5x5 perimeter, 3 high, door gap at (0, 1-2, -2)
|
|
854
|
+
for (let y = 1; y <= 3 && !outOfBlocks; y++) {
|
|
855
|
+
for (let x = -2; x <= 2 && !outOfBlocks; x++) {
|
|
856
|
+
for (let z = -2; z <= 2 && !outOfBlocks; z++) {
|
|
857
|
+
if (Math.abs(x) !== 2 && Math.abs(z) !== 2) continue
|
|
858
|
+
if (z === -2 && x === 0 && y <= 2) continue // door
|
|
859
|
+
|
|
860
|
+
const ok = await placeBlockAt(origin.offset(x, y, z), plankName)
|
|
861
|
+
if (!ok && countInInv([plankName]) === 0) { safeChat('Out of planks.'); outOfBlocks = true }
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// Roof
|
|
867
|
+
for (let x = -2; x <= 2 && !outOfBlocks; x++) {
|
|
868
|
+
for (let z = -2; z <= 2 && !outOfBlocks; z++) {
|
|
869
|
+
const ok = await placeBlockAt(origin.offset(x, 4, z), plankName)
|
|
870
|
+
if (!ok && countInInv([plankName]) === 0) { safeChat('Out of planks on roof.'); outOfBlocks = true }
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (!outOfBlocks) {
|
|
875
|
+
safeChat('House done!')
|
|
876
|
+
rememberLocation(memory, 'house', origin)
|
|
877
|
+
rememberEvent(memory, 'built_house', {})
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Smart agentic house building — auto-chains: gather wood → craft planks → place blocks
|
|
882
|
+
async function taskBuildHouseSmart() {
|
|
883
|
+
safeChat('Starting house. Will gather and craft if needed.')
|
|
884
|
+
|
|
885
|
+
let plankCount = countInInv(PLANK_NAMES)
|
|
886
|
+
|
|
887
|
+
// Step 1: convert any logs to planks
|
|
888
|
+
if (countInInv(LOG_NAMES) > 0) {
|
|
889
|
+
await taskCraftPlanks()
|
|
890
|
+
plankCount = countInInv(PLANK_NAMES)
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// Step 2: while not enough, gather more wood and craft
|
|
894
|
+
let attempts = 0
|
|
895
|
+
while (plankCount < 30 && attempts < 12) {
|
|
896
|
+
attempts++
|
|
897
|
+
const stillNeed = 30 - plankCount
|
|
898
|
+
const logsNeeded = Math.ceil(stillNeed / 4)
|
|
899
|
+
|
|
900
|
+
safeChat(`Have ${plankCount} planks, need ${30 - plankCount} more. Gathering wood (try ${attempts}/12).`)
|
|
901
|
+
|
|
902
|
+
let chopped = 0
|
|
903
|
+
while (chopped < logsNeeded) {
|
|
904
|
+
const ids = logIds()
|
|
905
|
+
const logBlock = bot.findBlock({ matching: ids, maxDistance: 60 })
|
|
906
|
+
if (!logBlock) { safeChat('No more trees in range. Stopping.'); return }
|
|
907
|
+
try {
|
|
908
|
+
await navNear(logBlock.position.x, logBlock.position.y, logBlock.position.z, 2)
|
|
909
|
+
const fresh = bot.blockAt(logBlock.position)
|
|
910
|
+
if (fresh && ids.includes(fresh.type)) {
|
|
911
|
+
await safeDig(bot, fresh)
|
|
912
|
+
chopped++
|
|
913
|
+
} else { break }
|
|
914
|
+
} catch (e) {
|
|
915
|
+
console.error(`[${BOT_NAME}] gather error:`, e.message)
|
|
916
|
+
break
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
await taskCraftPlanks()
|
|
921
|
+
plankCount = countInInv(PLANK_NAMES)
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (plankCount < 30) {
|
|
925
|
+
safeChat(`Couldn't gather enough planks (${plankCount}). Aborting.`)
|
|
926
|
+
return
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
safeChat(`Have ${plankCount} planks. Building now.`)
|
|
930
|
+
await buildHouseStructure()
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
return {
|
|
934
|
+
taskExplore, taskGatherWood, taskCraftPlanks, taskGoTo, taskAttackMobs, taskAttackPlayer,
|
|
935
|
+
taskCollectNearby, taskCraftItem, taskPlaceBlock, taskMineBlock, taskEatFood,
|
|
936
|
+
taskEscape, taskEvadeHazard, taskFlee, taskBuildHouseSmart, taskBlindSurvival,
|
|
937
|
+
FOOD_PRIORITY,
|
|
938
|
+
}
|
|
939
|
+
}
|