/** * Utility for generating descriptive text for battle moves based on their properties */ import type { BattleMove } from '$lib/db/schema'; import type { Move, BattleEffect, StatModification, MoveFlag, SpecialAbility, Trigger } from '$lib/battle-engine/types'; /** * Generate a natural language description for a move based on its properties */ export function generateMoveDescription(move: BattleMove | Move): string { const parts: string[] = []; // Start with power assessment if (move.power > 0) { const powerDescription = getPowerDescription(move.power); parts.push(`A ${powerDescription} ${move.type} attack`); } else { // Non-damaging move parts.push(`A ${move.type} support move`); } // Add accuracy information if not perfect if (move.accuracy < 100) { const accuracyDescription = getAccuracyDescription(move.accuracy); parts.push(`with ${accuracyDescription} accuracy`); } // Add priority information if (move.priority > 0) { parts.push(`that strikes with increased priority`); } else if (move.priority < 0) { parts.push(`that moves with reduced priority`); } // Add flag-based descriptions if (move.flags.length > 0) { const flagDescriptions = move.flags.map(flag => getFlagDescription(flag)).filter(Boolean); if (flagDescriptions.length > 0) { parts.push(flagDescriptions.join(', ')); } } // Add effects description if ('effects' in move && move.effects && move.effects.length > 0) { const effectDescriptions = getEffectsDescription(move.effects); if (effectDescriptions.length > 0) { parts.push(effectDescriptions); } } // Join all parts together let description = parts[0] || 'A mysterious move'; if (parts.length > 1) { const additionalParts = parts.slice(1); description += ' ' + additionalParts.join(' '); } return description + '.'; } /** * Convert power value to descriptive text */ function getPowerDescription(power: number): string { if (power <= 40) return 'weak'; if (power <= 60) return 'modest'; if (power <= 80) return 'strong'; if (power <= 100) return 'powerful'; if (power <= 120) return 'devastating'; return 'overwhelming'; } /** * Convert accuracy to descriptive text */ function getAccuracyDescription(accuracy: number): string { if (accuracy >= 95) return 'near-perfect'; if (accuracy >= 85) return 'reliable'; if (accuracy >= 75) return 'moderate'; if (accuracy >= 65) return 'shaky'; return 'unreliable'; } /** * Convert move flag to descriptive text */ function getFlagDescription(flag: MoveFlag): string { switch (flag) { case 'contact': return 'requiring physical contact'; case 'bite': return 'using a biting attack'; case 'punch': return 'delivered as a punch'; case 'sound': return 'using sound waves'; case 'explosive': return 'with explosive force'; case 'draining': return 'that drains energy'; case 'ground': return 'affecting grounded targets'; case 'priority': return 'with quick execution'; case 'lowPriority': return 'with delayed execution'; case 'charging': return 'requiring preparation'; case 'recharge': return 'causing exhaustion afterward'; case 'multiHit': return 'striking multiple times'; case 'twoTurn': return 'taking two turns to complete'; case 'sacrifice': return 'at great cost to the user'; case 'gambling': return 'with unpredictable results'; case 'reckless': return 'with reckless abandon'; case 'reflectable': return 'that can be reflected'; case 'snatchable': return 'that can be stolen'; case 'copyable': return 'that can be copied'; case 'protectable': return 'blocked by protection moves'; case 'bypassProtect': return 'that bypasses protection'; default: return ''; } } /** * Generate description for move effects */ function getEffectsDescription(effects: BattleEffect[]): string { const descriptions: string[] = []; for (const effect of effects) { const desc = getEffectDescription(effect); if (desc) descriptions.push(desc); } if (descriptions.length === 0) return ''; if (descriptions.length === 1) return descriptions[0]; if (descriptions.length === 2) return descriptions.join(' and '); return descriptions.slice(0, -1).join(', ') + ', and ' + descriptions[descriptions.length - 1]; } /** * Generate description for a single effect */ function getEffectDescription(effect: BattleEffect): string { switch (effect.type) { case 'damage': return getDamageEffectDescription(effect); case 'modifyStats': return getStatModificationDescription(effect); case 'applyStatus': return getStatusEffectDescription(effect); case 'heal': return getHealEffectDescription(effect); case 'manipulatePP': return getPPEffectDescription(effect); case 'fieldEffect': return 'affects the battlefield'; case 'counter': return 'retaliates against attacks'; case 'removeStatus': return 'removes status conditions'; case 'mechanicOverride': return 'alters battle mechanics'; default: return ''; } } function getDamageEffectDescription(effect: any): string { if (effect.formula === 'recoil') return 'causes recoil damage to the user'; if (effect.formula === 'drain') return 'restores HP equal to damage dealt'; if (effect.formula === 'fixed') return 'deals fixed damage'; if (effect.formula === 'percentage') return 'deals percentage-based damage'; if (effect.target === 'self') return 'damages the user'; return ''; // Standard damage is implied by power } function getStatModificationDescription(effect: any): string { const target = effect.target === 'self' ? 'the user\'s' : 'the target\'s'; const stats = Object.keys(effect.stats); const modifications = Object.values(effect.stats) as StatModification[]; if (stats.length === 1) { const statName = stats[0]; const modification = modifications[0]; const modDesc = getStatModificationText(modification); return `${modDesc} ${target} ${statName}`; } return `modifies ${target} battle stats`; } function getStatModificationText(modification: StatModification): string { switch (modification) { case 'increase': return 'raises'; case 'greatly_increase': return 'sharply raises'; case 'decrease': return 'lowers'; case 'greatly_decrease': return 'sharply lowers'; default: return 'affects'; } } function getStatusEffectDescription(effect: any): string { const target = effect.target === 'self' ? 'the user' : 'the target'; const chance = effect.chance ? ` (${effect.chance}% chance)` : ''; switch (effect.status) { case 'burn': return `may burn ${target}${chance}`; case 'freeze': return `may freeze ${target}${chance}`; case 'paralyze': return `may paralyze ${target}${chance}`; case 'poison': return `may poison ${target}${chance}`; case 'sleep': return `may put ${target} to sleep${chance}`; case 'confuse': return `may confuse ${target}${chance}`; default: return `may inflict ${effect.status} on ${target}${chance}`; } } function getHealEffectDescription(effect: any): string { const target = effect.target === 'self' ? 'the user' : 'the target'; if (effect.amount === 'full') return `fully restores ${target}'s HP`; if (effect.amount === 'large') return `greatly restores ${target}'s HP`; if (effect.amount === 'medium') return `moderately restores ${target}'s HP`; if (effect.amount === 'small') return `slightly restores ${target}'s HP`; return `restores ${target}'s HP`; } function getPPEffectDescription(effect: any): string { const target = effect.target === 'self' ? 'the user\'s' : 'the target\'s'; switch (effect.action) { case 'drain': return `reduces ${target} move PP`; case 'restore': return `restores ${target} move PP`; case 'disable': return `disables ${target} moves`; default: return `affects ${target} move usage`; } } /** * Generate a natural language description for a special ability based on its properties */ export function generateAbilityDescription(ability: SpecialAbility): string { const parts: string[] = []; // Handle abilities with triggers (most common case) if (ability.triggers && ability.triggers.length > 0) { const triggerDescriptions = ability.triggers.map(trigger => getTriggerDescription(trigger)); const validTriggers = triggerDescriptions.filter(Boolean); if (validTriggers.length > 0) { parts.push(...validTriggers); } } // Handle abilities with direct effects (less common) if (ability.effects && ability.effects.length > 0) { const effectDescriptions = getEffectsDescription(ability.effects); if (effectDescriptions) { parts.push(effectDescriptions); } } // If no triggers or effects, provide a generic description if (parts.length === 0) { return `A mysterious ability called "${ability.name}".`; } // Combine all parts into a cohesive description if (parts.length === 1) { return parts[0] + '.'; } return parts.join(' ') + '.'; } /** * Convert trigger event and effects into descriptive text */ function getTriggerDescription(trigger: Trigger): string { const eventText = getTriggerEventText(trigger.event); const effectsText = getEffectsDescription(trigger.effects); const conditionText = trigger.condition ? getConditionText(trigger.condition) : ''; let description = eventText; if (conditionText) { description += ` ${conditionText}`; } if (effectsText) { description += `, ${effectsText}`; } return description; } /** * Convert trigger event to human-readable text */ function getTriggerEventText(event: string): string { switch (event) { case 'onDamageTaken': return 'When taking damage'; case 'onDamageDealt': return 'When dealing damage'; case 'onContactDamage': return 'When hit by contact moves'; case 'onStatusInflicted': return 'When afflicted with a status condition'; case 'onStatusMove': return 'When using status moves'; case 'onStatusMoveTargeted': return 'When targeted by status moves'; case 'onCriticalHit': return 'When landing critical hits'; case 'onHPDrained': return 'When HP is drained'; case 'onKO': return 'When knocked out'; case 'onSwitchIn': return 'When switching in'; case 'onSwitchOut': return 'When switching out'; case 'onWeatherChange': return 'When weather changes'; case 'beforeMoveUse': return 'Before using moves'; case 'afterMoveUse': return 'After using moves'; case 'onLowHP': return 'When HP is low'; case 'onFullHP': return 'When at full HP'; case 'endOfTurn': return 'At the end of each turn'; case 'onOpponentContactMove': return 'When the opponent uses contact moves'; case 'onStatChange': return 'When stats are changed'; case 'onTypeChange': return 'When type is changed'; default: return `When ${event.replace(/^on/, '').toLowerCase()}`; } } /** * Convert effect condition to descriptive text */ function getConditionText(condition: string): string { switch (condition) { case 'always': return ''; case 'onHit': return 'on hit'; case 'afterUse': return 'after use'; case 'onCritical': return 'on critical hits'; case 'ifLowHp': return 'when HP is low'; case 'ifHighHp': return 'when HP is high'; case 'thisTurn': return 'this turn'; case 'nextTurn': return 'next turn'; case 'turnAfterNext': return 'the turn after next'; case 'restOfBattle': return 'for the rest of battle'; case 'onCharging': return 'while charging'; case 'afterCharging': return 'after charging'; case 'ifDamagedThisTurn': return 'if damaged this turn'; case 'ifNotSuperEffective': return 'against non-super-effective moves'; case 'ifStatusMove': return 'with status moves'; case 'ifLucky50': return 'with 50% luck'; case 'ifUnlucky50': return 'with 50% bad luck'; case 'whileFrozen': return 'while frozen'; case 'whenStatusAfflicted': return 'when status is inflicted'; case 'vsPhysical': return 'against physical moves'; case 'vsSpecial': return 'against special moves'; default: // Handle type-specific conditions if (condition.startsWith('ifMoveType:')) { const type = condition.split(':')[1]; return `with ${type} moves`; } if (condition.startsWith('ifStatus:')) { const status = condition.split(':')[1]; return `when ${status}`; } if (condition.startsWith('ifWeather:')) { const weather = condition.split(':')[1]; return `in ${weather} weather`; } return condition; } }