Fraser commited on
Commit
139e455
·
1 Parent(s): 71e0909
src/lib/battle-engine/BattleEngine.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
  BattleAction,
11
  MoveAction,
12
  SwitchAction,
 
13
  BattleEffect,
14
  DamageAmount,
15
  StatModification,
@@ -20,6 +21,7 @@ import type {
20
  Trigger
21
  } from './types';
22
  import { getEffectivenessMultiplier } from '../types/picletTypes';
 
23
 
24
  export class BattleEngine {
25
  private state: BattleState;
@@ -123,6 +125,30 @@ export class BattleEngine {
123
  return this.state.winner;
124
  }
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  public executeActions(playerAction: BattleAction, opponentAction: BattleAction): void {
127
  if (this.state.phase !== 'selection') {
128
  throw new Error('Cannot execute actions - battle is not in selection phase');
@@ -226,6 +252,8 @@ export class BattleEngine {
226
  this.executeMove(action);
227
  } else if (action.type === 'switch') {
228
  this.executeSwitch(action as SwitchAction & { executor: 'player' | 'opponent' });
 
 
229
  }
230
  }
231
 
@@ -357,6 +385,90 @@ export class BattleEngine {
357
  this.triggerOnSwitchIn(newPiclet);
358
  }
359
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
360
  private getCurrentPicletIndex(executor: 'player' | 'opponent'): number {
361
  const isPlayer = executor === 'player';
362
  const roster = isPlayer ? this.playerRoster : this.opponentRoster;
 
10
  BattleAction,
11
  MoveAction,
12
  SwitchAction,
13
+ CaptureAction,
14
  BattleEffect,
15
  DamageAmount,
16
  StatModification,
 
21
  Trigger
22
  } from './types';
23
  import { getEffectivenessMultiplier } from '../types/picletTypes';
24
+ import { attemptCapture, getCatchRateForTier, calculateCapturePercentage } from '../services/captureService';
25
 
26
  export class BattleEngine {
27
  private state: BattleState;
 
125
  return this.state.winner;
126
  }
127
 
128
+ public getCapturePercentage(): number {
129
+ const targetPiclet = this.state.opponentPiclet;
130
+
131
+ // Get capture parameters
132
+ const maxHp = targetPiclet.maxHp;
133
+ const currentHp = targetPiclet.currentHp;
134
+ const tier = targetPiclet.definition.tier;
135
+ const baseCatchRate = getCatchRateForTier(tier);
136
+
137
+ // Get status effect for capture bonus
138
+ let statusEffect: string | null = null;
139
+ if (targetPiclet.statusEffects.length > 0) {
140
+ statusEffect = targetPiclet.statusEffects[0];
141
+ }
142
+
143
+ return calculateCapturePercentage({
144
+ maxHp,
145
+ currentHp,
146
+ baseCatchRate,
147
+ statusEffect,
148
+ picletLevel: targetPiclet.level
149
+ });
150
+ }
151
+
152
  public executeActions(playerAction: BattleAction, opponentAction: BattleAction): void {
153
  if (this.state.phase !== 'selection') {
154
  throw new Error('Cannot execute actions - battle is not in selection phase');
 
252
  this.executeMove(action);
253
  } else if (action.type === 'switch') {
254
  this.executeSwitch(action as SwitchAction & { executor: 'player' | 'opponent' });
255
+ } else if (action.type === 'capture') {
256
+ this.executeCapture(action as CaptureAction & { executor: 'player' | 'opponent' });
257
  }
258
  }
259
 
 
385
  this.triggerOnSwitchIn(newPiclet);
386
  }
387
 
388
+ private executeCapture(action: CaptureAction & { executor: 'player' | 'opponent' }): void {
389
+ // Only player can capture (wild battles only)
390
+ if (action.executor !== 'player') {
391
+ this.log('Only the player can capture Piclets!');
392
+ return;
393
+ }
394
+
395
+ // Can't capture in trainer battles (this would be determined by battle context)
396
+ // For now, we'll assume this is a wild battle
397
+
398
+ const targetPiclet = this.state.opponentPiclet;
399
+
400
+ // Get capture parameters
401
+ const maxHp = targetPiclet.maxHp;
402
+ const currentHp = targetPiclet.currentHp;
403
+ const tier = targetPiclet.definition.tier;
404
+ const baseCatchRate = getCatchRateForTier(tier);
405
+
406
+ // Get status effect for capture bonus
407
+ let statusEffect: string | null = null;
408
+ if (targetPiclet.statusEffects.length > 0) {
409
+ // Use the first status effect (most Pokemon games only allow one)
410
+ statusEffect = targetPiclet.statusEffects[0];
411
+ }
412
+
413
+ // Calculate capture percentage for display
414
+ const capturePercentage = calculateCapturePercentage({
415
+ maxHp,
416
+ currentHp,
417
+ baseCatchRate,
418
+ statusEffect,
419
+ picletLevel: targetPiclet.level
420
+ });
421
+
422
+ // Attempt the capture
423
+ const result = attemptCapture({
424
+ maxHp,
425
+ currentHp,
426
+ baseCatchRate,
427
+ statusEffect,
428
+ picletLevel: targetPiclet.level
429
+ });
430
+
431
+ // Store capture result in battle state
432
+ this.state.captureResult = {
433
+ success: result.success,
434
+ shakes: result.shakes,
435
+ odds: result.odds,
436
+ capturePercentage
437
+ };
438
+
439
+ // Log the attempt
440
+ this.log(`Player threw a camera at ${targetPiclet.definition.name}!`);
441
+
442
+ // Log shakes
443
+ if (result.shakes === 0) {
444
+ this.log('The camera broke immediately!');
445
+ } else {
446
+ const shakeText = result.shakes === 1 ? 'once' : result.shakes === 2 ? 'twice' : 'three times';
447
+ this.log(`The camera shook ${shakeText}...`);
448
+ }
449
+
450
+ if (result.success) {
451
+ this.log(`${targetPiclet.definition.name} was captured!`);
452
+ // Set winner to player (capture ends the battle)
453
+ this.state.winner = 'player';
454
+ this.state.phase = 'ended';
455
+ } else {
456
+ this.log(`${targetPiclet.definition.name} broke free!`);
457
+ // Capture failed, battle continues
458
+ // The opponent gets a turn after a failed capture attempt
459
+ }
460
+
461
+ console.log('📸 Capture attempt:', {
462
+ target: targetPiclet.definition.name,
463
+ hp: `${currentHp}/${maxHp}`,
464
+ status: statusEffect,
465
+ catchRate: baseCatchRate,
466
+ percentage: capturePercentage.toFixed(1) + '%',
467
+ result: result.success ? 'SUCCESS' : 'FAILED',
468
+ shakes: result.shakes
469
+ });
470
+ }
471
+
472
  private getCurrentPicletIndex(executor: 'player' | 'opponent'): number {
473
  const isPlayer = executor === 'player';
474
  const roster = isPlayer ? this.playerRoster : this.opponentRoster;
src/lib/battle-engine/types.ts CHANGED
@@ -231,6 +231,14 @@ export interface BattleState {
231
  log: string[];
232
 
233
  winner?: 'player' | 'opponent' | 'draw';
 
 
 
 
 
 
 
 
234
  }
235
 
236
  // Action Types
@@ -246,4 +254,9 @@ export interface SwitchAction {
246
  newPicletIndex: number;
247
  }
248
 
249
- export type BattleAction = MoveAction | SwitchAction;
 
 
 
 
 
 
231
  log: string[];
232
 
233
  winner?: 'player' | 'opponent' | 'draw';
234
+
235
+ // Capture result (for wild battles)
236
+ captureResult?: {
237
+ success: boolean;
238
+ shakes: number;
239
+ odds: number;
240
+ capturePercentage: number;
241
+ };
242
  }
243
 
244
  // Action Types
 
254
  newPicletIndex: number;
255
  }
256
 
257
+ export interface CaptureAction {
258
+ type: 'capture';
259
+ piclet: 'player'; // Only player can capture
260
+ }
261
+
262
+ export type BattleAction = MoveAction | SwitchAction | CaptureAction;
src/lib/components/Battle/ActionButtons.svelte CHANGED
@@ -10,6 +10,7 @@
10
  export let availablePiclets: PicletInstance[] = [];
11
  export let processingTurn: boolean = false;
12
  export let battleState: BattleState | undefined = undefined;
 
13
  export let onAction: (action: string) => void;
14
  export let onMoveSelect: (move: BattleMove) => void = () => {};
15
  export let onPicletSelect: (piclet: PicletInstance) => void = () => {};
@@ -53,6 +54,7 @@
53
  {enemyPiclet}
54
  {isWildBattle}
55
  {battleState}
 
56
  onMoveSelected={handleMoveSelected}
57
  onPicletSelected={handlePicletSelected}
58
  onCaptureAttempt={handleCaptureAttempt}
 
10
  export let availablePiclets: PicletInstance[] = [];
11
  export let processingTurn: boolean = false;
12
  export let battleState: BattleState | undefined = undefined;
13
+ export let capturePercentage: number = 0;
14
  export let onAction: (action: string) => void;
15
  export let onMoveSelect: (move: BattleMove) => void = () => {};
16
  export let onPicletSelect: (piclet: PicletInstance) => void = () => {};
 
54
  {enemyPiclet}
55
  {isWildBattle}
56
  {battleState}
57
+ {capturePercentage}
58
  onMoveSelected={handleMoveSelected}
59
  onPicletSelected={handlePicletSelected}
60
  onCaptureAttempt={handleCaptureAttempt}
src/lib/components/Battle/ActionViewSelector.svelte CHANGED
@@ -6,6 +6,7 @@
6
  import type { PicletInstance, BattleMove } from '$lib/db/schema';
7
  import type { BattleState } from '$lib/battle-engine/types';
8
  import { generateMoveDescription } from '$lib/utils/moveDescriptions';
 
9
 
10
  export let currentView: ActionView = 'main';
11
  export let onViewChange: (view: ActionView) => void;
@@ -14,6 +15,7 @@
14
  export let enemyPiclet: PicletInstance | null = null;
15
  export let isWildBattle: boolean = false;
16
  export let battleState: BattleState | undefined = undefined;
 
17
  export let onMoveSelected: (move: BattleMove) => void = () => {};
18
  export let onPicletSelected: (piclet: PicletInstance) => void = () => {};
19
  export let onCaptureAttempt: () => void = () => {};
@@ -173,8 +175,8 @@
173
  >
174
  <span class="item-icon">📸</span>
175
  <div class="item-info">
176
- <div class="item-name">Capture</div>
177
- <div class="item-desc">Snap to capture {enemyPiclet.nickname}</div>
178
  </div>
179
  </button>
180
  {:else}
 
6
  import type { PicletInstance, BattleMove } from '$lib/db/schema';
7
  import type { BattleState } from '$lib/battle-engine/types';
8
  import { generateMoveDescription } from '$lib/utils/moveDescriptions';
9
+ import { getCaptureDescription } from '$lib/services/captureService';
10
 
11
  export let currentView: ActionView = 'main';
12
  export let onViewChange: (view: ActionView) => void;
 
15
  export let enemyPiclet: PicletInstance | null = null;
16
  export let isWildBattle: boolean = false;
17
  export let battleState: BattleState | undefined = undefined;
18
+ export let capturePercentage: number = 0;
19
  export let onMoveSelected: (move: BattleMove) => void = () => {};
20
  export let onPicletSelected: (piclet: PicletInstance) => void = () => {};
21
  export let onCaptureAttempt: () => void = () => {};
 
175
  >
176
  <span class="item-icon">📸</span>
177
  <div class="item-info">
178
+ <div class="item-name">Capture ({capturePercentage.toFixed(1)}%)</div>
179
+ <div class="item-desc">{getCaptureDescription(capturePercentage)} - {enemyPiclet.nickname}</div>
180
  </div>
181
  </button>
182
  {:else}
src/lib/components/Battle/BattleControls.svelte CHANGED
@@ -13,6 +13,7 @@
13
  export let enemyPiclet: PicletInstance;
14
  export let rosterPiclets: PicletInstance[] = [];
15
  export let battleState: BattleState | undefined = undefined;
 
16
  export let onAction: (action: string) => void;
17
  export let onMoveSelect: (move: any) => void;
18
  export let onPicletSelect: (piclet: PicletInstance) => void;
@@ -47,6 +48,7 @@
47
  {availablePiclets}
48
  {processingTurn}
49
  {battleState}
 
50
  {onAction}
51
  {onMoveSelect}
52
  {onPicletSelect}
 
13
  export let enemyPiclet: PicletInstance;
14
  export let rosterPiclets: PicletInstance[] = [];
15
  export let battleState: BattleState | undefined = undefined;
16
+ export let capturePercentage: number = 0;
17
  export let onAction: (action: string) => void;
18
  export let onMoveSelect: (move: any) => void;
19
  export let onPicletSelect: (piclet: PicletInstance) => void;
 
48
  {availablePiclets}
49
  {processingTurn}
50
  {battleState}
51
+ {capturePercentage}
52
  {onAction}
53
  {onMoveSelect}
54
  {onPicletSelect}
src/lib/components/Pages/Battle.svelte CHANGED
@@ -10,6 +10,7 @@
10
  import { calculateBattleXp, processAllLevelUps } from '$lib/services/levelingService';
11
  import { db } from '$lib/db/index';
12
  import { getEffectivenessText, getEffectivenessColor } from '$lib/types/picletTypes';
 
13
 
14
  export let playerPiclet: PicletInstance;
15
  export let enemyPiclet: PicletInstance;
@@ -23,6 +24,9 @@
23
  let currentPlayerPiclet = playerPiclet;
24
  let currentEnemyPiclet = enemyPiclet;
25
 
 
 
 
26
  // Battle state
27
  let currentMessage = isWildBattle
28
  ? `A wild ${enemyPiclet.nickname} appeared!`
@@ -123,13 +127,76 @@
123
 
124
  switch (action) {
125
  case 'catch':
126
- if (isWildBattle) {
127
  processingTurn = true;
128
- currentMessage = 'You took a Pic-ture!';
129
- setTimeout(() => {
130
- currentMessage = 'The wild piclet broke free!';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
131
  processingTurn = false;
132
- }, 2000);
 
 
133
  }
134
  break;
135
  case 'run':
@@ -723,6 +790,7 @@
723
  enemyPiclet={currentEnemyPiclet}
724
  {rosterPiclets}
725
  {battleState}
 
726
  {waitingForContinue}
727
  onAction={handleAction}
728
  onMoveSelect={handleMoveSelect}
 
10
  import { calculateBattleXp, processAllLevelUps } from '$lib/services/levelingService';
11
  import { db } from '$lib/db/index';
12
  import { getEffectivenessText, getEffectivenessColor } from '$lib/types/picletTypes';
13
+ import { getCaptureDescription } from '$lib/services/captureService';
14
 
15
  export let playerPiclet: PicletInstance;
16
  export let enemyPiclet: PicletInstance;
 
24
  let currentPlayerPiclet = playerPiclet;
25
  let currentEnemyPiclet = enemyPiclet;
26
 
27
+ // Calculate capture percentage for UI display
28
+ $: capturePercentage = battleEngine && isWildBattle ? battleEngine.getCapturePercentage() : 0;
29
+
30
  // Battle state
31
  let currentMessage = isWildBattle
32
  ? `A wild ${enemyPiclet.nickname} appeared!`
 
127
 
128
  switch (action) {
129
  case 'catch':
130
+ if (isWildBattle && battleEngine) {
131
  processingTurn = true;
132
+
133
+ // Get capture percentage to show to player
134
+ const capturePercentage = battleEngine.getCapturePercentage();
135
+ const captureDescription = getCaptureDescription(capturePercentage);
136
+
137
+ console.log(`📸 Capture attempt: ${capturePercentage.toFixed(1)}% chance (${captureDescription})`);
138
+
139
+ try {
140
+ // Create capture action
141
+ const captureAction = { type: 'capture' as const, piclet: 'player' as const };
142
+ // Create a no-op enemy action
143
+ const enemyAction = { type: 'move' as const, piclet: 'opponent' as const, moveIndex: 0 };
144
+
145
+ // Get log entries before action to track new messages
146
+ const logBefore = battleEngine.getLog();
147
+
148
+ // Execute capture attempt
149
+ battleEngine.executeActions(captureAction, enemyAction);
150
+ battleState = battleEngine.getState();
151
+
152
+ // Get capture result and new log entries
153
+ const captureResult = battleState.captureResult;
154
+ const logAfter = battleEngine.getLog();
155
+ const newLogEntries = logAfter.slice(logBefore.length);
156
+
157
+ // Show log messages with proper timing
158
+ if (newLogEntries.length > 0) {
159
+ let messageIndex = 0;
160
+
161
+ const showNextMessage = () => {
162
+ if (messageIndex < newLogEntries.length) {
163
+ currentMessage = newLogEntries[messageIndex];
164
+ messageIndex++;
165
+ setTimeout(showNextMessage, 1500); // 1.5s between messages
166
+ } else {
167
+ // All messages shown, check final result
168
+ if (captureResult?.success) {
169
+ // Capture successful - end battle and add to roster
170
+ setTimeout(() => {
171
+ battleEnded = true;
172
+ onBattleEnd(true, 'captured'); // Pass special 'captured' result
173
+ }, 1000);
174
+ } else {
175
+ // Capture failed - continue battle
176
+ setTimeout(() => {
177
+ processingTurn = false;
178
+ currentMessage = `What will ${currentPlayerPiclet.nickname} do?`;
179
+ }, 1000);
180
+ }
181
+ }
182
+ };
183
+
184
+ showNextMessage();
185
+ } else {
186
+ // No messages, fall back to basic handling
187
+ currentMessage = 'The capture attempt failed!';
188
+ setTimeout(() => {
189
+ processingTurn = false;
190
+ }, 2000);
191
+ }
192
+
193
+ } catch (error) {
194
+ console.error('Capture error:', error);
195
+ currentMessage = 'Something went wrong with the capture attempt!';
196
  processingTurn = false;
197
+ }
198
+ } else if (!isWildBattle) {
199
+ currentMessage = "You can't capture a trainer's Piclet!";
200
  }
201
  break;
202
  case 'run':
 
790
  enemyPiclet={currentEnemyPiclet}
791
  {rosterPiclets}
792
  {battleState}
793
+ {capturePercentage}
794
  {waitingForContinue}
795
  onAction={handleAction}
796
  onMoveSelect={handleMoveSelect}
src/lib/services/captureService.ts ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Pokemon-style Capture Mechanics for Pictuary
3
+ * Based on Pokemon Emerald's capture formula from POKEMON_CAPTURE_MECHANICS.md
4
+ */
5
+
6
+ export interface CaptureResult {
7
+ success: boolean;
8
+ shakes: number; // 0-3 shakes before success/failure
9
+ odds: number; // Internal capture odds for debugging
10
+ }
11
+
12
+ export interface CaptureAttemptParams {
13
+ // Target Piclet stats
14
+ maxHp: number;
15
+ currentHp: number;
16
+ baseCatchRate: number; // Species-specific catch rate (3-255)
17
+
18
+ // Status effects (optional)
19
+ statusEffect?: 'sleep' | 'freeze' | 'poison' | 'burn' | 'paralysis' | 'toxic' | null;
20
+
21
+ // Battle context (optional - for future specialty ball mechanics)
22
+ battleTurn?: number;
23
+ picletLevel?: number;
24
+ }
25
+
26
+ /**
27
+ * Get the catch rate multiplier for a given tier
28
+ * Maps Pictuary tiers to Pokemon-style catch rates
29
+ */
30
+ export function getCatchRateForTier(tier: string): number {
31
+ switch (tier.toLowerCase()) {
32
+ case 'legendary': return 3; // Hardest to catch (like legendary Pokemon)
33
+ case 'high': return 25; // Hard to catch (like pseudolegendaries)
34
+ case 'medium': return 75; // Standard catch rate
35
+ case 'low': return 150; // Easy to catch (like common Pokemon)
36
+ default: return 75; // Default to medium
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Get status condition multiplier for capture rate
42
+ */
43
+ function getStatusMultiplier(status: string | null | undefined): number {
44
+ switch (status) {
45
+ case 'sleep':
46
+ case 'freeze':
47
+ return 2.0; // Best status conditions for catching
48
+ case 'poison':
49
+ case 'burn':
50
+ case 'paralysis':
51
+ case 'toxic':
52
+ return 1.5; // Good status conditions
53
+ default:
54
+ return 1.0; // No status effect
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Calculate initial capture odds using Pokemon formula
60
+ * Formula: odds = (catchRate × ballMultiplier ÷ 10) × (maxHP × 3 - currentHP × 2) ÷ (maxHP × 3) × statusMultiplier
61
+ */
62
+ function calculateCaptureOdds(params: CaptureAttemptParams): number {
63
+ const { maxHp, currentHp, baseCatchRate, statusEffect } = params;
64
+
65
+ // Ball multiplier - since we don't have different camera types, use baseline 1.0x (10 in Pokemon terms)
66
+ const ballMultiplier = 10;
67
+
68
+ // HP factor: (maxHP × 3 - currentHP × 2) ÷ (maxHP × 3)
69
+ // This creates the 3x capture boost when HP is at 1
70
+ const hpFactor = (maxHp * 3 - currentHp * 2) / (maxHp * 3);
71
+
72
+ // Status multiplier
73
+ const statusMultiplier = getStatusMultiplier(statusEffect);
74
+
75
+ // Core formula
76
+ const odds = (baseCatchRate * ballMultiplier / 10) * hpFactor * statusMultiplier;
77
+
78
+ return Math.max(0, Math.floor(odds));
79
+ }
80
+
81
+ /**
82
+ * Calculate shake probability when capture odds <= 254
83
+ * Formula: shakeOdds = 1048560 ÷ sqrt(sqrt(16711680 ÷ odds))
84
+ */
85
+ function calculateShakeOdds(captureOdds: number): number {
86
+ if (captureOdds === 0) return 0;
87
+
88
+ const shakeOdds = 1048560 / Math.sqrt(Math.sqrt(16711680 / captureOdds));
89
+ return Math.floor(shakeOdds);
90
+ }
91
+
92
+ /**
93
+ * Simulate individual shake success
94
+ * Each shake has a (shakeOdds / 65536) chance of success
95
+ */
96
+ function simulateShake(shakeOdds: number): boolean {
97
+ const randomValue = Math.floor(Math.random() * 65536);
98
+ return randomValue < shakeOdds;
99
+ }
100
+
101
+ /**
102
+ * Attempt to capture a Piclet using Pokemon mechanics
103
+ * Returns detailed results including number of shakes
104
+ */
105
+ export function attemptCapture(params: CaptureAttemptParams): CaptureResult {
106
+ const odds = calculateCaptureOdds(params);
107
+
108
+ // Immediate capture if odds > 254
109
+ if (odds > 254) {
110
+ return {
111
+ success: true,
112
+ shakes: 3,
113
+ odds
114
+ };
115
+ }
116
+
117
+ // If odds are 0, capture fails immediately
118
+ if (odds === 0) {
119
+ return {
120
+ success: false,
121
+ shakes: 0,
122
+ odds
123
+ };
124
+ }
125
+
126
+ // Calculate shake probability
127
+ const shakeOdds = calculateShakeOdds(odds);
128
+
129
+ // Simulate up to 3 shakes
130
+ let shakes = 0;
131
+ for (let i = 0; i < 3; i++) {
132
+ if (simulateShake(shakeOdds)) {
133
+ shakes++;
134
+ } else {
135
+ // Shake failed, capture fails
136
+ return {
137
+ success: false,
138
+ shakes,
139
+ odds
140
+ };
141
+ }
142
+ }
143
+
144
+ // All 3 shakes succeeded - capture success!
145
+ return {
146
+ success: true,
147
+ shakes: 3,
148
+ odds
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Calculate capture rate percentage for display purposes
154
+ * This gives players an approximate idea of their chances
155
+ */
156
+ export function calculateCapturePercentage(params: CaptureAttemptParams): number {
157
+ const odds = calculateCaptureOdds(params);
158
+
159
+ // Immediate capture
160
+ if (odds > 254) return 100;
161
+
162
+ // No chance
163
+ if (odds === 0) return 0;
164
+
165
+ // For odds <= 254, we need to calculate the probability of getting 3 successful shakes
166
+ const shakeOdds = calculateShakeOdds(odds);
167
+ const shakeSuccessRate = shakeOdds / 65536;
168
+
169
+ // Probability of 3 consecutive successful shakes
170
+ const captureRate = Math.pow(shakeSuccessRate, 3) * 100;
171
+
172
+ return Math.min(100, Math.max(0.1, captureRate)); // At least 0.1% to show something
173
+ }
174
+
175
+ /**
176
+ * Get a user-friendly description of capture difficulty based on percentage
177
+ */
178
+ export function getCaptureDescription(percentage: number): string {
179
+ if (percentage >= 95) return "Almost certain";
180
+ if (percentage >= 75) return "Very likely";
181
+ if (percentage >= 50) return "Good chance";
182
+ if (percentage >= 25) return "Moderate chance";
183
+ if (percentage >= 10) return "Low chance";
184
+ if (percentage >= 5) return "Very low chance";
185
+ return "Extremely difficult";
186
+ }
187
+
188
+ /**
189
+ * Simulate multiple capture attempts to get average results (for testing/balancing)
190
+ */
191
+ export function simulateMultipleCaptures(params: CaptureAttemptParams, attempts: number = 1000): {
192
+ successRate: number;
193
+ averageShakes: number;
194
+ distribution: { [key: number]: number };
195
+ } {
196
+ let successes = 0;
197
+ let totalShakes = 0;
198
+ const shakeDistribution: { [key: number]: number } = { 0: 0, 1: 0, 2: 0, 3: 0 };
199
+
200
+ for (let i = 0; i < attempts; i++) {
201
+ const result = attemptCapture(params);
202
+ if (result.success) successes++;
203
+ totalShakes += result.shakes;
204
+ shakeDistribution[result.shakes]++;
205
+ }
206
+
207
+ return {
208
+ successRate: (successes / attempts) * 100,
209
+ averageShakes: totalShakes / attempts,
210
+ distribution: shakeDistribution
211
+ };
212
+ }