File size: 6,123 Bytes
139e455
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
/**
 * 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
  };
}