/** * Test script for DougBot crafting — connects to BDS and tests crafting sticks. * * Usage: * node --experimental-strip-types --disable-warning=ExperimentalWarning test-craft.js * * Or with full path: * /Users/roberts/.local/share/fnm/node-versions/v22.22.2/installation/bin/node \ * --experimental-strip-types --disable-warning=ExperimentalWarning test-craft.js */ // --- Patch prismarine-physics for missing bedrock attributes --- const origPhysicsPath = require.resolve('prismarine-physics'); const origPhysics = require('prismarine-physics'); const { Physics: OrigPhysics, PlayerState } = origPhysics; function PatchedPhysics(mcData, world) { if (!mcData.attributesByName || !mcData.attributesByName.movementSpeed) { const attrs = [ { name: 'movementSpeed', resource: 'minecraft:movement', min: 0, max: 3.4028235E38, default: 0.1 }, { name: 'followRange', resource: 'minecraft:follow_range', min: 0, max: 2048, default: 16 }, { name: 'knockbackResistance', resource: 'minecraft:knockback_resistance', min: 0, max: 1, default: 0 }, { name: 'attackDamage', resource: 'minecraft:attack_damage', min: 0, max: 3.4028235E38, default: 1 }, { name: 'armor', resource: 'minecraft:armor', min: 0, max: 30, default: 0 }, { name: 'armorToughness', resource: 'minecraft:armor_toughness', min: 0, max: 20, default: 0 }, { name: 'attackSpeed', resource: 'minecraft:attack_speed', min: 0, max: 1024, default: 4 }, { name: 'luck', resource: 'minecraft:luck', min: -1024, max: 1024, default: 0 }, { name: 'maxHealth', resource: 'minecraft:health', min: 0, max: 1024, default: 20 }, ]; mcData.attributesArray = attrs; mcData.attributes = {}; mcData.attributesByName = {}; for (const attr of attrs) { mcData.attributes[attr.resource] = attr; mcData.attributesByName[attr.name] = attr; } } return OrigPhysics(mcData, world); } require.cache[origPhysicsPath] = { id: origPhysicsPath, exports: { Physics: PatchedPhysics, PlayerState }, loaded: true, }; // --- Imports --- const path = require('path'); const mineflayer = require(path.join(__dirname, 'bridge', 'lib', 'mineflayer')); const { pathfinder: pathfinderPlugin, Movements, goals } = require('mineflayer-pathfinder'); const { GoalNear } = goals; // --- Config --- const HOST = process.env.BDS_HOST || '192.168.1.90'; const PORT = parseInt(process.env.BDS_PORT || '19140'); const USERNAME = process.env.BDS_USER || 'CraftTest'; function log(msg) { console.log(`[${new Date().toISOString().slice(11, 19)}] ${msg}`); } log(`Connecting to ${HOST}:${PORT} as ${USERNAME}...`); const bot = mineflayer.createBot({ host: HOST, port: PORT, username: USERNAME, version: 'bedrock_1.21.130', bedrockProtocolVersion: '26.10', auth: 'offline', offline: true, raknetBackend: 'jsp-raknet', }); bot.loadPlugin(pathfinderPlugin); let testPhase = 'waiting'; bot.once('spawn', async () => { log('Spawned! Setting up pathfinder...'); const movements = new Movements(bot); movements.canDig = false; bot.pathfinder.setMovements(movements); const pos = bot.entity.position; log(`Position: (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)})`); // Wait a moment for recipes to load await sleep(3000); // --- Run test sequence --- try { await testResolveNames(); await testInventoryCheck(); await testCraftSticks(); log('=== ALL TESTS COMPLETE ==='); } catch (e) { log(`TEST FAILED: ${e.message}`); console.error(e); } // Disconnect after tests await sleep(2000); log('Disconnecting...'); bot.quit(); setTimeout(() => process.exit(0), 1000); }); bot.on('error', (err) => { log(`ERROR: ${err.message}`); }); bot.on('kicked', (reason) => { log(`KICKED: ${reason}`); process.exit(1); }); // --- Test: Name Resolution --- async function testResolveNames() { log('--- TEST: Item Name Resolution ---'); const mcData = require('minecraft-data')(bot.version); const testCases = [ ['sticks', 'stick'], ['stick', 'stick'], ['planks', 'planks'], ['oak_planks', 'oak_planks'], ['wooden_pickaxe', 'wooden_pickaxe'], ['wood_pickaxe', 'wooden_pickaxe'], ['wooden pickaxe', 'wooden_pickaxe'], ['crafting_table', 'crafting_table'], ['workbench', 'crafting_table'], ['torches', 'torch'], ['iron', 'iron_ingot'], ['cobble', 'cobblestone'], ['stone_sword', 'stone_sword'], ['diamond_pickaxe', 'diamond_pickaxe'], ]; // Inline version of resolveItemName for testing const ITEM_ALIASES = { 'sticks': 'stick', 'planks': 'planks', 'torches': 'torch', 'logs': 'oak_log', 'wood': 'oak_log', 'wood_planks': 'planks', 'plank': 'planks', 'log': 'oak_log', 'cobble': 'cobblestone', 'wood_pickaxe': 'wooden_pickaxe', 'wood_sword': 'wooden_sword', 'wood_axe': 'wooden_axe', 'wood_shovel': 'wooden_shovel', 'wood_hoe': 'wooden_hoe', 'workbench': 'crafting_table', 'bench': 'crafting_table', 'iron': 'iron_ingot', 'gold': 'gold_ingot', }; function resolveItemName(rawName) { let name = rawName.toLowerCase().trim().replace(/\s+/g, '_'); if (ITEM_ALIASES[name]) name = ITEM_ALIASES[name]; let item = mcData.itemsByName[name]; if (item) return item.name; if (name.endsWith('s') && !name.endsWith('ss')) { item = mcData.itemsByName[name.slice(0, -1)]; if (item) return item.name; } if (name.startsWith('wooden_')) { item = mcData.itemsByName[name.replace('wooden_', 'wood_')]; if (item) return item.name; } if (name.startsWith('wood_')) { item = mcData.itemsByName[name.replace('wood_', 'wooden_')]; if (item) return item.name; } for (const [n] of Object.entries(mcData.itemsByName)) { if (n === name || n.includes(name) || name.includes(n)) return n; } return null; } let passed = 0; for (const [input, expected] of testCases) { const result = resolveItemName(input); const ok = result === expected; log(` ${ok ? 'PASS' : 'FAIL'}: "${input}" → "${result}" (expected "${expected}")`); if (ok) passed++; } log(` Results: ${passed}/${testCases.length} passed`); } // --- Test: Inventory Check --- async function testInventoryCheck() { log('--- TEST: Inventory Contents ---'); const items = bot.inventory.items(); if (items.length === 0) { log(' Inventory is EMPTY. Give CraftTest some planks to test crafting.'); log(' Use: /give CraftTest oak_planks 4'); } else { for (const item of items) { log(` ${item.count}x ${item.name} (id=${item.type}, slot=${item.slot})`); } } } // --- Test: Craft Sticks --- async function testCraftSticks() { log('--- TEST: Craft Sticks ---'); const mcData = require('minecraft-data')(bot.version); // Check if we have planks const planks = bot.inventory.items().find(i => { const n = i.name.replace('minecraft:', ''); return n === 'planks' || n.endsWith('_planks'); }); if (!planks) { log(' SKIP: No planks in inventory. Give CraftTest planks first.'); log(' Hint: /give CraftTest oak_planks 4'); return; } log(` Have ${planks.count}x ${planks.name}`); // Look up stick item const stickItem = mcData.itemsByName['stick']; if (!stickItem) { log(' FAIL: "stick" not found in minecraft-data!'); return; } log(` stick item id = ${stickItem.id}`); // Check recipes const recipesNoTable = bot.recipesFor(stickItem.id, null, null, undefined); log(` Recipes without table: ${recipesNoTable.length}`); // Find crafting table nearby const craftingTable = bot.findBlock({ matching: (block) => { const n = block.name.replace('minecraft:', ''); return n === 'crafting_table' || n === 'workbench'; }, maxDistance: 32, }); if (craftingTable) { log(` Found crafting table at ${craftingTable.position}`); const recipesWithTable = bot.recipesFor(stickItem.id, null, null, craftingTable); log(` Recipes with table: ${recipesWithTable.length}`); } else { log(' No crafting table found nearby'); } // Try to craft (sticks are 2x2, should work without a table) const recipes = recipesNoTable.length > 0 ? recipesNoTable : (craftingTable ? bot.recipesFor(stickItem.id, null, null, craftingTable) : []); if (recipes.length === 0) { log(' FAIL: No recipes available for sticks'); return; } log(` Using recipe: ${JSON.stringify(recipes[0], null, 2).slice(0, 200)}`); try { const table = recipesNoTable.length > 0 ? undefined : craftingTable; await bot.craft(recipes[0], 1, table); log(' SUCCESS: Crafted sticks!'); // Verify sticks in inventory const sticks = bot.inventory.items().find(i => i.name.replace('minecraft:', '') === 'stick'); if (sticks) { log(` Verified: ${sticks.count}x stick in inventory`); } } catch (e) { log(` FAIL: Craft error: ${e.message}`); } } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }