File size: 10,006 Bytes
e8aa797
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d871370
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8aa797
d871370
e8aa797
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d871370
 
 
 
e8aa797
 
 
d871370
 
 
 
e8aa797
 
 
 
 
d871370
 
 
 
e8aa797
d871370
e8aa797
 
 
 
 
d871370
 
 
 
e8aa797
 
d871370
 
e8aa797
 
 
 
82b6f62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e8aa797
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d871370
e8aa797
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
/**
 * Pokemon-style Leveling and Stat Calculation Service
 * Implements accurate Pokemon stat formulas based on pokemon_stat_calculation.md
 */

import type { PicletInstance } from '$lib/db/schema';

// Pokemon nature effects: [boosted_stat, lowered_stat] or [null, null] for neutral
export const NATURES = {
  'Hardy': [null, null],           // Neutral
  'Lonely': ['attack', 'defense'],
  'Brave': ['attack', 'speed'],
  'Adamant': ['attack', 'sp_attack'],
  'Naughty': ['attack', 'sp_defense'],
  'Bold': ['defense', 'attack'],
  'Docile': [null, null],          // Neutral
  'Relaxed': ['defense', 'speed'],
  'Impish': ['defense', 'sp_attack'],
  'Lax': ['defense', 'sp_defense'],
  'Timid': ['speed', 'attack'],
  'Hasty': ['speed', 'defense'],
  'Serious': [null, null],         // Neutral
  'Jolly': ['speed', 'sp_attack'],
  'Naive': ['speed', 'sp_defense'],
  'Modest': ['sp_attack', 'attack'],
  'Mild': ['sp_attack', 'defense'],
  'Quiet': ['sp_attack', 'speed'],
  'Bashful': [null, null],         // Neutral
  'Rash': ['sp_attack', 'sp_defense'],
  'Calm': ['sp_defense', 'attack'],
  'Gentle': ['sp_defense', 'defense'],
  'Sassy': ['sp_defense', 'speed'],
  'Careful': ['sp_defense', 'sp_attack'],
  'Quirky': [null, null],          // Neutral
} as const;

export type NatureName = keyof typeof NATURES;

// Growth rate multipliers for different tiers
const TIER_XP_MULTIPLIERS = {
  'low': 0.8,        // 20% less XP required (faster leveling)
  'medium': 1.0,     // Base XP requirements
  'high': 1.4,       // 40% more XP required (slower leveling)
  'legendary': 1.8   // 80% more XP required (much slower leveling)
} as const;

type TierType = keyof typeof TIER_XP_MULTIPLIERS;

/**
 * Convert string tier to TierType, defaulting to 'medium' for unknown values
 */
function normalizeTier(tier: string): TierType {
  if (tier in TIER_XP_MULTIPLIERS) {
    return tier as TierType;
  }
  return 'medium'; // Default fallback
}

// Base experience requirements for Medium Fast growth rate (level³)
// Other tiers will use multipliers of this base
const BASE_XP_REQUIREMENTS: number[] = [];
for (let level = 1; level <= 100; level++) {
  BASE_XP_REQUIREMENTS[level] = level * level * level;
}

export interface LevelUpInfo {
  oldLevel: number;
  newLevel: number;
  statChanges: {
    hp: number;
    attack: number;
    defense: number;
    speed: number;
  };
}

export interface NatureModifiers {
  attack: number;
  defense: number;
  speed: number;
}

/**
 * Calculate HP using Pokemon's HP formula
 * Formula: floor((2 * base_hp * level) / 100) + level + 10
 */
export function calculateHp(baseHp: number, level: number): number {
  if (level === 1) {
    return Math.max(1, Math.floor(baseHp / 10) + 11); // Special case for level 1
  }
  
  return Math.floor((2 * baseHp * level) / 100) + level + 10;
}

/**
 * Calculate non-HP stat using Pokemon's standard formula
 * Formula: floor((floor((2 * base_stat * level) / 100) + 5) * nature_modifier)
 */
export function calculateStat(baseStat: number, level: number, natureModifier: number = 1.0): number {
  if (level === 1) {
    return Math.max(1, Math.floor(baseStat / 10) + 5); // Special case for level 1
  }
  
  const baseValue = Math.floor((2 * baseStat * level) / 100) + 5;
  return Math.floor(baseValue * natureModifier);
}

/**
 * Get nature modifiers for all stats
 */
export function getNatureModifiers(nature: string): NatureModifiers {
  const natureName = nature as NatureName;
  const [boosted, lowered] = NATURES[natureName] || NATURES['Hardy'];
  
  const modifiers: NatureModifiers = {
    attack: 1.0,
    defense: 1.0,  
    speed: 1.0,
  };
  
  if (boosted) {
    (modifiers as any)[boosted] = 1.1; // +10%
  }
  if (lowered) {
    (modifiers as any)[lowered] = 0.9; // -10%
  }
  
  return modifiers;
}

/**
 * Get XP required to reach a specific level
 */
/**
 * Get XP required for a specific level based on tier
 */
export function getXpForLevel(level: number, tier: string = 'medium'): number {
  if (level < 1 || level > 100) {
    throw new Error('Level must be between 1 and 100');
  }
  const normalizedTier = normalizeTier(tier);
  const baseXp = BASE_XP_REQUIREMENTS[level];
  const multiplier = TIER_XP_MULTIPLIERS[normalizedTier];
  return Math.floor(baseXp * multiplier);
}

/**
 * Get XP required for next level
 */
/**
 * Get XP required for next level based on tier
 */
export function getXpForNextLevel(currentLevel: number, tier: string = 'medium'): number {
  if (currentLevel >= 100) return 0; // Max level
  return getXpForLevel(currentLevel + 1, tier);
}

/**
 * Calculate XP progress percentage for current level
 */
/**
 * Get XP progress percentage towards next level based on tier
 */
export function getXpProgress(currentXp: number, currentLevel: number, tier: string = 'medium'): number {
  if (currentLevel >= 100) return 100;
  
  const currentLevelXp = getXpForLevel(currentLevel, tier);
  const nextLevelXp = getXpForLevel(currentLevel + 1, tier);
  const xpIntoLevel = currentXp - currentLevelXp;
  const xpNeededForLevel = nextLevelXp - currentLevelXp;
  
  return Math.min(100, Math.max(0, (xpIntoLevel / xpNeededForLevel) * 100));
}

/**
 * Get current XP towards next level in X/Y format
 */
export function getXpTowardsNextLevel(currentXp: number, currentLevel: number, tier: string = 'medium'): {
  current: number;
  needed: number;
  percentage: number;
} {
  if (currentLevel >= 100) {
    return { current: 0, needed: 0, percentage: 100 };
  }
  
  const currentLevelXp = getXpForLevel(currentLevel, tier);
  const nextLevelXp = getXpForLevel(currentLevel + 1, tier);
  const xpIntoLevel = Math.max(0, currentXp - currentLevelXp);
  const xpNeededForLevel = nextLevelXp - currentLevelXp;
  const percentage = Math.min(100, Math.max(0, (xpIntoLevel / xpNeededForLevel) * 100));
  
  return {
    current: xpIntoLevel,
    needed: xpNeededForLevel,
    percentage
  };
}

/**
 * Recalculate all stats for a Piclet based on current level and nature
 */
export function recalculatePicletStats(instance: PicletInstance): PicletInstance {
  const natureModifiers = getNatureModifiers(instance.nature);
  
  // Calculate new stats
  const newMaxHp = calculateHp(instance.baseHp, instance.level);
  const newAttack = calculateStat(instance.baseAttack, instance.level, natureModifiers.attack);
  const newDefense = calculateStat(instance.baseDefense, instance.level, natureModifiers.defense);
  const newSpeed = calculateStat(instance.baseSpeed, instance.level, natureModifiers.speed);
  
  // Field stats are 80% of main stats (existing logic)
  const newFieldAttack = Math.floor(newAttack * 0.8);
  const newFieldDefense = Math.floor(newDefense * 0.8);
  
  // Maintain current HP ratio when stats change
  const hpRatio = instance.maxHp > 0 ? instance.currentHp / instance.maxHp : 1;
  const newCurrentHp = Math.ceil(newMaxHp * hpRatio);
  
  return {
    ...instance,
    maxHp: newMaxHp,
    currentHp: newCurrentHp,
    attack: newAttack,
    defense: newDefense,
    speed: newSpeed,
    fieldAttack: newFieldAttack,
    fieldDefense: newFieldDefense
  };
}

/**
 * Process potential level up and return results
 */
export function processLevelUp(instance: PicletInstance): {
  newInstance: PicletInstance;
  levelUpInfo: LevelUpInfo | null;
} {
  const requiredXp = getXpForNextLevel(instance.level, instance.tier);
  
  // Check if level up is possible
  if (instance.level >= 100 || instance.xp < requiredXp) {
    return { newInstance: instance, levelUpInfo: null };
  }
  
  // Calculate old stats for comparison
  const oldStats = {
    hp: instance.maxHp,
    attack: instance.attack,
    defense: instance.defense,
    speed: instance.speed
  };
  
  // Level up the Piclet
  const leveledUpInstance = {
    ...instance,
    level: instance.level + 1
  };
  
  // Recalculate stats with new level
  const newInstance = recalculatePicletStats(leveledUpInstance);
  
  // Heal to full HP on level up (Pokemon tradition)
  const finalInstance = {
    ...newInstance,
    currentHp: newInstance.maxHp
  };
  
  // Calculate stat changes
  const statChanges = {
    hp: finalInstance.maxHp - oldStats.hp,
    attack: finalInstance.attack - oldStats.attack,
    defense: finalInstance.defense - oldStats.defense,
    speed: finalInstance.speed - oldStats.speed
  };
  
  const levelUpInfo: LevelUpInfo = {
    oldLevel: instance.level,
    newLevel: finalInstance.level,
    statChanges
  };
  
  return { newInstance: finalInstance, levelUpInfo };
}

/**
 * Calculate XP gained from defeating a Piclet in battle
 * Based on Pokemon formula: (baseExpYield * level) / 7
 */
export function calculateBattleXp(defeatedPiclet: PicletInstance, participantCount: number = 1): number {
  // Use BST as basis for exp yield (common Pokemon approach)
  const bst = defeatedPiclet.baseHp + defeatedPiclet.baseAttack + defeatedPiclet.baseDefense + 
             defeatedPiclet.baseSpeed + defeatedPiclet.baseFieldAttack + defeatedPiclet.baseFieldDefense;
  
  // Convert BST to exp yield (roughly BST/4, minimum 50)
  const baseExpYield = Math.max(50, Math.floor(bst / 4));
  
  // Pokemon formula
  const baseXp = Math.floor((baseExpYield * defeatedPiclet.level) / 7);
  
  // Divide among participants
  return Math.max(1, Math.floor(baseXp / participantCount));
}

/**
 * Check if a level up should occur and process it recursively
 * (Handles multiple level ups from large XP gains)
 */
export function processAllLevelUps(instance: PicletInstance): {
  newInstance: PicletInstance;
  levelUpInfo: LevelUpInfo[];
} {
  const levelUps: LevelUpInfo[] = [];
  let currentInstance = instance;
  
  // Process level ups until no more are possible
  while (currentInstance.level < 100) {
    const result = processLevelUp(currentInstance);
    
    if (result.levelUpInfo) {
      levelUps.push(result.levelUpInfo);
      currentInstance = result.newInstance;
    } else {
      break;
    }
  }
  
  return {
    newInstance: currentInstance,
    levelUpInfo: levelUps
  };
}