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>
425 lines
16 KiB
JavaScript
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);
|
|
});
|