/** * Pokemon-style Capture Mechanics for Pictuary * Based on Pokemon Emerald's capture formula from POKEMON_CAPTURE_MECHANICS.md */ export interface CaptureResult { success: boolean; shakes: number; // 0-3 shakes before success/failure odds: number; // Internal capture odds for debugging } export interface CaptureAttemptParams { // Target Piclet stats maxHp: number; currentHp: number; baseCatchRate: number; // Species-specific catch rate (3-255) // Status effects (optional) statusEffect?: 'sleep' | 'freeze' | 'poison' | 'burn' | 'paralysis' | 'toxic' | null; // Battle context (optional - for future specialty ball mechanics) battleTurn?: number; picletLevel?: number; } /** * Get the catch rate multiplier for a given tier * Maps Pictuary tiers to Pokemon-style catch rates */ export function getCatchRateForTier(tier: string): number { switch (tier.toLowerCase()) { case 'legendary': return 3; // Hardest to catch (like legendary Pokemon) case 'high': return 25; // Hard to catch (like pseudolegendaries) case 'medium': return 75; // Standard catch rate case 'low': return 150; // Easy to catch (like common Pokemon) default: return 75; // Default to medium } } /** * Get status condition multiplier for capture rate */ function getStatusMultiplier(status: string | null | undefined): number { switch (status) { case 'sleep': case 'freeze': return 2.0; // Best status conditions for catching case 'poison': case 'burn': case 'paralysis': case 'toxic': return 1.5; // Good status conditions default: return 1.0; // No status effect } } /** * Calculate initial capture odds using Pokemon formula * Formula: odds = (catchRate × ballMultiplier ÷ 10) × (maxHP × 3 - currentHP × 2) ÷ (maxHP × 3) × statusMultiplier */ function calculateCaptureOdds(params: CaptureAttemptParams): number { const { maxHp, currentHp, baseCatchRate, statusEffect } = params; // Ball multiplier - since we don't have different camera types, use baseline 1.0x (10 in Pokemon terms) const ballMultiplier = 10; // HP factor: (maxHP × 3 - currentHP × 2) ÷ (maxHP × 3) // This creates the 3x capture boost when HP is at 1 const hpFactor = (maxHp * 3 - currentHp * 2) / (maxHp * 3); // Status multiplier const statusMultiplier = getStatusMultiplier(statusEffect); // Core formula const odds = (baseCatchRate * ballMultiplier / 10) * hpFactor * statusMultiplier; return Math.max(0, Math.floor(odds)); } /** * Calculate shake probability when capture odds <= 254 * Formula: shakeOdds = 1048560 ÷ sqrt(sqrt(16711680 ÷ odds)) */ function calculateShakeOdds(captureOdds: number): number { if (captureOdds === 0) return 0; const shakeOdds = 1048560 / Math.sqrt(Math.sqrt(16711680 / captureOdds)); return Math.floor(shakeOdds); } /** * Simulate individual shake success * Each shake has a (shakeOdds / 65536) chance of success */ function simulateShake(shakeOdds: number): boolean { const randomValue = Math.floor(Math.random() * 65536); return randomValue < shakeOdds; } /** * Attempt to capture a Piclet using Pokemon mechanics * Returns detailed results including number of shakes */ export function attemptCapture(params: CaptureAttemptParams): CaptureResult { const odds = calculateCaptureOdds(params); // Immediate capture if odds > 254 if (odds > 254) { return { success: true, shakes: 3, odds }; } // If odds are 0, capture fails immediately if (odds === 0) { return { success: false, shakes: 0, odds }; } // Calculate shake probability const shakeOdds = calculateShakeOdds(odds); // Simulate up to 3 shakes let shakes = 0; for (let i = 0; i < 3; i++) { if (simulateShake(shakeOdds)) { shakes++; } else { // Shake failed, capture fails return { success: false, shakes, odds }; } } // All 3 shakes succeeded - capture success! return { success: true, shakes: 3, odds }; } /** * Calculate capture rate percentage for display purposes * This gives players an approximate idea of their chances */ export function calculateCapturePercentage(params: CaptureAttemptParams): number { const odds = calculateCaptureOdds(params); // Immediate capture if (odds > 254) return 100; // No chance if (odds === 0) return 0; // For odds <= 254, we need to calculate the probability of getting 3 successful shakes const shakeOdds = calculateShakeOdds(odds); const shakeSuccessRate = shakeOdds / 65536; // Probability of 3 consecutive successful shakes const captureRate = Math.pow(shakeSuccessRate, 3) * 100; return Math.min(100, Math.max(0.1, captureRate)); // At least 0.1% to show something } /** * Get a user-friendly description of capture difficulty based on percentage */ export function getCaptureDescription(percentage: number): string { if (percentage >= 95) return "Almost certain"; if (percentage >= 75) return "Very likely"; if (percentage >= 50) return "Good chance"; if (percentage >= 25) return "Moderate chance"; if (percentage >= 10) return "Low chance"; if (percentage >= 5) return "Very low chance"; return "Extremely difficult"; } /** * Simulate multiple capture attempts to get average results (for testing/balancing) */ export function simulateMultipleCaptures(params: CaptureAttemptParams, attempts: number = 1000): { successRate: number; averageShakes: number; distribution: { [key: number]: number }; } { let successes = 0; let totalShakes = 0; const shakeDistribution: { [key: number]: number } = { 0: 0, 1: 0, 2: 0, 3: 0 }; for (let i = 0; i < attempts; i++) { const result = attemptCapture(params); if (result.success) successes++; totalShakes += result.shakes; shakeDistribution[result.shakes]++; } return { successRate: (successes / attempts) * 100, averageShakes: totalShakes / attempts, distribution: shakeDistribution }; }