diff --git a/bridge/src/index.js b/bridge/src/index.js index d8d33e1..75b0267 100644 --- a/bridge/src/index.js +++ b/bridge/src/index.js @@ -456,6 +456,130 @@ function isHostile(entity) { } // --- 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 = {}) { if (!spawned && action !== 'status') { 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' }; hostiles.sort((a, b) => a.dist - b.dist); 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; - const maxHits = 20; + const maxHits = 15; const combatPromise = new Promise((resolve) => { - const attackInterval = setInterval(() => { - // Check if target is still alive and in range - const ent = bot.entities[target.id]; + // Track if target dies via entityGone event + let targetDead = false; + 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) { clearInterval(attackInterval); + bot.removeListener('entityGone', onGone); resolve({ attacked: true, hits, result: 'target_gone' }); return; } + const d = ent.position.distanceTo(bot.entity.position); - if (d > range + 2) { + if (d > range + 4) { clearInterval(attackInterval); + bot.removeListener('entityGone', onGone); resolve({ attacked: true, hits, result: 'out_of_range' }); return; } + if (hits >= maxHits) { clearInterval(attackInterval); + bot.removeListener('entityGone', onGone); resolve({ attacked: true, hits, result: 'max_hits' }); return; } - // Look at target and attack - 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 + // Chase if too far if (d > 3) { 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 setTimeout(() => { clearInterval(attackInterval); + bot.removeListener('entityGone', onGone); resolve({ attacked: true, hits, result: 'timeout' }); - }, 10000); + }, 12000); }); 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 --- case 'list_recipes': { const { itemName } = params; diff --git a/dougbot/core/brain.py b/dougbot/core/brain.py index a567b6a..844f919 100644 --- a/dougbot/core/brain.py +++ b/dougbot/core/brain.py @@ -222,7 +222,15 @@ class DougBrain(QObject): def _on_inventory(self, response: ResponseMessage): if response.status != "success": return + old_items = set(i.get("name", "") for i in self._behaviors.inventory) 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) ── @@ -232,8 +240,8 @@ class DougBrain(QObject): if self._tasks._interruption and self._tasks._interruption.status == TaskStatus.ACTIVE: return - # Cooldown after combat — don't re-engage for 5 seconds - if hasattr(self, '_last_combat_time') and time.time() - self._last_combat_time < 5: + # Cooldown after combat — don't re-engage for 10 seconds + if hasattr(self, '_last_combat_time') and time.time() - self._last_combat_time < 10: return b = self._behaviors