piclets / src /lib /battle-engine /BattleEngine.ts
Fraser's picture
better battle
94e4b64
/**
* 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<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;
// 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<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);
}
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<string, string> = {
'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 });
}
}