Fraser commited on
Commit
e8aa797
·
1 Parent(s): 06a5e70
src/lib/battle-engine/BattleEngine.ts CHANGED
@@ -61,8 +61,8 @@ export class BattleEngine {
61
 
62
  private initializeRosterStates(roster: PicletDefinition[], level: number): Array<{ currentHp: number; maxHp: number; fainted: boolean; moves: Array<{move: Move; currentPP: number}> }> {
63
  return roster.map(piclet => {
64
- const statMultiplier = 1 + (level - 50) * 0.02;
65
- const hp = Math.floor(piclet.baseStats.hp * statMultiplier);
66
  return {
67
  currentHp: hp,
68
  maxHp: hp,
@@ -76,12 +76,12 @@ export class BattleEngine {
76
  }
77
 
78
  private createBattlePiclet(definition: PicletDefinition, level: number): BattlePiclet {
79
- const statMultiplier = 1 + (level - 50) * 0.02; // 2% per level above/below 50
80
-
81
  const hp = definition.baseStats.hp;
82
- const attack = Math.floor(definition.baseStats.attack * statMultiplier);
83
- const defense = Math.floor(definition.baseStats.defense * statMultiplier);
84
- const speed = Math.floor(definition.baseStats.speed * statMultiplier);
85
 
86
  const piclet: BattlePiclet = {
87
  definition,
 
61
 
62
  private initializeRosterStates(roster: PicletDefinition[], level: number): Array<{ currentHp: number; maxHp: number; fainted: boolean; moves: Array<{move: Move; currentPP: number}> }> {
63
  return roster.map(piclet => {
64
+ // Use pre-calculated HP from definition (already includes level scaling)
65
+ const hp = piclet.baseStats.hp;
66
  return {
67
  currentHp: hp,
68
  maxHp: hp,
 
76
  }
77
 
78
  private createBattlePiclet(definition: PicletDefinition, level: number): BattlePiclet {
79
+ // Battle engine now uses pre-calculated stats from levelingService
80
+ // No level scaling needed here - stats already include level and nature effects
81
  const hp = definition.baseStats.hp;
82
+ const attack = definition.baseStats.attack;
83
+ const defense = definition.baseStats.defense;
84
+ const speed = definition.baseStats.speed;
85
 
86
  const piclet: BattlePiclet = {
87
  definition,
src/lib/components/Battle/PicletInfo.svelte CHANGED
@@ -1,11 +1,16 @@
1
  <script lang="ts">
2
  import type { PicletInstance } from '$lib/db/schema';
 
3
 
4
  export let piclet: PicletInstance;
5
  export let hpPercentage: number;
6
- export let xpPercentage: number = 0;
7
  export let isPlayer: boolean;
8
 
 
 
 
 
9
  $: hpColor = hpPercentage > 0.5 ? '#4caf50' : hpPercentage > 0.2 ? '#ffc107' : '#f44336';
10
  $: displayHp = Math.ceil(piclet.currentHp);
11
 
@@ -50,9 +55,20 @@
50
  <div class="xp-bar">
51
  <div
52
  class="xp-fill"
53
- style="width: {xpPercentage}%"
54
  ></div>
55
  </div>
 
 
 
 
 
 
 
 
 
 
 
56
  {/if}
57
  </div>
58
 
@@ -160,12 +176,23 @@
160
  background: #e0e0e0;
161
  border-radius: 2px;
162
  overflow: hidden;
 
163
  }
164
 
165
  .xp-fill {
166
  height: 100%;
167
  background: #2196f3;
168
- transition: width 0.5s ease;
 
 
 
 
 
 
 
 
 
 
169
  }
170
 
171
  /* Triangle Pointer */
 
1
  <script lang="ts">
2
  import type { PicletInstance } from '$lib/db/schema';
3
+ import { getXpProgress, getXpForNextLevel } from '$lib/services/levelingService';
4
 
5
  export let piclet: PicletInstance;
6
  export let hpPercentage: number;
7
+ export let xpPercentage: number = 0; // Legacy prop - will be overridden for players
8
  export let isPlayer: boolean;
9
 
10
+ // Calculate real XP percentage using levelingService
11
+ $: realXpPercentage = isPlayer ? getXpProgress(piclet.xp, piclet.level) : 0;
12
+ $: nextLevelXp = isPlayer ? getXpForNextLevel(piclet.level) : 0;
13
+
14
  $: hpColor = hpPercentage > 0.5 ? '#4caf50' : hpPercentage > 0.2 ? '#ffc107' : '#f44336';
15
  $: displayHp = Math.ceil(piclet.currentHp);
16
 
 
55
  <div class="xp-bar">
56
  <div
57
  class="xp-fill"
58
+ style="width: {realXpPercentage}%"
59
  ></div>
60
  </div>
61
+
62
+ <!-- XP Progress Text (Player only) -->
63
+ {#if piclet.level < 100}
64
+ <div class="xp-text">
65
+ <span class="xp-progress">{Math.floor(realXpPercentage)}% to next level</span>
66
+ </div>
67
+ {:else}
68
+ <div class="xp-text">
69
+ <span class="xp-progress">MAX LEVEL</span>
70
+ </div>
71
+ {/if}
72
  {/if}
73
  </div>
74
 
 
176
  background: #e0e0e0;
177
  border-radius: 2px;
178
  overflow: hidden;
179
+ margin-bottom: 2px;
180
  }
181
 
182
  .xp-fill {
183
  height: 100%;
184
  background: #2196f3;
185
+ transition: width 0.8s ease;
186
+ }
187
+
188
+ /* XP Text */
189
+ .xp-text {
190
+ font-size: 10px;
191
+ color: #666;
192
+ }
193
+
194
+ .xp-progress {
195
+ font-weight: 500;
196
  }
197
 
198
  /* Triangle Pointer */
src/lib/components/Pages/Battle.svelte CHANGED
@@ -7,6 +7,8 @@
7
  import { BattleEngine } from '$lib/battle-engine/BattleEngine';
8
  import type { BattleState, MoveAction } from '$lib/battle-engine/types';
9
  import { picletInstanceToBattleDefinition, battlePicletToInstance, stripBattlePrefix } from '$lib/utils/battleConversion';
 
 
10
  import { getEffectivenessText, getEffectivenessColor } from '$lib/types/picletTypes';
11
 
12
  export let playerPiclet: PicletInstance;
@@ -41,6 +43,15 @@
41
  let playerFaint = false;
42
  let enemyFaint = false;
43
 
 
 
 
 
 
 
 
 
 
44
 
45
  onMount(() => {
46
  // Initialize battle engine with converted piclet definitions
@@ -163,8 +174,10 @@
163
  ? `${currentEnemyPiclet.nickname} fainted! You won!`
164
  : `${currentPlayerPiclet.nickname} fainted! You lost!`;
165
  currentMessage = winMessage;
166
- setTimeout(() => {
167
- onBattleEnd(battleState.winner === 'player');
 
 
168
  }, 2000);
169
  } else {
170
  setTimeout(() => {
@@ -346,6 +359,61 @@
346
  function handleBack() {
347
  battlePhase = 'main';
348
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
349
  </script>
350
 
351
  <div class="battle-page" transition:fade={{ duration: 300 }}>
@@ -389,6 +457,45 @@
389
  onBack={handleBack}
390
  />
391
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
392
  </div>
393
 
394
  <style>
@@ -456,4 +563,99 @@
456
  position: relative;
457
  background: #f8f9fa;
458
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
  </style>
 
7
  import { BattleEngine } from '$lib/battle-engine/BattleEngine';
8
  import type { BattleState, MoveAction } from '$lib/battle-engine/types';
9
  import { picletInstanceToBattleDefinition, battlePicletToInstance, stripBattlePrefix } from '$lib/utils/battleConversion';
10
+ import { calculateBattleXp, processAllLevelUps, getXpProgress } 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;
 
43
  let playerFaint = false;
44
  let enemyFaint = false;
45
 
46
+ // Battle results state
47
+ let battleResultsVisible = false;
48
+ let battleResults = {
49
+ victory: false,
50
+ xpGained: 0,
51
+ levelUps: [],
52
+ newLevel: 0
53
+ };
54
+
55
 
56
  onMount(() => {
57
  // Initialize battle engine with converted piclet definitions
 
174
  ? `${currentEnemyPiclet.nickname} fainted! You won!`
175
  : `${currentPlayerPiclet.nickname} fainted! You lost!`;
176
  currentMessage = winMessage;
177
+
178
+ // Process battle results with XP and level ups
179
+ setTimeout(async () => {
180
+ await handleBattleResults(battleState.winner === 'player');
181
  }, 2000);
182
  } else {
183
  setTimeout(() => {
 
359
  function handleBack() {
360
  battlePhase = 'main';
361
  }
362
+
363
+ async function handleBattleResults(playerWon: boolean) {
364
+ if (playerWon) {
365
+ // Calculate XP gained from defeating the enemy
366
+ const xpGained = calculateBattleXp(currentEnemyPiclet, 1);
367
+
368
+ // Apply XP to player's Piclet
369
+ const updatedPlayerPiclet = {
370
+ ...currentPlayerPiclet,
371
+ xp: currentPlayerPiclet.xp + xpGained
372
+ };
373
+
374
+ // Process any level ups
375
+ const { newInstance, levelUpInfo } = processAllLevelUps(updatedPlayerPiclet);
376
+
377
+ // Save updated Piclet to database
378
+ if (newInstance.id) {
379
+ await db.picletInstances.update(newInstance.id, newInstance);
380
+ }
381
+
382
+ // Update local state
383
+ currentPlayerPiclet = newInstance;
384
+
385
+ // Prepare battle results for display
386
+ battleResults = {
387
+ victory: true,
388
+ xpGained,
389
+ levelUps: levelUpInfo,
390
+ newLevel: newInstance.level
391
+ };
392
+
393
+ // Show battle results screen
394
+ if (xpGained > 0 || levelUpInfo.length > 0) {
395
+ battleResultsVisible = true;
396
+
397
+ // Auto-dismiss after showing results
398
+ setTimeout(() => {
399
+ battleResultsVisible = false;
400
+ onBattleEnd(true);
401
+ }, levelUpInfo.length > 0 ? 4000 : 2500);
402
+ } else {
403
+ onBattleEnd(true);
404
+ }
405
+ } else {
406
+ // Player lost - no XP gained
407
+ battleResults = {
408
+ victory: false,
409
+ xpGained: 0,
410
+ levelUps: [],
411
+ newLevel: currentPlayerPiclet.level
412
+ };
413
+
414
+ onBattleEnd(false);
415
+ }
416
+ }
417
  </script>
418
 
419
  <div class="battle-page" transition:fade={{ duration: 300 }}>
 
457
  onBack={handleBack}
458
  />
459
  </div>
460
+
461
+ <!-- Battle Results Overlay -->
462
+ {#if battleResultsVisible}
463
+ <div class="battle-results-overlay" transition:fade={{ duration: 300 }}>
464
+ <div class="battle-results-card">
465
+ <h2>{battleResults.victory ? 'Victory!' : 'Defeat!'}</h2>
466
+
467
+ {#if battleResults.victory && battleResults.xpGained > 0}
468
+ <div class="xp-gain">
469
+ <p><strong>{currentPlayerPiclet.nickname}</strong> gained <strong>{battleResults.xpGained} XP</strong>!</p>
470
+ </div>
471
+ {/if}
472
+
473
+ {#if battleResults.levelUps.length > 0}
474
+ {#each battleResults.levelUps as levelUp}
475
+ <div class="level-up" transition:fade={{ duration: 500 }}>
476
+ <h3>🎉 Level Up! 🎉</h3>
477
+ <p><strong>{currentPlayerPiclet.nickname}</strong> grew to level <strong>{levelUp.newLevel}</strong>!</p>
478
+
479
+ <div class="stat-changes">
480
+ {#if levelUp.statChanges.hp > 0}
481
+ <div class="stat-change">HP +{levelUp.statChanges.hp}</div>
482
+ {/if}
483
+ {#if levelUp.statChanges.attack > 0}
484
+ <div class="stat-change">Attack +{levelUp.statChanges.attack}</div>
485
+ {/if}
486
+ {#if levelUp.statChanges.defense > 0}
487
+ <div class="stat-change">Defense +{levelUp.statChanges.defense}</div>
488
+ {/if}
489
+ {#if levelUp.statChanges.speed > 0}
490
+ <div class="stat-change">Speed +{levelUp.statChanges.speed}</div>
491
+ {/if}
492
+ </div>
493
+ </div>
494
+ {/each}
495
+ {/if}
496
+ </div>
497
+ </div>
498
+ {/if}
499
  </div>
500
 
501
  <style>
 
563
  position: relative;
564
  background: #f8f9fa;
565
  }
566
+
567
+ /* Battle Results Overlay */
568
+ .battle-results-overlay {
569
+ position: fixed;
570
+ inset: 0;
571
+ background: rgba(0, 0, 0, 0.8);
572
+ display: flex;
573
+ align-items: center;
574
+ justify-content: center;
575
+ z-index: 2000;
576
+ }
577
+
578
+ .battle-results-card {
579
+ background: white;
580
+ border-radius: 16px;
581
+ padding: 2rem;
582
+ max-width: 400px;
583
+ width: 90%;
584
+ text-align: center;
585
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
586
+ }
587
+
588
+ .battle-results-card h2 {
589
+ margin: 0 0 1rem 0;
590
+ font-size: 1.8rem;
591
+ font-weight: 700;
592
+ color: #1a1a1a;
593
+ }
594
+
595
+ .xp-gain {
596
+ background: #e3f2fd;
597
+ border-radius: 8px;
598
+ padding: 1rem;
599
+ margin: 1rem 0;
600
+ border: 2px solid #2196f3;
601
+ }
602
+
603
+ .xp-gain p {
604
+ margin: 0;
605
+ color: #1565c0;
606
+ font-size: 1.1rem;
607
+ }
608
+
609
+ .level-up {
610
+ background: linear-gradient(135deg, #fff3e0 0%, #ffcc02 100%);
611
+ border-radius: 12px;
612
+ padding: 1.5rem;
613
+ margin: 1rem 0;
614
+ border: 3px solid #ff6f00;
615
+ animation: levelUpPulse 0.6s ease-in-out;
616
+ }
617
+
618
+ .level-up h3 {
619
+ margin: 0 0 0.5rem 0;
620
+ font-size: 1.4rem;
621
+ color: #e65100;
622
+ text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.1);
623
+ }
624
+
625
+ .level-up p {
626
+ margin: 0 0 1rem 0;
627
+ font-size: 1.2rem;
628
+ color: #bf360c;
629
+ }
630
+
631
+ .stat-changes {
632
+ display: flex;
633
+ flex-wrap: wrap;
634
+ gap: 0.5rem;
635
+ justify-content: center;
636
+ }
637
+
638
+ .stat-change {
639
+ background: rgba(76, 175, 80, 0.2);
640
+ border: 1px solid #4caf50;
641
+ border-radius: 20px;
642
+ padding: 0.25rem 0.75rem;
643
+ font-size: 0.9rem;
644
+ font-weight: 600;
645
+ color: #2e7d32;
646
+ }
647
+
648
+ @keyframes levelUpPulse {
649
+ 0% {
650
+ transform: scale(0.9);
651
+ opacity: 0;
652
+ }
653
+ 50% {
654
+ transform: scale(1.05);
655
+ }
656
+ 100% {
657
+ transform: scale(1);
658
+ opacity: 1;
659
+ }
660
+ }
661
  </style>
src/lib/services/levelingService.ts ADDED
@@ -0,0 +1,274 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Pokemon-style Leveling and Stat Calculation Service
3
+ * Implements accurate Pokemon stat formulas based on pokemon_stat_calculation.md
4
+ */
5
+
6
+ import type { PicletInstance } from '$lib/db/schema';
7
+
8
+ // Pokemon nature effects: [boosted_stat, lowered_stat] or [null, null] for neutral
9
+ export const NATURES = {
10
+ 'Hardy': [null, null], // Neutral
11
+ 'Lonely': ['attack', 'defense'],
12
+ 'Brave': ['attack', 'speed'],
13
+ 'Adamant': ['attack', 'sp_attack'],
14
+ 'Naughty': ['attack', 'sp_defense'],
15
+ 'Bold': ['defense', 'attack'],
16
+ 'Docile': [null, null], // Neutral
17
+ 'Relaxed': ['defense', 'speed'],
18
+ 'Impish': ['defense', 'sp_attack'],
19
+ 'Lax': ['defense', 'sp_defense'],
20
+ 'Timid': ['speed', 'attack'],
21
+ 'Hasty': ['speed', 'defense'],
22
+ 'Serious': [null, null], // Neutral
23
+ 'Jolly': ['speed', 'sp_attack'],
24
+ 'Naive': ['speed', 'sp_defense'],
25
+ 'Modest': ['sp_attack', 'attack'],
26
+ 'Mild': ['sp_attack', 'defense'],
27
+ 'Quiet': ['sp_attack', 'speed'],
28
+ 'Bashful': [null, null], // Neutral
29
+ 'Rash': ['sp_attack', 'sp_defense'],
30
+ 'Calm': ['sp_defense', 'attack'],
31
+ 'Gentle': ['sp_defense', 'defense'],
32
+ 'Sassy': ['sp_defense', 'speed'],
33
+ 'Careful': ['sp_defense', 'sp_attack'],
34
+ 'Quirky': [null, null], // Neutral
35
+ } as const;
36
+
37
+ export type NatureName = keyof typeof NATURES;
38
+
39
+ // Experience requirements for Medium Fast growth rate (level³)
40
+ const XP_REQUIREMENTS: number[] = [];
41
+ for (let level = 1; level <= 100; level++) {
42
+ XP_REQUIREMENTS[level] = level * level * level;
43
+ }
44
+
45
+ export interface LevelUpInfo {
46
+ oldLevel: number;
47
+ newLevel: number;
48
+ statChanges: {
49
+ hp: number;
50
+ attack: number;
51
+ defense: number;
52
+ speed: number;
53
+ };
54
+ }
55
+
56
+ export interface NatureModifiers {
57
+ attack: number;
58
+ defense: number;
59
+ speed: number;
60
+ }
61
+
62
+ /**
63
+ * Calculate HP using Pokemon's HP formula
64
+ * Formula: floor((2 * base_hp * level) / 100) + level + 10
65
+ */
66
+ export function calculateHp(baseHp: number, level: number): number {
67
+ if (level === 1) {
68
+ return Math.max(1, Math.floor(baseHp / 10) + 11); // Special case for level 1
69
+ }
70
+
71
+ return Math.floor((2 * baseHp * level) / 100) + level + 10;
72
+ }
73
+
74
+ /**
75
+ * Calculate non-HP stat using Pokemon's standard formula
76
+ * Formula: floor((floor((2 * base_stat * level) / 100) + 5) * nature_modifier)
77
+ */
78
+ export function calculateStat(baseStat: number, level: number, natureModifier: number = 1.0): number {
79
+ if (level === 1) {
80
+ return Math.max(1, Math.floor(baseStat / 10) + 5); // Special case for level 1
81
+ }
82
+
83
+ const baseValue = Math.floor((2 * baseStat * level) / 100) + 5;
84
+ return Math.floor(baseValue * natureModifier);
85
+ }
86
+
87
+ /**
88
+ * Get nature modifiers for all stats
89
+ */
90
+ export function getNatureModifiers(nature: string): NatureModifiers {
91
+ const natureName = nature as NatureName;
92
+ const [boosted, lowered] = NATURES[natureName] || NATURES['Hardy'];
93
+
94
+ const modifiers: NatureModifiers = {
95
+ attack: 1.0,
96
+ defense: 1.0,
97
+ speed: 1.0,
98
+ };
99
+
100
+ if (boosted) {
101
+ (modifiers as any)[boosted] = 1.1; // +10%
102
+ }
103
+ if (lowered) {
104
+ (modifiers as any)[lowered] = 0.9; // -10%
105
+ }
106
+
107
+ return modifiers;
108
+ }
109
+
110
+ /**
111
+ * Get XP required to reach a specific level
112
+ */
113
+ export function getXpForLevel(level: number): number {
114
+ if (level < 1 || level > 100) {
115
+ throw new Error('Level must be between 1 and 100');
116
+ }
117
+ return XP_REQUIREMENTS[level];
118
+ }
119
+
120
+ /**
121
+ * Get XP required for next level
122
+ */
123
+ export function getXpForNextLevel(currentLevel: number): number {
124
+ if (currentLevel >= 100) return 0; // Max level
125
+ return getXpForLevel(currentLevel + 1);
126
+ }
127
+
128
+ /**
129
+ * Calculate XP progress percentage for current level
130
+ */
131
+ export function getXpProgress(currentXp: number, currentLevel: number): number {
132
+ if (currentLevel >= 100) return 100;
133
+
134
+ const currentLevelXp = getXpForLevel(currentLevel);
135
+ const nextLevelXp = getXpForLevel(currentLevel + 1);
136
+ const xpIntoLevel = currentXp - currentLevelXp;
137
+ const xpNeededForLevel = nextLevelXp - currentLevelXp;
138
+
139
+ return Math.min(100, Math.max(0, (xpIntoLevel / xpNeededForLevel) * 100));
140
+ }
141
+
142
+ /**
143
+ * Recalculate all stats for a Piclet based on current level and nature
144
+ */
145
+ export function recalculatePicletStats(instance: PicletInstance): PicletInstance {
146
+ const natureModifiers = getNatureModifiers(instance.nature);
147
+
148
+ // Calculate new stats
149
+ const newMaxHp = calculateHp(instance.baseHp, instance.level);
150
+ const newAttack = calculateStat(instance.baseAttack, instance.level, natureModifiers.attack);
151
+ const newDefense = calculateStat(instance.baseDefense, instance.level, natureModifiers.defense);
152
+ const newSpeed = calculateStat(instance.baseSpeed, instance.level, natureModifiers.speed);
153
+
154
+ // Field stats are 80% of main stats (existing logic)
155
+ const newFieldAttack = Math.floor(newAttack * 0.8);
156
+ const newFieldDefense = Math.floor(newDefense * 0.8);
157
+
158
+ // Maintain current HP ratio when stats change
159
+ const hpRatio = instance.maxHp > 0 ? instance.currentHp / instance.maxHp : 1;
160
+ const newCurrentHp = Math.ceil(newMaxHp * hpRatio);
161
+
162
+ return {
163
+ ...instance,
164
+ maxHp: newMaxHp,
165
+ currentHp: newCurrentHp,
166
+ attack: newAttack,
167
+ defense: newDefense,
168
+ speed: newSpeed,
169
+ fieldAttack: newFieldAttack,
170
+ fieldDefense: newFieldDefense
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Process potential level up and return results
176
+ */
177
+ export function processLevelUp(instance: PicletInstance): {
178
+ newInstance: PicletInstance;
179
+ levelUpInfo: LevelUpInfo | null;
180
+ } {
181
+ const requiredXp = getXpForNextLevel(instance.level);
182
+
183
+ // Check if level up is possible
184
+ if (instance.level >= 100 || instance.xp < requiredXp) {
185
+ return { newInstance: instance, levelUpInfo: null };
186
+ }
187
+
188
+ // Calculate old stats for comparison
189
+ const oldStats = {
190
+ hp: instance.maxHp,
191
+ attack: instance.attack,
192
+ defense: instance.defense,
193
+ speed: instance.speed
194
+ };
195
+
196
+ // Level up the Piclet
197
+ const leveledUpInstance = {
198
+ ...instance,
199
+ level: instance.level + 1
200
+ };
201
+
202
+ // Recalculate stats with new level
203
+ const newInstance = recalculatePicletStats(leveledUpInstance);
204
+
205
+ // Heal to full HP on level up (Pokemon tradition)
206
+ const finalInstance = {
207
+ ...newInstance,
208
+ currentHp: newInstance.maxHp
209
+ };
210
+
211
+ // Calculate stat changes
212
+ const statChanges = {
213
+ hp: finalInstance.maxHp - oldStats.hp,
214
+ attack: finalInstance.attack - oldStats.attack,
215
+ defense: finalInstance.defense - oldStats.defense,
216
+ speed: finalInstance.speed - oldStats.speed
217
+ };
218
+
219
+ const levelUpInfo: LevelUpInfo = {
220
+ oldLevel: instance.level,
221
+ newLevel: finalInstance.level,
222
+ statChanges
223
+ };
224
+
225
+ return { newInstance: finalInstance, levelUpInfo };
226
+ }
227
+
228
+ /**
229
+ * Calculate XP gained from defeating a Piclet in battle
230
+ * Based on Pokemon formula: (baseExpYield * level) / 7
231
+ */
232
+ export function calculateBattleXp(defeatedPiclet: PicletInstance, participantCount: number = 1): number {
233
+ // Use BST as basis for exp yield (common Pokemon approach)
234
+ const bst = defeatedPiclet.baseHp + defeatedPiclet.baseAttack + defeatedPiclet.baseDefense +
235
+ defeatedPiclet.baseSpeed + defeatedPiclet.baseFieldAttack + defeatedPiclet.baseFieldDefense;
236
+
237
+ // Convert BST to exp yield (roughly BST/4, minimum 50)
238
+ const baseExpYield = Math.max(50, Math.floor(bst / 4));
239
+
240
+ // Pokemon formula
241
+ const baseXp = Math.floor((baseExpYield * defeatedPiclet.level) / 7);
242
+
243
+ // Divide among participants
244
+ return Math.max(1, Math.floor(baseXp / participantCount));
245
+ }
246
+
247
+ /**
248
+ * Check if a level up should occur and process it recursively
249
+ * (Handles multiple level ups from large XP gains)
250
+ */
251
+ export function processAllLevelUps(instance: PicletInstance): {
252
+ newInstance: PicletInstance;
253
+ levelUpInfo: LevelUpInfo[];
254
+ } {
255
+ const levelUps: LevelUpInfo[] = [];
256
+ let currentInstance = instance;
257
+
258
+ // Process level ups until no more are possible
259
+ while (currentInstance.level < 100) {
260
+ const result = processLevelUp(currentInstance);
261
+
262
+ if (result.levelUpInfo) {
263
+ levelUps.push(result.levelUpInfo);
264
+ currentInstance = result.newInstance;
265
+ } else {
266
+ break;
267
+ }
268
+ }
269
+
270
+ return {
271
+ newInstance: currentInstance,
272
+ levelUpInfo: levelUps
273
+ };
274
+ }
src/lib/utils/battleConversion.ts CHANGED
@@ -6,16 +6,22 @@ import type { PicletInstance, BattleMove } from '$lib/db/schema';
6
  import type { PicletDefinition, Move, BaseStats, SpecialAbility } from '$lib/battle-engine/types';
7
  import type { PicletStats, BattleEffect, AbilityTrigger } from '$lib/types';
8
  import { PicletType, AttackType } from '$lib/types/picletTypes';
 
9
 
10
  /**
11
  * Convert PicletInstance to PicletDefinition for battle engine use
 
12
  */
13
  export function picletInstanceToBattleDefinition(instance: PicletInstance): PicletDefinition {
 
 
 
 
14
  const baseStats: BaseStats = {
15
- hp: instance.maxHp,
16
- attack: Math.floor((instance.baseAttack - 30) / 1.5),
17
- defense: Math.floor((instance.baseDefense - 30) / 1.5),
18
- speed: Math.floor((instance.baseSpeed - 30) / 1.5)
19
  };
20
 
21
  // Convert simple moves to full battle moves
 
6
  import type { PicletDefinition, Move, BaseStats, SpecialAbility } from '$lib/battle-engine/types';
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
13
+ * Uses levelingService to ensure stats are properly calculated for current level
14
  */
15
  export function picletInstanceToBattleDefinition(instance: PicletInstance): PicletDefinition {
16
+ // First ensure stats are up-to-date for current level and nature
17
+ const updatedInstance = recalculatePicletStats(instance);
18
+
19
+ // Use the calculated stats directly (no need for complex reverse formulas)
20
  const baseStats: BaseStats = {
21
+ hp: updatedInstance.maxHp, // Pokemon-calculated HP
22
+ attack: updatedInstance.attack, // Includes level scaling and nature modifiers
23
+ defense: updatedInstance.defense, // Includes level scaling and nature modifiers
24
+ speed: updatedInstance.speed // Includes level scaling and nature modifiers
25
  };
26
 
27
  // Convert simple moves to full battle moves