CORE REFLEX: Auto-combat and auto-equip baked into bridge

Architecture change — 3 layers:
  CORE (bridge): Combat + equipment evaluation — ALWAYS runs, interrupts everything
  PRIMARY (brain): Player-given or self-directed goals
  SUBTASK (brain): Steps within primary tasks

Core reflexes in bridge (run independently of Python brain):
- Auto-combat: scans for hostiles every 2s, engages nearest
  - Equips best weapon before fighting
  - Chases target, attacks every 450ms
  - Detects death via entityGone event
  - 15s safety timeout per engagement
  - Sends combat_started/combat_ended events to Python

- Auto-equip: evaluates gear when items are picked up
  - Equips best weapon (netherite > diamond > iron > ...)
  - Equips best armor in each slot
  - Waits until combat ends before evaluating
  - Sends equipment_changed event to Python

Brain changes:
- Brain PAUSES all task execution during combat (waits for combat_ended)
- Brain no longer manages combat — only flee for cowardly Dougs (bravery < 30)
- Combat events logged: COMBAT: Fighting zombie, COMBAT: Ended (target_dead)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
roberts 2026-03-30 20:12:56 -05:00
parent c4e1416f5f
commit aa0a937171
2 changed files with 189 additions and 41 deletions

View file

@ -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

View file

@ -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,37 +255,19 @@ 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)
# 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 not hostile:
return
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:
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)
@ -274,7 +276,7 @@ class DougBrain(QObject):
flee_z = b.position["z"] + (dz / d) * 15
self._tasks.interrupt(make_interrupt(
"flee", f"Fleeing from {mob_type}!",
"flee", f"Fleeing from {hostile.get('type', 'mob')}!",
"move_to", {"x": flee_x, "y": b.position["y"], "z": flee_z, "range": 3},
timeout=15,
))