|
|
|
|
|
|
|
|
|
import type { BattleMove } from '$lib/db/schema'; |
|
import type { Move, BattleEffect, StatModification, MoveFlag, SpecialAbility, Trigger } from '$lib/battle-engine/types'; |
|
|
|
|
|
|
|
|
|
export function generateMoveDescription(move: BattleMove | Move): string { |
|
const parts: string[] = []; |
|
|
|
|
|
if (move.power > 0) { |
|
const powerDescription = getPowerDescription(move.power); |
|
parts.push(`A ${powerDescription} ${move.type} attack`); |
|
} else { |
|
|
|
parts.push(`A ${move.type} support move`); |
|
} |
|
|
|
|
|
if (move.accuracy < 100) { |
|
const accuracyDescription = getAccuracyDescription(move.accuracy); |
|
parts.push(`with ${accuracyDescription} accuracy`); |
|
} |
|
|
|
|
|
if (move.priority > 0) { |
|
parts.push(`that strikes with increased priority`); |
|
} else if (move.priority < 0) { |
|
parts.push(`that moves with reduced priority`); |
|
} |
|
|
|
|
|
if (move.flags.length > 0) { |
|
const flagDescriptions = move.flags.map(flag => getFlagDescription(flag)).filter(Boolean); |
|
if (flagDescriptions.length > 0) { |
|
parts.push(flagDescriptions.join(', ')); |
|
} |
|
} |
|
|
|
|
|
if ('effects' in move && move.effects && move.effects.length > 0) { |
|
const effectDescriptions = getEffectsDescription(move.effects); |
|
if (effectDescriptions.length > 0) { |
|
parts.push(effectDescriptions); |
|
} |
|
} |
|
|
|
|
|
let description = parts[0] || 'A mysterious move'; |
|
if (parts.length > 1) { |
|
const additionalParts = parts.slice(1); |
|
description += ' ' + additionalParts.join(' '); |
|
} |
|
|
|
return description + '.'; |
|
} |
|
|
|
|
|
|
|
|
|
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'; |
|
} |
|
|
|
|
|
|
|
|
|
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'; |
|
} |
|
|
|
|
|
|
|
|
|
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 ''; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
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]; |
|
} |
|
|
|
|
|
|
|
|
|
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 ''; |
|
} |
|
|
|
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`; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
export function generateAbilityDescription(ability: SpecialAbility): string { |
|
const parts: string[] = []; |
|
|
|
|
|
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); |
|
} |
|
} |
|
|
|
|
|
if (ability.effects && ability.effects.length > 0) { |
|
const effectDescriptions = getEffectsDescription(ability.effects); |
|
if (effectDescriptions) { |
|
parts.push(effectDescriptions); |
|
} |
|
} |
|
|
|
|
|
if (parts.length === 0) { |
|
return `A mysterious ability called "${ability.name}".`; |
|
} |
|
|
|
|
|
if (parts.length === 1) { |
|
return parts[0] + '.'; |
|
} |
|
|
|
return parts.join(' ') + '.'; |
|
} |
|
|
|
|
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
|
|
|
|
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()}`; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
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: |
|
|
|
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; |
|
} |
|
} |