diff --git a/bridge/src/index.js b/bridge/src/index.js index 547c88f..72d0117 100644 --- a/bridge/src/index.js +++ b/bridge/src/index.js @@ -253,6 +253,152 @@ bot.on('path_update', (results) => { } }); +// ═══════════════════════════════════════════════════════════════ +// CORE REFLEXES — these run automatically, not managed by Python +// ═══════════════════════════════════════════════════════════════ + +// --- Auto-Combat: Attack hostile mobs on sight --- +let combatActive = false; +let combatTargetId = null; +let combatInterval = null; +let lastEquipCheck = 0; + +function startCombat(target) { + if (combatActive) return; + combatActive = true; + combatTargetId = target.id; + + // Equip best weapon first + equipBestWeapon().then(() => { + log('client', 'INFO', `⚔ Engaging ${target.name || target.type} (id=${Number(target.id)})`); + sendEvent('combat_started', { target: target.name || target.type, id: Number(target.id) }); + + combatInterval = setInterval(async () => { + const ent = bot.entities[combatTargetId]; + + // Target gone — combat over + if (!ent || !ent.position || !ent.isValid) { + endCombat('target_dead'); + return; + } + + const dist = ent.position.distanceTo(bot.entity.position); + + // Target too far — give up + if (dist > 16) { + endCombat('out_of_range'); + return; + } + + // Chase if not in melee range + if (dist > 3.5) { + bot.pathfinder.setGoal(new GoalNear(ent.position.x, ent.position.y, ent.position.z, 2)); + } + + // Attack if in range + if (dist <= 4.5) { + try { + await bot.lookAt(ent.position.offset(0, (ent.height || 1) * 0.5, 0), true); + await bot.attack(ent); + } catch (e) { + // Entity died between check and attack + endCombat('target_dead'); + } + } + }, 450); + + // Safety timeout — 15 seconds max combat per target + setTimeout(() => { + if (combatActive && combatTargetId === target.id) { + endCombat('timeout'); + } + }, 15000); + }); +} + +function endCombat(reason) { + if (!combatActive) return; + if (combatInterval) clearInterval(combatInterval); + combatInterval = null; + combatActive = false; + log('client', 'INFO', `⚔ Combat ended: ${reason}`); + sendEvent('combat_ended', { reason }); + combatTargetId = null; +} + +// Check for hostile mobs every 2 seconds +setInterval(() => { + if (!spawned || combatActive) return; + + const pos = bot.entity.position; + let closestHostile = null; + let closestDist = 8; // Aggro range + + for (const entity of Object.values(bot.entities)) { + if (entity === bot.entity || !entity.position) continue; + if (!isHostile(entity)) continue; + const dist = entity.position.distanceTo(pos); + if (dist < closestDist) { + closestDist = dist; + closestHostile = entity; + } + } + + if (closestHostile) { + startCombat(closestHostile); + } +}, 2000); + +// When an entity we're fighting disappears, end combat +bot.on('entityGone', (entity) => { + if (combatActive && entity.id === combatTargetId) { + endCombat('target_dead'); + } +}); + +// --- Auto-Equip: Evaluate gear when inventory changes --- +let lastInventoryHash = ''; + +bot.on('playerCollect', async (collector, collected) => { + if (collector !== bot.entity) return; + + // Small delay to let the item appear in inventory + await new Promise(r => setTimeout(r, 500)); + + // Don't interrupt combat for gear evaluation + if (combatActive) { + // Queue it for after combat + const checkAfterCombat = setInterval(() => { + if (!combatActive) { + clearInterval(checkAfterCombat); + evaluateEquipment(); + } + }, 1000); + setTimeout(() => clearInterval(checkAfterCombat), 20000); + return; + } + + evaluateEquipment(); +}); + +async function evaluateEquipment() { + // Don't evaluate too frequently + if (Date.now() - lastEquipCheck < 3000) return; + lastEquipCheck = Date.now(); + + const weaponResult = await equipBestWeapon(); + if (weaponResult.equipped) { + log('client', 'INFO', `🛡 Auto-equipped weapon: ${weaponResult.item}`); + sendEvent('equipment_changed', { type: 'weapon', item: weaponResult.item }); + } + + const armorResult = await equipBestArmor(); + if (armorResult.equipped) { + log('client', 'INFO', `🛡 Auto-equipped armor: ${armorResult.items.join(', ')}`); + sendEvent('equipment_changed', { type: 'armor', items: armorResult.items }); + } +} + // --- Player-friendly name → Bedrock item ID mapping --- const ITEM_ALIASES = { // Plural → singular diff --git a/dougbot/core/brain.py b/dougbot/core/brain.py index 844f919..cec0c0b 100644 --- a/dougbot/core/brain.py +++ b/dougbot/core/brain.py @@ -75,6 +75,7 @@ class DougBrain(QObject): # Action tracking self._action_sent_time = 0.0 self._last_combat_time = 0.0 + self._in_combat = False # Bridge handles combat as core reflex def start(self): self._running = True @@ -120,6 +121,21 @@ class DougBrain(QObject): self._goals.on_death() log.info("Doug died — all tasks cleared") + elif event == "combat_started": + self._in_combat = True + target = data.get("target", "mob") + log.info(f"COMBAT: Fighting {target}") + + elif event == "combat_ended": + self._in_combat = False + reason = data.get("reason", "unknown") + log.info(f"COMBAT: Ended ({reason}) — resuming tasks") + + elif event == "equipment_changed": + etype = data.get("type", "") + item = data.get("item", data.get("items", "")) + log.info(f"EQUIP: Auto-equipped {etype}: {item}") + elif event == "player_joined": username = data.get("username", "") if username and username != self._doug_name: @@ -175,14 +191,18 @@ class DougBrain(QObject): if self._pending_scan: return - # Step 3: Check for interrupts (combat/flee) + # Step 3: If bridge is handling combat, WAIT — don't do anything else + if self._in_combat: + return + + # Step 4: Check for flee (cowardly Dougs only) self._check_interrupts() - # Step 4: If stack is empty, generate self-directed tasks + # Step 5: If stack is empty, generate self-directed tasks if not self._tasks.current_task: self._generate_self_directed() - # Step 5: Execute next subtask from the stack + # Step 6: Execute next subtask from the stack self._execute_next() # ── Scanning ── @@ -235,49 +255,31 @@ class DougBrain(QObject): # ── Interrupts (combat/flee — temporary, don't affect stack) ── def _check_interrupts(self): - """Check for immediate threats that need a temporary interrupt.""" - # Don't stack interrupts — if already fighting/fleeing, let it finish - if self._tasks._interruption and self._tasks._interruption.status == TaskStatus.ACTIVE: - return - - # 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 + """Check for threats. Combat is handled by bridge core reflex. + Brain only handles fleeing for cowardly Dougs.""" + # Combat is automatic in the bridge — don't manage it here + if self._in_combat: + return # Bridge is fighting, wait b = self._behaviors bravery = self._traits.get("bravery", 50) - hostile = self._nearest_hostile(10) - if not hostile: - return + # Only cowardly Dougs flee (bravery < 30) — brave ones let the bridge fight + if bravery < 30 and b.health < 14: + hostile = self._nearest_hostile(10) + if hostile and hostile.get("distance", 99) < 8: + hpos = hostile.get("position", b.position) + dx = b.position["x"] - hpos.get("x", 0) + dz = b.position["z"] - hpos.get("z", 0) + d = max(0.1, math.sqrt(dx * dx + dz * dz)) + flee_x = b.position["x"] + (dx / d) * 15 + flee_z = b.position["z"] + (dz / d) * 15 - dist = hostile.get("distance", 99) - mob_type = hostile.get("type", "mob") - - # FIGHT if brave enough and healthy - should_fight = bravery > 30 and b.health > 8 and dist < 6 - # FLEE if scared or hurt - should_flee = (not should_fight) and (dist < 8) and (b.health < 14 or bravery < 30) - - if should_fight: - self._last_combat_time = time.time() - self._tasks.interrupt(make_interrupt( - "combat", f"Fighting {mob_type}!", - "attack_nearest_hostile", {"range": 6}, timeout=12, - )) - elif should_flee: - hpos = hostile.get("position", b.position) - dx = b.position["x"] - hpos.get("x", 0) - dz = b.position["z"] - hpos.get("z", 0) - d = max(0.1, math.sqrt(dx * dx + dz * dz)) - flee_x = b.position["x"] + (dx / d) * 15 - flee_z = b.position["z"] + (dz / d) * 15 - - self._tasks.interrupt(make_interrupt( - "flee", f"Fleeing from {mob_type}!", - "move_to", {"x": flee_x, "y": b.position["y"], "z": flee_z, "range": 3}, - timeout=15, - )) + self._tasks.interrupt(make_interrupt( + "flee", f"Fleeing from {hostile.get('type', 'mob')}!", + "move_to", {"x": flee_x, "y": b.position["y"], "z": flee_z, "range": 3}, + timeout=15, + )) # ── Self-directed task generation ──