/** * 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 === 'ended') break; this.executeAction(action); } // End of turn processing this.processTurnEnd(); // Check for battle end this.checkBattleEnd(); if (this.state.phase !== '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.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': const mechanicTarget = this.resolveTarget(effect.target, attacker, defender); if (mechanicTarget) this.processMechanicOverrideEffect(effect, mechanicTarget); 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) const stab = (move.type === attacker.definition.primaryType || move.type === attacker.definition.secondaryType) ? 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) const stab = (move.type === attacker.definition.primaryType || move.type === attacker.definition.secondaryType) ? 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); } 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 === 'percentage' && effect.value !== undefined) { // Handle percentage healing when specified as amount instead of formula healAmount = Math.floor(target.maxHp * (effect.value / 100)); } else if (effect.amount) { healAmount = this.getHealAmount(effect.amount, target.maxHp); } 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); } } } } }