/** * Combat Test Script * * Connects to a Bedrock BDS server and attempts to attack the nearest mob * using multiple methods to determine which one actually deals damage. * * Usage: * node --experimental-strip-types --disable-warning=ExperimentalWarning test-combat.js * * Run from the bridge/ directory so node_modules resolve correctly. */ // --- Ensure bridge/node_modules is on the require path --- const path = require('path'); const Module = require('module'); const bridgeDir = path.join(__dirname, 'bridge'); const originalResolvePaths = Module._nodeModulePaths; Module._nodeModulePaths = function(from) { const paths = originalResolvePaths.call(this, from); const bridgeNodeModules = path.join(bridgeDir, 'node_modules'); if (!paths.includes(bridgeNodeModules)) { paths.unshift(bridgeNodeModules); } return paths; }; // --- Patch prismarine-physics (same as bridge/src/index.js) --- 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, }; const mineflayer = require('./bridge/lib/mineflayer'); const { pathfinder: pathfinderPlugin, Movements, goals } = require('mineflayer-pathfinder'); const { GoalNear, GoalFollow } = goals; const { Vec3 } = require('vec3'); const HOST = '192.168.1.90'; const PORT = 19140; const USERNAME = 'CombatTest'; const NAMED_ENTITY_HEIGHT = 1.62; console.log(`[CombatTest] 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', respawn: true, }); bot.loadPlugin(pathfinderPlugin); // Track entity health changes via update_attributes const entityHealthTracker = new Map(); bot._client.on('update_attributes', (packet) => { for (const attr of packet.attributes) { if (attr.name === 'minecraft:health') { const prev = entityHealthTracker.get(packet.runtime_entity_id); entityHealthTracker.set(packet.runtime_entity_id, attr.current); if (prev !== undefined && attr.current < prev) { console.log(`[HealthTracker] Entity ${packet.runtime_entity_id} took damage: ${prev} -> ${attr.current}`); } } } }); // Track entity_event for hurt animations bot._client.on('entity_event', (packet) => { if (packet.event_id === 'hurt_animation' || packet.event_id === 2 || packet.event_id === 'hurt') { console.log(`[EntityEvent] Entity ${packet.runtime_entity_id} received hurt event (event_id=${packet.event_id})`); } }); // Track animate packets for server-side swing confirmation bot._client.on('animate', (packet) => { if (packet.runtime_entity_id === bot.entity?.id) { console.log(`[Animate] Server acknowledged arm swing (action_id=${packet.action_id})`); } }); let spawned = false; let movements = null; bot.once('spawn', async () => { spawned = true; movements = new Movements(bot); movements.canDig = false; movements.canOpenDoors = false; bot.pathfinder.setMovements(movements); const pos = bot.entity.position; console.log(`[CombatTest] Spawned at (${pos.x.toFixed(1)}, ${pos.y.toFixed(1)}, ${pos.z.toFixed(1)})`); console.log(`[CombatTest] Entity ID: ${bot.entity.id}`); console.log(`[CombatTest] quickBarSlot: ${bot.quickBarSlot}`); console.log(`[CombatTest] heldItem: ${JSON.stringify(bot.heldItem)}`); // Wait a moment for entities to load console.log('[CombatTest] Waiting 3s for entities to populate...'); await sleep(3000); // Find nearest mob const target = bot.nearestEntity((e) => e.type === 'mob' || (e.type === 'object' && e.name !== 'unknown' && e.name !== 'item')); if (!target) { console.log('[CombatTest] No mobs found nearby! Listing all entities:'); for (const [id, e] of Object.entries(bot.entities)) { if (e === bot.entity) continue; console.log(` Entity ${id}: type=${e.type} name=${e.name} pos=${e.position}`); } console.log('[CombatTest] Try spawning mobs near the bot with /summon'); // Keep running so we can try later waitForMobAndTest(); return; } await runCombatTests(target); }); async function waitForMobAndTest() { console.log('[CombatTest] Polling for mobs every 5s...'); const interval = setInterval(async () => { const target = bot.nearestEntity((e) => e.type === 'mob' || (e.type === 'object' && e.name !== 'unknown' && e.name !== 'item')); if (target) { clearInterval(interval); await runCombatTests(target); } }, 5000); } async function runCombatTests(target) { console.log(`\n[CombatTest] ====== TARGET FOUND ======`); console.log(`[CombatTest] Target: ${target.name} (id=${target.id}, type=${target.type})`); console.log(`[CombatTest] Target position: ${target.position}`); console.log(`[CombatTest] Target height: ${target.height}`); const dist = bot.entity.position.distanceTo(target.position); console.log(`[CombatTest] Distance: ${dist.toFixed(2)}`); // Walk towards the target if too far if (dist > 3.5) { console.log(`[CombatTest] Walking to target...`); try { bot.pathfinder.setGoal(new GoalNear(target.position.x, target.position.y, target.position.z, 2)); await sleep(5000); } catch (e) { console.log(`[CombatTest] Pathfinding issue: ${e.message}`); } bot.pathfinder.setGoal(null); } const finalDist = bot.entity.position.distanceTo(target.position); console.log(`[CombatTest] Final distance: ${finalDist.toFixed(2)}`); if (finalDist > 6) { console.log('[CombatTest] Still too far. Teleport closer or summon a mob near the bot.'); return; } // Item helpers const Item = require('prismarine-item')(bot.registry); function getHeldItemNotch() { const item = bot.heldItem; if (!item) return { network_id: 0 }; return Item.toNotch(item, 0); } // ======================================================================== // METHOD 1: inventory_transaction with item_use_on_entity (FIXED VERSION) // This is the standard Bedrock protocol way to attack entities. // ======================================================================== console.log(`\n[CombatTest] --- METHOD 1: inventory_transaction (fixed) ---`); { // Look at the target first const targetEyePos = target.position.offset(0, (target.height || 0) * 0.5, 0); await bot.lookAt(targetEyePos, true); await sleep(100); // Swing arm first bot.swingArm(); const heldItemNotch = getHeldItemNotch(); const playerPos = { x: bot.entity.position.x, y: bot.entity.position.y + NAMED_ENTITY_HEIGHT, z: bot.entity.position.z, }; const clickPos = { x: target.position.x - bot.entity.position.x, y: (target.position.y + (target.height || 0) * 0.5) - (bot.entity.position.y + NAMED_ENTITY_HEIGHT), z: target.position.z - bot.entity.position.z, }; const packet = { transaction: { legacy: { legacy_request_id: 0 }, transaction_type: 'item_use_on_entity', actions: [], transaction_data: { entity_runtime_id: target.id, action_type: 'attack', hotbar_slot: bot.quickBarSlot ?? 0, held_item: heldItemNotch, player_pos: playerPos, click_pos: clickPos, }, }, }; console.log(`[CombatTest] Sending inventory_transaction:`, JSON.stringify(packet, null, 2)); bot._client.write('inventory_transaction', packet); console.log('[CombatTest] Sent! Waiting 2s to observe result...'); await sleep(2000); } // ======================================================================== // METHOD 2: bot.attack() using the mineflayer API (uses the updated code) // ======================================================================== console.log(`\n[CombatTest] --- METHOD 2: bot.attack() (mineflayer API) ---`); { // Re-find the target in case it moved const currentTarget = bot.entities[target.id]; if (currentTarget) { console.log(`[CombatTest] Calling bot.attack(target)...`); await bot.attack(currentTarget); console.log('[CombatTest] Sent! Waiting 2s to observe result...'); await sleep(2000); } else { console.log('[CombatTest] Target entity no longer exists'); } } // ======================================================================== // METHOD 3: player_action with missed_swing (tests if server needs this) // ======================================================================== console.log(`\n[CombatTest] --- METHOD 3: player_action missed_swing ---`); { // Look at target const targetEyePos = target.position.offset(0, (target.height || 0) * 0.5, 0); await bot.lookAt(targetEyePos, true); const actionPacket = { runtime_entity_id: bot.entity.id, action: 'missed_swing', position: { x: 0, y: 0, z: 0 }, result_position: { x: 0, y: 0, z: 0 }, face: 0, }; console.log(`[CombatTest] Sending player_action (missed_swing)...`); try { bot._client.write('player_action', actionPacket); console.log('[CombatTest] Sent (this should NOT deal damage by itself).'); } catch (e) { console.log(`[CombatTest] Error: ${e.message}`); } await sleep(1000); } // ======================================================================== // METHOD 4: Rapid attack combo (swing + transaction, 5 times, 500ms apart) // Tests if cooldown or timing matters // ======================================================================== console.log(`\n[CombatTest] --- METHOD 4: Rapid attack combo (5x, 500ms apart) ---`); { for (let i = 0; i < 5; i++) { const currentTarget = bot.entities[target.id]; if (!currentTarget) { console.log(`[CombatTest] Target died or despawned after ${i} hits!`); break; } // Look at target const targetEyePos = currentTarget.position.offset(0, (currentTarget.height || 0) * 0.5, 0); await bot.lookAt(targetEyePos, true); // Swing arm bot.swingArm(); // Send attack const heldItemNotch = getHeldItemNotch(); bot._client.write('inventory_transaction', { transaction: { legacy: { legacy_request_id: 0 }, transaction_type: 'item_use_on_entity', actions: [], transaction_data: { entity_runtime_id: currentTarget.id, action_type: 'attack', hotbar_slot: bot.quickBarSlot ?? 0, held_item: heldItemNotch, player_pos: { x: bot.entity.position.x, y: bot.entity.position.y + NAMED_ENTITY_HEIGHT, z: bot.entity.position.z, }, click_pos: { x: currentTarget.position.x - bot.entity.position.x, y: (currentTarget.position.y + (currentTarget.height || 0) * 0.5) - (bot.entity.position.y + NAMED_ENTITY_HEIGHT), z: currentTarget.position.z - bot.entity.position.z, }, }, }, }); console.log(`[CombatTest] Attack ${i + 1}/5 sent`); await sleep(500); } console.log('[CombatTest] Waiting 2s to observe results...'); await sleep(2000); } // ======================================================================== // METHOD 5: interact packet (mouse_over_entity) before attack // Some servers need the interact packet first to register targeting // ======================================================================== console.log(`\n[CombatTest] --- METHOD 5: interact (mouse_over) + attack ---`); { const currentTarget = bot.entities[target.id]; if (currentTarget) { // Look at target const targetEyePos = currentTarget.position.offset(0, (currentTarget.height || 0) * 0.5, 0); await bot.lookAt(targetEyePos, true); // Send interact mouse_over_entity first try { bot._client.write('interact', { action_id: 'mouse_over_entity', target_entity_id: currentTarget.id, has_position: false, }); console.log('[CombatTest] Sent interact mouse_over_entity'); } catch (e) { console.log(`[CombatTest] interact packet error: ${e.message}`); } await sleep(100); // Then swing and attack bot.swingArm(); const heldItemNotch = getHeldItemNotch(); bot._client.write('inventory_transaction', { transaction: { legacy: { legacy_request_id: 0 }, transaction_type: 'item_use_on_entity', actions: [], transaction_data: { entity_runtime_id: currentTarget.id, action_type: 'attack', hotbar_slot: bot.quickBarSlot ?? 0, held_item: heldItemNotch, player_pos: { x: bot.entity.position.x, y: bot.entity.position.y + NAMED_ENTITY_HEIGHT, z: bot.entity.position.z, }, click_pos: { x: currentTarget.position.x - bot.entity.position.x, y: (currentTarget.position.y + (currentTarget.height || 0) * 0.5) - (bot.entity.position.y + NAMED_ENTITY_HEIGHT), z: currentTarget.position.z - bot.entity.position.z, }, }, }, }); console.log('[CombatTest] Sent attack after interact. Waiting 2s...'); await sleep(2000); } else { console.log('[CombatTest] Target no longer exists'); } } console.log('\n[CombatTest] ====== ALL TESTS COMPLETE ======'); console.log('[CombatTest] Check output above for:'); console.log(' - [HealthTracker] lines = entity took damage (health decreased)'); console.log(' - [EntityEvent] lines = hurt animation played'); console.log(' - [Animate] lines = server acknowledged arm swing'); console.log('[CombatTest] Bot will stay connected for manual testing. Press Ctrl+C to exit.'); } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } bot.on('error', (err) => { console.error('[CombatTest] Error:', err.message); }); bot.on('end', (reason) => { console.log('[CombatTest] Disconnected:', reason); process.exit(0); }); bot.on('kicked', (reason) => { console.log('[CombatTest] Kicked:', reason); });