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

# Combat System

> OSRS-accurate tick-based combat mechanics

# Combat System

Hyperscape implements a **tick-based combat system** inspired by Old School RuneScape. Combat operates on discrete 600ms ticks, with authentic damage formulas, accuracy rolls, and attack styles.

<Info>
  Combat code lives in `packages/shared/src/systems/shared/combat/` and uses constants from `packages/shared/src/constants/CombatConstants.ts`.
</Info>

## Core Constants

From `CombatConstants.ts`:

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
export const COMBAT_CONSTANTS = {
  // Tick timing
  TICK_DURATION_MS: 600,              // 0.6 seconds per tick (OSRS standard)

  // Attack ranges
  MELEE_RANGE_STANDARD: 1,            // Cardinal only (N/S/E/W)
  MELEE_RANGE_HALBERD: 2,             // Can attack diagonally
  RANGED_RANGE: 10,                   // Maximum ranged attack distance

  // Attack speeds (in ticks)
  DEFAULT_ATTACK_SPEED_TICKS: 4,      // 2.4 seconds (standard sword)
  FAST_ATTACK_SPEED_TICKS: 3,         // 1.8 seconds (scimitar, dagger)
  SLOW_ATTACK_SPEED_TICKS: 6,         // 3.6 seconds (2H sword)

  // Damage
  MIN_DAMAGE: 0,
  MAX_DAMAGE: 200,

  // XP rates (per damage dealt)
  XP: {
    COMBAT_XP_PER_DAMAGE: 4,          // 4 XP per damage for main skill
    HITPOINTS_XP_PER_DAMAGE: 1.33,    // 1.33 XP for Constitution
    CONTROLLED_XP_PER_DAMAGE: 1.33,   // Split across all combat skills
  },

  // Food consumption (OSRS-accurate)
  EAT_DELAY_TICKS: 3,                 // 1.8 seconds between foods
  EAT_ATTACK_DELAY_TICKS: 3,          // Added to attack cooldown when eating mid-combat
  MAX_HEAL_AMOUNT: 99,                // Security cap on healing

  // Combat timeout
  COMBAT_TIMEOUT_TICKS: 16,           // 9.6 seconds out of combat

  // Food consumption (OSRS-accurate)
  EAT_DELAY_TICKS: 3,                 // 1.8s cooldown between eating
  EAT_ATTACK_DELAY_TICKS: 3,          // Attack delay when eating during combat
  MAX_HEAL_AMOUNT: 99,                // Maximum heal per food item
};
```

***

## Session Interruption

### Combat Closes Bank/Store/Dialogue

When a player is attacked, all interaction sessions are automatically closed (OSRS-accurate behavior):

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From InteractionSessionManager.ts
// OSRS-accurate: Being attacked (even a splash/miss) interrupts banking
world.on(EventType.COMBAT_DAMAGE_DEALT, (event) => {
  if (event.targetType === "player" && this.sessions.has(event.targetId)) {
    this.closeSession(event.targetId, "combat");
  }
});
```

**Session Close Reasons:**

* `user_action` — Player explicitly closed UI
* `distance` — Player moved too far from target
* `disconnect` — Player disconnected
* `new_session` — Replaced by new session
* `target_gone` — Target entity no longer exists
* `combat` — Player was attacked (OSRS-style)

<Info>
  **OSRS-Accurate**: Even a splash attack (0 damage) closes the bank/store/dialogue. Being in combat matters, not just taking damage.
</Info>

***

## Food Consumption & Combat

### Eat Delay Mechanics

Food consumption integrates with the combat system using OSRS-accurate timing:

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From EatDelayManager.ts
export class EatDelayManager {
  canEat(playerId: string, currentTick: number): boolean;
  recordEat(playerId: string, currentTick: number): void;
  getRemainingCooldown(playerId: string, currentTick: number): number;
}
```

**OSRS Rules:**

* **3-tick delay** between eating (1.8 seconds)
* Food consumed **even at full health**
* Attack delay **only added if already on cooldown**
* If weapon is ready to attack, eating does NOT add delay

### Attack Delay Integration

When eating during combat, the system checks if the player is on attack cooldown:

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From CombatSystem.ts
public isPlayerOnAttackCooldown(playerId: string, currentTick: number): boolean {
  const nextAllowedTick = this.nextAttackTicks.get(playerId) ?? 0;
  return currentTick < nextAllowedTick;
}

public addAttackDelay(playerId: string, delayTicks: number): void {
  const currentNext = this.nextAttackTicks.get(playerId);
  if (currentNext !== undefined) {
    // Add delay to existing cooldown
    this.nextAttackTicks.set(playerId, currentNext + delayTicks);
  }
  // If no cooldown, do nothing (OSRS-accurate)
}
```

**Example Scenario:**

1. Player attacks with longsword (4-tick weapon)
2. Attack lands at tick 100, next attack at tick 104
3. Player eats at tick 102 (while on cooldown)
4. Eat delay adds 3 ticks: next attack now at tick 107
5. If player eats at tick 104+ (weapon ready), no delay added

### Healing Formula

Healing is capped and validated server-side:

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From PlayerSystem.ts
const healAmount = Math.min(
  Math.max(0, Math.floor(itemData.healAmount)),
  COMBAT_CONSTANTS.MAX_HEAL_AMOUNT  // 99 max
);

this.healPlayer(playerId, healAmount);
```

***

## Combat Styles

Combat styles determine which skill gains XP and provide stat bonuses.

| Style          | XP Distribution         | Bonus       |
| -------------- | ----------------------- | ----------- |
| **Accurate**   | Attack only             | +3 Attack   |
| **Aggressive** | Strength only           | +3 Strength |
| **Defensive**  | Defense only            | +3 Defense  |
| **Controlled** | All four skills equally | +1 to each  |

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From CombatCalculations.ts
const STYLE_BONUSES: Record<CombatStyle, StyleBonus> = {
  accurate: { attack: 3, strength: 0, defense: 0 },
  aggressive: { attack: 0, strength: 3, defense: 0 },
  defensive: { attack: 0, strength: 0, defense: 3 },
  controlled: { attack: 1, strength: 1, defense: 1 },
};
```

### Combat Style Icons

Each combat style has a unique SVG icon with active state colors:

| Style          | Icon            | Active Color     | Description        |
| -------------- | --------------- | ---------------- | ------------------ |
| **Accurate**   | Target/bullseye | Red (#ef4444)    | Concentric circles |
| **Aggressive** | Double chevrons | Green (#22c55e)  | Power attack       |
| **Defensive**  | Shield          | Blue (#3b82f6)   | Protection         |
| **Controlled** | Balance symbol  | Purple (#a855f7) | Balanced training  |

### Action Bar Integration

Combat styles can be dragged from the combat panel to the action bar for quick switching:

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From packages/client/src/game/panels/CombatPanel.tsx
<DraggableCombatStyleButton
  style="accurate"
  icon={<AccurateIcon />}
  active={activeStyle === "accurate"}
/>

// Action bar slot handles combat style drops
if (dragData.type === "combatstyle") {
  setSlot(slotIndex, {
    type: "combatstyle",
    combatStyleId: dragData.combatStyleId,
  });
}

// Clicking combat style slot switches attack style
if (slot.type === "combatstyle") {
  world.network.send("changeCombatStyle", {
    style: slot.combatStyleId,
  });
}
```

**Action Bar Features:**

* Drag combat styles from combat panel
* Click to switch attack style
* Visual highlight when style is active
* Supports 4-12 customizable slots (default: 9)
* Persistent across sessions

***

## Damage Calculation

Damage uses the authentic OSRS formula from [the wiki](https://oldschool.runescape.wiki/w/Damage_per_second/Melee). **Prayer bonuses are applied as multipliers** to effective levels.

### Maximum Hit Formula

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From CombatCalculations.ts
function calculateMaxHit(
  strengthLevel: number,
  strengthBonus: number,
  styleBonus: number,
  prayerMultiplier: number = 1.0,  // NEW: Prayer bonus
): number {
  // Effective Strength = floor((Strength Level + 8 + Style Bonus) × Prayer Multiplier)
  const effectiveStrength = Math.floor(
    (strengthLevel + 8 + styleBonus) * prayerMultiplier
  );

// Apply prayer bonuses (NEW in PR #563)
const prayerBonuses = getPrayerBonuses(attacker);
const prayerMultiplier = prayerBonuses.strength ?? 1;
const effectiveStrengthWithPrayer = Math.floor(effectiveStrength * prayerMultiplier);

// Strength Bonus from equipment
const strengthBonus = equipmentStats?.strength || 0;

// Max Hit = floor(0.5 + (Effective Strength × (Strength Bonus + 64)) / 640)
const maxHit = Math.floor(0.5 + (effectiveStrengthWithPrayer * (strengthBonus + 64)) / 640);
```

<Info>
  Prayer bonuses are applied to effective levels before damage calculation. For example, Burst of Strength (+5%) multiplies effective strength by 1.05.
</Info>

### Accuracy Formula

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From CombatCalculations.ts
function calculateAccuracy(
  attackerAttackLevel: number,
  attackerAttackBonus: number,
  targetDefenseLevel: number,
  targetDefenseBonus: number,
  attackerStyle: CombatStyle = "accurate",
  attackerPrayerBonuses?: PrayerBonuses,  // NEW in PR #563
  defenderPrayerBonuses?: PrayerBonuses,  // NEW in PR #563
): boolean {
  // Apply prayer bonuses to effective levels
  const prayerAttackMult = attackerPrayerBonuses?.attack ?? 1;
  const prayerDefenseMult = defenderPrayerBonuses?.defense ?? 1;
  
  const effectiveAttack = Math.floor((attackerAttackLevel + 8 + styleBonus.attack) * prayerAttackMult);
  const attackRoll = effectiveAttack * (attackerAttackBonus + 64);

  const effectiveDefence = Math.floor((targetDefenseLevel + 9 + defenderStyleBonus.defense) * prayerDefenseMult);
  const defenceRoll = effectiveDefence * (targetDefenseBonus + 64);

  let hitChance: number;
  if (attackRoll > defenceRoll) {
    hitChance = 1 - (defenceRoll + 2) / (2 * (attackRoll + 1));
  } else {
    hitChance = attackRoll / (2 * (defenceRoll + 1));
  }

  return random.random() < hitChance;
}
```

<Info>
  Prayer bonuses from active prayers (e.g., Clarity of Thought +5% attack, Thick Skin +5% defense) are applied to effective levels before calculating attack and defense rolls.
</Info>

### Damage Roll

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// If hit succeeds, roll damage from 0 to maxHit
const damage = didHit ? rng.damageRoll(maxHit) : 0;
```

***

## Attack Range System

### Melee Range

<Warning>
  **OSRS Accuracy**: Standard melee (range 1) can only attack in **cardinal directions** (N/S/E/W). Diagonal attacks require range 2+ weapons like halberds.
</Warning>

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From TileSystem.ts
export function tilesWithinMeleeRange(
  attacker: TileCoord,
  target: TileCoord,
  meleeRange: number,
): boolean {
  const dx = Math.abs(attacker.x - target.x);
  const dz = Math.abs(attacker.z - target.z);

  // Range 1 (standard melee): CARDINAL ONLY
  if (meleeRange === 1) {
    return (dx === 1 && dz === 0) || (dx === 0 && dz === 1);
  }

  // Range 2+ (halberd): Allow diagonal attacks
  const chebyshevDistance = Math.max(dx, dz);
  return chebyshevDistance <= meleeRange && chebyshevDistance > 0;
}
```

### Ranged Combat

Ranged attacks use Chebyshev distance and require:

* A ranged weapon (bow)
* Ammunition (arrows)

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// Ranged range check
export function isInAttackRange(
  attackerPos: Position3D,
  targetPos: Position3D,
  attackType: AttackType,
): boolean {
  const attackerTile = worldToTile(attackerPos.x, attackerPos.z);
  const targetTile = worldToTile(targetPos.x, targetPos.z);

  if (attackType === AttackType.MELEE) {
    return tilesWithinMeleeRange(attackerTile, targetTile, 1);
  } else {
    const tileDistance = tileChebyshevDistance(attackerTile, targetTile);
    return tileDistance <= COMBAT_CONSTANTS.RANGED_RANGE && tileDistance > 0;
  }
}
```

***

## Attack Speed & Cooldowns

Attacks occur on tick boundaries with weapon-specific speeds.

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// Convert weapon attack speed to ticks
export function attackSpeedMsToTicks(ms: number): number {
  return Math.max(1, Math.round(ms / COMBAT_CONSTANTS.TICK_DURATION_MS));
}

// Check if attack is on cooldown
export function isAttackOnCooldownTicks(
  currentTick: number,
  nextAttackTick: number,
): boolean {
  return currentTick < nextAttackTick;
}

// Auto-retaliate delay: ceil(weapon_speed / 2) + 1 ticks
export function calculateRetaliationDelay(attackSpeedTicks: number): number {
  return Math.ceil(attackSpeedTicks / 2) + 1;
}
```

### Weapon Speed Examples

| Weapon Type | Speed (ticks) | Speed (seconds) |
| ----------- | ------------- | --------------- |
| Scimitar    | 3             | 1.8s            |
| Longsword   | 4             | 2.4s            |
| Battleaxe   | 5             | 3.0s            |
| 2H Sword    | 6             | 3.6s            |
| Shortbow    | 3             | 1.8s            |
| Longbow     | 5             | 3.0s            |

***

## Aggro System

NPCs have configurable aggression behaviors.

### Aggro Types

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From types/core/core.ts
export type AggressionType =
  | "passive"           // Never attacks first
  | "aggressive"        // Attacks players below double its level
  | "always_aggressive" // Attacks all players
  | "level_gated";      // Only attacks below specific level
```

### Aggro Constants

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
export const AGGRO_CONSTANTS = {
  CHECK_INTERVAL_MS: 600,       // Check every tick
  PASSIVE_AGGRO_RANGE: 0,       // No aggro range
  STANDARD_AGGRO_RANGE: 4,      // 4 tiles
  BOSS_AGGRO_RANGE: 8,          // 8 tiles for bosses
  EXTENDED_AGGRO_RANGE: 6,      // 6 tiles for always_aggressive
};

export const LEVEL_CONSTANTS = {
  DOUBLE_LEVEL_MULTIPLIER: 2,   // Aggro stops when player is 2x NPC level
};
```

### Aggro Logic

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// Simplified from AggroSystem.ts
function shouldAggroPlayer(mob: MobEntity, player: PlayerEntity): boolean {
  const mobData = mob.getMobData();
  const distance = tileChebyshevDistance(mob.tile, player.tile);

  switch (mobData.aggression.type) {
    case "passive":
      return false;

    case "aggressive":
      // Only aggro if player level < 2 × mob level
      if (player.combatLevel >= mobData.stats.level * 2) return false;
      return distance <= AGGRO_CONSTANTS.STANDARD_AGGRO_RANGE;

    case "always_aggressive":
      return distance <= AGGRO_CONSTANTS.EXTENDED_AGGRO_RANGE;

    case "level_gated":
      if (player.combatLevel > mobData.aggression.maxLevel) return false;
      return distance <= AGGRO_CONSTANTS.STANDARD_AGGRO_RANGE;
  }
}
```

***

## Death Mechanics

### Player Death

When a player dies:

1. **Headstone spawns** at death location
2. **Items drop** to headstone (kept for 15 minutes)
3. **Player respawns** at starter town
4. **3 most valuable items** are kept (Protect Item prayer adds 1)

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From DeathSystem.ts
handlePlayerDeath(playerId: string, deathPosition: Position3D): void {
  // Create headstone entity
  const headstone = new HeadstoneEntity(this.world, {
    position: deathPosition,
    ownerId: playerId,
    items: droppedItems,
    expiresAt: Date.now() + 15 * 60 * 1000, // 15 minutes
  });

  // Respawn player at starter town
  this.respawnPlayer(playerId, STARTER_TOWN_POSITION);
}
```

### Mob Death

When a mob dies:

1. **Loot drops** based on drop table
2. **XP granted** to all attackers
3. **Respawn timer** starts (based on mob type)
4. **Entity destroyed** after death animation

***

## XP Distribution

XP is granted based on damage dealt and combat style.

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From SkillsSystem.ts
handleCombatKill(data: CombatKillData): void {
  const totalDamage = data.damageDealt;

  // Combat skill XP: 4 per damage
  const combatXP = totalDamage * COMBAT_CONSTANTS.XP.COMBAT_XP_PER_DAMAGE;

  // Constitution XP: 1.33 per damage (always)
  const hpXP = totalDamage * COMBAT_CONSTANTS.XP.HITPOINTS_XP_PER_DAMAGE;

  switch (data.attackStyle) {
    case "accurate":
      this.grantXP(attackerId, "attack", combatXP);
      break;
    case "aggressive":
      this.grantXP(attackerId, "strength", combatXP);
      break;
    case "defensive":
      this.grantXP(attackerId, "defense", combatXP);
      break;
    case "controlled":
      // Split evenly across all 4 skills
      const splitXP = totalDamage * COMBAT_CONSTANTS.XP.CONTROLLED_XP_PER_DAMAGE;
      this.grantXP(attackerId, "attack", splitXP);
      this.grantXP(attackerId, "strength", splitXP);
      this.grantXP(attackerId, "defense", splitXP);
      this.grantXP(attackerId, "constitution", splitXP);
      return; // HP included above
  }

  // Grant Constitution XP
  this.grantXP(attackerId, "constitution", hpXP);
}
```

***

## Food & Combat Interaction

### Eating During Combat

When a player eats food while in combat, OSRS-accurate timing rules apply:

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From PlayerSystem.ts
// OSRS Rule: Foods only add to EXISTING attack delay
// If weapon is ready to attack, eating does NOT add delay

const isOnCooldown = combatSystem.isPlayerOnAttackCooldown(playerId, currentTick);

if (isOnCooldown) {
  // Add 3 ticks to attack cooldown
  combatSystem.addAttackDelay(playerId, COMBAT_CONSTANTS.EAT_ATTACK_DELAY_TICKS);
}
// If weapon is ready (cooldown expired), eating does NOT add delay
```

### Eat Delay Mechanics

Players cannot eat again until the eat delay expires:

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// 3-tick (1.8 second) cooldown between foods
const canEat = eatDelayManager.canEat(playerId, currentTick);

if (!canEat) {
  // Show "You are already eating." message
  return;
}

// Record eat action
eatDelayManager.recordEat(playerId, currentTick);
```

<Info>
  **OSRS-Accurate**: Food is consumed even at full health. The eat delay and attack delay apply regardless of current HP.
</Info>

### Attack Delay API

The `CombatSystem` provides methods for eat delay integration:

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// Check if player is on attack cooldown
isPlayerOnAttackCooldown(playerId: string, currentTick: number): boolean;

// Add delay ticks to player's next attack
addAttackDelay(playerId: string, delayTicks: number): void;
```

***

## Combat Events

The combat system emits events for UI and logging:

| Event                   | Data                                                   | Description                      |
| ----------------------- | ------------------------------------------------------ | -------------------------------- |
| `COMBAT_ATTACK`         | `attackerId`, `targetId`, `damage`, `didHit`           | Attack executed                  |
| `COMBAT_KILL`           | `attackerId`, `targetId`, `damageDealt`, `attackStyle` | Kill confirmed                   |
| `COMBAT_STARTED`        | `entityId`, `targetId`                                 | Entity entered combat            |
| `COMBAT_ENDED`          | `entityId`                                             | Entity left combat               |
| `ENTITY_DAMAGED`        | `entityId`, `damage`, `sourceId`, `remainingHealth`    | Damage taken                     |
| `ENTITY_DEATH`          | `entityId`, `killerId`, `position`                     | Entity died                      |
| `PLAYER_HEALTH_UPDATED` | `playerId`, `health`, `maxHealth`                      | Health changed (healing, damage) |

***

***

## Duel Arena Integration

The combat system integrates with the Duel Arena for PvP combat:

### Rule Enforcement

The DuelSystem provides APIs for rule checking:

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
const duelSystem = world.getSystem("duel");

// Check if player can use specific combat types
if (duelSystem && !duelSystem.canUseRanged(attackerId)) {
  return; // Block ranged attack in duel
}

if (duelSystem && !duelSystem.canUseMelee(attackerId)) {
  return; // Block melee attack in duel
}

if (duelSystem && !duelSystem.canEatFood(playerId)) {
  return; // Block food consumption in duel
}
```

### Death Handling

Duel deaths are handled differently than normal deaths:

```typescript theme={"theme":{"light":"github-light","dark":"css-variables"}}
// From PlayerDeathSystem.ts
const duelSystem = this.world.getSystem("duel");
if (duelSystem?.isPlayerInActiveDuel(playerId)) {
  // Duel death - DuelSystem handles resolution
  // No items lost, no headstone, winner gets stakes
  return;
}

// Normal death - respawn at hospital with item loss
this.handleNormalDeath(playerId);
```

<Info>
  See the [Duel Arena documentation](/wiki/game-systems/duel-arena) for complete PvP mechanics.
</Info>

***

## Related Documentation

* [Duel Arena](/wiki/game-systems/duel-arena) (PvP dueling system)
* [Inventory System](/wiki/game-systems/inventory) (for food consumption)
* [Tile Movement System](/wiki/game-systems/movement)
* [Skills & Progression](/wiki/game-systems/skills)
* [NPC Data Structure](/wiki/data/npcs)
* [Item Stats](/wiki/data/items)
