piclets / src /lib /utils /moveDescriptions.ts
Fraser's picture
describe abilities
38d460f
/**
* 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;
}
}