Skip to main content

Prayer System

The prayer system provides temporary combat and utility bonuses at the cost of prayer points. Prayers drain over time and can be recharged at altars. The system uses OSRS-accurate drain formulas and manifest-driven prayer definitions.
Prayer code lives in:
  • packages/shared/src/systems/shared/character/PrayerSystem.ts - Core prayer logic
  • packages/shared/src/data/PrayerDataProvider.ts - Prayer manifest loading
  • packages/server/src/systems/ServerNetwork/handlers/prayer.ts - Network handlers
  • packages/server/world/assets/manifests/prayers.json - Prayer definitions

Overview

Prayers are temporary buffs that:
  • Provide combat bonuses (attack, strength, defense)
  • Drain prayer points over time
  • Can be toggled on/off via the Skills panel
  • Require specific Prayer levels to activate
  • Conflict with other prayers in the same category

Prayer Points

Maximum Prayer Points

Prayer points scale with Prayer level using the OSRS formula:
maxPoints = Math.floor(prayerLevel / 2) + Math.floor(prayerLevel / 4) + 10
Prayer LevelMax Points
110
1017
2025
3032
4040
5047
6055
7062
8070
9077
9984

Recharging Prayer Points

Prayer points can be recharged by:
  1. Praying at altars - Restores to maximum instantly
  2. Prayer potions (not yet implemented)
  3. Leveling up Prayer - Restores to new maximum

Prayer Drain

Prayers drain points over time based on their drain effect and your prayer bonus.

Drain Formula

// OSRS-accurate drain formula
drainResistance = 2 × prayerBonus + 60

// Points drained per minute
pointsPerMinute = drainEffect × 60 / drainResistance

// Drain interval (ms between drains)
drainInterval = (drainResistance / drainEffect) × 1000

Drain Examples

With 0 prayer bonus (no equipment):
PrayerDrain EffectDrain ResistancePoints/MinDrain Interval
Thick Skin3603.020s
Rock Skin6606.010s
Burst of Strength3603.020s
With +10 prayer bonus (holy symbol):
PrayerDrain EffectDrain ResistancePoints/MinDrain Interval
Thick Skin3802.2526.7s
Rock Skin6804.513.3s
Prayer bonus from equipment reduces drain rate. Each +1 prayer bonus adds 2 to drain resistance.

Drain Processing

The system processes drain every game tick (600ms):
processTick(currentTick: number): void {
  for (const [playerId, state] of this.playerStates) {
    if (state.activePrayers.length === 0) continue;
    
    // Calculate total drain from all active prayers
    const totalDrain = this.calculateTotalDrain(playerId, state);
    
    // Deduct points
    state.currentPoints = Math.max(0, state.currentPoints - totalDrain);
    
    // Deactivate all prayers if points reach 0
    if (state.currentPoints <= 0) {
      this.deactivateAllPrayers(playerId, "no_points");
    }
  }
}

Available Prayers

Prayers are defined in manifests/prayers.json and loaded via PrayerDataProvider.

Defensive Prayers

PrayerLevelIconEffectDrain/Min
Thick Skin1🛡️+5% Defense3
Rock Skin10🪨+10% Defense6

Offensive Prayers

PrayerLevelIconEffectDrain/Min
Burst of Strength4💪+5% Strength3
Clarity of Thought7🧠+5% Attack3
Superhuman Strength13+10% Strength6
More prayers can be added by editing manifests/prayers.json without code changes.

Prayer Bonuses

Prayers modify effective combat levels for damage and accuracy calculations:
interface PrayerBonuses {
  attack?: number;      // Attack level multiplier (e.g., 1.05 = +5%)
  strength?: number;    // Strength level multiplier
  defense?: number;     // Defense level multiplier
  ranged?: number;      // Ranged level multiplier
  magic?: number;       // Magic level multiplier
}

Bonus Application

Bonuses are applied to effective levels before damage calculation:
// From CombatCalculations.ts
const prayerBonuses = getPrayerBonuses(attacker);

// Apply prayer multipliers to effective levels
effectiveAttack = Math.floor(effectiveAttack × (prayerBonuses.attack ?? 1));
effectiveStrength = Math.floor(effectiveStrength × (prayerBonuses.strength ?? 1));
effectiveDefense = Math.floor(effectiveDefense × (prayerBonuses.defense ?? 1));

Example Calculation

Without prayer:
  • Strength level: 70
  • Effective strength: 70 + 8 + 3 = 81
  • Max hit: 18
With Burst of Strength (+5%):
  • Strength level: 70
  • Effective strength: (70 + 8 + 3) × 1.05 = 85
  • Max hit: 19

Prayer Conflicts

Prayers in the same category conflict with each other. Activating a new prayer automatically deactivates conflicting prayers.
{
  "id": "thick_skin",
  "conflicts": ["rock_skin"]
}

Conflict Resolution

// When activating a prayer
const conflicts = prayerDataProvider.getConflictsWithActive(
  newPrayerId,
  state.activePrayers
);

// Deactivate conflicting prayers
for (const conflictId of conflicts) {
  this.deactivatePrayer(playerId, conflictId, "conflict");
}

// Activate new prayer
state.activePrayers.push(newPrayerId);

Prayer Altars

Altars are interactable entities that restore prayer points to maximum.

Altar Entity

const altar = new AltarEntity(world, {
  id: "altar_lumbridge",
  name: "Altar",
  position: { x: 10, y: 0, z: 15 },
  footprint: "standard",  // 1×1 tile
});

Interaction

Players can:
  • Left-click: Pray at altar (restores prayer points)
  • Right-click: Context menu with “Pray Altar” and “Examine Altar”
// Client sends altar pray request
world.network.send("altarPray", { altarId });

// Server validates and restores points
world.emit(EventType.ALTAR_PRAY, { playerId, altarId });

Prayer Training

Prayer XP is gained by:
  1. Burying bones - Primary training method
  2. Using bones on altars (not yet implemented)
  3. Offering bones at gilded altars (not yet implemented)

Bone Burying

Burying bones grants Prayer XP with a 2-tick (1.2s) delay:
// From BuryDelayManager.ts
const BURY_DELAY_TICKS = 2;  // OSRS-accurate

// Player uses bone from inventory
world.network.send("useItem", { itemId: "bones", slot: 5 });

// Server validates and grants XP after delay

Bone Types

BonePrayer XPSource
Bones4.5Most monsters
Big bones15Large monsters
Dragon bones72Dragons
Bone types are defined in manifests/items.json with prayerXp property.

Network Protocol

Client → Server

Toggle Prayer:
world.network.send("prayerToggle", {
  prayerId: "thick_skin",
  timestamp: Date.now(),
});
Pray at Altar:
world.network.send("altarPray", {
  altarId: "altar_lumbridge",
});
Deactivate All:
world.network.send("prayerDeactivateAll", {
  timestamp: Date.now(),
});

Server → Client

Prayer State Sync:
{
  playerId: string,
  points: number,
  maxPoints: number,
  active: string[],  // Array of active prayer IDs
}
Prayer Toggled:
{
  playerId: string,
  prayerId: string,
  active: boolean,
  points: number,
}
Prayer Points Changed:
{
  playerId: string,
  points: number,
  maxPoints: number,
  reason?: "drain" | "altar" | "level_up",
}

Security Features

Input Validation

// Prayer ID format validation
const PRAYER_ID_PATTERN = /^[a-z0-9_]{1,64}$/;
const MAX_PRAYER_ID_LENGTH = 64;

function isValidPrayerId(id: unknown): id is string {
  if (typeof id !== "string") return false;
  if (id.length === 0 || id.length > MAX_PRAYER_ID_LENGTH) return false;
  return PRAYER_ID_PATTERN.test(id);
}

Rate Limiting

// From SlidingWindowRateLimiter.ts
const prayerLimiter = createRateLimiter({
  maxPerSecond: 5,
  name: "prayer-toggle",
});
Limits:
  • 5 toggles per second - Prevents spam
  • 100ms cooldown - Minimum time between toggles

Validation Checks

Before activating a prayer, the system validates:
  1. Prayer exists in manifest
  2. Player meets level requirement
  3. Player has prayer points remaining
  4. Not already active
  5. Not exceeding max active prayers (currently 5)
const validation = prayerDataProvider.canActivatePrayer(
  prayerId,
  prayerLevel,
  currentPoints,
  activePrayers
);

if (!validation.valid) {
  // Send error to client
  world.emit(EventType.UI_TOAST, {
    playerId,
    message: validation.reason,
    type: "error",
  });
  return;
}

Database Schema

Prayer state is persisted to the characters table:
-- Prayer skill
prayerLevel INTEGER DEFAULT 1,
prayerXp INTEGER DEFAULT 0,

-- Prayer points
prayerPoints INTEGER DEFAULT 1,
prayerMaxPoints INTEGER DEFAULT 1,

-- Active prayers (JSON array of prayer IDs)
activePrayers TEXT DEFAULT '[]'

Active Prayers Format

["thick_skin", "burst_of_strength"]
The activePrayers column stores a JSON array of prayer ID strings. IDs must match valid entries in prayers.json.

Prayer Events

EventDataDescription
PRAYER_TOGGLEplayerId, prayerIdPlayer toggled prayer
PRAYER_ACTIVATEDplayerId, prayerIdPrayer activated
PRAYER_DEACTIVATEDplayerId, prayerId, reasonPrayer deactivated
PRAYER_STATE_SYNCplayerId, points, maxPoints, activeFull state sync
PRAYER_TOGGLEDplayerId, prayerId, active, pointsToggle confirmation
PRAYER_POINTS_CHANGEDplayerId, points, maxPoints, reasonPoints changed
ALTAR_PRAYplayerId, altarIdPlayer prayed at altar

API Reference

PrayerSystem

class PrayerSystem extends SystemBase {
  // Toggle a prayer on/off
  togglePrayer(playerId: string, prayerId: string): boolean;
  
  // Activate a specific prayer
  activatePrayer(playerId: string, prayerId: string): boolean;
  
  // Deactivate a specific prayer
  deactivatePrayer(
    playerId: string,
    prayerId: string,
    reason: "manual" | "no_points" | "conflict"
  ): boolean;
  
  // Deactivate all prayers
  deactivateAllPrayers(playerId: string, reason: string): void;
  
  // Restore prayer points
  restorePrayerPoints(playerId: string, amount: number): void;
  
  // Get current prayer state
  getPrayerState(playerId: string): PrayerState | null;
  
  // Get combined bonuses from all active prayers
  getCombinedBonuses(playerId: string): PrayerBonuses;
}

PrayerDataProvider

class PrayerDataProvider {
  // Get prayer definition
  getPrayer(prayerId: string): PrayerDefinition | null;
  
  // Check if prayer exists
  prayerExists(prayerId: string): boolean;
  
  // Get all prayers
  getAllPrayers(): readonly PrayerDefinition[];
  
  // Get prayers available at level
  getAvailablePrayers(prayerLevel: number): PrayerDefinition[];
  
  // Get prayers by category
  getPrayersByCategory(category: PrayerCategory): readonly PrayerDefinition[];
  
  // Check for conflicts
  getConflictingPrayerIds(prayerId: string): readonly string[];
  prayersConflict(prayerIdA: string, prayerIdB: string): boolean;
  
  // Validate activation
  canActivatePrayer(
    prayerId: string,
    prayerLevel: number,
    currentPoints: number,
    activePrayers: readonly string[]
  ): { valid: boolean; reason?: string };
}

Adding New Prayers

Prayers are defined in packages/server/world/assets/manifests/prayers.json:
{
  "prayers": [
    {
      "id": "thick_skin",
      "name": "Thick Skin",
      "description": "Increases Defense by 5%",
      "icon": "🛡️",
      "level": 1,
      "category": "defensive",
      "drainEffect": 3,
      "bonuses": {
        "defense": 1.05
      },
      "conflicts": ["rock_skin"]
    }
  ]
}

Prayer Definition Fields

FieldTypeDescription
idstringUnique prayer ID (snake_case, 1-64 chars)
namestringDisplay name
descriptionstringTooltip description
iconstringEmoji icon for UI
levelnumberRequired Prayer level (1-99)
categorystring”offensive”, “defensive”, or “utility”
drainEffectnumberDrain rate (higher = faster drain)
bonusesobjectCombat stat multipliers
conflictsstring[]Prayer IDs that conflict

Bonus Multipliers

Bonuses are multipliers applied to effective combat levels:
{
  "attack": 1.05,     // +5% attack
  "strength": 1.10,   // +10% strength
  "defense": 1.15,    // +15% defense
  "ranged": 1.05,     // +5% ranged
  "magic": 1.05       // +5% magic
}

Client UI

Skills Panel Prayer Tab

The prayer tab displays:
  • Prayer points bar - Current/max with color coding
  • Prayer cards - Organized by category (offensive, defensive, utility)
  • Lock indicators - Prayers above player level show 🔒
  • Active state - Green border for active prayers
  • Tooltips - Hover for description and drain rate
// From SkillsPanel.tsx
const prayers: Prayer[] = [
  {
    id: "thick_skin",
    name: "Thick Skin",
    icon: "🛡️",
    level: 1,
    description: "Increases Defense by 5%",
    drainRate: 3,
    active: activePrayers.has("thick_skin"),
    category: "defensive",
  },
  // ...
];

Prayer Card States

StateVisualBehavior
LockedGray, 60% opacity, 🔒 iconCannot activate, shows level requirement
AvailableNormal colorsCan activate if points available
ActiveGreen border, glow effectCurrently providing bonuses
No PointsRed text on points barCannot activate any prayers

Implementation Details

Memory Optimization

The system uses pre-allocated buffers to avoid allocations in hot paths:
// From PrayerSystem.ts
private readonly deactivateBuffer: string[] = [];
private readonly combinedBonusesBuffer: MutablePrayerBonuses = {
  attack: 1,
  strength: 1,
  defense: 1,
  ranged: 1,
  magic: 1,
};
Do not store references to these buffers - contents change between calls.

Type Safety

All prayer operations use type guards for runtime validation:
// Prayer ID validation
export function isValidPrayerId(id: unknown): id is string {
  if (typeof id !== "string") return false;
  if (id.length === 0 || id.length > MAX_PRAYER_ID_LENGTH) return false;
  return PRAYER_ID_PATTERN.test(id);
}

// Prayer toggle payload validation
export function isValidPrayerTogglePayload(
  data: unknown
): data is PrayerTogglePayload {
  if (!data || typeof data !== "object") return false;
  const payload = data as Record<string, unknown>;
  return isValidPrayerId(payload.prayerId);
}

Display Points Rounding

Prayer points are displayed using Math.ceil() to prevent showing 0 when points are low but not empty:
// Display points (rounded up)
const displayPoints = Math.ceil(state.currentPoints);

// Actual points (precise)
const actualPoints = state.currentPoints;  // e.g., 0.98
This prevents the UI from showing “0 / 10” when the player still has 0.98 points remaining.

Testing

The prayer system includes 62 unit tests covering:
  • Type guard validation (all edge cases)
  • Bounds checking (overflow, underflow, NaN, Infinity)
  • Prayer ID format validation (security)
  • Rate limiting behavior
  • Input validation for all payload types
  • Drain calculations
  • Conflict resolution
  • Altar interactions
# Run prayer system tests
bun test packages/shared/src/systems/shared/character/__tests__/PrayerSystem.test.ts