/** * 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 }; }