Fraser commited on
Commit
74f8c2c
·
1 Parent(s): 4906999
src/lib/components/Battle/ActionButtons.svelte CHANGED
@@ -2,6 +2,7 @@
2
  import ActionViewSelector, { type ActionView } from './ActionViewSelector.svelte';
3
  import type { PicletInstance, BattleMove } from '$lib/db/schema';
4
  import type { BattleState } from '$lib/battle-engine/types';
 
5
 
6
  export let isWildBattle: boolean;
7
  export let playerPiclet: PicletInstance;
@@ -15,6 +16,9 @@
15
 
16
  let currentView: ActionView = 'main';
17
 
 
 
 
18
  function handleViewChange(view: ActionView) {
19
  currentView = view;
20
  }
@@ -44,7 +48,7 @@
44
  <ActionViewSelector
45
  {currentView}
46
  onViewChange={handleViewChange}
47
- moves={playerPiclet.moves}
48
  {availablePiclets}
49
  {enemyPiclet}
50
  {isWildBattle}
 
2
  import ActionViewSelector, { type ActionView } from './ActionViewSelector.svelte';
3
  import type { PicletInstance, BattleMove } from '$lib/db/schema';
4
  import type { BattleState } from '$lib/battle-engine/types';
5
+ import { getUnlockedMoves } from '$lib/services/unlockLevels';
6
 
7
  export let isWildBattle: boolean;
8
  export let playerPiclet: PicletInstance;
 
16
 
17
  let currentView: ActionView = 'main';
18
 
19
+ // Only show unlocked moves in battle
20
+ $: unlockedMoves = getUnlockedMoves(playerPiclet.moves, playerPiclet.level);
21
+
22
  function handleViewChange(view: ActionView) {
23
  currentView = view;
24
  }
 
48
  <ActionViewSelector
49
  {currentView}
50
  onViewChange={handleViewChange}
51
+ moves={unlockedMoves}
52
  {availablePiclets}
53
  {enemyPiclet}
54
  {isWildBattle}
src/lib/components/Pages/Battle.svelte CHANGED
@@ -160,21 +160,13 @@
160
  };
161
 
162
  try {
163
- // Choose random enemy move (could be improved with AI)
164
- const availableEnemyMoves = battleState.opponentPiclet.moves.filter(m => m.currentPP > 0);
165
- if (availableEnemyMoves.length === 0) {
166
- currentMessage = `${currentEnemyPiclet.nickname} has no moves left!`;
167
  processingTurn = false;
168
  return;
169
  }
170
 
171
- const randomEnemyMove = availableEnemyMoves[Math.floor(Math.random() * availableEnemyMoves.length)];
172
- const enemyMoveIndex = battleState.opponentPiclet.moves.indexOf(randomEnemyMove);
173
- const enemyAction: MoveAction = {
174
- type: 'move',
175
- moveIndex: enemyMoveIndex
176
- };
177
-
178
  // Get log entries before action to track new messages
179
  const logBefore = battleEngine.getLog();
180
 
@@ -412,6 +404,35 @@
412
  }, 500); // Allow time for typewriter text to complete
413
  }
414
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
  function handleContinueTap() {
416
  if (!waitingForContinue || !messageQueue.length) return;
417
 
@@ -496,21 +517,13 @@
496
  };
497
 
498
  try {
499
- // Choose random enemy move (AI continues to act)
500
- const availableEnemyMoves = battleState.opponentPiclet.moves.filter(m => m.currentPP > 0);
501
- if (availableEnemyMoves.length === 0) {
502
- currentMessage = `${currentEnemyPiclet.nickname} has no moves left!`;
503
  processingTurn = false;
504
  return;
505
  }
506
 
507
- const randomEnemyMove = availableEnemyMoves[Math.floor(Math.random() * availableEnemyMoves.length)];
508
- const enemyMoveIndex = battleState.opponentPiclet.moves.indexOf(randomEnemyMove);
509
- const enemyAction: MoveAction = {
510
- type: 'move',
511
- moveIndex: enemyMoveIndex
512
- };
513
-
514
  // Allow time for the visual switch to be seen before processing the turn
515
  setTimeout(() => {
516
  // Get log entries before action to track new messages
 
160
  };
161
 
162
  try {
163
+ // Select enemy move (wild Piclets always random, trainers could use AI)
164
+ const enemyAction = selectEnemyMove();
165
+ if (!enemyAction) {
 
166
  processingTurn = false;
167
  return;
168
  }
169
 
 
 
 
 
 
 
 
170
  // Get log entries before action to track new messages
171
  const logBefore = battleEngine.getLog();
172
 
 
404
  }, 500); // Allow time for typewriter text to complete
405
  }
406
 
407
+ function selectEnemyMove(): MoveAction | null {
408
+ const availableEnemyMoves = battleState.opponentPiclet.moves.filter(m => m.currentPP > 0);
409
+
410
+ if (availableEnemyMoves.length === 0) {
411
+ currentMessage = `${currentEnemyPiclet.nickname} has no moves left!`;
412
+ return null;
413
+ }
414
+
415
+ if (isWildBattle) {
416
+ // Wild Piclets always use completely random moves (no trainer strategy)
417
+ const randomEnemyMove = availableEnemyMoves[Math.floor(Math.random() * availableEnemyMoves.length)];
418
+ const enemyMoveIndex = battleState.opponentPiclet.moves.indexOf(randomEnemyMove);
419
+
420
+ return {
421
+ type: 'move',
422
+ moveIndex: enemyMoveIndex
423
+ };
424
+ } else {
425
+ // Trainer battles - currently also random, but could be enhanced with AI later
426
+ const randomEnemyMove = availableEnemyMoves[Math.floor(Math.random() * availableEnemyMoves.length)];
427
+ const enemyMoveIndex = battleState.opponentPiclet.moves.indexOf(randomEnemyMove);
428
+
429
+ return {
430
+ type: 'move',
431
+ moveIndex: enemyMoveIndex
432
+ };
433
+ }
434
+ }
435
+
436
  function handleContinueTap() {
437
  if (!waitingForContinue || !messageQueue.length) return;
438
 
 
517
  };
518
 
519
  try {
520
+ // Select enemy move (wild Piclets always random, trainers could use AI)
521
+ const enemyAction = selectEnemyMove();
522
+ if (!enemyAction) {
 
523
  processingTurn = false;
524
  return;
525
  }
526
 
 
 
 
 
 
 
 
527
  // Allow time for the visual switch to be seen before processing the turn
528
  setTimeout(() => {
529
  // Get log entries before action to track new messages
src/lib/components/Piclets/PicletDetail.svelte CHANGED
@@ -9,6 +9,7 @@
9
  import MoveDisplay from './MoveDisplay.svelte';
10
  import { picletInstanceToBattleDefinition } from '$lib/utils/battleConversion';
11
  import { recalculatePicletStats, getXpProgress, getXpTowardsNextLevel } from '$lib/services/levelingService';
 
12
 
13
  interface Props {
14
  instance: PicletInstance;
@@ -240,20 +241,46 @@
240
  {:else if selectedTab === 'abilities'}
241
  <div class="content-card">
242
  <h3 class="section-heading">Special Ability</h3>
243
- <AbilityDisplay
244
- ability={battleDefinition.specialAbility}
245
- expanded={true}
246
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
  <div class="divider"></div>
249
 
250
  <h3 class="section-heading">Moves</h3>
251
  <div class="moves-list">
252
  {#each updatedInstance.moves as move, index}
253
- <MoveDisplay
254
- {move}
255
- expanded={true}
256
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
257
  {/each}
258
  </div>
259
  </div>
@@ -547,6 +574,50 @@
547
  gap: 4px;
548
  }
549
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
550
  /* Level and Status Section */
551
  .level-xp-section {
552
  background: white;
 
9
  import MoveDisplay from './MoveDisplay.svelte';
10
  import { picletInstanceToBattleDefinition } from '$lib/utils/battleConversion';
11
  import { recalculatePicletStats, getXpProgress, getXpTowardsNextLevel } from '$lib/services/levelingService';
12
+ import { isSpecialAbilityUnlocked } from '$lib/services/unlockLevels';
13
 
14
  interface Props {
15
  instance: PicletInstance;
 
241
  {:else if selectedTab === 'abilities'}
242
  <div class="content-card">
243
  <h3 class="section-heading">Special Ability</h3>
244
+ {#if isSpecialAbilityUnlocked(updatedInstance.specialAbilityUnlockLevel, updatedInstance.level)}
245
+ <AbilityDisplay
246
+ ability={updatedInstance.specialAbility}
247
+ expanded={true}
248
+ />
249
+ {:else}
250
+ <div class="locked-ability">
251
+ <div class="lock-header">
252
+ <span class="lock-icon">🔒</span>
253
+ <span class="lock-text">Unlocks at Level {updatedInstance.specialAbilityUnlockLevel}</span>
254
+ </div>
255
+ <div class="locked-content">
256
+ <h4>{updatedInstance.specialAbility.name}</h4>
257
+ <p>This special ability will be unlocked when {updatedInstance.nickname} reaches level {updatedInstance.specialAbilityUnlockLevel}.</p>
258
+ </div>
259
+ </div>
260
+ {/if}
261
 
262
  <div class="divider"></div>
263
 
264
  <h3 class="section-heading">Moves</h3>
265
  <div class="moves-list">
266
  {#each updatedInstance.moves as move, index}
267
+ {#if move.unlockLevel <= updatedInstance.level}
268
+ <MoveDisplay
269
+ {move}
270
+ expanded={true}
271
+ />
272
+ {:else}
273
+ <div class="locked-move">
274
+ <div class="lock-header">
275
+ <span class="lock-icon">🔒</span>
276
+ <span class="lock-text">Unlocks at Level {move.unlockLevel}</span>
277
+ </div>
278
+ <div class="locked-content">
279
+ <h4>{move.name}</h4>
280
+ <p>This move will be unlocked when {updatedInstance.nickname} reaches level {move.unlockLevel}.</p>
281
+ </div>
282
+ </div>
283
+ {/if}
284
  {/each}
285
  </div>
286
  </div>
 
574
  gap: 4px;
575
  }
576
 
577
+ /* Locked content styles */
578
+ .locked-ability,
579
+ .locked-move {
580
+ background: #f8f9fa;
581
+ border: 1px dashed #dee2e6;
582
+ border-radius: 8px;
583
+ padding: 12px;
584
+ margin-bottom: 8px;
585
+ opacity: 0.7;
586
+ }
587
+
588
+ .lock-header {
589
+ display: flex;
590
+ align-items: center;
591
+ gap: 8px;
592
+ margin-bottom: 8px;
593
+ }
594
+
595
+ .lock-icon {
596
+ font-size: 16px;
597
+ }
598
+
599
+ .lock-text {
600
+ font-size: 12px;
601
+ font-weight: 600;
602
+ color: #6c757d;
603
+ text-transform: uppercase;
604
+ letter-spacing: 0.5px;
605
+ }
606
+
607
+ .locked-content h4 {
608
+ margin: 0 0 4px 0;
609
+ font-size: 16px;
610
+ font-weight: 600;
611
+ color: #495057;
612
+ }
613
+
614
+ .locked-content p {
615
+ margin: 0;
616
+ font-size: 14px;
617
+ color: #6c757d;
618
+ font-style: italic;
619
+ }
620
+
621
  /* Level and Status Section */
622
  .level-xp-section {
623
  background: white;
src/lib/db/piclets.ts CHANGED
@@ -2,6 +2,7 @@ import { db } from './index';
2
  import type { PicletInstance, Monster, BattleMove } from './schema';
3
  import { PicletType, AttackType, getTypeFromConcept } from '../types/picletTypes';
4
  import type { PicletStats } from '../types';
 
5
 
6
  // Convert a generated Monster to a PicletInstance
7
  export async function monsterToPicletInstance(monster: Monster, level: number = 5): Promise<Omit<PicletInstance, 'id'>> {
@@ -31,17 +32,22 @@ export async function monsterToPicletInstance(monster: Monster, level: number =
31
  console.warn(`Invalid primaryType "${stats.primaryType}" from stats, falling back to concept detection`);
32
  }
33
 
34
- // Create moves from battle-ready format
35
- const moves: BattleMove[] = stats.movepool.map(move => ({
36
  name: move.name,
37
  type: move.type as unknown as AttackType,
38
  power: move.power,
39
  accuracy: move.accuracy,
40
  pp: move.pp,
41
  currentPp: move.pp,
42
- description: move.description
 
43
  }));
44
 
 
 
 
 
45
  // Field stats are variations of regular stats
46
  const baseFieldAttack = Math.floor(baseAttack * 0.8);
47
  const baseFieldDefense = Math.floor(baseDefense * 0.8);
@@ -96,6 +102,7 @@ export async function monsterToPicletInstance(monster: Monster, level: number =
96
  moves,
97
  nature: stats.nature,
98
  specialAbility: stats.specialAbility,
 
99
 
100
  // Roster
101
  isInRoster: false,
 
2
  import type { PicletInstance, Monster, BattleMove } from './schema';
3
  import { PicletType, AttackType, getTypeFromConcept } from '../types/picletTypes';
4
  import type { PicletStats } from '../types';
5
+ import { generateUnlockLevels } from '../services/unlockLevels';
6
 
7
  // Convert a generated Monster to a PicletInstance
8
  export async function monsterToPicletInstance(monster: Monster, level: number = 5): Promise<Omit<PicletInstance, 'id'>> {
 
32
  console.warn(`Invalid primaryType "${stats.primaryType}" from stats, falling back to concept detection`);
33
  }
34
 
35
+ // Create moves from battle-ready format (without unlock levels initially)
36
+ const baseMoves: BattleMove[] = stats.movepool.map(move => ({
37
  name: move.name,
38
  type: move.type as unknown as AttackType,
39
  power: move.power,
40
  accuracy: move.accuracy,
41
  pp: move.pp,
42
  currentPp: move.pp,
43
+ description: move.description,
44
+ unlockLevel: 1 // Temporary, will be set below
45
  }));
46
 
47
+ // Generate unlock levels for moves and special ability
48
+ const { movesWithUnlocks, abilityUnlockLevel } = generateUnlockLevels(baseMoves, stats.specialAbility);
49
+ const moves = movesWithUnlocks;
50
+
51
  // Field stats are variations of regular stats
52
  const baseFieldAttack = Math.floor(baseAttack * 0.8);
53
  const baseFieldDefense = Math.floor(baseDefense * 0.8);
 
102
  moves,
103
  nature: stats.nature,
104
  specialAbility: stats.specialAbility,
105
+ specialAbilityUnlockLevel: abilityUnlockLevel,
106
 
107
  // Roster
108
  isInRoster: false,
src/lib/db/schema.ts CHANGED
@@ -18,6 +18,7 @@ export interface BattleMove {
18
  pp: number;
19
  currentPp: number;
20
  description: string;
 
21
  }
22
 
23
  // PicletInstance - Individual monster instances owned by the player
@@ -53,6 +54,7 @@ export interface PicletInstance {
53
  moves: BattleMove[];
54
  nature: string;
55
  specialAbility: SpecialAbility;
 
56
 
57
  // Roster
58
  isInRoster: boolean;
 
18
  pp: number;
19
  currentPp: number;
20
  description: string;
21
+ unlockLevel: number; // Level at which this move is unlocked
22
  }
23
 
24
  // PicletInstance - Individual monster instances owned by the player
 
54
  moves: BattleMove[];
55
  nature: string;
56
  specialAbility: SpecialAbility;
57
+ specialAbilityUnlockLevel: number; // Level at which special ability is unlocked
58
 
59
  // Roster
60
  isInRoster: boolean;
src/lib/services/unlockLevels.ts ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Utility functions for calculating unlock levels for moves and abilities
3
+ */
4
+
5
+ import type { BattleMove } from '$lib/db/schema';
6
+ import type { SpecialAbility } from '$lib/battle-engine/types';
7
+
8
+ /**
9
+ * Calculate unlock level for a move based on its power and characteristics
10
+ * More powerful moves unlock later, with some randomness
11
+ */
12
+ export function calculateMoveUnlockLevel(move: BattleMove, moveIndex: number): number {
13
+ // Base unlock level based on power
14
+ let baseLevel = 1;
15
+
16
+ if (move.power === 0) {
17
+ // Status/support moves - unlock early to mid game
18
+ baseLevel = Math.floor(Math.random() * 20) + 5; // Level 5-25
19
+ } else if (move.power <= 40) {
20
+ // Weak moves - unlock early
21
+ baseLevel = Math.floor(Math.random() * 15) + 1; // Level 1-15
22
+ } else if (move.power <= 60) {
23
+ // Medium moves - unlock early to mid
24
+ baseLevel = Math.floor(Math.random() * 25) + 10; // Level 10-35
25
+ } else if (move.power <= 80) {
26
+ // Strong moves - unlock mid to late
27
+ baseLevel = Math.floor(Math.random() * 30) + 25; // Level 25-55
28
+ } else {
29
+ // Very powerful moves - unlock late
30
+ baseLevel = Math.floor(Math.random() * 25) + 40; // Level 40-65
31
+ }
32
+
33
+ // Adjust based on move position (later moves tend to be more powerful)
34
+ const positionBonus = moveIndex * 5; // 0, 5, 10, 15 for moves 1-4
35
+ baseLevel += positionBonus;
36
+
37
+ // Ensure first move is always available at level 1
38
+ if (moveIndex === 0) {
39
+ baseLevel = 1;
40
+ }
41
+
42
+ // Cap at level 80 (everything must be unlocked by then)
43
+ return Math.min(baseLevel, 80);
44
+ }
45
+
46
+ /**
47
+ * Calculate unlock level for special ability based on its power and effects
48
+ */
49
+ export function calculateSpecialAbilityUnlockLevel(ability: SpecialAbility): number {
50
+ // Base level based on ability impact
51
+ let baseLevel = 15; // Default mid-level unlock
52
+
53
+ // Analyze ability effects to determine power level
54
+ const effects = ability.effects || [];
55
+ let powerScore = 0;
56
+
57
+ for (const effect of effects) {
58
+ switch (effect.type) {
59
+ case 'damage':
60
+ powerScore += effect.amount === 'strong' ? 3 : effect.amount === 'normal' ? 2 : 1;
61
+ break;
62
+ case 'heal':
63
+ powerScore += effect.amount === 'large' ? 3 : effect.amount === 'medium' ? 2 : 1;
64
+ break;
65
+ case 'modifyStats':
66
+ // Stat modifications are powerful
67
+ powerScore += 2;
68
+ break;
69
+ case 'applyStatus':
70
+ powerScore += 2;
71
+ break;
72
+ case 'removeStatus':
73
+ powerScore += 1;
74
+ break;
75
+ case 'manipulatePP':
76
+ powerScore += 1;
77
+ break;
78
+ default:
79
+ powerScore += 1;
80
+ }
81
+ }
82
+
83
+ // Convert power score to unlock level
84
+ if (powerScore <= 2) {
85
+ baseLevel = Math.floor(Math.random() * 20) + 10; // Level 10-30
86
+ } else if (powerScore <= 4) {
87
+ baseLevel = Math.floor(Math.random() * 25) + 20; // Level 20-45
88
+ } else if (powerScore <= 6) {
89
+ baseLevel = Math.floor(Math.random() * 25) + 30; // Level 30-55
90
+ } else {
91
+ baseLevel = Math.floor(Math.random() * 25) + 40; // Level 40-65
92
+ }
93
+
94
+ // Cap at level 80
95
+ return Math.min(baseLevel, 80);
96
+ }
97
+
98
+ /**
99
+ * Get all unlocked moves for a Piclet at a given level
100
+ */
101
+ export function getUnlockedMoves(moves: BattleMove[], currentLevel: number): BattleMove[] {
102
+ return moves.filter(move => move.unlockLevel <= currentLevel);
103
+ }
104
+
105
+ /**
106
+ * Check if special ability is unlocked at given level
107
+ */
108
+ export function isSpecialAbilityUnlocked(unlockLevel: number, currentLevel: number): boolean {
109
+ return currentLevel >= unlockLevel;
110
+ }
111
+
112
+ /**
113
+ * Generate unlock levels for a new Piclet's moves and ability
114
+ * This should be called when a Piclet is first generated
115
+ */
116
+ export function generateUnlockLevels(moves: BattleMove[], specialAbility: SpecialAbility): {
117
+ movesWithUnlocks: BattleMove[];
118
+ abilityUnlockLevel: number;
119
+ } {
120
+ const movesWithUnlocks = moves.map((move, index) => ({
121
+ ...move,
122
+ unlockLevel: calculateMoveUnlockLevel(move, index)
123
+ }));
124
+
125
+ const abilityUnlockLevel = calculateSpecialAbilityUnlockLevel(specialAbility);
126
+
127
+ return {
128
+ movesWithUnlocks,
129
+ abilityUnlockLevel
130
+ };
131
+ }
src/lib/utils/battleConversion.ts CHANGED
@@ -7,6 +7,7 @@ import type { PicletDefinition, Move, BaseStats, SpecialAbility } from '$lib/bat
7
  import type { PicletStats, BattleEffect, AbilityTrigger } from '$lib/types';
8
  import { PicletType, AttackType } from '$lib/types/picletTypes';
9
  import { recalculatePicletStats } from '$lib/services/levelingService';
 
10
 
11
  /**
12
  * Convert PicletInstance to PicletDefinition for battle engine use
@@ -24,15 +25,38 @@ export function picletInstanceToBattleDefinition(instance: PicletInstance): Picl
24
  speed: updatedInstance.speed // Includes level scaling and nature modifiers
25
  };
26
 
27
- // Convert simple moves to full battle moves
28
- const movepool: Move[] = instance.moves.map(move => convertBattleMoveToMove(move, instance.primaryType));
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  // All Piclets must now have special abilities
31
  if (!instance.specialAbility) {
32
  throw new Error('Piclet must have a special ability. Legacy Piclets are no longer supported.');
33
  }
34
 
35
- const specialAbility: SpecialAbility = instance.specialAbility;
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  // Determine tier based on BST (Base Stat Total)
38
  const bst = baseStats.hp + baseStats.attack + baseStats.defense + baseStats.speed;
 
7
  import type { PicletStats, BattleEffect, AbilityTrigger } from '$lib/types';
8
  import { PicletType, AttackType } from '$lib/types/picletTypes';
9
  import { recalculatePicletStats } from '$lib/services/levelingService';
10
+ import { getUnlockedMoves, isSpecialAbilityUnlocked } from '$lib/services/unlockLevels';
11
 
12
  /**
13
  * Convert PicletInstance to PicletDefinition for battle engine use
 
25
  speed: updatedInstance.speed // Includes level scaling and nature modifiers
26
  };
27
 
28
+ // Only include unlocked moves
29
+ const unlockedMoves = getUnlockedMoves(instance.moves, updatedInstance.level);
30
+ const movepool: Move[] = unlockedMoves.map(move => convertBattleMoveToMove(move, instance.primaryType));
31
+
32
+ // Ensure at least one move is available (first move should always be unlocked at level 1)
33
+ if (movepool.length === 0) {
34
+ console.warn(`Piclet ${instance.nickname} has no unlocked moves at level ${updatedInstance.level}!`);
35
+ // Emergency fallback - unlock first move
36
+ if (instance.moves.length > 0) {
37
+ const firstMove = convertBattleMoveToMove(instance.moves[0], instance.primaryType);
38
+ movepool.push(firstMove);
39
+ }
40
+ }
41
 
42
  // All Piclets must now have special abilities
43
  if (!instance.specialAbility) {
44
  throw new Error('Piclet must have a special ability. Legacy Piclets are no longer supported.');
45
  }
46
 
47
+ // Only include special ability if unlocked
48
+ let specialAbility: SpecialAbility | undefined;
49
+ if (isSpecialAbilityUnlocked(instance.specialAbilityUnlockLevel, updatedInstance.level)) {
50
+ specialAbility = instance.specialAbility;
51
+ } else {
52
+ // Create a placeholder ability for locked special abilities
53
+ specialAbility = {
54
+ name: "Locked Ability",
55
+ description: `Unlocks at level ${instance.specialAbilityUnlockLevel}`,
56
+ trigger: "onBattleStart" as any,
57
+ effects: []
58
+ };
59
+ }
60
 
61
  // Determine tier based on BST (Base Stat Total)
62
  const bst = baseStats.hp + baseStats.attack + baseStats.defense + baseStats.speed;