piclets / src /lib /battle-engine /BattleEngine.ts
Fraser's picture
alll working??
a6cd8d1
raw
history blame
35.7 kB
/**
* Core Battle Engine for Pictuary
* Implements the battle system as defined in battle_system_design.md
*/
import type {
BattleState,
BattlePiclet,
PicletDefinition,
BattleAction,
MoveAction,
BattleEffect,
DamageAmount,
StatModification,
HealAmount,
StatusEffect,
BaseStats,
Move
} from './types';
import { getEffectivenessMultiplier } from '../types/picletTypes';
export class BattleEngine {
private state: BattleState;
constructor(playerPiclet: PicletDefinition, opponentPiclet: PicletDefinition, playerLevel = 50, opponentLevel = 50) {
this.state = {
turn: 1,
phase: 'selection',
playerPiclet: this.createBattlePiclet(playerPiclet, playerLevel),
opponentPiclet: this.createBattlePiclet(opponentPiclet, opponentLevel),
fieldEffects: [],
log: [],
winner: undefined
};
this.log('Battle started!');
this.log(`${playerPiclet.name} vs ${opponentPiclet.name}`);
}
private createBattlePiclet(definition: PicletDefinition, level: number): BattlePiclet {
// Calculate stats based on level (simplified formula)
const statMultiplier = 1 + (level - 50) * 0.02; // 2% per level above/below 50
const hp = Math.floor(definition.baseStats.hp * statMultiplier);
const attack = Math.floor(definition.baseStats.attack * statMultiplier);
const defense = Math.floor(definition.baseStats.defense * statMultiplier);
const speed = Math.floor(definition.baseStats.speed * statMultiplier);
const piclet: BattlePiclet = {
definition,
currentHp: hp,
maxHp: hp,
level,
attack,
defense,
speed,
accuracy: 100, // 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';
this.log(`Turn ${this.state.turn} - Actions: ${playerAction.type} vs ${opponentAction.type}`);
// 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();
}
// 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.log(`${action.executor} attempted to switch (not implemented)`);
}
}
private executeMove(action: MoveAction & { executor: 'player' | 'opponent' }): void {
const attacker = action.executor === 'player' ? this.state.playerPiclet : this.state.opponentPiclet;
const defender = action.executor === 'player' ? this.state.opponentPiclet : this.state.playerPiclet;
const moveData = attacker.moves[action.moveIndex];
if (!moveData || moveData.currentPP <= 0) {
this.log(`${attacker.definition.name} has no PP left for that move!`);
return;
}
const move = moveData.move;
this.log(`${attacker.definition.name} used ${move.name}!`);
// Consume PP
moveData.currentPP--;
// Check if move hits
if (!this.checkMoveHits(move, attacker, defender)) {
this.log(`${attacker.definition.name}'s attack missed!`);
return;
}
// 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);
}
}
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);
// Check for critical hits
const critMod = this.checkCriticalHitModification(attacker, target);
if (critMod === 'always' || (critMod === 'normal' && Math.random() < 0.0625)) { // 1/16 base crit rate
damage = Math.floor(damage * 1.5);
this.log("A critical hit!");
}
// Apply damage
if (damage > 0) {
target.currentHp = Math.max(0, target.currentHp - damage);
this.log(`${target.definition.name} took ${damage} damage!`);
// 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!`);
}
} 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);
}
this.log(`${target.definition.name}'s ${stat} ${modification.includes('increase') ? 'rose' : 'fell'}!`);
}
}
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;
}
if (!target.statusEffects.includes(effect.status)) {
target.statusEffects.push(effect.status);
this.log(`${target.definition.name} was ${effect.status}ed!`);
}
}
private processHealEffect(effect: { amount?: HealAmount; formula?: string; value?: number }, target: BattlePiclet): void {
let healAmount = 0;
if (effect.formula) {
switch (effect.formula) {
case 'percentage':
healAmount = Math.floor(target.maxHp * ((effect.value || 0) / 100));
break;
case 'fixed':
healAmount = effect.value || 0;
break;
default:
healAmount = this.getHealAmount(effect.amount || 'medium', target.maxHp);
}
} else if (effect.amount) {
healAmount = this.getHealAmount(effect.amount, target.maxHp);
}
// 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!`);
}
}
}
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);
// Process field effects
this.processFieldEffects();
// Decrement temporary effects
this.processTemporaryEffects(this.state.playerPiclet);
this.processTemporaryEffects(this.state.opponentPiclet);
}
private processStatusEffects(piclet: BattlePiclet): void {
for (const status of piclet.statusEffects) {
switch (status) {
case 'burn':
case 'poison':
const damage = Math.floor(piclet.maxHp / 8);
piclet.currentHp = Math.max(0, piclet.currentHp - damage);
this.log(`${piclet.definition.name} was hurt by ${status}!`);
break;
// Other status effects can be implemented later
}
}
}
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 {
// Process field effects at end of turn
for (const fieldEffect of this.state.fieldEffects) {
switch (fieldEffect.name) {
case 'spikes':
// Spikes damage any piclet that switches in (for now, just log)
this.log('Spikes are scattered on the battlefield!');
break;
case 'stealth_rock':
// Stealth Rock damages based on type effectiveness
this.log('Pointed stones float in the air!');
break;
case 'reflect':
// Reduce physical damage
this.log('A barrier reflects physical attacks!');
break;
case 'light_screen':
// Reduce special damage
this.log('A barrier weakens special attacks!');
break;
}
}
// Decrement field effect durations
this.state.fieldEffects = this.state.fieldEffects.filter(effect => {
effect.duration--;
if (effect.duration <= 0) {
this.log(`${effect.name} faded away!`);
return false;
}
return true;
});
}
private checkBattleEnd(): void {
if (this.state.playerPiclet.currentHp <= 0 && this.state.opponentPiclet.currentHp <= 0) {
this.state.winner = 'draw';
this.state.phase = 'ended';
this.log('Battle ended in a draw!');
} else if (this.state.playerPiclet.currentHp <= 0) {
this.state.winner = 'opponent';
this.state.phase = 'ended';
this.log(`${this.state.opponentPiclet.definition.name} wins!`);
} else if (this.state.opponentPiclet.currentHp <= 0) {
this.state.winner = 'player';
this.state.phase = 'ended';
this.log(`${this.state.playerPiclet.definition.name} wins!`);
}
}
private log(message: string): void {
this.state.log.push(message);
}
// Public method to get battle log
public getLog(): string[] {
return [...this.state.log];
}
// 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 {
// Add field effect to battle state
const fieldEffect = {
name: effect.effect,
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 !== effect.effect);
}
this.state.fieldEffects.push(fieldEffect);
switch (effect.effect) {
case 'spikes':
this.log('Spikes were set on the field!');
break;
case 'reflect':
this.log('Reflect was applied to the field!');
break;
case 'lightScreen':
this.log('Light Screen was applied to the field!');
break;
case 'stealthRock':
this.log('Stealth Rock was applied to the field!');
break;
default:
this.log(`${effect.effect.charAt(0).toUpperCase() + effect.effect.slice(1)} was applied to the field!`);
}
}
private processCounterEffect(effect: { counterType: string; strength: string }, attacker: BattlePiclet, _target: BattlePiclet): void {
// 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',
counterType: effect.counterType,
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 = counterEffect.counterType === 'any' ||
(counterEffect.counterType === 'physical' && move.flags.includes('contact')) ||
(counterEffect.counterType === 'special' && !move.flags.includes('contact'));
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);
}
}
}
}
}