> ## Documentation Index
> Fetch the complete documentation index at: https://hyperscape-ai-mintlify-docs-update.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Combat AI

> Tick-based PvP combat controller with LLM tactics and trash talk

## Overview

`DuelCombatAI` is a specialized tick-based combat controller that takes over an AI agent's behavior during arena duels. It uses the `EmbeddedHyperscapeService` to execute game actions and makes priority-based combat decisions: heal, attack, or switch style.

<Info>
  DuelCombatAI operates at the same 600ms tick rate as the game's combat system, ensuring synchronized attack timing.
</Info>

***

## Architecture

### Lifecycle

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From packages/server/src/arena/DuelCombatAI.ts
const combatAI = new DuelCombatAI(
  service,           // EmbeddedHyperscapeService
  opponentId,        // Target character ID
  config,            // Combat configuration
  runtime,           // AgentRuntime for LLM calls (optional)
  sendChat           // Callback for trash talk (optional)
);

// Start combat AI
combatAI.start();

// Tick externally (called by StreamingDuelScheduler)
await combatAI.externalTick();

// Stop combat AI
combatAI.stop();
```

**Key Design Decision (commit 51453dae):**

The combat system's auto-attack loop (`processPlayerCombatTick` → `processAutoAttackOnTick`) drives the actual attack cadence once combat is established. The AI only needs to re-engage when:

* Combat has dropped
* Target has changed

Calling `executeAttack` on every cooldown cycle creates a redundant second driver that competes for the same cooldown slot, silently dropping attacks (especially for slow weapons like 2h swords).

***

## Combat Phases

DuelCombatAI operates in four distinct phases:

| Phase         | Trigger            | Behavior                                     |
| ------------- | ------------------ | -------------------------------------------- |
| **Opening**   | First 5 ticks      | Activate offensive prayers, use buff potions |
| **Trading**   | Normal combat      | Balanced approach, maintain pressure         |
| **Finishing** | Opponent \< 25% HP | Aggressive style, maximize damage            |
| **Desperate** | Own HP \< 30%      | Defensive style, activate protection prayers |

***

## Decision Priority

Each tick, the AI evaluates actions in this order:

1. **Heal** - If HP below threshold, eat food
2. **Buff** - If opening phase, use buff potions
3. **Strategy** - Apply LLM-planned tactics (if enabled)
4. **Attack** - Re-engage if combat dropped or target changed

***

## Configuration

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
interface DuelCombatConfig {
  healThresholdPct: number;        // HP% to start healing (default: 40)
  aggressiveThresholdPct: number;  // HP% to use aggressive style (default: 70)
  defensiveThresholdPct: number;   // HP% to switch defensive (default: 30)
  maxTicksWithoutAttack: number;   // Unused (default: 5)
  useLlmTactics: boolean;          // Enable LLM strategy planning (default: false)
}
```

### Default Configuration

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
const DEFAULT_CONFIG: DuelCombatConfig = {
  healThresholdPct: 40,
  aggressiveThresholdPct: 70,
  defensiveThresholdPct: 30,
  maxTicksWithoutAttack: 5,
  useLlmTactics: false,
};
```

***

## LLM Strategy Planning

When `useLlmTactics: true`, the AI uses LLM to generate combat strategies:

### Strategy Object

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
interface CombatStrategy {
  approach: "aggressive" | "defensive" | "balanced" | "outlast";
  attackStyle: string;
  prayer: string | null;
  protectionPrayer: string | null;
  foodThreshold: number;        // HP% to eat at (20-60)
  switchDefensiveAt: number;    // HP% to go defensive (20-40)
  reasoning: string;
}
```

### Planning Triggers

Strategy replanning occurs when:

* First tick (initial strategy)
* HP changes by more than 20%
* Opponent drops below 25% HP
* Entering desperate phase

**Replanning Cooldown**: 8 seconds minimum between LLM calls

### LLM Prompt

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
const prompt = `
You are ${agentName} in a PvP duel arena. Plan your combat strategy.

YOUR STATE: HP ${healthPct}%, ${foodCount} food, tick ${tickCount}
OPPONENT: HP ${oppHpPct}%, combat level ${opponentCombatLevel}
DAMAGE SO FAR: dealt ${totalDamageDealt}, received ${totalDamageReceived}

Available prayers: ultimate_strength (+15% str), steel_skin (+15% def), rock_skin (+10% def)
Available styles: aggressive (max damage), defensive (less damage taken), controlled (balanced), accurate (hit more often)

Respond with a JSON object:
{
  "approach": "aggressive" | "defensive" | "balanced" | "outlast",
  "attackStyle": "aggressive" | "defensive" | "controlled" | "accurate",
  "prayer": "ultimate_strength" | "steel_skin" | null,
  "foodThreshold": 20-60 (HP% to eat at, lower = riskier),
  "switchDefensiveAt": 20-40 (HP% to go defensive),
  "reasoning": "brief explanation"
}
`;
```

### Background Planning

**Critical**: Strategy planning runs in the background and NEVER blocks the tick loop:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Fire-and-forget LLM call
this._llmPlanningInFlight = true;
this.planStrategy(state, healthPct, opponentData)
  .then(() => {
    this.lastReplanTime = Date.now();
    this.strategyPlanned = true;
  })
  .finally(() => {
    this._llmPlanningInFlight = false;
  });
```

**LLM Timeout**: 3 seconds maximum wait for LLM response before giving up

***

## Trash Talk System

AI agents taunt opponents during combat using health-triggered and ambient messages.

### Health Threshold Taunts

Triggered when HP crosses these milestones:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
const TRASH_TALK_THRESHOLDS = [90, 80, 70, 60, 50, 40, 30, 20, 10];
```

**Own HP Low** (agent taking damage):

* "Not even close!"
* "I've had worse"
* "Is that all?"
* "Still standing"

**Opponent HP Low** (agent winning):

* "GG soon"
* "You're done!"
* "Sit down"
* "One more hit..."

### Ambient Taunts

Random taunts every 5-12 ticks with no specific trigger:

* "Let's go!"
* "Fight me!"
* "Too slow"
* "Bring it"
* "Nice try lol"

### Opening Taunts

Fired immediately when the duel starts:

* "You're going down"
* "Let's dance"
* "Ready to lose?"
* "This won't take long"

### LLM-Generated Trash Talk

When an `AgentRuntime` is provided, trash talk uses the agent's character bio and communication style:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
const prompt = `
You are ${agentName} in a PvP duel against ${opponentName}.
Your personality: ${bioText}
Your communication style: ${styleHints}
Your HP: ${healthPct}%. Opponent HP: ${oppPct}.
Situation: ${situation}

Generate a SHORT trash talk message (under 40 characters) for the overhead chat bubble.
Stay in character. Be creative, funny, competitive. No quotes. Just the message.
`;
```

**LLM Settings:**

* Model: `TEXT_SMALL`
* Max tokens: 30
* Temperature: 0.9 (high creativity)
* Timeout: 3 seconds

**Cooldown**: 8 seconds between trash talk messages (configurable via `TRASH_TALK_COOLDOWN_MS`)

### Fallback Behavior

If LLM fails or times out, the system uses scripted fallback taunts to ensure trash talk always works.

***

## Attack Timing Fix (commit 51453dae)

### Problem

Previously, DuelCombatAI manually tracked attack speed and called `executeAttack` on every cooldown cycle:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// ❌ OLD CODE (removed)
if (this.ticksSinceLastAttack >= attackSpeed) {
  await this.service.executeAttack(this.opponentId);
  this.ticksSinceLastAttack = 0;
}
```

This created a redundant second attack driver that competed with the combat system's built-in auto-attack loop, causing:

* Silent attack drops for slow weapons (2h swords, halberds)
* Desynchronization between AI and combat system
* Missed attacks due to cooldown conflicts

### Solution

The AI now only re-engages when combat has dropped or the target changed:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// ✅ NEW CODE (commit 51453dae)
const needsEngagement =
  !state.inCombat || state.currentTarget !== this.opponentId;

if (needsEngagement) {
  await this.service.executeAttack(this.opponentId);
}
```

The combat system's auto-attack loop handles all subsequent attacks at the correct weapon speed.

***

## TWO\_HAND\_SWORD Default Style (commit 51453dae)

Added missing default attack style for two-handed swords in `WeaponStyleConfig.ts`:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From packages/shared/src/constants/WeaponStyleConfig.ts
[WeaponType.TWO_HAND_SWORD]: [
  "accurate",
  "aggressive",
  "defensive",
  "controlled",
],
```

Previously, two-handed swords had no configured styles, causing the combat system to fall back to "accurate" without showing available options.

***

## Statistics Tracking

DuelCombatAI tracks combat statistics for analysis:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
const stats = combatAI.getStats();
// {
//   tickCount: 150,
//   attacksLanded: 45,
//   healsUsed: 8,
//   totalDamageDealt: 234,
//   totalDamageReceived: 189
// }
```

***

## Environment Variables

```bash theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
# Enable DuelCombatAI for streaming duels
STREAMING_DUEL_COMBAT_AI_ENABLED=true

# Enable LLM-based combat strategy planning
STREAMING_DUEL_LLM_TACTICS_ENABLED=true

# Trash talk system configuration
TRASH_TALK_COOLDOWN_MS=8000           # Cooldown between messages (default: 8s)
TRASH_TALK_LLM_TIMEOUT_MS=3000        # LLM generation timeout (default: 3s)
TRASH_TALK_ENABLED=true               # Enable trash talk (default: true)

# Combat stall fallback
STREAMING_COMBAT_STALL_NUDGE_MS=15000 # Fallback damage nudge delay
```

***

## Food Detection

DuelCombatAI automatically detects food items in inventory:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
const FOOD_DATA: Record<string, number> = {
  shrimp: 3,
  bread: 5,
  meat: 3,
  trout: 7,
  salmon: 9,
  tuna: 10,
  lobster: 12,
  bass: 13,
  swordfish: 14,
  monkfish: 16,
  karambwan: 18,
  shark: 20,
  manta: 22,
  anglerfish: 22,
  // ... more food items
};
```

The AI selects the highest-healing food available when HP drops below threshold.

***

## Potion Detection

Buff potions are detected by name patterns:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
const POTION_PATTERNS = [
  "potion",
  "brew",
  "restore",
  "prayer",
  "super",
  "ranging",
  "magic",
  "antifire",
  "antidote",
  "stamina",
];
```

Potions are used during the opening phase (first 2 ticks) for combat buffs.

***

## Performance Characteristics

* **Tick Rate**: 600ms (synchronized with game combat system)
* **LLM Timeout**: 3 seconds maximum
* **Planning Cooldown**: 8 seconds between strategy replans
* **Trash Talk Cooldown**: 8 seconds between messages
* **Background Processing**: All LLM calls are fire-and-forget, never block ticks

***

## Integration Example

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From packages/server/src/systems/StreamingDuelScheduler/
import { DuelCombatAI } from "../../arena/DuelCombatAI";

// Create combat AI for each agent
const combatAI = new DuelCombatAI(
  service,
  opponentId,
  {
    useLlmTactics: true,
    healThresholdPct: 40,
  },
  runtime,
  (text) => {
    // Send chat message above agent's head
    world.emit("chat:message", {
      characterId: agentId,
      message: text,
      type: "overhead",
    });
  }
);

// Start combat
combatAI.start();

// Tick in combat loop
setInterval(async () => {
  await combatAI.externalTick();
}, 600);

// Stop when duel ends
combatAI.stop();
```

***

## Debugging

Enable debug logging to see combat AI decisions:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// In DuelCombatAI.ts
console.log(`[DuelCombatAI] Strategy planned: ${this.strategy.approach}`);
console.log(`[DuelCombatAI] Heal failed (${food.itemId}):`, err);
console.log(`[DuelCombatAI] Attack failed:`, err);
```

Statistics are logged on stop:

```
[DuelCombatAI] Stopped after 150 ticks. Attacks: 45, Heals: 8, Dmg dealt: 234, Dmg received: 189
```

***

## Related Documentation

<CardGroup cols={2}>
  <Card title="AI Agents Overview" icon="bot" href="/wiki/ai-agents/overview">
    ElizaOS integration and plugin architecture
  </Card>

  <Card title="Combat System" icon="swords" href="/wiki/game-systems/combat">
    Core combat mechanics and tick-based system
  </Card>

  <Card title="Actions Reference" icon="play" href="/wiki/ai-agents/actions">
    All available agent actions including combat
  </Card>

  <Card title="Configuration" icon="sliders" href="/devops/configuration">
    Environment variables for combat AI
  </Card>
</CardGroup>
