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 ---
|
// --- Player-friendly name → Bedrock item ID mapping ---
|
||||||
const ITEM_ALIASES = {
|
const ITEM_ALIASES = {
|
||||||
// Plural → singular
|
// Plural → singular
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ class DougBrain(QObject):
|
||||||
# Action tracking
|
# Action tracking
|
||||||
self._action_sent_time = 0.0
|
self._action_sent_time = 0.0
|
||||||
self._last_combat_time = 0.0
|
self._last_combat_time = 0.0
|
||||||
|
self._in_combat = False # Bridge handles combat as core reflex
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self._running = True
|
self._running = True
|
||||||
|
|
@ -120,6 +121,21 @@ class DougBrain(QObject):
|
||||||
self._goals.on_death()
|
self._goals.on_death()
|
||||||
log.info("Doug died — all tasks cleared")
|
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":
|
elif event == "player_joined":
|
||||||
username = data.get("username", "")
|
username = data.get("username", "")
|
||||||
if username and username != self._doug_name:
|
if username and username != self._doug_name:
|
||||||
|
|
@ -175,14 +191,18 @@ class DougBrain(QObject):
|
||||||
if self._pending_scan:
|
if self._pending_scan:
|
||||||
return
|
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()
|
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:
|
if not self._tasks.current_task:
|
||||||
self._generate_self_directed()
|
self._generate_self_directed()
|
||||||
|
|
||||||
# Step 5: Execute next subtask from the stack
|
# Step 6: Execute next subtask from the stack
|
||||||
self._execute_next()
|
self._execute_next()
|
||||||
|
|
||||||
# ── Scanning ──
|
# ── Scanning ──
|
||||||
|
|
@ -235,49 +255,31 @@ class DougBrain(QObject):
|
||||||
# ── Interrupts (combat/flee — temporary, don't affect stack) ──
|
# ── Interrupts (combat/flee — temporary, don't affect stack) ──
|
||||||
|
|
||||||
def _check_interrupts(self):
|
def _check_interrupts(self):
|
||||||
"""Check for immediate threats that need a temporary interrupt."""
|
"""Check for threats. Combat is handled by bridge core reflex.
|
||||||
# Don't stack interrupts — if already fighting/fleeing, let it finish
|
Brain only handles fleeing for cowardly Dougs."""
|
||||||
if self._tasks._interruption and self._tasks._interruption.status == TaskStatus.ACTIVE:
|
# Combat is automatic in the bridge — don't manage it here
|
||||||
return
|
if self._in_combat:
|
||||||
|
return # Bridge is fighting, wait
|
||||||
# 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
|
b = self._behaviors
|
||||||
bravery = self._traits.get("bravery", 50)
|
bravery = self._traits.get("bravery", 50)
|
||||||
|
|
||||||
hostile = self._nearest_hostile(10)
|
# Only cowardly Dougs flee (bravery < 30) — brave ones let the bridge fight
|
||||||
if not hostile:
|
if bravery < 30 and b.health < 14:
|
||||||
return
|
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)
|
self._tasks.interrupt(make_interrupt(
|
||||||
mob_type = hostile.get("type", "mob")
|
"flee", f"Fleeing from {hostile.get('type', 'mob')}!",
|
||||||
|
"move_to", {"x": flee_x, "y": b.position["y"], "z": flee_z, "range": 3},
|
||||||
# FIGHT if brave enough and healthy
|
timeout=15,
|
||||||
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-directed task generation ──
|
# ── Self-directed task generation ──
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue