|
|
|
|
|
|
|
|
|
|
|
import type { |
|
BattleState, |
|
BattlePiclet, |
|
PicletDefinition, |
|
BattleAction, |
|
MoveAction, |
|
BattleEffect, |
|
DamageAmount, |
|
StatModification, |
|
HealAmount, |
|
StatusEffect, |
|
BaseStats, |
|
Move |
|
} from './types'; |
|
import { getEffectivenessMultiplier } from '../types/picletTypes'; |
|
|
|
export class BattleEngine { |
|
private state: BattleState; |
|
|
|
constructor(playerPiclet: PicletDefinition, opponentPiclet: PicletDefinition, playerLevel = 50, opponentLevel = 50) { |
|
this.state = { |
|
turn: 1, |
|
phase: 'selection', |
|
playerPiclet: this.createBattlePiclet(playerPiclet, playerLevel), |
|
opponentPiclet: this.createBattlePiclet(opponentPiclet, opponentLevel), |
|
fieldEffects: [], |
|
log: [], |
|
winner: undefined |
|
}; |
|
|
|
this.log('Battle started!'); |
|
this.log(`${playerPiclet.name} vs ${opponentPiclet.name}`); |
|
} |
|
|
|
private createBattlePiclet(definition: PicletDefinition, level: number): BattlePiclet { |
|
|
|
const statMultiplier = 1 + (level - 50) * 0.02; |
|
|
|
const hp = Math.floor(definition.baseStats.hp * statMultiplier); |
|
const attack = Math.floor(definition.baseStats.attack * statMultiplier); |
|
const defense = Math.floor(definition.baseStats.defense * statMultiplier); |
|
const speed = Math.floor(definition.baseStats.speed * statMultiplier); |
|
|
|
const piclet: BattlePiclet = { |
|
definition, |
|
currentHp: hp, |
|
maxHp: hp, |
|
level, |
|
attack, |
|
defense, |
|
speed, |
|
accuracy: 100, |
|
statusEffects: [], |
|
moves: definition.movepool.slice(0, 4).map(move => ({ |
|
move, |
|
currentPP: move.pp |
|
})), |
|
statModifiers: {}, |
|
temporaryEffects: [] |
|
}; |
|
|
|
|
|
if (definition.specialAbility?.effects) { |
|
for (const effect of definition.specialAbility.effects) { |
|
this.applyEffectToPiclet(effect, piclet); |
|
} |
|
} |
|
|
|
return piclet; |
|
} |
|
|
|
public getState(): BattleState { |
|
return JSON.parse(JSON.stringify(this.state)); |
|
} |
|
|
|
public isGameOver(): boolean { |
|
return this.state.phase === 'ended'; |
|
} |
|
|
|
public getWinner(): 'player' | 'opponent' | 'draw' | undefined { |
|
return this.state.winner; |
|
} |
|
|
|
public executeActions(playerAction: BattleAction, opponentAction: BattleAction): void { |
|
if (this.state.phase !== 'selection') { |
|
throw new Error('Cannot execute actions - battle is not in selection phase'); |
|
} |
|
|
|
this.state.phase = 'execution'; |
|
this.log(`Turn ${this.state.turn} - Actions: ${playerAction.type} vs ${opponentAction.type}`); |
|
|
|
|
|
const actions = this.determineActionOrder(playerAction, opponentAction); |
|
|
|
|
|
for (const action of actions) { |
|
if ((this.state.phase as string) === 'ended') break; |
|
this.executeAction(action); |
|
|
|
|
|
this.checkBattleEnd(); |
|
if ((this.state.phase as string) === 'ended') break; |
|
} |
|
|
|
|
|
if ((this.state.phase as string) !== 'ended') { |
|
this.processTurnEnd(); |
|
} |
|
|
|
|
|
this.checkBattleEnd(); |
|
|
|
if ((this.state.phase as string) !== 'ended') { |
|
this.state.turn++; |
|
this.state.phase = 'selection'; |
|
} |
|
} |
|
|
|
private determineActionOrder(playerAction: BattleAction, opponentAction: BattleAction): Array<BattleAction & { executor: 'player' | 'opponent' }> { |
|
const playerPriority = this.getActionPriority(playerAction, this.state.playerPiclet); |
|
const opponentPriority = this.getActionPriority(opponentAction, this.state.opponentPiclet); |
|
|
|
const playerSpeed = this.state.playerPiclet.speed; |
|
const opponentSpeed = this.state.opponentPiclet.speed; |
|
|
|
|
|
let playerFirst = false; |
|
if (playerPriority > opponentPriority) { |
|
playerFirst = true; |
|
} else if (playerPriority < opponentPriority) { |
|
playerFirst = false; |
|
} else if (playerSpeed > opponentSpeed) { |
|
playerFirst = true; |
|
} else if (playerSpeed < opponentSpeed) { |
|
playerFirst = false; |
|
} else { |
|
playerFirst = Math.random() < 0.5; |
|
} |
|
|
|
return playerFirst |
|
? [ |
|
{ ...playerAction, executor: 'player' as const }, |
|
{ ...opponentAction, executor: 'opponent' as const } |
|
] |
|
: [ |
|
{ ...opponentAction, executor: 'opponent' as const }, |
|
{ ...playerAction, executor: 'player' as const } |
|
]; |
|
} |
|
|
|
private getActionPriority(action: BattleAction, piclet: BattlePiclet): number { |
|
let priority = 0; |
|
|
|
if (action.type === 'move') { |
|
const move = piclet.moves[action.moveIndex]?.move; |
|
priority = move?.priority || 0; |
|
|
|
|
|
if (move?.effects) { |
|
for (const effect of move.effects) { |
|
if (effect.type === 'priority' && (!effect.condition || this.checkCondition(effect.condition, piclet, piclet))) { |
|
priority += (effect as any).value || 0; |
|
} |
|
} |
|
} |
|
|
|
|
|
const priorityMod = piclet.statModifiers.priority || 0; |
|
priority += priorityMod; |
|
} else { |
|
priority = 6; |
|
} |
|
|
|
return priority; |
|
} |
|
|
|
private executeAction(action: BattleAction & { executor: 'player' | 'opponent' }): void { |
|
if (action.type === 'move') { |
|
this.executeMove(action); |
|
} else if (action.type === 'switch') { |
|
this.log(`${action.executor} attempted to switch (not implemented)`); |
|
} |
|
} |
|
|
|
private executeMove(action: MoveAction & { executor: 'player' | 'opponent' }): void { |
|
const attacker = action.executor === 'player' ? this.state.playerPiclet : this.state.opponentPiclet; |
|
const defender = action.executor === 'player' ? this.state.opponentPiclet : this.state.playerPiclet; |
|
|
|
const moveData = attacker.moves[action.moveIndex]; |
|
if (!moveData || moveData.currentPP <= 0) { |
|
this.log(`${attacker.definition.name} has no PP left for that move!`); |
|
return; |
|
} |
|
|
|
const move = moveData.move; |
|
this.log(`${attacker.definition.name} used ${move.name}!`); |
|
|
|
|
|
moveData.currentPP--; |
|
|
|
|
|
if (!this.checkMoveHits(move, attacker, defender)) { |
|
this.log(`${attacker.definition.name}'s attack missed!`); |
|
return; |
|
} |
|
|
|
|
|
const luckyRoll = Math.random() < 0.5; |
|
|
|
|
|
for (const effect of move.effects) { |
|
this.processEffect(effect, attacker, defender, move, luckyRoll); |
|
} |
|
} |
|
|
|
private checkMoveHits(move: Move, _attacker: BattlePiclet, _defender: BattlePiclet): boolean { |
|
|
|
const accuracy = move.accuracy; |
|
const roll = Math.random() * 100; |
|
return roll < accuracy; |
|
} |
|
|
|
private processEffect(effect: BattleEffect, attacker: BattlePiclet, defender: BattlePiclet, move: Move, luckyRoll?: boolean): void { |
|
|
|
if (effect.condition && !this.checkCondition(effect.condition, attacker, defender, luckyRoll)) { |
|
return; |
|
} |
|
|
|
switch (effect.type) { |
|
case 'damage': |
|
if (effect.target === 'all') { |
|
|
|
this.processDamageEffect(effect, attacker, attacker, move); |
|
this.processDamageEffect(effect, attacker, defender, move); |
|
} else { |
|
const damageTarget = this.resolveTarget(effect.target, attacker, defender); |
|
if (damageTarget) this.processDamageEffect(effect, attacker, damageTarget, move); |
|
} |
|
break; |
|
case 'modifyStats': |
|
const statsTarget = this.resolveTarget(effect.target, attacker, defender); |
|
if (statsTarget) this.processModifyStatsEffect(effect, statsTarget); |
|
break; |
|
case 'applyStatus': |
|
const statusTarget = this.resolveTarget(effect.target, attacker, defender); |
|
if (statusTarget) this.processApplyStatusEffect(effect, statusTarget); |
|
break; |
|
case 'heal': |
|
const healTarget = this.resolveTarget(effect.target, attacker, defender); |
|
if (healTarget) this.processHealEffect(effect, healTarget); |
|
break; |
|
case 'manipulatePP': |
|
const ppTarget = this.resolveTarget(effect.target, attacker, defender); |
|
if (ppTarget) this.processManipulatePPEffect(effect, ppTarget); |
|
break; |
|
case 'fieldEffect': |
|
this.processFieldEffect(effect); |
|
break; |
|
case 'counter': |
|
this.processCounterEffect(effect, attacker, defender); |
|
break; |
|
case 'priority': |
|
const priorityTarget = this.resolveTarget(effect.target, attacker, defender); |
|
if (priorityTarget) this.processPriorityEffect(effect, priorityTarget); |
|
break; |
|
case 'removeStatus': |
|
const removeStatusTarget = this.resolveTarget(effect.target, attacker, defender); |
|
if (removeStatusTarget) this.processRemoveStatusEffect(effect, removeStatusTarget); |
|
break; |
|
case 'mechanicOverride': |
|
|
|
this.processMechanicOverrideEffect(effect, attacker); |
|
break; |
|
default: |
|
this.log(`Effect ${(effect as any).type} not implemented yet`); |
|
} |
|
} |
|
|
|
private checkCondition(condition: string, attacker: BattlePiclet, _defender: BattlePiclet, luckyRoll?: boolean): boolean { |
|
switch (condition) { |
|
case 'always': |
|
return true; |
|
case 'ifLowHp': |
|
return attacker.currentHp / attacker.maxHp < 0.25; |
|
case 'ifHighHp': |
|
return attacker.currentHp / attacker.maxHp > 0.75; |
|
case 'ifLucky50': |
|
return luckyRoll !== undefined ? luckyRoll : Math.random() < 0.5; |
|
case 'ifUnlucky50': |
|
return luckyRoll !== undefined ? !luckyRoll : Math.random() >= 0.5; |
|
case 'whileFrozen': |
|
return attacker.statusEffects.includes('freeze'); |
|
|
|
case 'ifMoveType:flora': |
|
case 'ifMoveType:space': |
|
case 'ifMoveType:beast': |
|
case 'ifMoveType:bug': |
|
case 'ifMoveType:aquatic': |
|
case 'ifMoveType:mineral': |
|
case 'ifMoveType:machina': |
|
case 'ifMoveType:structure': |
|
case 'ifMoveType:culture': |
|
case 'ifMoveType:cuisine': |
|
case 'ifMoveType:normal': |
|
|
|
return true; |
|
|
|
case 'ifStatus:burn': |
|
return attacker.statusEffects.includes('burn'); |
|
case 'ifStatus:freeze': |
|
return attacker.statusEffects.includes('freeze'); |
|
case 'ifStatus:paralyze': |
|
return attacker.statusEffects.includes('paralyze'); |
|
case 'ifStatus:poison': |
|
return attacker.statusEffects.includes('poison'); |
|
case 'ifStatus:sleep': |
|
return attacker.statusEffects.includes('sleep'); |
|
case 'ifStatus:confuse': |
|
return attacker.statusEffects.includes('confuse'); |
|
|
|
case 'ifWeather:storm': |
|
case 'ifWeather:rain': |
|
case 'ifWeather:sun': |
|
case 'ifWeather:snow': |
|
return false; |
|
|
|
case 'ifDamagedThisTurn': |
|
|
|
|
|
|
|
return attacker.currentHp < attacker.maxHp; |
|
case 'ifNotSuperEffective': |
|
|
|
return false; |
|
case 'ifStatusMove': |
|
|
|
return false; |
|
case 'afterUse': |
|
|
|
return true; |
|
default: |
|
return true; |
|
} |
|
} |
|
|
|
private resolveTarget(target: string, attacker: BattlePiclet, defender: BattlePiclet): BattlePiclet | null { |
|
switch (target) { |
|
case 'self': |
|
return attacker; |
|
case 'opponent': |
|
return defender; |
|
default: |
|
return null; |
|
} |
|
} |
|
|
|
private processDamageEffect(effect: { amount?: DamageAmount; formula?: string; value?: number; multiplier?: number }, attacker: BattlePiclet, target: BattlePiclet, move: Move): void { |
|
let damage = 0; |
|
|
|
|
|
if (this.checkTypeImmunity(target, move.type)) { |
|
this.log(`${target.definition.name} is immune to ${move.type} type moves!`); |
|
return; |
|
} |
|
|
|
|
|
if (this.checkFlagBasedTypeImmunity(target, move.flags)) { |
|
this.log(`${target.definition.name} had no effect!`); |
|
return; |
|
} |
|
|
|
|
|
const flagInteraction = this.checkFlagInteraction(target, move.flags); |
|
if (flagInteraction === 'immune') { |
|
this.log(`It had no effect on ${target.definition.name}!`); |
|
return; |
|
} |
|
|
|
|
|
if (effect.formula) { |
|
damage = this.calculateDamageByFormula(effect, attacker, target, move); |
|
} else if (effect.amount) { |
|
damage = this.calculateStandardDamage(effect.amount, attacker, target, move); |
|
} |
|
|
|
|
|
if (flagInteraction === 'weak') { |
|
damage = Math.floor(damage * 1.5); |
|
this.log("It's super effective!"); |
|
} else if (flagInteraction === 'resist') { |
|
damage = Math.floor(damage * 0.5); |
|
this.log("It's not very effective..."); |
|
} |
|
|
|
|
|
const damageMultiplier = this.getDamageMultiplier(attacker); |
|
damage = Math.floor(damage * damageMultiplier); |
|
|
|
|
|
const critMod = this.checkCriticalHitModification(attacker, target); |
|
if (critMod === 'always' || (critMod === 'normal' && Math.random() < 0.0625)) { |
|
damage = Math.floor(damage * 1.5); |
|
this.log("A critical hit!"); |
|
} |
|
|
|
|
|
if (damage > 0) { |
|
target.currentHp = Math.max(0, target.currentHp - damage); |
|
this.log(`${target.definition.name} took ${damage} damage!`); |
|
|
|
|
|
this.checkCounterEffects(target, attacker, move); |
|
} |
|
|
|
|
|
if (effect.formula === 'drain') { |
|
const healAmount = Math.floor(damage * (effect.value || 0.5)); |
|
attacker.currentHp = Math.min(attacker.maxHp, attacker.currentHp + healAmount); |
|
if (healAmount > 0) { |
|
this.log(`${attacker.definition.name} recovered ${healAmount} HP from draining!`); |
|
} |
|
} else if (effect.formula === 'recoil') { |
|
const recoilDamage = Math.floor(damage * (effect.value || 0.25)); |
|
attacker.currentHp = Math.max(0, attacker.currentHp - recoilDamage); |
|
if (recoilDamage > 0) { |
|
this.log(`${attacker.definition.name} took ${recoilDamage} recoil damage!`); |
|
} |
|
} |
|
} |
|
|
|
private calculateDamageByFormula(effect: { formula?: string; value?: number; multiplier?: number }, attacker: BattlePiclet, target: BattlePiclet, move: Move): number { |
|
switch (effect.formula) { |
|
case 'fixed': |
|
return effect.value || 0; |
|
|
|
case 'percentage': |
|
return Math.floor(target.maxHp * ((effect.value || 0) / 100)); |
|
|
|
case 'recoil': |
|
case 'drain': |
|
case 'standard': |
|
|
|
return this.calculateStandardDamageWithPower(move.power, attacker, target, move) * (effect.multiplier || 1); |
|
|
|
default: |
|
return 0; |
|
} |
|
} |
|
|
|
private calculateStandardDamageWithPower(power: number, attacker: BattlePiclet, target: BattlePiclet, move: Move): number { |
|
const baseDamage = power; |
|
|
|
|
|
const effectiveness = getEffectivenessMultiplier( |
|
move.type, |
|
target.definition.primaryType, |
|
target.definition.secondaryType |
|
); |
|
|
|
|
|
const stab = (move.type.toString() === attacker.definition.primaryType?.toString() || |
|
move.type.toString() === attacker.definition.secondaryType?.toString()) ? 1.5 : 1; |
|
|
|
|
|
const attackStat = attacker.attack; |
|
const defenseStat = target.defense; |
|
|
|
let damage = Math.floor((baseDamage * (attackStat / defenseStat) * 0.5) + 10); |
|
damage = Math.floor(damage * effectiveness * stab); |
|
|
|
|
|
damage = Math.floor(damage * (0.85 + Math.random() * 0.15)); |
|
|
|
|
|
if (effectiveness > 0 && damage < 1) { |
|
damage = 1; |
|
} |
|
|
|
|
|
if (effectiveness === 0) { |
|
this.log("It had no effect!"); |
|
} else if (effectiveness > 1) { |
|
this.log("It's super effective!"); |
|
} else if (effectiveness < 1) { |
|
this.log("It's not very effective..."); |
|
} |
|
|
|
return damage; |
|
} |
|
|
|
private calculateStandardDamage(amount: DamageAmount, attacker: BattlePiclet, target: BattlePiclet, move: Move): number { |
|
const baseDamage = this.getDamageAmount(amount); |
|
|
|
|
|
const effectiveness = getEffectivenessMultiplier( |
|
move.type, |
|
target.definition.primaryType, |
|
target.definition.secondaryType |
|
); |
|
|
|
|
|
const stab = (move.type.toString() === attacker.definition.primaryType?.toString() || |
|
move.type.toString() === attacker.definition.secondaryType?.toString()) ? 1.5 : 1; |
|
|
|
|
|
const attackStat = attacker.attack; |
|
const defenseStat = target.defense; |
|
|
|
let damage = Math.floor((baseDamage * (attackStat / defenseStat) * 0.5) + 10); |
|
damage = Math.floor(damage * effectiveness * stab); |
|
|
|
|
|
damage = Math.floor(damage * (0.85 + Math.random() * 0.15)); |
|
|
|
|
|
if (effectiveness > 0 && damage < 1) { |
|
damage = 1; |
|
} |
|
|
|
|
|
if (effectiveness === 0) { |
|
this.log("It had no effect!"); |
|
} else if (effectiveness > 1) { |
|
this.log("It's super effective!"); |
|
} else if (effectiveness < 1) { |
|
this.log("It's not very effective..."); |
|
} |
|
|
|
return damage; |
|
} |
|
|
|
private processModifyStatsEffect(effect: { stats: Partial<Record<keyof BaseStats | 'accuracy', StatModification>> }, target: BattlePiclet): void { |
|
for (const [stat, modification] of Object.entries(effect.stats)) { |
|
const multiplier = this.getStatModifier(modification); |
|
if (stat === 'accuracy') { |
|
target.accuracy = Math.floor(target.accuracy * multiplier); |
|
} else { |
|
const statKey = stat as keyof BaseStats; |
|
(target as any)[statKey] = Math.floor((target as any)[statKey] * multiplier); |
|
} |
|
|
|
this.log(`${target.definition.name}'s ${stat} ${modification.includes('increase') ? 'rose' : 'fell'}!`); |
|
} |
|
} |
|
|
|
private processApplyStatusEffect(effect: { status: StatusEffect; chance?: number }, target: BattlePiclet): void { |
|
|
|
if (effect.chance !== undefined) { |
|
const roll = Math.random() * 100; |
|
if (roll >= effect.chance) { |
|
return; |
|
} |
|
} |
|
|
|
|
|
if (this.checkStatusImmunity(target, effect.status)) { |
|
this.log(`${target.definition.name} is immune to ${effect.status}!`); |
|
return; |
|
} |
|
|
|
if (!target.statusEffects.includes(effect.status)) { |
|
target.statusEffects.push(effect.status); |
|
this.log(`${target.definition.name} was ${effect.status}ed!`); |
|
} |
|
} |
|
|
|
private processHealEffect(effect: { amount?: HealAmount; formula?: string; value?: number }, target: BattlePiclet): void { |
|
let healAmount = 0; |
|
|
|
if (effect.formula) { |
|
switch (effect.formula) { |
|
case 'percentage': |
|
healAmount = Math.floor(target.maxHp * ((effect.value || 0) / 100)); |
|
break; |
|
case 'fixed': |
|
healAmount = effect.value || 0; |
|
break; |
|
default: |
|
healAmount = this.getHealAmount(effect.amount || 'medium', target.maxHp); |
|
} |
|
} else if (effect.amount) { |
|
healAmount = this.getHealAmount(effect.amount, target.maxHp); |
|
} |
|
|
|
|
|
if (this.shouldInvertHealing(target)) { |
|
|
|
const oldHp = target.currentHp; |
|
target.currentHp = Math.max(0, target.currentHp - healAmount); |
|
const actualDamage = oldHp - target.currentHp; |
|
|
|
if (actualDamage > 0) { |
|
this.log(`${target.definition.name} took ${actualDamage} damage from inverted healing!`); |
|
} |
|
} else { |
|
|
|
const oldHp = target.currentHp; |
|
target.currentHp = Math.min(target.maxHp, target.currentHp + healAmount); |
|
const actualHeal = target.currentHp - oldHp; |
|
|
|
if (actualHeal > 0) { |
|
this.log(`${target.definition.name} recovered ${actualHeal} HP!`); |
|
} |
|
} |
|
} |
|
|
|
private getDamageAmount(amount: DamageAmount): number { |
|
switch (amount) { |
|
case 'weak': return 40; |
|
case 'normal': return 70; |
|
case 'strong': return 100; |
|
case 'extreme': return 140; |
|
default: return 70; |
|
} |
|
} |
|
|
|
private getStatModifier(modification: StatModification): number { |
|
switch (modification) { |
|
case 'increase': return 1.25; |
|
case 'decrease': return 0.75; |
|
case 'greatly_increase': return 1.5; |
|
case 'greatly_decrease': return 0.5; |
|
default: return 1.0; |
|
} |
|
} |
|
|
|
private getHealAmount(amount: HealAmount, maxHp: number): number { |
|
switch (amount) { |
|
case 'small': return Math.floor(maxHp * 0.25); |
|
case 'medium': return Math.floor(maxHp * 0.5); |
|
case 'large': return Math.floor(maxHp * 0.75); |
|
case 'full': return maxHp; |
|
default: return Math.floor(maxHp * 0.5); |
|
} |
|
} |
|
|
|
private processTurnEnd(): void { |
|
|
|
this.processStatusEffects(this.state.playerPiclet); |
|
this.processStatusEffects(this.state.opponentPiclet); |
|
|
|
|
|
this.processFieldEffects(); |
|
|
|
|
|
this.processTemporaryEffects(this.state.playerPiclet); |
|
this.processTemporaryEffects(this.state.opponentPiclet); |
|
} |
|
|
|
private processStatusEffects(piclet: BattlePiclet): void { |
|
for (const status of piclet.statusEffects) { |
|
switch (status) { |
|
case 'burn': |
|
case 'poison': |
|
const damage = Math.floor(piclet.maxHp / 8); |
|
piclet.currentHp = Math.max(0, piclet.currentHp - damage); |
|
this.log(`${piclet.definition.name} was hurt by ${status}!`); |
|
break; |
|
|
|
} |
|
} |
|
} |
|
|
|
private processTemporaryEffects(piclet: BattlePiclet): void { |
|
|
|
piclet.temporaryEffects = piclet.temporaryEffects.filter(effect => { |
|
effect.duration--; |
|
return effect.duration > 0; |
|
}); |
|
} |
|
|
|
private processFieldEffects(): void { |
|
|
|
for (const fieldEffect of this.state.fieldEffects) { |
|
switch (fieldEffect.name) { |
|
case 'spikes': |
|
|
|
this.log('Spikes are scattered on the battlefield!'); |
|
break; |
|
case 'stealth_rock': |
|
|
|
this.log('Pointed stones float in the air!'); |
|
break; |
|
case 'reflect': |
|
|
|
this.log('A barrier reflects physical attacks!'); |
|
break; |
|
case 'light_screen': |
|
|
|
this.log('A barrier weakens special attacks!'); |
|
break; |
|
} |
|
} |
|
|
|
|
|
this.state.fieldEffects = this.state.fieldEffects.filter(effect => { |
|
effect.duration--; |
|
if (effect.duration <= 0) { |
|
this.log(`${effect.name} faded away!`); |
|
return false; |
|
} |
|
return true; |
|
}); |
|
} |
|
|
|
private checkBattleEnd(): void { |
|
if (this.state.playerPiclet.currentHp <= 0 && this.state.opponentPiclet.currentHp <= 0) { |
|
this.state.winner = 'draw'; |
|
this.state.phase = 'ended'; |
|
this.log('Battle ended in a draw!'); |
|
} else if (this.state.playerPiclet.currentHp <= 0) { |
|
this.state.winner = 'opponent'; |
|
this.state.phase = 'ended'; |
|
this.log(`${this.state.opponentPiclet.definition.name} wins!`); |
|
} else if (this.state.opponentPiclet.currentHp <= 0) { |
|
this.state.winner = 'player'; |
|
this.state.phase = 'ended'; |
|
this.log(`${this.state.playerPiclet.definition.name} wins!`); |
|
} |
|
} |
|
|
|
private log(message: string): void { |
|
this.state.log.push(message); |
|
} |
|
|
|
|
|
public getLog(): string[] { |
|
return [...this.state.log]; |
|
} |
|
|
|
|
|
private processManipulatePPEffect(effect: { action: string; amount?: string; value?: number; targetMove?: string }, target: BattlePiclet): void { |
|
const ppChange = this.getPPAmount(effect.amount, effect.value || 5); |
|
|
|
switch (effect.action) { |
|
case 'drain': |
|
|
|
for (const moveSlot of target.moves) { |
|
if (moveSlot.currentPP > 0) { |
|
const drained = Math.min(moveSlot.currentPP, ppChange); |
|
moveSlot.currentPP -= drained; |
|
this.log(`${target.definition.name}'s PP was drained from ${moveSlot.move.name}!`); |
|
break; |
|
} |
|
} |
|
break; |
|
case 'restore': |
|
|
|
for (const moveSlot of target.moves) { |
|
if (moveSlot.currentPP < moveSlot.move.pp) { |
|
const restored = Math.min(moveSlot.move.pp - moveSlot.currentPP, ppChange); |
|
moveSlot.currentPP += restored; |
|
this.log(`${target.definition.name}'s PP was restored to ${moveSlot.move.name}!`); |
|
break; |
|
} |
|
} |
|
break; |
|
case 'disable': |
|
|
|
for (const moveSlot of target.moves) { |
|
if (moveSlot.currentPP > 0) { |
|
moveSlot.currentPP = 0; |
|
this.log(`${target.definition.name}'s ${moveSlot.move.name} was disabled!`); |
|
break; |
|
} |
|
} |
|
break; |
|
} |
|
} |
|
|
|
private getPPAmount(amount?: string, value?: number): number { |
|
if (value !== undefined) return value; |
|
switch (amount) { |
|
case 'small': return 3; |
|
case 'medium': return 5; |
|
case 'large': return 8; |
|
default: return 5; |
|
} |
|
} |
|
|
|
private processFieldEffect(effect: { effect: string; target: string; stackable?: boolean }): void { |
|
|
|
const fieldEffect = { |
|
name: effect.effect, |
|
duration: 5, |
|
effect: effect |
|
}; |
|
|
|
|
|
if (!effect.stackable) { |
|
this.state.fieldEffects = this.state.fieldEffects.filter(fe => fe.name !== effect.effect); |
|
} |
|
|
|
this.state.fieldEffects.push(fieldEffect); |
|
switch (effect.effect) { |
|
case 'spikes': |
|
this.log('Spikes were set on the field!'); |
|
break; |
|
case 'reflect': |
|
this.log('Reflect was applied to the field!'); |
|
break; |
|
case 'lightScreen': |
|
this.log('Light Screen was applied to the field!'); |
|
break; |
|
case 'stealthRock': |
|
this.log('Stealth Rock was applied to the field!'); |
|
break; |
|
default: |
|
this.log(`${effect.effect.charAt(0).toUpperCase() + effect.effect.slice(1)} was applied to the field!`); |
|
} |
|
} |
|
|
|
private processCounterEffect(effect: { counterType: string; strength: string }, attacker: BattlePiclet, _target: BattlePiclet): void { |
|
|
|
|
|
attacker.temporaryEffects.push({ |
|
effect: { |
|
type: 'counter', |
|
counterType: effect.counterType, |
|
strength: effect.strength |
|
} as any, |
|
duration: 5 |
|
}); |
|
this.log(`${attacker.definition.name} is preparing to counter!`); |
|
} |
|
|
|
private processPriorityEffect(effect: { value: number; condition?: string }, target: BattlePiclet): void { |
|
|
|
target.statModifiers.priority = (target.statModifiers.priority || 0) + effect.value; |
|
this.log(`${target.definition.name}'s move priority changed by ${effect.value}!`); |
|
} |
|
|
|
private processRemoveStatusEffect(effect: { status: string }, target: BattlePiclet): void { |
|
if (target.statusEffects.includes(effect.status as any)) { |
|
target.statusEffects = target.statusEffects.filter(s => s !== effect.status); |
|
this.log(`${target.definition.name} was cured of ${effect.status}!`); |
|
} |
|
} |
|
|
|
private processMechanicOverrideEffect(effect: { mechanic: string; value: any; condition?: string }, target: BattlePiclet): void { |
|
|
|
target.temporaryEffects.push({ |
|
effect: { |
|
type: 'mechanicOverride', |
|
mechanic: effect.mechanic, |
|
value: effect.value, |
|
condition: effect.condition, |
|
target: 'self' |
|
} as any, |
|
duration: effect.condition === 'restOfBattle' ? 999 : 1 |
|
}); |
|
|
|
this.log(`Mechanic override '${effect.mechanic}' applied to ${target.definition.name}!`); |
|
} |
|
|
|
|
|
private hasMechanicOverride(piclet: BattlePiclet, mechanic: string): any { |
|
const override = piclet.temporaryEffects.find( |
|
effect => effect.effect.type === 'mechanicOverride' && |
|
(effect.effect as any).mechanic === mechanic |
|
); |
|
return override ? (override.effect as any).value : null; |
|
} |
|
|
|
private checkCriticalHitModification(attacker: BattlePiclet, target: BattlePiclet): 'always' | 'never' | 'normal' { |
|
|
|
const attackerOverride = this.hasMechanicOverride(attacker, 'criticalHits'); |
|
if (attackerOverride === true) return 'always'; |
|
|
|
|
|
const targetOverride = this.hasMechanicOverride(target, 'criticalHits'); |
|
if (targetOverride === false) return 'never'; |
|
|
|
return 'normal'; |
|
} |
|
|
|
private checkStatusImmunity(target: BattlePiclet, status: string): boolean { |
|
const immunity = this.hasMechanicOverride(target, 'statusImmunity'); |
|
if (Array.isArray(immunity)) { |
|
return immunity.includes(status); |
|
} |
|
return false; |
|
} |
|
|
|
private checkTypeImmunity(target: BattlePiclet, attackType: string): boolean { |
|
const immunity = this.hasMechanicOverride(target, 'typeImmunity'); |
|
if (Array.isArray(immunity)) { |
|
return immunity.includes(attackType); |
|
} |
|
return false; |
|
} |
|
|
|
private checkFlagBasedTypeImmunity(target: BattlePiclet, flags: string[]): boolean { |
|
const immunity = this.hasMechanicOverride(target, 'typeImmunity'); |
|
if (Array.isArray(immunity)) { |
|
|
|
return flags.some(flag => immunity.includes(flag)); |
|
} |
|
return false; |
|
} |
|
|
|
private checkFlagInteraction(target: BattlePiclet, flags: string[]): 'immune' | 'weak' | 'resist' | 'normal' { |
|
|
|
const immunity = this.hasMechanicOverride(target, 'flagImmunity'); |
|
if (Array.isArray(immunity) && flags.some(flag => immunity.includes(flag))) { |
|
return 'immune'; |
|
} |
|
|
|
|
|
const weakness = this.hasMechanicOverride(target, 'flagWeakness'); |
|
if (Array.isArray(weakness) && flags.some(flag => weakness.includes(flag))) { |
|
return 'weak'; |
|
} |
|
|
|
|
|
const resistance = this.hasMechanicOverride(target, 'flagResistance'); |
|
if (Array.isArray(resistance) && flags.some(flag => resistance.includes(flag))) { |
|
return 'resist'; |
|
} |
|
|
|
return 'normal'; |
|
} |
|
|
|
private getDamageMultiplier(piclet: BattlePiclet): number { |
|
const multiplier = this.hasMechanicOverride(piclet, 'damageMultiplier'); |
|
return typeof multiplier === 'number' ? multiplier : 1.0; |
|
} |
|
|
|
private shouldInvertHealing(target: BattlePiclet): boolean { |
|
return !!this.hasMechanicOverride(target, 'healingInversion'); |
|
} |
|
|
|
private applyEffectToPiclet(effect: BattleEffect, piclet: BattlePiclet): void { |
|
switch (effect.type) { |
|
case 'modifyStats': |
|
|
|
for (const [stat, modification] of Object.entries(effect.stats)) { |
|
const multiplier = this.getStatModifier(modification); |
|
if (stat === 'accuracy') { |
|
piclet.accuracy = Math.floor(piclet.accuracy * multiplier); |
|
} else { |
|
const statKey = stat as keyof BaseStats; |
|
(piclet as any)[statKey] = Math.floor((piclet as any)[statKey] * multiplier); |
|
} |
|
} |
|
break; |
|
case 'mechanicOverride': |
|
|
|
piclet.temporaryEffects.push({ |
|
effect: effect, |
|
duration: 999 |
|
}); |
|
break; |
|
|
|
} |
|
} |
|
|
|
private checkCounterEffects(target: BattlePiclet, attacker: BattlePiclet, move: Move): void { |
|
|
|
for (let i = target.temporaryEffects.length - 1; i >= 0; i--) { |
|
const tempEffect = target.temporaryEffects[i]; |
|
if (tempEffect.effect.type === 'counter') { |
|
const counterEffect = tempEffect.effect as any; |
|
const shouldCounter = counterEffect.counterType === 'any' || |
|
(counterEffect.counterType === 'physical' && move.flags.includes('contact')) || |
|
(counterEffect.counterType === 'special' && !move.flags.includes('contact')); |
|
|
|
if (shouldCounter) { |
|
|
|
let counterDamage = 0; |
|
switch (counterEffect.strength) { |
|
case 'weak': counterDamage = 20; break; |
|
case 'normal': counterDamage = 40; break; |
|
case 'strong': counterDamage = 60; break; |
|
default: counterDamage = 40; |
|
} |
|
|
|
attacker.currentHp = Math.max(0, attacker.currentHp - counterDamage); |
|
this.log(`${target.definition.name} countered with ${counterDamage} damage!`); |
|
|
|
|
|
target.temporaryEffects.splice(i, 1); |
|
} |
|
} |
|
} |
|
} |
|
} |