dougbot/test-combat.js
roberts 195ef2d860 Major overhaul: combat fix, smart brain, Ollama stability, crafting
COMBAT:
- Fixed attack serialization: proper heldItemToNotch() for empty hand
- Attack now looks at target before swinging (server validates aim)
- Player position includes eye height in attack packet
- Click position calculated relative to target entity

INTELLIGENCE (complete rewrite):
- NeedsSystem: Sims-like needs (safety, hunger, social, shelter, boredom)
  that decay over time and drive behavior priorities
- GoalManager: Long-term goals broken into steps (gather_wood,
  explore_area, find_food, check_container, go_home)
- SpatialMemory: Remembers locations of containers, crafting tables,
  interesting blocks, and home position
- DailyRoutine: Morning/day/evening/night phases with trait-influenced
  activity selection
- Brain now WAITS for tasks to complete instead of piling on new ones
- Goal-based decision making replaces random wander

OLLAMA:
- Pre-warm model on deploy (loads into GPU before first chat)
- Keep-alive pings every 2 minutes (prevents model unload)
- Adaptive timeouts: 60s cold, 15s warm, 90s retry
- Auto-retry on timeout failure

CRAFTING:
- Quantity parsing ("craft 5 sticks", "craft a wooden pickaxe")
- Number words supported ("craft three planks")
- Smarter item name extraction with preposition stopping

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-30 16:07:23 -05:00

425 lines
16 KiB
JavaScript

/**
* 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);
});