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:
parent
c4e1416f5f
commit
aa0a937171
2 changed files with 189 additions and 41 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ──
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue