/** * Core Battle Engine for Pictuary * Implements the battle system as defined in battle_system_design.md */ import type { BattleState, BattlePiclet, PicletDefinition, BattleAction, MoveAction, SwitchAction, BattleEffect, DamageAmount, StatModification, HealAmount, StatusEffect, BaseStats, Move, Trigger } from './types'; import { getEffectivenessMultiplier } from '../types/picletTypes'; export class BattleEngine { private state: BattleState; private playerRoster: PicletDefinition[]; private opponentRoster: PicletDefinition[]; private playerRosterStates: Array<{ currentHp: number; maxHp: number; fainted: boolean; moves: Array<{move: Move; currentPP: number}> }>; private opponentRosterStates: Array<{ currentHp: number; maxHp: number; fainted: boolean; moves: Array<{move: Move; currentPP: number}> }>; constructor( playerPiclet: PicletDefinition | PicletDefinition[], opponentPiclet: PicletDefinition | PicletDefinition[], playerLevel = 50, opponentLevel = 50 ) { // Handle roster setup with internal prefixes for reliable animation targeting this.playerRoster = (Array.isArray(playerPiclet) ? playerPiclet : [playerPiclet]) .map(piclet => ({ ...piclet, name: `player-${piclet.name}` })); this.opponentRoster = (Array.isArray(opponentPiclet) ? opponentPiclet : [opponentPiclet]) .map(piclet => ({ ...piclet, name: `enemy-${piclet.name}` })); // Initialize roster states this.playerRosterStates = this.initializeRosterStates(this.playerRoster, playerLevel); this.opponentRosterStates = this.initializeRosterStates(this.opponentRoster, opponentLevel); this.state = { turn: 1, phase: 'selection', playerPiclet: this.createBattlePiclet(this.playerRoster[0], playerLevel), opponentPiclet: this.createBattlePiclet(this.opponentRoster[0], opponentLevel), fieldEffects: [], log: [], winner: undefined }; // Sync initial states to roster for consistency this.syncActivePicketToRoster('player'); this.syncActivePicketToRoster('opponent'); } private initializeRosterStates(roster: PicletDefinition[], level: number): Array<{ currentHp: number; maxHp: number; fainted: boolean; moves: Array<{move: Move; currentPP: number}> }> { return roster.map(piclet => { // Use pre-calculated HP from definition (already includes level scaling) const hp = piclet.baseStats.hp; return { currentHp: hp, maxHp: hp, fainted: false, moves: piclet.movepool.slice(0, 4).map(move => ({ move, currentPP: move.pp })) }; }); } private createBattlePiclet(definition: PicletDefinition, level: number): BattlePiclet { // Battle engine now uses pre-calculated stats from levelingService // No level scaling needed here - stats already include level and nature effects const hp = definition.baseStats.hp; const attack = definition.baseStats.attack; const defense = definition.baseStats.defense; const speed = definition.baseStats.speed; const piclet: BattlePiclet = { definition, currentHp: hp, maxHp: hp, level, attack, defense, speed, accuracy: 100, // Base accuracy statusEffects: [], moves: definition.movepool.slice(0, 4).map(move => ({ move, currentPP: move.pp })), statModifiers: {}, temporaryEffects: [] }; // Apply special ability effects 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)); // Deep clone for immutability } 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'; // Determine action order based on priority and speed const actions = this.determineActionOrder(playerAction, opponentAction); // Execute actions in order for (const action of actions) { if ((this.state.phase as string) === 'ended') break; // Check if battle already ended this.executeAction(action); // Check for battle end after each action (important for self-destruct moves) this.checkBattleEnd(); if ((this.state.phase as string) === 'ended') break; } // End of turn processing if ((this.state.phase as string) !== 'ended') { this.processTurnEnd(); } // Sync active piclets to roster to preserve state changes if ((this.state.phase as string) !== 'ended') { this.syncActivePicketToRoster('player'); this.syncActivePicketToRoster('opponent'); } // Check for battle end this.checkBattleEnd(); if ((this.state.phase as string) !== 'ended') { this.state.turn++; this.state.phase = 'selection'; } } private determineActionOrder(playerAction: BattleAction, opponentAction: BattleAction): Array { 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; // Higher priority goes first, then speed, then random 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; // Speed tie } 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; // Check for conditional priority effects in the move 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; } } } // Add priority modifier from effects const priorityMod = piclet.statModifiers.priority || 0; priority += priorityMod; } else { priority = 6; // Switch actions have highest priority } return priority; } private executeAction(action: BattleAction & { executor: 'player' | 'opponent' }): void { if (action.type === 'move') { this.executeMove(action); } else if (action.type === 'switch') { this.executeSwitch(action as SwitchAction & { executor: 'player' | 'opponent' }); } } 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; // Check if attacker can act due to status effects if (!this.canPicletAct(attacker)) { return; // Skip this action } 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; // Trigger before move use this.triggerBeforeMoveUse(attacker, move); this.log(`${attacker.definition.name} used ${move.name}!`); // Consume PP moveData.currentPP--; // Check if move hits const moveHit = this.checkMoveHits(move, attacker, defender); if (!moveHit) { this.log(`${attacker.definition.name}'s attack missed!`); this.triggerAfterMoveUse(attacker, move, false); return; } // Trigger opponent contact move (if applicable) this.triggerOnOpponentContactMove(defender, attacker, move); // For gambling/luck-based moves, roll once and store the result const luckyRoll = Math.random() < 0.5; // Process effects for (const effect of move.effects) { this.processEffect(effect, attacker, defender, move, luckyRoll); } // Trigger after move use this.triggerAfterMoveUse(attacker, move, true); } private executeSwitch(action: SwitchAction & { executor: 'player' | 'opponent' }): void { const isPlayer = action.executor === 'player'; const roster = isPlayer ? this.playerRoster : this.opponentRoster; const rosterStates = isPlayer ? this.playerRosterStates : this.opponentRosterStates; const currentPiclet = isPlayer ? this.state.playerPiclet : this.state.opponentPiclet; // Validate switch action if (action.newPicletIndex < 0 || action.newPicletIndex >= roster.length) { this.log(`${action.executor} cannot switch - invalid piclet index!`); return; } if (action.newPicletIndex === this.getCurrentPicletIndex(action.executor)) { this.log(`${roster[action.newPicletIndex].name} is already active!`); return; } if (rosterStates[action.newPicletIndex].fainted) { this.log(`${roster[action.newPicletIndex].name} is unable to battle!`); return; } const oldPiclet = currentPiclet; const newPicletDef = roster[action.newPicletIndex]; // Trigger switch-out ability this.triggerOnSwitchOut(oldPiclet); // Save current piclet state back to roster this.savePicletToRoster(oldPiclet, action.executor); // Load new piclet from roster const newPiclet = this.loadPicletFromRoster(action.newPicletIndex, action.executor); // Update battle state if (isPlayer) { this.state.playerPiclet = newPiclet; } else { this.state.opponentPiclet = newPiclet; } this.log(`${action.executor} switched to ${newPicletDef.name}!`); // Apply entry hazards this.applyEntryHazards(newPiclet); // Trigger switch-in ability this.triggerOnSwitchIn(newPiclet); } private getCurrentPicletIndex(executor: 'player' | 'opponent'): number { const isPlayer = executor === 'player'; const roster = isPlayer ? this.playerRoster : this.opponentRoster; const currentPiclet = isPlayer ? this.state.playerPiclet : this.state.opponentPiclet; return roster.findIndex(piclet => piclet.name === currentPiclet.definition.name); } private savePicletToRoster(piclet: BattlePiclet, executor: 'player' | 'opponent'): void { const isPlayer = executor === 'player'; const rosterStates = isPlayer ? this.playerRosterStates : this.opponentRosterStates; const currentIndex = this.getCurrentPicletIndex(executor); if (currentIndex !== -1) { // Save current state back to roster rosterStates[currentIndex].currentHp = piclet.currentHp; rosterStates[currentIndex].fainted = piclet.currentHp <= 0; // Save current PP state for (let i = 0; i < piclet.moves.length; i++) { if (rosterStates[currentIndex].moves[i]) { rosterStates[currentIndex].moves[i].currentPP = piclet.moves[i].currentPP; } } } } private syncActivePicketToRoster(executor: 'player' | 'opponent'): void { const piclet = executor === 'player' ? this.state.playerPiclet : this.state.opponentPiclet; this.savePicletToRoster(piclet, executor); } private loadPicletFromRoster(index: number, executor: 'player' | 'opponent'): BattlePiclet { const isPlayer = executor === 'player'; const roster = isPlayer ? this.playerRoster : this.opponentRoster; const rosterStates = isPlayer ? this.playerRosterStates : this.opponentRosterStates; const level = isPlayer ? this.state.playerPiclet.level : this.state.opponentPiclet.level; const definition = roster[index]; const savedState = rosterStates[index]; // Create fresh battle piclet const piclet = this.createBattlePiclet(definition, level); // Restore saved state piclet.currentHp = savedState.currentHp; // Restore PP for (let i = 0; i < piclet.moves.length; i++) { if (savedState.moves[i]) { piclet.moves[i].currentPP = savedState.moves[i].currentPP; } } // Reset stat modifications (switching clears temporary stat changes) piclet.statModifiers = {}; return piclet; } private triggerOnSwitchIn(piclet: BattlePiclet): void { this.triggerAbilities('onSwitchIn', piclet); } private triggerOnSwitchOut(piclet: BattlePiclet): void { this.triggerAbilities('onSwitchOut', piclet); } private checkMoveHits(move: Move, _attacker: BattlePiclet, _defender: BattlePiclet): boolean { // Simple accuracy check - can be enhanced later 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 { // Check condition (simplified for now) if (effect.condition && !this.checkCondition(effect.condition, attacker, defender, luckyRoll)) { return; } switch (effect.type) { case 'damage': if (effect.target === 'all') { // Self-destruct style moves that damage all targets this.processDamageEffect(effect, attacker, attacker, move); // Self-damage this.processDamageEffect(effect, attacker, defender, move); // Opponent damage } 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': // MechanicOverride effects don't have a target - they apply to the user 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'); // Type-specific conditions 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': // Would need move context to check, placeholder for now return true; // Status-specific conditions 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'); // Weather conditions (placeholder) case 'ifWeather:storm': case 'ifWeather:rain': case 'ifWeather:sun': case 'ifWeather:snow': return false; // Weather system not implemented yet // Combat conditions case 'ifDamagedThisTurn': // Check if the attacker was damaged this turn // For now, we'll implement this by checking if currentHp < maxHp // This is a simplified implementation return attacker.currentHp < attacker.maxHp; case 'ifNotSuperEffective': // Would need move context, placeholder return false; case 'ifStatusMove': // Would need move context, placeholder return false; case 'afterUse': // This condition should be processed after the move's other effects return true; default: return true; // Default to true for unimplemented conditions } } private resolveTarget(target: string, attacker: BattlePiclet, defender: BattlePiclet): BattlePiclet | null { switch (target) { case 'self': return attacker; case 'opponent': return defender; default: return null; // Multi-target not implemented yet } } private processDamageEffect(effect: { amount?: DamageAmount; formula?: string; value?: number; multiplier?: number }, attacker: BattlePiclet, target: BattlePiclet, move: Move): void { let damage = 0; // Check type immunity first if (this.checkTypeImmunity(target, move.type)) { this.log(`${target.definition.name} is immune to ${move.type} type moves!`); return; } // Check flag-based type immunity (like ground immunity via levitate) if (this.checkFlagBasedTypeImmunity(target, move.flags)) { this.log(`${target.definition.name} had no effect!`); return; } // Check flag interactions const flagInteraction = this.checkFlagInteraction(target, move.flags); if (flagInteraction === 'immune') { this.log(`It had no effect on ${target.definition.name}!`); return; } // Handle different damage formulas if (effect.formula) { damage = this.calculateDamageByFormula(effect, attacker, target, move); } else if (effect.amount) { damage = this.calculateStandardDamage(effect.amount, attacker, target, move); } // Apply flag interaction modifiers 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..."); } // Apply damage multiplier from abilities const damageMultiplier = this.getDamageMultiplier(attacker); damage = Math.floor(damage * damageMultiplier); // Apply field effect damage multipliers const isPlayerAttacking = attacker === this.state.playerPiclet; const fieldEffectMultiplier = this.getFieldEffectDamageMultiplier(move, isPlayerAttacking); damage = Math.floor(damage * fieldEffectMultiplier); // Check for critical hits const critMod = this.checkCriticalHitModification(attacker, target); const isCriticalHit = critMod === 'always' || (critMod === 'normal' && Math.random() < 0.0625); if (isCriticalHit) { // 1/16 base crit rate damage = Math.floor(damage * 1.5); this.log("A critical hit!"); // Trigger critical hit ability this.triggerOnCriticalHit(attacker, target); } // Apply damage if (damage > 0) { target.currentHp = Math.max(0, target.currentHp - damage); this.log(`${target.definition.name} took ${damage} damage!`); // Wake up from sleep when damaged this.wakeUpFromSleep(target); // Trigger ability events this.triggerOnDamageTaken(target, damage, move.flags.includes('contact')); this.triggerOnDamageDealt(attacker, damage, target); this.triggerOnLowHP(target); // Check for counter effects on the target this.checkCounterEffects(target, attacker, move); } // Handle special formula effects 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!`); this.triggerOnHPDrained(attacker, target, healAmount); } } 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': // Use the move's actual power for standard formula 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; // Type effectiveness const effectiveness = getEffectivenessMultiplier( move.type, target.definition.primaryType, target.definition.secondaryType ); // STAB (Same Type Attack Bonus) - compare enum values as strings const stab = (move.type.toString() === attacker.definition.primaryType?.toString() || move.type.toString() === attacker.definition.secondaryType?.toString()) ? 1.5 : 1; // Damage calculation (simplified) const attackStat = attacker.attack; const defenseStat = target.defense; let damage = Math.floor((baseDamage * (attackStat / defenseStat) * 0.5) + 10); damage = Math.floor(damage * effectiveness * stab); // Random factor (85-100%) damage = Math.floor(damage * (0.85 + Math.random() * 0.15)); // Minimum 1 damage for effective moves if (effectiveness > 0 && damage < 1) { damage = 1; } // Log effectiveness messages 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); // Type effectiveness const effectiveness = getEffectivenessMultiplier( move.type, target.definition.primaryType, target.definition.secondaryType ); // STAB (Same Type Attack Bonus) - compare enum values as strings const stab = (move.type.toString() === attacker.definition.primaryType?.toString() || move.type.toString() === attacker.definition.secondaryType?.toString()) ? 1.5 : 1; // Damage calculation (simplified) const attackStat = attacker.attack; const defenseStat = target.defense; let damage = Math.floor((baseDamage * (attackStat / defenseStat) * 0.5) + 10); damage = Math.floor(damage * effectiveness * stab); // Random factor (85-100%) damage = Math.floor(damage * (0.85 + Math.random() * 0.15)); // Minimum 1 damage for effective moves if (effectiveness > 0 && damage < 1) { damage = 1; } // Log effectiveness messages 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> }, 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); } const changeType = modification.includes('increase') ? 'increase' : 'decrease'; this.log(`${target.definition.name}'s ${stat} ${modification.includes('increase') ? 'rose' : 'fell'}!`); this.triggerOnStatChange(target, stat, changeType); } } 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); } // Check for healing inversion mechanic if (this.shouldInvertHealing(target)) { // Healing becomes damage 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 { // Normal healing 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!`); this.triggerOnFullHP(target); } } } 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 { // Process status effects this.processStatusEffects(this.state.playerPiclet); this.processStatusEffects(this.state.opponentPiclet); // Apply field healing effects this.applyFieldHealingEffects(); // Process field effects (duration management) this.processFieldEffects(); // Trigger end of turn abilities this.triggerEndOfTurn(); // Decrement temporary effects this.processTemporaryEffects(this.state.playerPiclet); this.processTemporaryEffects(this.state.opponentPiclet); } private processStatusEffects(piclet: BattlePiclet): void { // Process status effects that trigger at end of turn const statusesToRemove: string[] = []; for (let i = 0; i < piclet.statusEffects.length; i++) { const status = piclet.statusEffects[i]; 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; case 'freeze': // Don't process freeze on the turn it was applied if ((piclet as any).freezeJustApplied) { delete (piclet as any).freezeJustApplied; } else { // 20% chance to thaw out each turn if (Math.random() < 0.2) { statusesToRemove.push(status); this.log(`${piclet.definition.name} thawed out!`); } } break; case 'sleep': // Don't process sleep on the turn it was applied if ((piclet as any).sleepJustApplied) { delete (piclet as any).sleepJustApplied; } else { // Decrement sleep turns and wake up const sleepTurns = (piclet as any).sleepTurns || 0; if (sleepTurns <= 1) { statusesToRemove.push(status); this.log(`${piclet.definition.name} woke up!`); delete (piclet as any).sleepTurns; } else { (piclet as any).sleepTurns = sleepTurns - 1; } } break; case 'confuse': // Decrement confusion turns const confusionTurns = (piclet as any).confusionTurns || 0; if (confusionTurns <= 1) { statusesToRemove.push(status); this.log(`${piclet.definition.name} snapped out of confusion!`); delete (piclet as any).confusionTurns; } else { (piclet as any).confusionTurns = confusionTurns - 1; } break; } } // Remove statuses that expired for (const statusToRemove of statusesToRemove) { const index = piclet.statusEffects.indexOf(statusToRemove as any); if (index > -1) { piclet.statusEffects.splice(index, 1); } } } private processTemporaryEffects(piclet: BattlePiclet): void { // Decrement duration of temporary effects piclet.temporaryEffects = piclet.temporaryEffects.filter(effect => { effect.duration--; return effect.duration > 0; }); } private processFieldEffects(): void { // Field effects are processed at end of turn for duration management // Their actual mechanics are applied during relevant battle phases // Decrement field effect durations and remove expired ones this.state.fieldEffects = this.state.fieldEffects.filter(effect => { effect.duration--; if (effect.duration <= 0) { this.log(`${this.formatFieldEffectName(effect.name)} faded away!`); return false; } return true; }); } private formatFieldEffectName(effectName: string): string { switch (effectName) { case 'entryHazardSpikes': return 'Entry spikes'; case 'contactDamageReduction': return 'Contact damage barrier'; case 'nonContactDamageReduction': return 'Non-contact damage barrier'; case 'healingField': return 'Healing field'; case 'poisonousField': return 'Poisonous field'; default: return effectName; } } private getFieldEffectDamageMultiplier(move: Move, isPlayerAttacking: boolean): number { let multiplier = 1.0; // Determine if this is a contact move const isContactMove = move.flags.includes('contact'); // Check field effects that modify damage for (const fieldEffect of this.state.fieldEffects) { const targetSide = fieldEffect.effect.target; // Field effects protect the side they're applied to from incoming attacks // So if playerSide has a barrier, it protects player from opponent attacks const protectsDefender = (!isPlayerAttacking && targetSide === 'playerSide') || (isPlayerAttacking && targetSide === 'opponentSide'); if (!protectsDefender) continue; switch (fieldEffect.name) { case 'contactDamageReduction': if (isContactMove) { multiplier *= 0.5; // Reduce contact move damage by 50% } break; case 'nonContactDamageReduction': if (!isContactMove) { multiplier *= 0.5; // Reduce non-contact move damage by 50% } break; } } return multiplier; } private applyEntryHazards(piclet: BattlePiclet): void { // Apply entry hazards when a piclet enters battle (switching mechanics) for (const fieldEffect of this.state.fieldEffects) { if (fieldEffect.name === 'spikes' || fieldEffect.name === 'entryHazardSpikes') { const targetSide = fieldEffect.effect.target; const isPlayerSide = piclet === this.state.playerPiclet; const appliesTo = (isPlayerSide && targetSide === 'playerSide') || (!isPlayerSide && targetSide === 'opponentSide'); if (appliesTo) { const damage = Math.floor(piclet.maxHp * 0.125); // 12.5% max HP damage piclet.currentHp = Math.max(0, piclet.currentHp - damage); this.log(`${piclet.definition.name} was hurt by spikes!`); } } else if (fieldEffect.name === 'toxicSpikes') { const targetSide = fieldEffect.effect.target; const isPlayerSide = piclet === this.state.playerPiclet; const appliesTo = (isPlayerSide && targetSide === 'playerSide') || (!isPlayerSide && targetSide === 'opponentSide'); if (appliesTo && !piclet.statusEffects.includes('poison')) { piclet.statusEffects.push('poison'); this.log(`${piclet.definition.name} was poisoned by toxic spikes!`); } } else if (fieldEffect.name === 'poisonousField') { const targetSide = fieldEffect.effect.target; const isPlayerSide = piclet === this.state.playerPiclet; const appliesTo = (isPlayerSide && targetSide === 'playerSide') || (!isPlayerSide && targetSide === 'opponentSide'); if (appliesTo && !piclet.statusEffects.includes('poison')) { piclet.statusEffects.push('poison'); this.log(`${piclet.definition.name} was poisoned by toxic spikes!`); } } } } private applyFieldHealingEffects(): void { // Apply healing field effects at end of turn const healingFields = this.state.fieldEffects.filter(effect => effect.name === 'healingField'); for (const healingField of healingFields) { const targetSide = healingField.effect.target; if (targetSide === 'playerSide' || targetSide === 'field') { const healAmount = Math.floor(this.state.playerPiclet.maxHp * 0.0625); // 6.25% max HP if (this.state.playerPiclet.currentHp < this.state.playerPiclet.maxHp) { this.state.playerPiclet.currentHp = Math.min( this.state.playerPiclet.maxHp, this.state.playerPiclet.currentHp + healAmount ); this.log(`${this.state.playerPiclet.definition.name} was healed by the healing field!`); } } if (targetSide === 'opponentSide' || targetSide === 'field') { const healAmount = Math.floor(this.state.opponentPiclet.maxHp * 0.0625); // 6.25% max HP if (this.state.opponentPiclet.currentHp < this.state.opponentPiclet.maxHp) { this.state.opponentPiclet.currentHp = Math.min( this.state.opponentPiclet.maxHp, this.state.opponentPiclet.currentHp + healAmount ); this.log(`${this.state.opponentPiclet.definition.name} was healed by the healing field!`); } } } } private checkBattleEnd(): void { const playerFainted = this.state.playerPiclet.currentHp <= 0; const opponentFainted = this.state.opponentPiclet.currentHp <= 0; // Mark fainted piclets in roster states and trigger KO events if (playerFainted) { const playerIndex = this.getCurrentPicletIndex('player'); if (playerIndex !== -1) { this.playerRosterStates[playerIndex].fainted = true; this.triggerOnKO(this.state.playerPiclet, this.state.opponentPiclet); } } if (opponentFainted) { const opponentIndex = this.getCurrentPicletIndex('opponent'); if (opponentIndex !== -1) { this.opponentRosterStates[opponentIndex].fainted = true; this.triggerOnKO(this.state.opponentPiclet, this.state.playerPiclet); } } // Check if any viable piclets remain const playerHasViablePiclets = this.playerRosterStates.some(state => !state.fainted); const opponentHasViablePiclets = this.opponentRosterStates.some(state => !state.fainted); if (!playerHasViablePiclets && !opponentHasViablePiclets) { this.state.winner = 'draw'; this.state.phase = 'ended'; this.log('Battle ended in a draw!'); } else if (!playerHasViablePiclets) { this.state.winner = 'opponent'; this.state.phase = 'ended'; // No win message - handled in UI } else if (!opponentHasViablePiclets) { this.state.winner = 'player'; this.state.phase = 'ended'; // No win message - handled in UI } else if (playerFainted || opponentFainted) { // Handle forced switching - at least one piclet fainted but viable alternatives exist this.handleForcedSwitching(playerFainted, opponentFainted); } } private handleForcedSwitching(playerFainted: boolean, opponentFainted: boolean): void { if (playerFainted) { this.log(`${this.state.playerPiclet.definition.name} fainted!`); const viablePiclets = this.playerRosterStates.map((state, index) => ({ index, state })) .filter(entry => !entry.state.fainted); if (viablePiclets.length === 1) { // Auto-switch to the only remaining piclet const autoSwitchIndex = viablePiclets[0].index; this.log(`Player must choose a new piclet! Auto-switching to ${this.playerRoster[autoSwitchIndex].name}!`); this.executeSwitch({ type: 'switch', piclet: 'player', newPicletIndex: autoSwitchIndex, executor: 'player' }); } else { this.log(`Player must choose a new piclet from ${viablePiclets.length} remaining options!`); // In a real implementation, this would pause and wait for player input // For testing, we can simulate choosing the first available } } if (opponentFainted) { this.log(`${this.state.opponentPiclet.definition.name} fainted!`); const viablePiclets = this.opponentRosterStates.map((state, index) => ({ index, state })) .filter(entry => !entry.state.fainted); if (viablePiclets.length === 1) { // Auto-switch to the only remaining piclet const autoSwitchIndex = viablePiclets[0].index; this.log(`Opponent must choose a new piclet! Auto-switching to ${this.opponentRoster[autoSwitchIndex].name}!`); this.executeSwitch({ type: 'switch', piclet: 'opponent', newPicletIndex: autoSwitchIndex, executor: 'opponent' }); } else { this.log(`Opponent must choose a new piclet from ${viablePiclets.length} remaining options!`); // In a real implementation, this would be AI logic } } } private log(message: string): void { this.state.log.push(message); } // Public method to get battle log public getLog(): string[] { // Strip battle prefixes from all log messages for display return this.state.log.map(message => this.stripBattlePrefixes(message)); } private stripBattlePrefixes(message: string): string { // Remove player- and enemy- prefixes from messages return message .replace(/player-/g, '') .replace(/enemy-/g, ''); } // Additional effect processors for advanced features 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': // Drain PP from target's moves 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; // Only drain from first move with PP } } break; case 'restore': // Restore PP to target's moves 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; // Only restore to first move that needs PP } } break; case 'disable': // Disable a move by setting its PP to 0 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; // Only disable first available move } } 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 { // Map old effect names to new descriptive names const effectNameMap: Record = { 'spikes': 'entryHazardSpikes', 'reflect': 'contactDamageReduction', 'lightScreen': 'nonContactDamageReduction', 'healingMist': 'healingField', 'toxicSpikes': 'poisonousField' }; const mappedName = effectNameMap[effect.effect] || effect.effect; // Add field effect to battle state const fieldEffect = { name: mappedName, duration: 5, // Default duration effect: effect }; // Check if effect already exists and is not stackable if (!effect.stackable) { this.state.fieldEffects = this.state.fieldEffects.filter(fe => fe.name !== mappedName); } this.state.fieldEffects.push(fieldEffect); // Log effect application with clear descriptions switch (mappedName) { case 'entryHazardSpikes': this.log('Entry spikes were scattered on the battlefield!'); break; case 'contactDamageReduction': this.log('A barrier was raised to reduce contact move damage!'); break; case 'nonContactDamageReduction': this.log('A barrier was raised to reduce non-contact move damage!'); break; case 'healingField': this.log('A healing field was created!'); break; case 'poisonousField': this.log('A poisonous field was created!'); break; default: this.log(`${mappedName} was applied to the field!`); } } private processCounterEffect(effect: { strength: string }, attacker: BattlePiclet, _target: BattlePiclet): void { // Store counter effect for later processing when the user is attacked // Counter effects should persist until triggered, not expire after 1 turn attacker.temporaryEffects.push({ effect: { type: 'counter', strength: effect.strength } as any, duration: 5 // Persist for multiple turns until triggered }); this.log(`${attacker.definition.name} is preparing to counter!`); } private processPriorityEffect(effect: { value: number; condition?: string }, target: BattlePiclet): void { // Store priority modification for next move 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 { // Store mechanic override as temporary effect for processing during relevant calculations 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}!`); } // Helper methods for checking mechanic overrides 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' { // Check attacker's critical hit modifiers const attackerOverride = this.hasMechanicOverride(attacker, 'criticalHits'); if (attackerOverride === true) return 'always'; // Check target's critical hit immunity 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)) { // Check if any of the move's flags match the type immunity return flags.some(flag => immunity.includes(flag)); } return false; } private checkFlagInteraction(target: BattlePiclet, flags: string[]): 'immune' | 'weak' | 'resist' | 'normal' { // Check immunities first const immunity = this.hasMechanicOverride(target, 'flagImmunity'); if (Array.isArray(immunity) && flags.some(flag => immunity.includes(flag))) { return 'immune'; } // Check weaknesses const weakness = this.hasMechanicOverride(target, 'flagWeakness'); if (Array.isArray(weakness) && flags.some(flag => weakness.includes(flag))) { return 'weak'; } // Check resistances 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': // Apply permanent stat modifications from abilities 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': // Store mechanic overrides as permanent effects piclet.temporaryEffects.push({ effect: effect, duration: 999 // Permanent ability effect }); break; // Other effects are handled during battle } } private checkCounterEffects(target: BattlePiclet, attacker: BattlePiclet, move: Move): void { // Check if the target has any counter effects ready 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 = true; // All counters now work against any attack type if (shouldCounter) { // Calculate counter damage 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!`); // Remove the counter effect after it triggers target.temporaryEffects.splice(i, 1); } } } } // Advanced Status Effect Checks private canPicletAct(piclet: BattlePiclet): boolean { // Check status effects that prevent action for (const status of piclet.statusEffects) { switch (status) { case 'freeze': this.log(`${piclet.definition.name} is frozen solid and cannot move!`); return false; case 'sleep': this.log(`${piclet.definition.name} is fast asleep and cannot wake up!`); return false; case 'paralyze': // 25% chance to be fully paralyzed if (Math.random() < 0.25) { this.log(`${piclet.definition.name} is fully paralyzed and cannot move!`); return false; } break; case 'confuse': // 33% chance to hurt self in confusion if (Math.random() < 0.33) { const selfDamage = Math.floor(piclet.maxHp * 0.125); // 12.5% max HP piclet.currentHp = Math.max(0, piclet.currentHp - selfDamage); this.log(`${piclet.definition.name} hurt itself in confusion for ${selfDamage} damage!`); return false; } break; } } return true; } // Enhanced Status Application private processApplyStatusEffect(effect: { status: StatusEffect; chance?: number }, target: BattlePiclet): void { // Check chance if specified if (effect.chance !== undefined) { const roll = Math.random() * 100; if (roll >= effect.chance) { return; // Status effect failed to apply } } // Check for status immunity if (this.checkStatusImmunity(target, effect.status)) { this.log(`${target.definition.name} is immune to ${effect.status}!`); return; } // Check for major status conflicts (freeze, paralyze, sleep are mutually exclusive) const majorStatuses = ['freeze', 'paralyze', 'sleep']; if (majorStatuses.includes(effect.status)) { const hasMajorStatus = target.statusEffects.some(status => majorStatuses.includes(status)); if (hasMajorStatus) { this.log(`${target.definition.name} is already affected by a major status condition!`); return; } } if (!target.statusEffects.includes(effect.status)) { target.statusEffects.push(effect.status); // Trigger status inflicted event this.triggerOnStatusInflicted(target, effect.status); // Apply immediate effects and set durations switch (effect.status) { case 'freeze': this.log(`${target.definition.name} was frozen solid!`); // Mark as just applied to prevent immediate thawing (target as any).freezeJustApplied = true; break; case 'paralyze': this.log(`${target.definition.name} was paralyzed!`); // Reduce speed by 50% target.speed = Math.floor(target.speed * 0.5); break; case 'sleep': this.log(`${target.definition.name} fell asleep!`); // Sleep lasts 1-3 turns (target as any).sleepTurns = 1 + Math.floor(Math.random() * 3); (target as any).sleepJustApplied = true; break; case 'confuse': this.log(`${target.definition.name} became confused!`); // Confusion lasts 2-5 turns (target as any).confusionTurns = 2 + Math.floor(Math.random() * 4); break; default: this.log(`${target.definition.name} was ${effect.status}ed!`); } } } // Wake up from sleep when damaged private wakeUpFromSleep(target: BattlePiclet): void { if (target.statusEffects.includes('sleep')) { const sleepIndex = target.statusEffects.indexOf('sleep'); if (sleepIndex > -1) { target.statusEffects.splice(sleepIndex, 1); this.log(`${target.definition.name} woke up from the attack!`); delete (target as any).sleepTurns; } } } // Ability Trigger System private triggerAbilities(event: string, piclet: BattlePiclet, context?: any): void { if (!piclet.definition.specialAbility?.triggers) return; for (const trigger of piclet.definition.specialAbility.triggers) { if (trigger.event === event && this.checkTriggerCondition(trigger, piclet, context)) { this.log(`${piclet.definition.name}'s ${piclet.definition.specialAbility.name} triggered!`); // Process all effects in the trigger for (const effect of trigger.effects) { this.processAbilityTriggerEffect(effect, piclet, context); } } } } private checkTriggerCondition(trigger: Trigger, piclet: BattlePiclet, context?: any): boolean { if (!trigger.condition || trigger.condition === 'always') { return true; } // Check various conditions switch (trigger.condition) { case 'ifLowHp': return (piclet.currentHp / piclet.maxHp) < 0.25; case 'ifHighHp': return piclet.currentHp === piclet.maxHp; case 'onCritical': return context?.isCriticalHit === true; case 'ifStatusMove': return context?.isStatusMove === true; default: return true; } } private processAbilityTriggerEffect(effect: BattleEffect, owner: BattlePiclet, context?: any): void { // Determine target for the effect based on effect type let targetType = 'self'; // default if ('target' in effect) { targetType = effect.target; } const target = this.resolveAbilityTarget(targetType, owner); if (!target) return; // Process the effect using existing effect processors switch (effect.type) { case 'damage': // Create a dummy move for damage calculation const dummyMove: Move = { name: `${owner.definition.specialAbility?.name} Effect`, type: 'normal' as any, power: 0, accuracy: 100, pp: 1, priority: 0, flags: [], effects: [] }; this.processDamageEffect(effect, owner, target, dummyMove); break; case 'modifyStats': this.processModifyStatsEffect(effect, target); break; case 'heal': this.processHealEffect(effect, target); break; case 'applyStatus': this.processApplyStatusEffect(effect, target); break; case 'removeStatus': this.processRemoveStatusEffect(effect, target); break; default: this.log(`Ability effect ${effect.type} not implemented yet`); } } private resolveAbilityTarget(targetType: string, owner: BattlePiclet): BattlePiclet | null { switch (targetType) { case 'self': return owner; case 'opponent': return owner === this.state.playerPiclet ? this.state.opponentPiclet : this.state.playerPiclet; default: return null; } } // Trigger Points Integration private triggerOnDamageTaken(piclet: BattlePiclet, damage: number, isContactMove: boolean): void { this.triggerAbilities('onDamageTaken', piclet, { damage, isContactMove }); if (isContactMove) { this.triggerAbilities('onContactDamage', piclet, { damage }); } } private triggerOnDamageDealt(piclet: BattlePiclet, damage: number, target: BattlePiclet): void { this.triggerAbilities('onDamageDealt', piclet, { damage, target }); } private triggerOnCriticalHit(piclet: BattlePiclet, target: BattlePiclet): void { this.triggerAbilities('onCriticalHit', piclet, { target, isCriticalHit: true }); } private triggerOnLowHP(piclet: BattlePiclet): void { if ((piclet.currentHp / piclet.maxHp) < 0.25) { this.triggerAbilities('onLowHP', piclet); } } private triggerEndOfTurn(): void { this.triggerAbilities('endOfTurn', this.state.playerPiclet); this.triggerAbilities('endOfTurn', this.state.opponentPiclet); } private triggerOnStatusInflicted(piclet: BattlePiclet, status: string): void { this.triggerAbilities('onStatusInflicted', piclet, { status }); } private triggerOnHPDrained(attacker: BattlePiclet, target: BattlePiclet, drainAmount: number): void { this.triggerAbilities('onHPDrained', attacker, { target, drainAmount }); } private triggerOnKO(knockedOut: BattlePiclet, attacker: BattlePiclet): void { this.triggerAbilities('onKO', knockedOut, { attacker }); this.triggerAbilities('onKO', attacker, { target: knockedOut, causedKO: true }); } private triggerBeforeMoveUse(piclet: BattlePiclet, move: Move): void { this.triggerAbilities('beforeMoveUse', piclet, { move }); } private triggerAfterMoveUse(piclet: BattlePiclet, move: Move, success: boolean): void { this.triggerAbilities('afterMoveUse', piclet, { move, success }); } private triggerOnFullHP(piclet: BattlePiclet): void { if (piclet.currentHp === piclet.maxHp) { this.triggerAbilities('onFullHP', piclet); } } private triggerOnOpponentContactMove(defender: BattlePiclet, attacker: BattlePiclet, move: Move): void { if (move.flags.includes('contact')) { this.triggerAbilities('onOpponentContactMove', defender, { attacker, move }); } } private triggerOnStatChange(piclet: BattlePiclet, stat: string, change: string): void { this.triggerAbilities('onStatChange', piclet, { stat, change }); } }