> ## 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.

# Death & Respawn System

> Player death handling, gravestones, and item recovery with OSRS keep-3 mechanics

# Death & Respawn System

The death system handles player deaths with zone-aware mechanics, gravestone spawning, and secure item recovery. Completely rewritten in PR #1094 (March 2026) to fix SQLite deadlock, equipment duplication, and implement OSRS-style "keep 3 most valuable items" for safe zone deaths.

<Info>
  Death code lives in `packages/shared/src/systems/shared/` with:

  * `combat/PlayerDeathSystem.ts` - Main death orchestrator
  * `combat/DeathUtils.ts` - Pure utility functions (sanitization, keep-3, validation)
  * `combat/DeathTypes.ts` - Type definitions
  * `death/DeathStateManager.ts` - Death lock persistence and crash recovery
  * `death/SafeAreaDeathHandler.ts` - Safe zone death logic (gravestone system)
  * `death/WildernessDeathHandler.ts` - Wilderness death logic (immediate ground drop)
  * `death/ZoneDetectionSystem.ts` - Zone type detection
</Info>

<Warning>
  **Breaking Change (March 2026)**: `PLAYER_DIED` event is deprecated. Use `PLAYER_SET_DEAD` for client death UI, or `ENTITY_DEATH` with type filter for server-side death processing.
</Warning>

## Death Zones

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
type ZoneType = 
  | "SAFE_AREA"   // Town, bank areas - keep 3 most valuable items (OSRS-style)
  | "WILDERNESS"; // PvP wilderness - all items drop on death
```

**OSRS-Accurate Mechanics**:

* **Safe Zone**: Keep 3 most valuable items (by manifest value), rest goes to gravestone
* **Wilderness**: All items drop immediately to ground (no gravestone)
* **Duel Arena**: No item drops (inventory/equipment preserved)

***

## Death Lock System

To prevent item duplication on server restart/crash, deaths are tracked with database persistence. **Updated in PR #1094** to include kept items for crash recovery.

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
interface DeathLock {
  playerId: string;
  gravestoneId?: string;           // If gravestone spawned
  position: { x: number; y: number; z: number };
  timestamp: number;
  zoneType: ZoneType;
  itemCount: number;               // Total items dropped
  items: Array<{                   // Dropped items (for gravestone)
    itemId: string;
    quantity: number;
  }>;
  keptItems: Array<{               // Kept items (for respawn) - NEW in PR #1094
    itemId: string;
    quantity: number;
  }>;
  killedBy: string;                // Killer name (sanitized)
}
```

**New in PR #1094**:

* `items` field stores dropped items for gravestone recovery
* `keptItems` field stores kept items for respawn recovery
* `killedBy` field stores sanitized killer name (XSS/injection protected)

### Creating a Death Lock

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
async createDeathLock(
  playerId: string,
  options: {
    gravestoneId?: string;
    groundItemIds?: string[];
    position: { x: number; y: number; z: number };
    zoneType: ZoneType;
    itemCount: number;
  },
  tx?: TransactionContext,  // Optional DB transaction
): Promise<void> {
  // Server authority check
  if (!this.world.isServer) {
    console.error(`[DeathStateManager] Client attempted death lock creation - BLOCKED`);
    return;
  }
  
  const deathData: DeathLock = {
    playerId,
    gravestoneId: options.gravestoneId,
    groundItemIds: options.groundItemIds,
    position: options.position,
    timestamp: Date.now(),
    zoneType: options.zoneType,
    itemCount: options.itemCount,
  };
  
  // Store in memory cache
  this.activeDeaths.set(playerId, deathData);
  
  // Persist to database (prevents duplication on restart)
  if (this.databaseSystem) {
    await this.databaseSystem.saveDeathLockAsync(deathData, tx);
  }
}
```

***

## Death Flow (Updated March 2026)

### Safe Zone Death (OSRS Keep-3)

1. Player HP reaches 0
2. **Transaction starts**:
   * Clear equipment in-memory (skip DB persist)
   * Clear inventory in-memory (skip DB persist)
   * Split items: keep 3 most valuable, drop rest
   * Create death lock with kept items
   * Commit transaction
3. **After transaction**:
   * Persist equipment clear to DB (retry on failure)
   * Persist inventory clear to DB (retry on failure)
   * Emit `PLAYER_SET_DEAD` event
4. **Respawn** (tick-based, deterministic):
   * Return kept items to inventory
   * Spawn gravestone with dropped items (5 minute timer)
   * Teleport to spawn town
   * Clear death lock
5. Player can return to gravestone to reclaim dropped items

**Two-Phase Persist Pattern**: In-memory clear inside transaction, DB persist after transaction. Prevents SQLite deadlock from nested transactions.

### Wilderness Death

1. Player HP reaches 0
2. **Transaction starts**:
   * Clear equipment in-memory
   * Clear inventory in-memory
   * All items marked for ground drop (no keep-3)
   * Create death lock
   * Commit transaction
3. **After transaction**:
   * Persist clears to DB
   * Drop all items to ground immediately (no gravestone)
   * Items despawn after 2 minutes
4. **Respawn**:
   * Teleport to spawn town
   * No items returned
   * Clear death lock after ground items despawn

### Duel Arena Death

1. Player HP reaches 0 in duel arena
2. **No item drops** (inventory/equipment preserved)
3. Death animation plays
4. DuelSystem handles respawn and stakes
5. No death lock created

***

## Gravestone System

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Gravestone entity config
const gravestone = new GravestoneEntity(this.world, {
  position: deathPosition,
  ownerId: playerId,
  items: droppedItems,
  expiresAt: Date.now() + 15 * 60 * 1000,  // 15 minutes
});

// From CombatConstants.ts
GRAVESTONE_TICKS: 1500,          // 15 minutes in ticks (1500 × 0.6s)
GRAVESTONE_WARNING_TICKS: 500,   // 5 minute warning
```

### Gravestone Expiration

When gravestone expires:

1. Items transition to ground items
2. Ground items have additional despawn timer
3. `onGravestoneExpired()` updates death lock

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
async onGravestoneExpired(playerId: string, groundItemIds: string[]): Promise<void> {
  const deathData = this.activeDeaths.get(playerId);
  if (!deathData) return;
  
  // Update tracking: gravestone → ground items
  deathData.gravestoneId = undefined;
  deathData.groundItemIds = groundItemIds;
  this.activeDeaths.set(playerId, deathData);
  
  // Update database
  await this.databaseSystem.updateGroundItemsAsync(playerId, groundItemIds);
}
```

***

## Item Recovery

### Looting from Gravestone

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Player interacts with their gravestone
async lootGravestone(playerId: string, gravestoneId: string): Promise<void> {
  const gravestone = this.world.entities.get(gravestoneId);
  
  // Verify ownership
  if (gravestone.ownerId !== playerId) {
    this.emitMessage(playerId, "This isn't your gravestone.", "error");
    return;
  }
  
  // Transfer items to inventory
  for (const item of gravestone.items) {
    const success = await this.inventorySystem.addItem(
      playerId,
      item.itemId,
      item.quantity
    );
    
    if (!success) {
      // Inventory full - leave remaining items
      break;
    }
    
    // Track item looted
    await this.deathStateManager.onItemLooted(playerId, item.id);
  }
  
  // Destroy gravestone if empty
  if (gravestone.items.length === 0) {
    this.entityManager.destroyEntity(gravestoneId);
    await this.deathStateManager.clearDeathLock(playerId);
  }
}
```

### Clearing Death Lock

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
async clearDeathLock(playerId: string): Promise<void> {
  // Remove from memory
  this.activeDeaths.delete(playerId);
  
  // Remove from database
  if (this.databaseSystem) {
    await this.databaseSystem.deleteDeathLockAsync(playerId);
  }
}
```

***

## Reconnect Validation

When a player reconnects, the system checks for active deaths:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
async hasActiveDeathLock(playerId: string): Promise<boolean> {
  // Check memory cache first
  if (this.activeDeaths.has(playerId)) return true;
  
  // Fallback to database (critical for crash recovery)
  if (this.databaseSystem) {
    const dbData = await this.databaseSystem.getDeathLockAsync(playerId);
    if (dbData) {
      // Restore to memory cache
      this.activeDeaths.set(playerId, dbData);
      return true;
    }
  }
  
  return false;
}
```

This prevents:

* Item duplication if server crashes mid-death
* Double death processing on reconnect
* Gravestone re-creation exploits

***

## Death Events

| Event                     | Data                                                      | Description                                                      |
| ------------------------- | --------------------------------------------------------- | ---------------------------------------------------------------- |
| `ENTITY_DEATH`            | `entityId`, `entityType`, `killedBy`, `deathPosition`     | Entity died (use this for death processing)                      |
| `PLAYER_SET_DEAD`         | `playerId`, `isDead`, `deathPosition?`                    | Player death state changed (use this for client UI)              |
| `PLAYER_DIED`             | `playerId`                                                | **DEPRECATED** - Use `PLAYER_SET_DEAD` or `ENTITY_DEATH` instead |
| `PLAYER_RESPAWNED`        | `playerId`, `spawnPosition`, `townName`, `deathLocation?` | Player respawned                                                 |
| `DEATH_RECOVERED`         | `playerId`, `position`, `items`, `killedBy`, `zoneType`   | Crash recovery (server restart)                                  |
| `CORPSE_EMPTY`            | `corpseId`, `playerId`                                    | All items looted from gravestone                                 |
| `DEATH_HEADSTONE_EXPIRED` | `headstoneId`, `playerId`                                 | Gravestone timed out                                             |
| `AUDIT_LOG`               | `action`, `playerId`, `success`, `failureReason?`         | Ops-visible audit events                                         |

**Event Migration** (PR #1094):

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// ❌ OLD (deprecated)
world.on(EventType.PLAYER_DIED, (data: { playerId: string }) => {
  // Handle player death
});

// ✅ NEW (use ENTITY_DEATH with type filter)
world.on(EventType.ENTITY_DEATH, (data: { 
  entityId: string; 
  entityType: string;
  killedBy?: string;
  deathPosition?: { x: number; y: number; z: number };
}) => {
  if (data.entityType === 'player') {
    // Handle player death
  }
});

// ✅ NEW (use PLAYER_SET_DEAD for client UI)
world.on(EventType.PLAYER_SET_DEAD, (data: {
  playerId: string;
  isDead: boolean;
  deathPosition?: [number, number, number];
}) => {
  // Update death screen UI
});
```

***

## Death Constants

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// From CombatConstants.ts
export const COMBAT_CONSTANTS = {
  DEATH: {
    ANIMATION_TICKS: 5,              // Death animation duration (3 seconds)
    COOLDOWN_TICKS: 1,               // Death cooldown (600ms)
    RECONNECT_RESPAWN_DELAY_TICKS: 2, // Delay before auto-respawn on reconnect
    STALE_LOCK_AGE_TICKS: 600000,    // 10 minutes (stale lock cleanup)
    DEFAULT_RESPAWN_POSITION: { x: 0, y: 10, z: 0 },
    DEFAULT_RESPAWN_TOWN: "Central Haven",
  },
  GRAVESTONE_TICKS: 500,             // 5 minutes to reclaim (reduced from 15 minutes)
  GROUND_ITEM_TICKS: 200,            // 2 minutes on ground
};

// From DeathUtils.ts
export const ITEMS_KEPT_ON_DEATH = 3;  // OSRS keep-3 system

export const POSITION_VALIDATION = {
  WORLD_BOUNDS: 10000,  // Max 10km from origin
  MAX_HEIGHT: 500,      // Max height
  MIN_HEIGHT: -50,      // Allow some underground (caves)
} as const;
```

***

## OSRS Keep-3 System (New in PR #1094)

Safe zone deaths keep the 3 most valuable items (by manifest value):

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
import { splitItemsForSafeDeath, ITEMS_KEPT_ON_DEATH } from './DeathUtils';

// Get all items (inventory + equipment)
const allItems = [...inventoryItems, ...equipmentItems];

// Split into kept/dropped
const { kept, dropped } = splitItemsForSafeDeath(allItems, ITEMS_KEPT_ON_DEATH);

// Store kept items for respawn
this.itemsKeptOnDeath.set(playerId, kept);

// Create gravestone with dropped items
await this.spawnGravestone(playerId, position, dropped, killedBy);
```

**Algorithm**:

1. Tag each item with its manifest value
2. Sort descending by value (most valuable first)
3. Greedily assign keep-count without expanding stacks
4. Split into kept/dropped lists

**Example**:

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
const allItems = [
  { itemId: "dragon_scimitar", quantity: 1 },  // value: 60000
  { itemId: "rune_platebody", quantity: 1 },   // value: 40000
  { itemId: "shark", quantity: 10 },           // value: 800 each
  { itemId: "coins", quantity: 50000 },        // value: 1 each
];

const { kept, dropped } = splitItemsForSafeDeath(allItems, 3);

// kept = [dragon_scimitar, rune_platebody, 1 shark]
// dropped = [9 sharks, 50000 coins]
```

**Reference**: [OSRS Wiki - Items Kept on Death](https://oldschool.runescape.wiki/w/Items_Kept_on_Death)

***

## Two-Phase Persist Pattern (New in PR #1094)

**Problem**: Death transaction called `clearEquipmentAndReturn()` and `clearInventoryImmediate()` which each opened nested DB transactions, causing SQLite to deadlock.

**Solution**: In-memory clear inside transaction, DB persist after transaction.

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Inside transaction
await databaseSystem.executeInTransaction(async (tx) => {
  // Clear in-memory, skip DB persist
  await equipmentSystem.clearEquipmentAndReturn(playerId, tx);
  await inventorySystem.clearInventoryImmediate(playerId, true); // skipPersist=true
  
  // Create death lock
  await deathStateManager.createDeathLock(playerId, { ... }, tx);
});

// After transaction (idempotent)
await equipmentSystem.clearEquipmentImmediate(playerId);
await inventorySystem.clearInventoryImmediate(playerId, false);
```

**Crash Recovery**: If server crashes between transaction commit and DB persist, death lock prevents reconnect inventory load. Items are not restored to player.

**Persist Retry Queue**: Single-retry queue (bounded to 100 entries) handles transient DB failures. Emits `AUDIT_LOG` on retry failure.

***

## Gravestone Privacy (New in PR #1094)

Gravestone loot items are hidden from network broadcast (OSRS-accurate):

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// HeadstoneEntity.getNetworkData()
getNetworkData(): Record<string, unknown> {
  return {
    lootItemCount: this.lootItems.length, // Only count is broadcast
    // lootItems NOT included (privacy)
  };
}

// Full loot data sent only to interacting player
world.network.sendTo(playerId, 'corpseLoot', {
  corpseId: gravestoneId,
  items: this.lootItems, // Full item list (targeted packet)
});
```

**Impact**: Other players cannot see gravestone contents until interaction.

***

## Security Features (New in PR #1094)

### Duel Escape Prevention

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
// Block respawn during active duel
if (duelSystem?.isPlayerInActiveDuel?.(playerId)) {
  this.logger.warn("Blocked respawn request during active duel", { playerId });
  return;
}
```

**Guards**:

* `handleRespawnRequest()` - Blocks manual respawn button
* `initiateRespawn()` - Defense-in-depth guard

### Position Validation

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
import { validatePosition, isPositionInBounds } from './DeathUtils';

// Validate and clamp position
const validPosition = validatePosition(deathPosition);
if (!validPosition) {
  this.logger.error("Invalid death position (NaN/Infinity)", { playerId });
  return;
}

// Log warning if clamped
if (!isPositionInBounds(deathPosition)) {
  this.logger.warn("Death position out of bounds, clamped", { playerId });
}
```

### Killer Name Sanitization

```typescript theme={"theme":{"light":"github-light","dark":"tokyo-night"}}
import { sanitizeKilledBy } from './DeathUtils';

// Sanitize killer name (XSS/injection protection)
const killedBy = sanitizeKilledBy(killedByRaw);

// Store sanitized name
await deathStateManager.createDeathLock(playerId, {
  killedBy, // Safe for display
  // ...
});
```

**Attack Vectors Prevented**:

* Homograph attacks (Cyrillic 'а' vs Latin 'a')
* Zero-width characters (invisible manipulation)
* BiDi overrides (text reversal)
* XSS injection (script tags)
* Buffer overflow (length capped at 64 chars)

***

## Related Documentation

* [Combat System](/wiki/game-systems/combat)
* [Ground Items & Loot](/wiki/game-systems/loot)
* [Database Schema](/wiki/engine/database)
* [DeathUtils API](../../../docs/death-system-troubleshooting.md) - Troubleshooting guide
* [OSRS Wiki - Death](https://oldschool.runescape.wiki/w/Death) - OSRS mechanics reference
