piclets / src /lib /services /captureService.ts
Fraser's picture
capture
139e455
raw
history blame
6.12 kB
/**
* 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
};
}