Fix combat loop, add equipment management
Combat: - Track entityGone event to detect mob death (not just entity check) - Auto-equip best weapon before fighting - 10-second cooldown between engagements (was 5) - Better target death detection Equipment system: - equipBestWeapon(): finds and equips highest-tier sword/axe - equipBestTool(blockType): picks right tool for block (pickaxe for stone, etc.) - equipBestArmor(): equips best armor in each slot - Auto-equip armor when new armor pieces appear in inventory - Weapon tiers: netherite > diamond > iron > golden > stone > wooden - Tool type mapping: pickaxe for ores, axe for wood, shovel for dirt New bridge actions: equip_best_weapon, equip_best_tool, equip_armor Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
413d18d997
commit
c2b996947a
2 changed files with 199 additions and 17 deletions
|
|
@ -456,6 +456,130 @@ function isHostile(entity) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- WebSocket Action Handlers ---
|
// --- WebSocket Action Handlers ---
|
||||||
|
// --- Equipment Management Helpers ---
|
||||||
|
|
||||||
|
// Weapon tiers (higher = better)
|
||||||
|
const WEAPON_TIERS = {
|
||||||
|
'netherite_sword': 10, 'diamond_sword': 9, 'iron_sword': 8,
|
||||||
|
'golden_sword': 6, 'stone_sword': 5, 'wooden_sword': 4,
|
||||||
|
'netherite_axe': 9, 'diamond_axe': 8, 'iron_axe': 7,
|
||||||
|
'golden_axe': 5, 'stone_axe': 4, 'wooden_axe': 3,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Tool type → block types it's best for
|
||||||
|
const TOOL_FOR_BLOCK = {
|
||||||
|
'pickaxe': ['stone', 'cobblestone', 'ore', 'iron', 'gold', 'diamond', 'netherite', 'brick', 'obsidian', 'deepslate', 'copper'],
|
||||||
|
'axe': ['log', 'planks', 'wood', 'fence', 'door', 'sign', 'bookshelf', 'chest', 'crafting_table', 'barrel'],
|
||||||
|
'shovel': ['dirt', 'grass', 'sand', 'gravel', 'clay', 'soul_sand', 'snow', 'mud', 'farmland'],
|
||||||
|
'hoe': ['hay', 'sponge', 'leaves', 'sculk', 'moss'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const TOOL_TIERS = ['netherite', 'diamond', 'iron', 'golden', 'stone', 'wooden'];
|
||||||
|
|
||||||
|
// Armor slots and tiers
|
||||||
|
const ARMOR_SLOTS = {
|
||||||
|
'helmet': 'head', 'chestplate': 'torso', 'leggings': 'legs', 'boots': 'feet',
|
||||||
|
};
|
||||||
|
const ARMOR_TIERS = ['netherite', 'diamond', 'iron', 'golden', 'chainmail', 'leather'];
|
||||||
|
|
||||||
|
async function equipBestWeapon() {
|
||||||
|
const items = bot.inventory.items();
|
||||||
|
let bestItem = null;
|
||||||
|
let bestTier = -1;
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const name = item.name.replace('minecraft:', '');
|
||||||
|
const tier = WEAPON_TIERS[name] || 0;
|
||||||
|
if (tier > bestTier) {
|
||||||
|
bestTier = tier;
|
||||||
|
bestItem = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestItem && bestItem !== bot.heldItem) {
|
||||||
|
try {
|
||||||
|
await bot.equip(bestItem, 'hand');
|
||||||
|
log('client', 'INFO', `Equipped ${bestItem.name}`);
|
||||||
|
return { equipped: true, item: bestItem.name };
|
||||||
|
} catch (e) {
|
||||||
|
return { equipped: false, error: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { equipped: false, reason: bestItem ? 'already_equipped' : 'no_weapons' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function equipBestTool(blockType) {
|
||||||
|
if (!blockType) return { equipped: false, reason: 'no_block_type' };
|
||||||
|
|
||||||
|
// Figure out which tool type is needed
|
||||||
|
let neededToolType = null;
|
||||||
|
const bt = blockType.toLowerCase();
|
||||||
|
for (const [toolType, blockTypes] of Object.entries(TOOL_FOR_BLOCK)) {
|
||||||
|
if (blockTypes.some(b => bt.includes(b))) {
|
||||||
|
neededToolType = toolType;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!neededToolType) return { equipped: false, reason: 'no_tool_needed' };
|
||||||
|
|
||||||
|
// Find best tool of that type in inventory
|
||||||
|
const items = bot.inventory.items();
|
||||||
|
let bestItem = null;
|
||||||
|
let bestTierIdx = 999;
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const name = item.name.replace('minecraft:', '');
|
||||||
|
if (!name.includes(neededToolType)) continue;
|
||||||
|
const tierIdx = TOOL_TIERS.findIndex(t => name.startsWith(t));
|
||||||
|
if (tierIdx >= 0 && tierIdx < bestTierIdx) {
|
||||||
|
bestTierIdx = tierIdx;
|
||||||
|
bestItem = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestItem && bestItem !== bot.heldItem) {
|
||||||
|
try {
|
||||||
|
await bot.equip(bestItem, 'hand');
|
||||||
|
log('client', 'INFO', `Equipped ${bestItem.name} for ${blockType}`);
|
||||||
|
return { equipped: true, item: bestItem.name, toolType: neededToolType };
|
||||||
|
} catch (e) {
|
||||||
|
return { equipped: false, error: e.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { equipped: false, reason: bestItem ? 'already_equipped' : `no_${neededToolType}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function equipBestArmor() {
|
||||||
|
const items = bot.inventory.items();
|
||||||
|
const equipped = [];
|
||||||
|
|
||||||
|
for (const [armorPiece, slot] of Object.entries(ARMOR_SLOTS)) {
|
||||||
|
let bestItem = null;
|
||||||
|
let bestTierIdx = 999;
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const name = item.name.replace('minecraft:', '');
|
||||||
|
if (!name.includes(armorPiece)) continue;
|
||||||
|
const tierIdx = ARMOR_TIERS.findIndex(t => name.startsWith(t));
|
||||||
|
if (tierIdx >= 0 && tierIdx < bestTierIdx) {
|
||||||
|
bestTierIdx = tierIdx;
|
||||||
|
bestItem = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestItem) {
|
||||||
|
try {
|
||||||
|
await bot.equip(bestItem, slot);
|
||||||
|
equipped.push(bestItem.name);
|
||||||
|
} catch (e) {
|
||||||
|
log('client', 'WARN', `Failed to equip ${bestItem.name}: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { equipped: equipped.length > 0, items: equipped };
|
||||||
|
}
|
||||||
|
|
||||||
async function handleAction(action, params = {}) {
|
async function handleAction(action, params = {}) {
|
||||||
if (!spawned && action !== 'status') {
|
if (!spawned && action !== 'status') {
|
||||||
throw new Error('Bot not spawned yet');
|
throw new Error('Bot not spawned yet');
|
||||||
|
|
@ -766,53 +890,103 @@ async function handleAction(action, params = {}) {
|
||||||
if (hostiles.length === 0) return { attacked: false, reason: 'no_hostiles' };
|
if (hostiles.length === 0) return { attacked: false, reason: 'no_hostiles' };
|
||||||
hostiles.sort((a, b) => a.dist - b.dist);
|
hostiles.sort((a, b) => a.dist - b.dist);
|
||||||
const target = hostiles[0].entity;
|
const target = hostiles[0].entity;
|
||||||
log('client', 'INFO', `Engaging ${target.name || target.type} (id=${target.id}, dist=${hostiles[0].dist.toFixed(1)}, gameMode=${bot.game?.gameMode})`);
|
const targetId = target.id;
|
||||||
|
|
||||||
// Sustained combat: keep attacking until target is dead or out of range
|
// Auto-equip best weapon before fighting
|
||||||
|
await equipBestWeapon();
|
||||||
|
|
||||||
|
log('client', 'INFO', `Fighting ${target.name || target.type} (id=${targetId}, dist=${hostiles[0].dist.toFixed(1)})`);
|
||||||
|
|
||||||
|
// Sustained combat loop
|
||||||
let hits = 0;
|
let hits = 0;
|
||||||
const maxHits = 20;
|
const maxHits = 15;
|
||||||
const combatPromise = new Promise((resolve) => {
|
const combatPromise = new Promise((resolve) => {
|
||||||
const attackInterval = setInterval(() => {
|
// Track if target dies via entityGone event
|
||||||
// Check if target is still alive and in range
|
let targetDead = false;
|
||||||
const ent = bot.entities[target.id];
|
const onGone = (entity) => {
|
||||||
|
if (entity.id === targetId) targetDead = true;
|
||||||
|
};
|
||||||
|
bot.on('entityGone', onGone);
|
||||||
|
|
||||||
|
const attackInterval = setInterval(async () => {
|
||||||
|
// Target despawned or dead
|
||||||
|
if (targetDead) {
|
||||||
|
clearInterval(attackInterval);
|
||||||
|
bot.removeListener('entityGone', onGone);
|
||||||
|
log('client', 'INFO', `Target killed after ${hits} hits`);
|
||||||
|
resolve({ attacked: true, hits, result: 'target_killed' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ent = bot.entities[targetId];
|
||||||
if (!ent || !ent.position) {
|
if (!ent || !ent.position) {
|
||||||
clearInterval(attackInterval);
|
clearInterval(attackInterval);
|
||||||
|
bot.removeListener('entityGone', onGone);
|
||||||
resolve({ attacked: true, hits, result: 'target_gone' });
|
resolve({ attacked: true, hits, result: 'target_gone' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const d = ent.position.distanceTo(bot.entity.position);
|
const d = ent.position.distanceTo(bot.entity.position);
|
||||||
if (d > range + 2) {
|
if (d > range + 4) {
|
||||||
clearInterval(attackInterval);
|
clearInterval(attackInterval);
|
||||||
|
bot.removeListener('entityGone', onGone);
|
||||||
resolve({ attacked: true, hits, result: 'out_of_range' });
|
resolve({ attacked: true, hits, result: 'out_of_range' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hits >= maxHits) {
|
if (hits >= maxHits) {
|
||||||
clearInterval(attackInterval);
|
clearInterval(attackInterval);
|
||||||
|
bot.removeListener('entityGone', onGone);
|
||||||
resolve({ attacked: true, hits, result: 'max_hits' });
|
resolve({ attacked: true, hits, result: 'max_hits' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look at target and attack
|
// Chase if too far
|
||||||
bot.lookAt(ent.position.offset(0, ent.height * 0.8, 0)).then(() => {
|
|
||||||
try { bot.attack(ent); hits++; } catch (e) {}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Move toward target if too far for melee
|
|
||||||
if (d > 3) {
|
if (d > 3) {
|
||||||
bot.pathfinder.setGoal(new GoalNear(ent.position.x, ent.position.y, ent.position.z, 2));
|
bot.pathfinder.setGoal(new GoalNear(ent.position.x, ent.position.y, ent.position.z, 2));
|
||||||
}
|
}
|
||||||
}, 500); // Attack every 500ms
|
|
||||||
|
// Look at and attack
|
||||||
|
try {
|
||||||
|
await bot.lookAt(ent.position.offset(0, (ent.height || 1) * 0.5, 0), true);
|
||||||
|
await bot.attack(ent);
|
||||||
|
hits++;
|
||||||
|
} catch (e) {
|
||||||
|
// Entity might have died between check and attack
|
||||||
|
clearInterval(attackInterval);
|
||||||
|
bot.removeListener('entityGone', onGone);
|
||||||
|
resolve({ attacked: true, hits, result: 'target_gone' });
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
|
||||||
// Safety timeout
|
// Safety timeout
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
clearInterval(attackInterval);
|
clearInterval(attackInterval);
|
||||||
|
bot.removeListener('entityGone', onGone);
|
||||||
resolve({ attacked: true, hits, result: 'timeout' });
|
resolve({ attacked: true, hits, result: 'timeout' });
|
||||||
}, 10000);
|
}, 12000);
|
||||||
});
|
});
|
||||||
|
|
||||||
return await combatPromise;
|
return await combatPromise;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Equipment Management ---
|
||||||
|
case 'equip_best_weapon': {
|
||||||
|
const result = await equipBestWeapon();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'equip_best_tool': {
|
||||||
|
const { blockType } = params;
|
||||||
|
const result = await equipBestTool(blockType);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'equip_armor': {
|
||||||
|
const result = await equipBestArmor();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Crafting ---
|
// --- Crafting ---
|
||||||
case 'list_recipes': {
|
case 'list_recipes': {
|
||||||
const { itemName } = params;
|
const { itemName } = params;
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,15 @@ class DougBrain(QObject):
|
||||||
def _on_inventory(self, response: ResponseMessage):
|
def _on_inventory(self, response: ResponseMessage):
|
||||||
if response.status != "success":
|
if response.status != "success":
|
||||||
return
|
return
|
||||||
|
old_items = set(i.get("name", "") for i in self._behaviors.inventory)
|
||||||
self._behaviors.inventory = response.data.get("items", [])
|
self._behaviors.inventory = response.data.get("items", [])
|
||||||
|
new_items = set(i.get("name", "") for i in self._behaviors.inventory)
|
||||||
|
|
||||||
|
# Auto-equip armor if we picked up new armor pieces
|
||||||
|
new_pickups = new_items - old_items
|
||||||
|
armor_words = ("helmet", "chestplate", "leggings", "boots")
|
||||||
|
if any(any(a in item for a in armor_words) for item in new_pickups):
|
||||||
|
self._ws.send_request("equip_armor", {})
|
||||||
|
|
||||||
# ── Interrupts (combat/flee — temporary, don't affect stack) ──
|
# ── Interrupts (combat/flee — temporary, don't affect stack) ──
|
||||||
|
|
||||||
|
|
@ -232,8 +240,8 @@ class DougBrain(QObject):
|
||||||
if self._tasks._interruption and self._tasks._interruption.status == TaskStatus.ACTIVE:
|
if self._tasks._interruption and self._tasks._interruption.status == TaskStatus.ACTIVE:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Cooldown after combat — don't re-engage for 5 seconds
|
# Cooldown after combat — don't re-engage for 10 seconds
|
||||||
if hasattr(self, '_last_combat_time') and time.time() - self._last_combat_time < 5:
|
if hasattr(self, '_last_combat_time') and time.time() - self._last_combat_time < 10:
|
||||||
return
|
return
|
||||||
|
|
||||||
b = self._behaviors
|
b = self._behaviors
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue