"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Pokemon = exports.RESTORATIVE_BERRIES = void 0;
/**
 * Simulator Pokemon
 * Pokemon Showdown - http://pokemonshowdown.com/
 *
 * @license MIT license
 */
const state_1 = require("./state");
const dex_1 = require("./dex");
// Berries which restore PP/HP and thus inflict external staleness when given to an opponent as
// there are very few non-malicious competitive reasons to do so
exports.RESTORATIVE_BERRIES = new Set([
    'leppaberry', 'aguavberry', 'enigmaberry', 'figyberry', 'iapapaberry', 'magoberry', 'sitrusberry', 'wikiberry', 'oranberry',
]);
class Pokemon {
    constructor(set, side) {
        this.getDetails = () => {
            const health = this.getHealth();
            let details = this.details;
            if (this.illusion) {
                let displayedSpeciesName = this.illusion.species.name;
                if (displayedSpeciesName === 'Greninja-Bond')
                    displayedSpeciesName = 'Greninja';
                const illusionDetails = displayedSpeciesName + (this.level === 100 ? '' : ', L' + this.level) +
                    (this.illusion.gender === '' ? '' : ', ' + this.illusion.gender) + (this.illusion.set.shiny ? ', shiny' : '');
                details = illusionDetails;
            }
            if (this.terastallized)
                details += `, tera:${this.terastallized}`;
            return { side: health.side, secret: `${details}|${health.secret}`, shared: `${details}|${health.shared}` };
        };
        this.getHealth = () => {
            if (!this.hp)
                return { side: this.side.id, secret: '0 fnt', shared: '0 fnt' };
            let secret = `${this.hp}/${this.maxhp}`;
            let shared;
            const ratio = this.hp / this.maxhp;
            if (this.battle.reportExactHP) {
                shared = secret;
            }
            else if (this.battle.reportPercentages || this.battle.gen >= 8) {
                // HP Percentage Mod mechanics
                let percentage = Math.ceil(ratio * 100);
                if ((percentage === 100) && (ratio < 1.0)) {
                    percentage = 99;
                }
                shared = `${percentage}/100`;
            }
            else {
                // In-game accurate pixel health mechanics
                const pixels = Math.floor(ratio * 48) || 1;
                shared = `${pixels}/48`;
                if ((pixels === 9) && (ratio > 0.2)) {
                    shared += 'y'; // force yellow HP bar
                }
                else if ((pixels === 24) && (ratio > 0.5)) {
                    shared += 'g'; // force green HP bar
                }
            }
            if (this.status) {
                secret += ` ${this.status}`;
                shared += ` ${this.status}`;
            }
            return { side: this.side.id, secret, shared };
        };
        this.side = side;
        this.battle = side.battle;
        this.m = {};
        const pokemonScripts = this.battle.format.pokemon || this.battle.dex.data.Scripts.pokemon;
        if (pokemonScripts)
            Object.assign(this, pokemonScripts);
        if (typeof set === 'string')
            set = { name: set };
        this.baseSpecies = this.battle.dex.species.get(set.species || set.name);
        if (!this.baseSpecies.exists) {
            throw new Error(`Unidentified species: ${this.baseSpecies.name}`);
        }
        this.set = set;
        this.species = this.baseSpecies;
        if (set.name === set.species || !set.name) {
            set.name = this.baseSpecies.baseSpecies;
        }
        this.speciesState = { id: this.species.id };
        this.name = set.name.substr(0, 20);
        this.fullname = this.side.id + ': ' + this.name;
        set.level = this.battle.clampIntRange(set.adjustLevel || set.level || 100, 1, 9999);
        this.level = set.level;
        const genders = { M: 'M', F: 'F', N: 'N' };
        this.gender = genders[set.gender] || this.species.gender || (this.battle.random() * 2 < 1 ? 'M' : 'F');
        if (this.gender === 'N')
            this.gender = '';
        this.happiness = typeof set.happiness === 'number' ? this.battle.clampIntRange(set.happiness, 0, 255) : 255;
        this.pokeball = this.set.pokeball || 'pokeball';
        this.dynamaxLevel = typeof set.dynamaxLevel === 'number' ? this.battle.clampIntRange(set.dynamaxLevel, 0, 10) : 10;
        this.gigantamax = this.set.gigantamax || false;
        this.baseMoveSlots = [];
        this.moveSlots = [];
        if (!this.set.moves?.length) {
            throw new Error(`Set ${this.name} has no moves`);
        }
        for (const moveid of this.set.moves) {
            let move = this.battle.dex.moves.get(moveid);
            if (!move.id)
                continue;
            if (move.id === 'hiddenpower' && move.type !== 'Normal') {
                if (!set.hpType)
                    set.hpType = move.type;
                move = this.battle.dex.moves.get('hiddenpower');
            }
            let basepp = (move.noPPBoosts || move.isZ) ? move.pp : move.pp * 8 / 5;
            if (this.battle.gen < 3)
                basepp = Math.min(61, basepp);
            this.baseMoveSlots.push({
                move: move.name,
                id: move.id,
                pp: basepp,
                maxpp: basepp,
                target: move.target,
                disabled: false,
                disabledSource: '',
                used: false,
            });
        }
        this.position = 0;
        let displayedSpeciesName = this.species.name;
        if (displayedSpeciesName === 'Greninja-Bond')
            displayedSpeciesName = 'Greninja';
        this.details = displayedSpeciesName + (this.level === 100 ? '' : ', L' + this.level) +
            (this.gender === '' ? '' : ', ' + this.gender) + (this.set.shiny ? ', shiny' : '');
        this.status = '';
        this.statusState = {};
        this.volatiles = {};
        this.showCure = undefined;
        if (!this.set.evs) {
            this.set.evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
        }
        if (!this.set.ivs) {
            this.set.ivs = { hp: 31, atk: 31, def: 31, spa: 31, spd: 31, spe: 31 };
        }
        const stats = { hp: 31, atk: 31, def: 31, spe: 31, spa: 31, spd: 31 };
        let stat;
        for (stat in stats) {
            if (!this.set.evs[stat])
                this.set.evs[stat] = 0;
            if (!this.set.ivs[stat] && this.set.ivs[stat] !== 0)
                this.set.ivs[stat] = 31;
        }
        for (stat in this.set.evs) {
            this.set.evs[stat] = this.battle.clampIntRange(this.set.evs[stat], 0, 255);
        }
        for (stat in this.set.ivs) {
            this.set.ivs[stat] = this.battle.clampIntRange(this.set.ivs[stat], 0, 31);
        }
        if (this.battle.gen && this.battle.gen <= 2) {
            // We represent DVs using even IVs. Ensure they are in fact even.
            for (stat in this.set.ivs) {
                this.set.ivs[stat] &= 30;
            }
        }
        const hpData = this.battle.dex.getHiddenPower(this.set.ivs);
        this.hpType = set.hpType || hpData.type;
        this.hpPower = hpData.power;
        this.baseHpType = this.hpType;
        this.baseHpPower = this.hpPower;
        // initialized in this.setSpecies(this.baseSpecies)
        this.baseStoredStats = null;
        this.storedStats = { atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
        this.boosts = { atk: 0, def: 0, spa: 0, spd: 0, spe: 0, accuracy: 0, evasion: 0 };
        this.baseAbility = (0, dex_1.toID)(set.ability);
        this.ability = this.baseAbility;
        this.abilityState = { id: this.ability };
        this.item = (0, dex_1.toID)(set.item);
        this.itemState = { id: this.item };
        this.lastItem = '';
        this.usedItemThisTurn = false;
        this.ateBerry = false;
        this.trapped = false;
        this.maybeTrapped = false;
        this.maybeDisabled = false;
        this.illusion = null;
        this.transformed = false;
        this.fainted = false;
        this.faintQueued = false;
        this.subFainted = null;
        this.types = this.baseSpecies.types;
        this.baseTypes = this.types;
        this.addedType = '';
        this.knownType = true;
        this.apparentType = this.baseSpecies.types.join('/');
        // Every Pokemon has a Terastal type
        this.teraType = this.set.teraType || this.types[0];
        this.switchFlag = false;
        this.forceSwitchFlag = false;
        this.skipBeforeSwitchOutEventFlag = false;
        this.draggedIn = null;
        this.newlySwitched = false;
        this.beingCalledBack = false;
        this.lastMove = null;
        // This is used in gen 2 only, here to avoid code repetition.
        // Only declared if gen 2 to avoid declaring an object we aren't going to need.
        if (this.battle.gen === 2)
            this.lastMoveEncore = null;
        this.lastMoveUsed = null;
        this.moveThisTurn = '';
        this.statsRaisedThisTurn = false;
        this.statsLoweredThisTurn = false;
        this.hurtThisTurn = null;
        this.lastDamage = 0;
        this.attackedBy = [];
        this.timesAttacked = 0;
        this.isActive = false;
        this.activeTurns = 0;
        this.activeMoveActions = 0;
        this.previouslySwitchedIn = 0;
        this.truantTurn = false;
        this.swordBoost = false;
        this.shieldBoost = false;
        this.syrupTriggered = false;
        this.isStarted = false;
        this.duringMove = false;
        this.weighthg = 1;
        this.speed = 0;
        /**
         * Determines the order in which redirect abilities like Lightning Rod
         * activate if speed tied. Surprisingly not random like every other speed
         * tie, but based on who first switched in or acquired the ability!
         */
        this.abilityOrder = 0;
        this.canMegaEvo = this.battle.actions.canMegaEvo(this);
        this.canUltraBurst = this.battle.actions.canUltraBurst(this);
        this.canGigantamax = this.baseSpecies.canGigantamax || null;
        this.canTerastallize = this.battle.actions.canTerastallize(this);
        // This is used in gen 1 only, here to avoid code repetition.
        // Only declared if gen 1 to avoid declaring an object we aren't going to need.
        if (this.battle.gen === 1)
            this.modifiedStats = { atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
        this.maxhp = 0;
        this.baseMaxhp = 0;
        this.hp = 0;
        this.clearVolatile();
        this.hp = this.maxhp;
    }
    toJSON() {
        return state_1.State.serializePokemon(this);
    }
    get moves() {
        return this.moveSlots.map(moveSlot => moveSlot.id);
    }
    get baseMoves() {
        return this.baseMoveSlots.map(moveSlot => moveSlot.id);
    }
    getSlot() {
        const positionOffset = Math.floor(this.side.n / 2) * this.side.active.length;
        const positionLetter = 'abcdef'.charAt(this.position + positionOffset);
        return (this.side.id + positionLetter);
    }
    toString() {
        const fullname = (this.illusion) ? this.illusion.fullname : this.fullname;
        return this.isActive ? this.getSlot() + fullname.slice(2) : fullname;
    }
    updateSpeed() {
        this.speed = this.getActionSpeed();
    }
    calculateStat(statName, boost, modifier, statUser) {
        statName = (0, dex_1.toID)(statName);
        // @ts-ignore - type checking prevents 'hp' from being passed, but we're paranoid
        if (statName === 'hp')
            throw new Error("Please read `maxhp` directly");
        // base stat
        let stat = this.storedStats[statName];
        // Wonder Room swaps defenses before calculating anything else
        if ('wonderroom' in this.battle.field.pseudoWeather) {
            if (statName === 'def') {
                stat = this.storedStats['spd'];
            }
            else if (statName === 'spd') {
                stat = this.storedStats['def'];
            }
        }
        // stat boosts
        let boosts = {};
        const boostName = statName;
        boosts[boostName] = boost;
        boosts = this.battle.runEvent('ModifyBoost', statUser || this, null, null, boosts);
        boost = boosts[boostName];
        const boostTable = [1, 1.5, 2, 2.5, 3, 3.5, 4];
        if (boost > 6)
            boost = 6;
        if (boost < -6)
            boost = -6;
        if (boost >= 0) {
            stat = Math.floor(stat * boostTable[boost]);
        }
        else {
            stat = Math.floor(stat / boostTable[-boost]);
        }
        // stat modifier
        return this.battle.modify(stat, (modifier || 1));
    }
    getStat(statName, unboosted, unmodified) {
        statName = (0, dex_1.toID)(statName);
        // @ts-ignore - type checking prevents 'hp' from being passed, but we're paranoid
        if (statName === 'hp')
            throw new Error("Please read `maxhp` directly");
        // base stat
        let stat = this.storedStats[statName];
        // Download ignores Wonder Room's effect, but this results in
        // stat stages being calculated on the opposite defensive stat
        if (unmodified && 'wonderroom' in this.battle.field.pseudoWeather) {
            if (statName === 'def') {
                statName = 'spd';
            }
            else if (statName === 'spd') {
                statName = 'def';
            }
        }
        // stat boosts
        if (!unboosted) {
            const boosts = this.battle.runEvent('ModifyBoost', this, null, null, { ...this.boosts });
            let boost = boosts[statName];
            const boostTable = [1, 1.5, 2, 2.5, 3, 3.5, 4];
            if (boost > 6)
                boost = 6;
            if (boost < -6)
                boost = -6;
            if (boost >= 0) {
                stat = Math.floor(stat * boostTable[boost]);
            }
            else {
                stat = Math.floor(stat / boostTable[-boost]);
            }
        }
        // stat modifier effects
        if (!unmodified) {
            const statTable = { atk: 'Atk', def: 'Def', spa: 'SpA', spd: 'SpD', spe: 'Spe' };
            stat = this.battle.runEvent('Modify' + statTable[statName], this, null, null, stat);
        }
        if (statName === 'spe' && stat > 10000 && !this.battle.format.battle?.trunc)
            stat = 10000;
        return stat;
    }
    getActionSpeed() {
        let speed = this.getStat('spe', false, false);
        if (this.battle.field.getPseudoWeather('trickroom')) {
            speed = 10000 - speed;
        }
        return this.battle.trunc(speed, 13);
    }
    /**
     * Gets the Pokemon's best stat.
     * Moved to its own method due to frequent use of the same code.
     * Used by Beast Boost, Quark Drive, and Protosynthesis.
     */
    getBestStat(unboosted, unmodified) {
        let statName = 'atk';
        let bestStat = 0;
        const stats = ['atk', 'def', 'spa', 'spd', 'spe'];
        for (const i of stats) {
            if (this.getStat(i, unboosted, unmodified) > bestStat) {
                statName = i;
                bestStat = this.getStat(i, unboosted, unmodified);
            }
        }
        return statName;
    }
    /* Commented out for now until a use for Combat Power is found in Let's Go
    getCombatPower() {
        let statSum = 0;
        let awakeningSum = 0;
        for (const stat in this.stats) {
            statSum += this.calculateStat(stat, this.boosts[stat as BoostName]);
            awakeningSum += this.calculateStat(
                stat, this.boosts[stat as BoostName]) + this.set.evs[stat];
        }
        const combatPower = Math.floor(Math.floor(statSum * this.level * 6 / 100) +
            (Math.floor(awakeningSum) * Math.floor((this.level * 4) / 100 + 2)));
        return this.battle.clampIntRange(combatPower, 0, 10000);
    }
    */
    getWeight() {
        const weighthg = this.battle.runEvent('ModifyWeight', this, null, null, this.weighthg);
        return Math.max(1, weighthg);
    }
    getMoveData(move) {
        move = this.battle.dex.moves.get(move);
        for (const moveSlot of this.moveSlots) {
            if (moveSlot.id === move.id) {
                return moveSlot;
            }
        }
        return null;
    }
    getMoveHitData(move) {
        if (!move.moveHitData)
            move.moveHitData = {};
        const slot = this.getSlot();
        return move.moveHitData[slot] || (move.moveHitData[slot] = {
            crit: false,
            typeMod: 0,
            zBrokeProtect: false,
        });
    }
    alliesAndSelf() {
        return this.side.allies();
    }
    allies() {
        return this.side.allies().filter(ally => ally !== this);
    }
    adjacentAllies() {
        return this.side.allies().filter(ally => this.isAdjacent(ally));
    }
    foes(all) {
        return this.side.foes(all);
    }
    adjacentFoes() {
        if (this.battle.activePerHalf <= 2)
            return this.side.foes();
        return this.side.foes().filter(foe => this.isAdjacent(foe));
    }
    isAlly(pokemon) {
        return !!pokemon && (this.side === pokemon.side || this.side.allySide === pokemon.side);
    }
    isAdjacent(pokemon2) {
        if (this.fainted || pokemon2.fainted)
            return false;
        if (this.battle.activePerHalf <= 2)
            return this !== pokemon2;
        if (this.side === pokemon2.side)
            return Math.abs(this.position - pokemon2.position) === 1;
        return Math.abs(this.position + pokemon2.position + 1 - this.side.active.length) <= 1;
    }
    getUndynamaxedHP(amount) {
        const hp = amount || this.hp;
        if (this.volatiles['dynamax']) {
            return Math.ceil(hp * this.baseMaxhp / this.maxhp);
        }
        return hp;
    }
    /** Get targets for Dragon Darts */
    getSmartTargets(target, move) {
        const target2 = target.adjacentAllies()[0];
        if (!target2 || target2 === this || !target2.hp) {
            move.smartTarget = false;
            return [target];
        }
        if (!target.hp) {
            move.smartTarget = false;
            return [target2];
        }
        return [target, target2];
    }
    getAtLoc(targetLoc) {
        let side = this.battle.sides[targetLoc < 0 ? this.side.n % 2 : (this.side.n + 1) % 2];
        targetLoc = Math.abs(targetLoc);
        if (targetLoc > side.active.length) {
            targetLoc -= side.active.length;
            side = this.battle.sides[side.n + 2];
        }
        return side.active[targetLoc - 1];
    }
    /**
     * Returns a relative location: 1-3, positive for foe, and negative for ally.
     * Use `getAtLoc` to reverse.
     */
    getLocOf(target) {
        const positionOffset = Math.floor(target.side.n / 2) * target.side.active.length;
        const position = target.position + positionOffset + 1;
        const sameHalf = (this.side.n % 2) === (target.side.n % 2);
        return sameHalf ? -position : position;
    }
    getMoveTargets(move, target) {
        let targets = [];
        switch (move.target) {
            case 'all':
            case 'foeSide':
            case 'allySide':
            case 'allyTeam':
                if (!move.target.startsWith('foe')) {
                    targets.push(...this.alliesAndSelf());
                }
                if (!move.target.startsWith('ally')) {
                    targets.push(...this.foes(true));
                }
                if (targets.length && !targets.includes(target)) {
                    this.battle.retargetLastMove(targets[targets.length - 1]);
                }
                break;
            case 'allAdjacent':
                targets.push(...this.adjacentAllies());
            // falls through
            case 'allAdjacentFoes':
                targets.push(...this.adjacentFoes());
                if (targets.length && !targets.includes(target)) {
                    this.battle.retargetLastMove(targets[targets.length - 1]);
                }
                break;
            case 'allies':
                targets = this.alliesAndSelf();
                break;
            default:
                const selectedTarget = target;
                if (!target || (target.fainted && !target.isAlly(this)) && this.battle.gameType !== 'freeforall') {
                    // If a targeted foe faints, the move is retargeted
                    const possibleTarget = this.battle.getRandomTarget(this, move);
                    if (!possibleTarget)
                        return { targets: [], pressureTargets: [] };
                    target = possibleTarget;
                }
                if (this.battle.activePerHalf > 1 && !move.tracksTarget) {
                    const isCharging = move.flags['charge'] && !this.volatiles['twoturnmove'] &&
                        !(move.id.startsWith('solarb') && this.battle.field.isWeather(['sunnyday', 'desolateland'])) &&
                        !(this.hasItem('powerherb') && move.id !== 'skydrop');
                    if (!isCharging) {
                        target = this.battle.priorityEvent('RedirectTarget', this, this, move, target);
                    }
                }
                if (move.smartTarget) {
                    targets = this.getSmartTargets(target, move);
                    target = targets[0];
                }
                else {
                    targets.push(target);
                }
                if (target.fainted && !move.flags['futuremove']) {
                    return { targets: [], pressureTargets: [] };
                }
                if (selectedTarget !== target) {
                    this.battle.retargetLastMove(target);
                }
        }
        // Resolve apparent targets for Pressure.
        let pressureTargets = targets;
        if (move.target === 'foeSide') {
            pressureTargets = [];
        }
        if (move.flags['mustpressure']) {
            pressureTargets = this.foes();
        }
        return { targets, pressureTargets };
    }
    ignoringAbility() {
        if (this.battle.gen >= 5 && !this.isActive)
            return true;
        if (this.getAbility().isPermanent)
            return false;
        if (this.volatiles['gastroacid'])
            return true;
        // Check if any active pokemon have the ability Neutralizing Gas
        if (this.hasItem('Ability Shield') || this.ability === 'neutralizinggas')
            return false;
        for (const pokemon of this.battle.getAllActive()) {
            // can't use hasAbility because it would lead to infinite recursion
            if (pokemon.ability === 'neutralizinggas' && !pokemon.volatiles['gastroacid'] &&
                !pokemon.transformed && !pokemon.abilityState.ending && !this.volatiles['commanding']) {
                return true;
            }
        }
        return false;
    }
    ignoringItem() {
        return !!(this.itemState.knockedOff || // Gen 3-4
            (this.battle.gen >= 5 && !this.isActive) ||
            (!this.getItem().ignoreKlutz && this.hasAbility('klutz')) ||
            this.volatiles['embargo'] || this.battle.field.pseudoWeather['magicroom']);
    }
    deductPP(move, amount, target) {
        const gen = this.battle.gen;
        move = this.battle.dex.moves.get(move);
        const ppData = this.getMoveData(move);
        if (!ppData)
            return 0;
        ppData.used = true;
        if (!ppData.pp && gen > 1)
            return 0;
        if (!amount)
            amount = 1;
        ppData.pp -= amount;
        if (ppData.pp < 0 && gen > 1) {
            amount += ppData.pp;
            ppData.pp = 0;
        }
        return amount;
    }
    moveUsed(move, targetLoc) {
        this.lastMove = move;
        if (this.battle.gen === 2)
            this.lastMoveEncore = move;
        this.lastMoveTargetLoc = targetLoc;
        this.moveThisTurn = move.id;
    }
    gotAttacked(move, damage, source) {
        const damageNumber = (typeof damage === 'number') ? damage : 0;
        move = this.battle.dex.moves.get(move);
        this.attackedBy.push({
            source,
            damage: damageNumber,
            move: move.id,
            thisTurn: true,
            slot: source.getSlot(),
            damageValue: damage,
        });
    }
    getLastAttackedBy() {
        if (this.attackedBy.length === 0)
            return undefined;
        return this.attackedBy[this.attackedBy.length - 1];
    }
    getLastDamagedBy(filterOutSameSide) {
        const damagedBy = this.attackedBy.filter(attacker => (typeof attacker.damageValue === 'number' &&
            (filterOutSameSide === undefined || !this.isAlly(attacker.source))));
        if (damagedBy.length === 0)
            return undefined;
        return damagedBy[damagedBy.length - 1];
    }
    /**
     * This refers to multi-turn moves like SolarBeam and Outrage and
     * Sky Drop, which remove all choice (no dynamax, switching, etc).
     * Don't use it for "soft locks" like Choice Band.
     */
    getLockedMove() {
        const lockedMove = this.battle.runEvent('LockMove', this);
        return (lockedMove === true) ? null : lockedMove;
    }
    getMoves(lockedMove, restrictData) {
        if (lockedMove) {
            lockedMove = (0, dex_1.toID)(lockedMove);
            this.trapped = true;
            if (lockedMove === 'recharge') {
                return [{
                        move: 'Recharge',
                        id: 'recharge',
                    }];
            }
            for (const moveSlot of this.moveSlots) {
                if (moveSlot.id !== lockedMove)
                    continue;
                return [{
                        move: moveSlot.move,
                        id: moveSlot.id,
                    }];
            }
            // does this happen?
            return [{
                    move: this.battle.dex.moves.get(lockedMove).name,
                    id: lockedMove,
                }];
        }
        const moves = [];
        let hasValidMove = false;
        for (const moveSlot of this.moveSlots) {
            let moveName = moveSlot.move;
            if (moveSlot.id === 'hiddenpower') {
                moveName = 'Hidden Power ' + this.hpType;
                if (this.battle.gen < 6)
                    moveName += ' ' + this.hpPower;
            }
            else if (moveSlot.id === 'return' || moveSlot.id === 'frustration') {
                const basePowerCallback = this.battle.dex.moves.get(moveSlot.id).basePowerCallback;
                moveName += ' ' + basePowerCallback(this);
            }
            let target = moveSlot.target;
            if (moveSlot.id === 'curse') {
                if (!this.hasType('Ghost')) {
                    target = this.battle.dex.moves.get('curse').nonGhostTarget || moveSlot.target;
                }
                // Heal Block only prevents Pollen Puff from targeting an ally when the user has Heal Block
            }
            else if (moveSlot.id === 'pollenpuff' && this.volatiles['healblock']) {
                target = 'adjacentFoe';
            }
            let disabled = moveSlot.disabled;
            if (this.volatiles['dynamax']) {
                // if each of a Pokemon's base moves are disabled by one of these effects, it will Struggle
                const canCauseStruggle = ['Encore', 'Disable', 'Taunt', 'Assault Vest', 'Belch', 'Stuff Cheeks'];
                disabled = this.maxMoveDisabled(moveSlot.id) || disabled && canCauseStruggle.includes(moveSlot.disabledSource);
            }
            else if ((moveSlot.pp <= 0 && !this.volatiles['partialtrappinglock']) || disabled &&
                this.side.active.length >= 2 && this.battle.actions.targetTypeChoices(target)) {
                disabled = true;
            }
            if (!disabled) {
                hasValidMove = true;
            }
            else if (disabled === 'hidden' && restrictData) {
                disabled = false;
            }
            moves.push({
                move: moveName,
                id: moveSlot.id,
                pp: moveSlot.pp,
                maxpp: moveSlot.maxpp,
                target,
                disabled,
            });
        }
        return hasValidMove ? moves : [];
    }
    /** This should be passed the base move and not the corresponding max move so we can check how much PP is left. */
    maxMoveDisabled(baseMove) {
        baseMove = this.battle.dex.moves.get(baseMove);
        if (!this.getMoveData(baseMove.id)?.pp)
            return true;
        return !!(baseMove.category === 'Status' && (this.hasItem('assaultvest') || this.volatiles['taunt']));
    }
    getDynamaxRequest(skipChecks) {
        // {gigantamax?: string, maxMoves: {[k: string]: string} | null}[]
        if (!skipChecks) {
            if (!this.side.canDynamaxNow())
                return;
            if (this.species.isMega || this.species.isPrimal || this.species.forme === "Ultra" ||
                this.getItem().zMove || this.canMegaEvo) {
                return;
            }
            // Some pokemon species are unable to dynamax
            if (this.species.cannotDynamax || this.illusion?.species.cannotDynamax)
                return;
        }
        const result = { maxMoves: [] };
        let atLeastOne = false;
        for (const moveSlot of this.moveSlots) {
            const move = this.battle.dex.moves.get(moveSlot.id);
            const maxMove = this.battle.actions.getMaxMove(move, this);
            if (maxMove) {
                if (this.maxMoveDisabled(move)) {
                    result.maxMoves.push({ move: maxMove.id, target: maxMove.target, disabled: true });
                }
                else {
                    result.maxMoves.push({ move: maxMove.id, target: maxMove.target });
                    atLeastOne = true;
                }
            }
        }
        if (!atLeastOne)
            return;
        if (this.canGigantamax)
            result.gigantamax = this.canGigantamax;
        return result;
    }
    getMoveRequestData() {
        let lockedMove = this.getLockedMove();
        // Information should be restricted for the last active Pokémon
        const isLastActive = this.isLastActive();
        const canSwitchIn = this.battle.canSwitch(this.side) > 0;
        let moves = this.getMoves(lockedMove, isLastActive);
        if (!moves.length) {
            moves = [{ move: 'Struggle', id: 'struggle', target: 'randomNormal', disabled: false }];
            lockedMove = 'struggle';
        }
        const data = {
            moves,
        };
        if (isLastActive) {
            if (this.maybeDisabled) {
                data.maybeDisabled = true;
            }
            if (canSwitchIn) {
                if (this.trapped === true) {
                    data.trapped = true;
                }
                else if (this.maybeTrapped) {
                    data.maybeTrapped = true;
                }
            }
        }
        else if (canSwitchIn) {
            // Discovered by selecting a valid Pokémon as a switch target and cancelling.
            if (this.trapped)
                data.trapped = true;
        }
        if (!lockedMove) {
            if (this.canMegaEvo)
                data.canMegaEvo = true;
            if (this.canUltraBurst)
                data.canUltraBurst = true;
            const canZMove = this.battle.actions.canZMove(this);
            if (canZMove)
                data.canZMove = canZMove;
            if (this.getDynamaxRequest())
                data.canDynamax = true;
            if (data.canDynamax || this.volatiles['dynamax'])
                data.maxMoves = this.getDynamaxRequest(true);
            if (this.canTerastallize)
                data.canTerastallize = this.canTerastallize;
        }
        return data;
    }
    getSwitchRequestData(forAlly) {
        const entry = {
            ident: this.fullname,
            details: this.details,
            condition: this.getHealth().secret,
            active: (this.position < this.side.active.length),
            stats: {
                atk: this.baseStoredStats['atk'],
                def: this.baseStoredStats['def'],
                spa: this.baseStoredStats['spa'],
                spd: this.baseStoredStats['spd'],
                spe: this.baseStoredStats['spe'],
            },
            moves: this[forAlly ? 'baseMoves' : 'moves'].map(move => {
                if (move === 'hiddenpower') {
                    return move + (0, dex_1.toID)(this.hpType) + (this.battle.gen < 6 ? '' : this.hpPower);
                }
                if (move === 'frustration' || move === 'return') {
                    const basePowerCallback = this.battle.dex.moves.get(move).basePowerCallback;
                    return move + basePowerCallback(this);
                }
                return move;
            }),
            baseAbility: this.baseAbility,
            item: this.item,
            pokeball: this.pokeball,
        };
        if (this.battle.gen > 6)
            entry.ability = this.ability;
        if (this.battle.gen >= 9) {
            entry.commanding = !!this.volatiles['commanding'] && !this.fainted;
            entry.reviving = this.isActive && !!this.side.slotConditions[this.position]['revivalblessing'];
        }
        if (this.battle.gen === 9) {
            entry.teraType = this.teraType;
            entry.terastallized = this.terastallized || '';
        }
        return entry;
    }
    isLastActive() {
        if (!this.isActive)
            return false;
        const allyActive = this.side.active;
        for (let i = this.position + 1; i < allyActive.length; i++) {
            if (allyActive[i] && !allyActive[i].fainted)
                return false;
        }
        return true;
    }
    positiveBoosts() {
        let boosts = 0;
        let boost;
        for (boost in this.boosts) {
            if (this.boosts[boost] > 0)
                boosts += this.boosts[boost];
        }
        return boosts;
    }
    getCappedBoost(boosts) {
        const cappedBoost = {};
        let boostName;
        for (boostName in boosts) {
            const boost = boosts[boostName];
            if (!boost)
                continue;
            cappedBoost[boostName] = this.battle.clampIntRange(this.boosts[boostName] + boost, -6, 6) - this.boosts[boostName];
        }
        return cappedBoost;
    }
    boostBy(boosts) {
        boosts = this.getCappedBoost(boosts);
        let delta = 0;
        let boostName;
        for (boostName in boosts) {
            delta = boosts[boostName];
            this.boosts[boostName] += delta;
        }
        return delta;
    }
    clearBoosts() {
        let boostName;
        for (boostName in this.boosts) {
            this.boosts[boostName] = 0;
        }
    }
    setBoost(boosts) {
        let boostName;
        for (boostName in boosts) {
            this.boosts[boostName] = boosts[boostName];
        }
    }
    copyVolatileFrom(pokemon, switchCause) {
        this.clearVolatile();
        if (switchCause !== 'shedtail')
            this.boosts = pokemon.boosts;
        for (const i in pokemon.volatiles) {
            if (switchCause === 'shedtail' && i !== 'substitute')
                continue;
            if (this.battle.dex.conditions.getByID(i).noCopy)
                continue;
            // shallow clones
            this.volatiles[i] = { ...pokemon.volatiles[i] };
            if (this.volatiles[i].linkedPokemon) {
                delete pokemon.volatiles[i].linkedPokemon;
                delete pokemon.volatiles[i].linkedStatus;
                for (const linkedPoke of this.volatiles[i].linkedPokemon) {
                    const linkedPokeLinks = linkedPoke.volatiles[this.volatiles[i].linkedStatus].linkedPokemon;
                    linkedPokeLinks[linkedPokeLinks.indexOf(pokemon)] = this;
                }
            }
        }
        pokemon.clearVolatile();
        for (const i in this.volatiles) {
            const volatile = this.getVolatile(i);
            this.battle.singleEvent('Copy', volatile, this.volatiles[i], this);
        }
    }
    transformInto(pokemon, effect) {
        const species = pokemon.species;
        if (pokemon.fainted || this.illusion || pokemon.illusion || (pokemon.volatiles['substitute'] && this.battle.gen >= 5) ||
            (pokemon.transformed && this.battle.gen >= 2) || (this.transformed && this.battle.gen >= 5) ||
            species.name === 'Eternatus-Eternamax' || (species.baseSpecies === 'Ogerpon' &&
            (this.terastallized || pokemon.terastallized))) {
            return false;
        }
        if (this.battle.dex.currentMod === 'gen1stadium' && (species.name === 'Ditto' ||
            (this.species.name === 'Ditto' && pokemon.moves.includes('transform')))) {
            return false;
        }
        if (!this.setSpecies(species, effect, true))
            return false;
        this.transformed = true;
        this.weighthg = pokemon.weighthg;
        const types = pokemon.getTypes(true, true);
        this.setType(pokemon.volatiles['roost'] ? pokemon.volatiles['roost'].typeWas : types, true);
        this.addedType = pokemon.addedType;
        this.knownType = this.isAlly(pokemon) && pokemon.knownType;
        this.apparentType = pokemon.apparentType;
        let statName;
        for (statName in this.storedStats) {
            this.storedStats[statName] = pokemon.storedStats[statName];
            if (this.modifiedStats)
                this.modifiedStats[statName] = pokemon.modifiedStats[statName]; // Gen 1: Copy modified stats.
        }
        this.moveSlots = [];
        this.hpType = (this.battle.gen >= 5 ? this.hpType : pokemon.hpType);
        this.hpPower = (this.battle.gen >= 5 ? this.hpPower : pokemon.hpPower);
        this.timesAttacked = pokemon.timesAttacked;
        for (const moveSlot of pokemon.moveSlots) {
            let moveName = moveSlot.move;
            if (moveSlot.id === 'hiddenpower') {
                moveName = 'Hidden Power ' + this.hpType;
            }
            this.moveSlots.push({
                move: moveName,
                id: moveSlot.id,
                pp: moveSlot.maxpp === 1 ? 1 : 5,
                maxpp: this.battle.gen >= 5 ? (moveSlot.maxpp === 1 ? 1 : 5) : moveSlot.maxpp,
                target: moveSlot.target,
                disabled: false,
                used: false,
                virtual: true,
            });
        }
        let boostName;
        for (boostName in pokemon.boosts) {
            this.boosts[boostName] = pokemon.boosts[boostName];
        }
        if (this.battle.gen >= 6) {
            const volatilesToCopy = ['focusenergy', 'gmaxchistrike', 'laserfocus'];
            for (const volatile of volatilesToCopy) {
                if (pokemon.volatiles[volatile]) {
                    this.addVolatile(volatile);
                    if (volatile === 'gmaxchistrike')
                        this.volatiles[volatile].layers = pokemon.volatiles[volatile].layers;
                }
                else {
                    this.removeVolatile(volatile);
                }
            }
        }
        if (effect) {
            this.battle.add('-transform', this, pokemon, '[from] ' + effect.fullname);
        }
        else {
            this.battle.add('-transform', this, pokemon);
        }
        if (this.terastallized) {
            this.knownType = true;
            this.apparentType = this.terastallized;
        }
        if (this.battle.gen > 2)
            this.setAbility(pokemon.ability, this, true, true);
        // Change formes based on held items (for Transform)
        // Only ever relevant in Generation 4 since Generation 3 didn't have item-based forme changes
        if (this.battle.gen === 4) {
            if (this.species.num === 487) {
                // Giratina formes
                if (this.species.name === 'Giratina' && this.item === 'griseousorb') {
                    this.formeChange('Giratina-Origin');
                }
                else if (this.species.name === 'Giratina-Origin' && this.item !== 'griseousorb') {
                    this.formeChange('Giratina');
                }
            }
            if (this.species.num === 493) {
                // Arceus formes
                const item = this.getItem();
                const targetForme = (item?.onPlate ? 'Arceus-' + item.onPlate : 'Arceus');
                if (this.species.name !== targetForme) {
                    this.formeChange(targetForme);
                }
            }
        }
        // Pokemon transformed into Ogerpon cannot Terastallize
        // restoring their ability to tera after they untransform is handled ELSEWHERE
        if (this.species.baseSpecies === 'Ogerpon' && this.canTerastallize)
            this.canTerastallize = false;
        return true;
    }
    /**
     * Changes this Pokemon's species to the given speciesId (or species).
     * This function only handles changes to stats and type.
     * Use formChange to handle changes to ability and sending client messages.
     */
    setSpecies(rawSpecies, source = this.battle.effect, isTransform = false) {
        const species = this.battle.runEvent('ModifySpecies', this, null, source, rawSpecies);
        if (!species)
            return null;
        this.species = species;
        this.setType(species.types, true);
        this.apparentType = rawSpecies.types.join('/');
        this.addedType = species.addedType || '';
        this.knownType = true;
        this.weighthg = species.weighthg;
        const stats = this.battle.spreadModify(this.species.baseStats, this.set);
        if (this.species.maxHP)
            stats.hp = this.species.maxHP;
        if (!this.maxhp) {
            this.baseMaxhp = stats.hp;
            this.maxhp = stats.hp;
            this.hp = stats.hp;
        }
        if (!isTransform)
            this.baseStoredStats = stats;
        let statName;
        for (statName in this.storedStats) {
            this.storedStats[statName] = stats[statName];
            if (this.modifiedStats)
                this.modifiedStats[statName] = stats[statName]; // Gen 1: Reset modified stats.
        }
        if (this.battle.gen <= 1) {
            // Gen 1: Re-Apply burn and para drops.
            if (this.status === 'par')
                this.modifyStat('spe', 0.25);
            if (this.status === 'brn')
                this.modifyStat('atk', 0.5);
        }
        this.speed = this.storedStats.spe;
        return species;
    }
    /**
     * Changes this Pokemon's forme to match the given speciesId (or species).
     * This function handles all changes to stats, ability, type, species, etc.
     * as well as sending all relevant messages sent to the client.
     */
    formeChange(speciesId, source = this.battle.effect, isPermanent, message) {
        const rawSpecies = this.battle.dex.species.get(speciesId);
        const species = this.setSpecies(rawSpecies, source);
        if (!species)
            return false;
        if (this.battle.gen <= 2)
            return true;
        // The species the opponent sees
        const apparentSpecies = this.illusion ? this.illusion.species.name : species.baseSpecies;
        if (isPermanent) {
            this.baseSpecies = rawSpecies;
            this.details = species.name + (this.level === 100 ? '' : ', L' + this.level) +
                (this.gender === '' ? '' : ', ' + this.gender) + (this.set.shiny ? ', shiny' : '');
            let details = (this.illusion || this).details;
            if (this.terastallized)
                details += `, tera:${this.terastallized}`;
            this.battle.add('detailschange', this, details);
            if (source.effectType === 'Item') {
                this.canTerastallize = null; // National Dex behavior
                if (source.zMove) {
                    this.battle.add('-burst', this, apparentSpecies, species.requiredItem);
                    this.moveThisTurnResult = true; // Ultra Burst counts as an action for Truant
                }
                else if (source.onPrimal) {
                    if (this.illusion) {
                        this.ability = '';
                        this.battle.add('-primal', this.illusion, species.requiredItem);
                    }
                    else {
                        this.battle.add('-primal', this, species.requiredItem);
                    }
                }
                else {
                    // So a Mega Evolution message isn't sent while we're waiting on Ogerpon text
                    if (source.megaEvolves) {
                        this.battle.add('-mega', this, apparentSpecies, species.requiredItem);
                    }
                    this.moveThisTurnResult = true; // Mega Evolution counts as an action for Truant
                }
            }
            else if (source.effectType === 'Status') {
                // Shaymin-Sky -> Shaymin
                this.battle.add('-formechange', this, species.name, message);
            }
        }
        else {
            if (source.effectType === 'Ability') {
                this.battle.add('-formechange', this, species.name, message, `[from] ability: ${source.name}`);
            }
            else {
                this.battle.add('-formechange', this, this.illusion ? this.illusion.species.name : species.name, message);
            }
        }
        if (isPermanent && !['disguise', 'iceface'].includes(source.id)) {
            if (this.illusion) {
                this.ability = ''; // Don't allow Illusion to wear off
            }
            this.setAbility(species.abilities['0'], null, true);
            this.baseAbility = this.ability;
        }
        if (this.terastallized) {
            this.knownType = true;
            this.apparentType = this.terastallized;
        }
        return true;
    }
    clearVolatile(includeSwitchFlags = true) {
        this.boosts = {
            atk: 0,
            def: 0,
            spa: 0,
            spd: 0,
            spe: 0,
            accuracy: 0,
            evasion: 0,
        };
        if (this.battle.gen === 1 && this.baseMoves.includes('mimic') && !this.transformed) {
            const moveslot = this.baseMoves.indexOf('mimic');
            const mimicPP = this.moveSlots[moveslot] ? this.moveSlots[moveslot].pp : 16;
            this.moveSlots = this.baseMoveSlots.slice();
            this.moveSlots[moveslot].pp = mimicPP;
        }
        else {
            this.moveSlots = this.baseMoveSlots.slice();
        }
        this.transformed = false;
        this.ability = this.baseAbility;
        this.hpType = this.baseHpType;
        this.hpPower = this.baseHpPower;
        if (this.canTerastallize === false)
            this.canTerastallize = this.teraType;
        for (const i in this.volatiles) {
            if (this.volatiles[i].linkedStatus) {
                this.removeLinkedVolatiles(this.volatiles[i].linkedStatus, this.volatiles[i].linkedPokemon);
            }
        }
        if (this.species.name === 'Eternatus-Eternamax' && this.volatiles['dynamax']) {
            this.volatiles = { dynamax: this.volatiles['dynamax'] };
        }
        else {
            this.volatiles = {};
        }
        if (includeSwitchFlags) {
            this.switchFlag = false;
            this.forceSwitchFlag = false;
        }
        this.lastMove = null;
        if (this.battle.gen === 2)
            this.lastMoveEncore = null;
        this.lastMoveUsed = null;
        this.moveThisTurn = '';
        this.lastDamage = 0;
        this.attackedBy = [];
        this.hurtThisTurn = null;
        this.newlySwitched = true;
        this.beingCalledBack = false;
        this.volatileStaleness = undefined;
        this.setSpecies(this.baseSpecies);
    }
    hasType(type) {
        const thisTypes = this.getTypes();
        if (typeof type === 'string') {
            return thisTypes.includes(type);
        }
        for (const typeName of type) {
            if (thisTypes.includes(typeName))
                return true;
        }
        return false;
    }
    /**
     * This function only puts the pokemon in the faint queue;
     * actually setting of this.fainted comes later when the
     * faint queue is resolved.
     *
     * Returns the amount of damage actually dealt
     */
    faint(source = null, effect = null) {
        if (this.fainted || this.faintQueued)
            return 0;
        const d = this.hp;
        this.hp = 0;
        this.switchFlag = false;
        this.faintQueued = true;
        this.battle.faintQueue.push({
            target: this,
            source,
            effect,
        });
        return d;
    }
    damage(d, source = null, effect = null) {
        if (!this.hp || isNaN(d) || d <= 0)
            return 0;
        if (d < 1 && d > 0)
            d = 1;
        d = this.battle.trunc(d);
        this.hp -= d;
        if (this.hp <= 0) {
            d += this.hp;
            this.faint(source, effect);
        }
        return d;
    }
    tryTrap(isHidden = false) {
        if (!this.runStatusImmunity('trapped'))
            return false;
        if (this.trapped && isHidden)
            return true;
        this.trapped = isHidden ? 'hidden' : true;
        return true;
    }
    hasMove(moveid) {
        moveid = (0, dex_1.toID)(moveid);
        if (moveid.substr(0, 11) === 'hiddenpower')
            moveid = 'hiddenpower';
        for (const moveSlot of this.moveSlots) {
            if (moveid === moveSlot.id) {
                return moveid;
            }
        }
        return false;
    }
    disableMove(moveid, isHidden, sourceEffect) {
        if (!sourceEffect && this.battle.event) {
            sourceEffect = this.battle.effect;
        }
        moveid = (0, dex_1.toID)(moveid);
        for (const moveSlot of this.moveSlots) {
            if (moveSlot.id === moveid && moveSlot.disabled !== true) {
                moveSlot.disabled = (isHidden || true);
                moveSlot.disabledSource = (sourceEffect?.name || moveSlot.move);
            }
        }
    }
    /** Returns the amount of damage actually healed */
    heal(d, source = null, effect = null) {
        if (!this.hp)
            return false;
        d = this.battle.trunc(d);
        if (isNaN(d))
            return false;
        if (d <= 0)
            return false;
        if (this.hp >= this.maxhp)
            return false;
        this.hp += d;
        if (this.hp > this.maxhp) {
            d -= this.hp - this.maxhp;
            this.hp = this.maxhp;
        }
        return d;
    }
    /** Sets HP, returns delta */
    sethp(d) {
        if (!this.hp)
            return 0;
        d = this.battle.trunc(d);
        if (isNaN(d))
            return;
        if (d < 1)
            d = 1;
        d = d - this.hp;
        this.hp += d;
        if (this.hp > this.maxhp) {
            d -= this.hp - this.maxhp;
            this.hp = this.maxhp;
        }
        return d;
    }
    trySetStatus(status, source = null, sourceEffect = null) {
        return this.setStatus(this.status || status, source, sourceEffect);
    }
    /** Unlike clearStatus, gives cure message */
    cureStatus(silent = false) {
        if (!this.hp || !this.status)
            return false;
        this.battle.add('-curestatus', this, this.status, silent ? '[silent]' : '[msg]');
        if (this.status === 'slp' && this.removeVolatile('nightmare')) {
            this.battle.add('-end', this, 'Nightmare', '[silent]');
        }
        this.setStatus('');
        return true;
    }
    setStatus(status, source = null, sourceEffect = null, ignoreImmunities = false) {
        if (!this.hp)
            return false;
        status = this.battle.dex.conditions.get(status);
        if (this.battle.event) {
            if (!source)
                source = this.battle.event.source;
            if (!sourceEffect)
                sourceEffect = this.battle.effect;
        }
        if (!source)
            source = this;
        if (this.status === status.id) {
            if (sourceEffect?.status === this.status) {
                this.battle.add('-fail', this, this.status);
            }
            else if (sourceEffect?.status) {
                this.battle.add('-fail', source);
                this.battle.attrLastMove('[still]');
            }
            return false;
        }
        if (!ignoreImmunities && status.id &&
            !(source?.hasAbility('corrosion') && ['tox', 'psn'].includes(status.id))) {
            // the game currently never ignores immunities
            if (!this.runStatusImmunity(status.id === 'tox' ? 'psn' : status.id)) {
                this.battle.debug('immune to status');
                if (sourceEffect?.status) {
                    this.battle.add('-immune', this);
                }
                return false;
            }
        }
        const prevStatus = this.status;
        const prevStatusState = this.statusState;
        if (status.id) {
            const result = this.battle.runEvent('SetStatus', this, source, sourceEffect, status);
            if (!result) {
                this.battle.debug('set status [' + status.id + '] interrupted');
                return result;
            }
        }
        this.status = status.id;
        this.statusState = { id: status.id, target: this };
        if (source)
            this.statusState.source = source;
        if (status.duration)
            this.statusState.duration = status.duration;
        if (status.durationCallback) {
            this.statusState.duration = status.durationCallback.call(this.battle, this, source, sourceEffect);
        }
        if (status.id && !this.battle.singleEvent('Start', status, this.statusState, this, source, sourceEffect)) {
            this.battle.debug('status start [' + status.id + '] interrupted');
            // cancel the setstatus
            this.status = prevStatus;
            this.statusState = prevStatusState;
            return false;
        }
        if (status.id && !this.battle.runEvent('AfterSetStatus', this, source, sourceEffect, status)) {
            return false;
        }
        return true;
    }
    /**
     * Unlike cureStatus, does not give cure message
     */
    clearStatus() {
        if (!this.hp || !this.status)
            return false;
        if (this.status === 'slp' && this.removeVolatile('nightmare')) {
            this.battle.add('-end', this, 'Nightmare', '[silent]');
        }
        this.setStatus('');
        return true;
    }
    getStatus() {
        return this.battle.dex.conditions.getByID(this.status);
    }
    eatItem(force, source, sourceEffect) {
        if (!this.item || this.itemState.knockedOff)
            return false;
        if ((!this.hp && this.item !== 'jabocaberry' && this.item !== 'rowapberry') || !this.isActive)
            return false;
        if (!sourceEffect && this.battle.effect)
            sourceEffect = this.battle.effect;
        if (!source && this.battle.event && this.battle.event.target)
            source = this.battle.event.target;
        const item = this.getItem();
        if (this.battle.runEvent('UseItem', this, null, null, item) &&
            (force || this.battle.runEvent('TryEatItem', this, null, null, item))) {
            this.battle.add('-enditem', this, item, '[eat]');
            this.battle.singleEvent('Eat', item, this.itemState, this, source, sourceEffect);
            this.battle.runEvent('EatItem', this, null, null, item);
            if (exports.RESTORATIVE_BERRIES.has(item.id)) {
                switch (this.pendingStaleness) {
                    case 'internal':
                        if (this.staleness !== 'external')
                            this.staleness = 'internal';
                        break;
                    case 'external':
                        this.staleness = 'external';
                        break;
                }
                this.pendingStaleness = undefined;
            }
            this.lastItem = this.item;
            this.item = '';
            this.itemState = { id: '', target: this };
            this.usedItemThisTurn = true;
            this.ateBerry = true;
            this.battle.runEvent('AfterUseItem', this, null, null, item);
            return true;
        }
        return false;
    }
    useItem(source, sourceEffect) {
        if ((!this.hp && !this.getItem().isGem) || !this.isActive)
            return false;
        if (!this.item || this.itemState.knockedOff)
            return false;
        if (!sourceEffect && this.battle.effect)
            sourceEffect = this.battle.effect;
        if (!source && this.battle.event && this.battle.event.target)
            source = this.battle.event.target;
        const item = this.getItem();
        if (this.battle.runEvent('UseItem', this, null, null, item)) {
            switch (item.id) {
                case 'redcard':
                    this.battle.add('-enditem', this, item, '[of] ' + source);
                    break;
                default:
                    if (item.isGem) {
                        this.battle.add('-enditem', this, item, '[from] gem');
                    }
                    else {
                        this.battle.add('-enditem', this, item);
                    }
                    break;
            }
            if (item.boosts) {
                this.battle.boost(item.boosts, this, source, item);
            }
            this.battle.singleEvent('Use', item, this.itemState, this, source, sourceEffect);
            this.lastItem = this.item;
            this.item = '';
            this.itemState = { id: '', target: this };
            this.usedItemThisTurn = true;
            this.battle.runEvent('AfterUseItem', this, null, null, item);
            return true;
        }
        return false;
    }
    takeItem(source) {
        if (!this.isActive)
            return false;
        if (!this.item || this.itemState.knockedOff)
            return false;
        if (!source)
            source = this;
        if (this.battle.gen === 4) {
            if ((0, dex_1.toID)(this.ability) === 'multitype')
                return false;
            if ((0, dex_1.toID)(source.ability) === 'multitype')
                return false;
        }
        const item = this.getItem();
        if (this.battle.runEvent('TakeItem', this, source, null, item)) {
            this.item = '';
            const oldItemState = this.itemState;
            this.itemState = { id: '', target: this };
            this.pendingStaleness = undefined;
            this.battle.singleEvent('End', item, oldItemState, this);
            this.battle.runEvent('AfterTakeItem', this, null, null, item);
            return item;
        }
        return false;
    }
    setItem(item, source, effect) {
        if (!this.hp || !this.isActive)
            return false;
        if (this.itemState.knockedOff)
            return false;
        if (typeof item === 'string')
            item = this.battle.dex.items.get(item);
        const effectid = this.battle.effect ? this.battle.effect.id : '';
        if (exports.RESTORATIVE_BERRIES.has('leppaberry')) {
            const inflicted = ['trick', 'switcheroo'].includes(effectid);
            const external = inflicted && source && !source.isAlly(this);
            this.pendingStaleness = external ? 'external' : 'internal';
        }
        else {
            this.pendingStaleness = undefined;
        }
        const oldItem = this.getItem();
        const oldItemState = this.itemState;
        this.item = item.id;
        this.itemState = { id: item.id, target: this };
        if (oldItem.exists)
            this.battle.singleEvent('End', oldItem, oldItemState, this);
        if (item.id) {
            this.battle.singleEvent('Start', item, this.itemState, this, source, effect);
        }
        return true;
    }
    getItem() {
        return this.battle.dex.items.getByID(this.item);
    }
    hasItem(item) {
        if (Array.isArray(item)) {
            if (!item.map(dex_1.toID).includes(this.item))
                return false;
        }
        else {
            if ((0, dex_1.toID)(item) !== this.item)
                return false;
        }
        return !this.ignoringItem();
    }
    clearItem() {
        return this.setItem('');
    }
    setAbility(ability, source, isFromFormeChange = false, isTransform = false) {
        if (!this.hp)
            return false;
        if (typeof ability === 'string')
            ability = this.battle.dex.abilities.get(ability);
        const oldAbility = this.ability;
        if (!isFromFormeChange) {
            if (ability.isPermanent || this.getAbility().isPermanent)
                return false;
        }
        if (!isTransform) {
            const setAbilityEvent = this.battle.runEvent('SetAbility', this, source, this.battle.effect, ability);
            if (!setAbilityEvent)
                return setAbilityEvent;
        }
        this.battle.singleEvent('End', this.battle.dex.abilities.get(oldAbility), this.abilityState, this, source);
        if (this.battle.effect && this.battle.effect.effectType === 'Move' && !isFromFormeChange) {
            this.battle.add('-endability', this, this.battle.dex.abilities.get(oldAbility), '[from] move: ' +
                this.battle.dex.moves.get(this.battle.effect.id));
        }
        this.ability = ability.id;
        this.abilityState = { id: ability.id, target: this };
        if (ability.id && this.battle.gen > 3 &&
            (!isTransform || oldAbility !== ability.id || this.battle.gen <= 4)) {
            this.battle.singleEvent('Start', ability, this.abilityState, this, source);
        }
        this.abilityOrder = this.battle.abilityOrder++;
        return oldAbility;
    }
    getAbility() {
        return this.battle.dex.abilities.getByID(this.ability);
    }
    hasAbility(ability) {
        if (Array.isArray(ability)) {
            if (!ability.map(dex_1.toID).includes(this.ability))
                return false;
        }
        else {
            if ((0, dex_1.toID)(ability) !== this.ability)
                return false;
        }
        return !this.ignoringAbility();
    }
    clearAbility() {
        return this.setAbility('');
    }
    getNature() {
        return this.battle.dex.natures.get(this.set.nature);
    }
    addVolatile(status, source = null, sourceEffect = null, linkedStatus = null) {
        let result;
        status = this.battle.dex.conditions.get(status);
        if (!this.hp && !status.affectsFainted)
            return false;
        if (linkedStatus && source && !source.hp)
            return false;
        if (this.battle.event) {
            if (!source)
                source = this.battle.event.source;
            if (!sourceEffect)
                sourceEffect = this.battle.effect;
        }
        if (!source)
            source = this;
        if (this.volatiles[status.id]) {
            if (!status.onRestart)
                return false;
            return this.battle.singleEvent('Restart', status, this.volatiles[status.id], this, source, sourceEffect);
        }
        if (!this.runStatusImmunity(status.id)) {
            this.battle.debug('immune to volatile status');
            if (sourceEffect?.status) {
                this.battle.add('-immune', this);
            }
            return false;
        }
        result = this.battle.runEvent('TryAddVolatile', this, source, sourceEffect, status);
        if (!result) {
            this.battle.debug('add volatile [' + status.id + '] interrupted');
            return result;
        }
        this.volatiles[status.id] = { id: status.id, name: status.name, target: this };
        if (source) {
            this.volatiles[status.id].source = source;
            this.volatiles[status.id].sourceSlot = source.getSlot();
        }
        if (sourceEffect)
            this.volatiles[status.id].sourceEffect = sourceEffect;
        if (status.duration)
            this.volatiles[status.id].duration = status.duration;
        if (status.durationCallback) {
            this.volatiles[status.id].duration = status.durationCallback.call(this.battle, this, source, sourceEffect);
        }
        result = this.battle.singleEvent('Start', status, this.volatiles[status.id], this, source, sourceEffect);
        if (!result) {
            // cancel
            delete this.volatiles[status.id];
            return result;
        }
        if (linkedStatus && source) {
            if (!source.volatiles[linkedStatus.toString()]) {
                source.addVolatile(linkedStatus, this, sourceEffect);
                source.volatiles[linkedStatus.toString()].linkedPokemon = [this];
                source.volatiles[linkedStatus.toString()].linkedStatus = status;
            }
            else {
                source.volatiles[linkedStatus.toString()].linkedPokemon.push(this);
            }
            this.volatiles[status.toString()].linkedPokemon = [source];
            this.volatiles[status.toString()].linkedStatus = linkedStatus;
        }
        return true;
    }
    getVolatile(status) {
        status = this.battle.dex.conditions.get(status);
        if (!this.volatiles[status.id])
            return null;
        return status;
    }
    removeVolatile(status) {
        if (!this.hp)
            return false;
        status = this.battle.dex.conditions.get(status);
        if (!this.volatiles[status.id])
            return false;
        this.battle.singleEvent('End', status, this.volatiles[status.id], this);
        const linkedPokemon = this.volatiles[status.id].linkedPokemon;
        const linkedStatus = this.volatiles[status.id].linkedStatus;
        delete this.volatiles[status.id];
        if (linkedPokemon) {
            this.removeLinkedVolatiles(linkedStatus, linkedPokemon);
        }
        return true;
    }
    removeLinkedVolatiles(linkedStatus, linkedPokemon) {
        linkedStatus = linkedStatus.toString();
        for (const linkedPoke of linkedPokemon) {
            const volatileData = linkedPoke.volatiles[linkedStatus];
            if (!volatileData)
                continue;
            volatileData.linkedPokemon.splice(volatileData.linkedPokemon.indexOf(this), 1);
            if (volatileData.linkedPokemon.length === 0) {
                linkedPoke.removeVolatile(linkedStatus);
            }
        }
    }
    /**
     * Sets a type (except on Arceus, who resists type changes)
     */
    setType(newType, enforce = false) {
        if (!enforce) {
            // First type of Arceus, Silvally cannot be normally changed
            if ((this.battle.gen >= 5 && (this.species.num === 493 || this.species.num === 773)) ||
                (this.battle.gen === 4 && this.hasAbility('multitype'))) {
                return false;
            }
            // Terastallized Pokemon cannot have their base type changed except via forme change
            if (this.terastallized)
                return false;
        }
        if (!newType)
            throw new Error("Must pass type to setType");
        this.types = (typeof newType === 'string' ? [newType] : newType);
        this.addedType = '';
        this.knownType = true;
        this.apparentType = this.types.join('/');
        return true;
    }
    /** Removes any types added previously and adds another one. */
    addType(newType) {
        if (this.terastallized)
            return false;
        this.addedType = newType;
        return true;
    }
    getTypes(excludeAdded, preterastallized) {
        if (!preterastallized && this.terastallized)
            return [this.terastallized];
        const types = this.battle.runEvent('Type', this, null, null, this.types);
        if (!excludeAdded && this.addedType)
            return types.concat(this.addedType);
        if (types.length)
            return types;
        return [this.battle.gen >= 5 ? 'Normal' : '???'];
    }
    isGrounded(negateImmunity = false) {
        if ('gravity' in this.battle.field.pseudoWeather)
            return true;
        if ('ingrain' in this.volatiles && this.battle.gen >= 4)
            return true;
        if ('smackdown' in this.volatiles)
            return true;
        const item = (this.ignoringItem() ? '' : this.item);
        if (item === 'ironball')
            return true;
        // If a Fire/Flying type uses Burn Up and Roost, it becomes ???/Flying-type, but it's still grounded.
        if (!negateImmunity && this.hasType('Flying') && !(this.hasType('???') && 'roost' in this.volatiles))
            return false;
        if (this.hasAbility('levitate') && !this.battle.suppressingAbility(this))
            return null;
        if ('magnetrise' in this.volatiles)
            return false;
        if ('telekinesis' in this.volatiles)
            return false;
        return item !== 'airballoon';
    }
    isSemiInvulnerable() {
        return (this.volatiles['fly'] || this.volatiles['bounce'] || this.volatiles['dive'] || this.volatiles['dig'] ||
            this.volatiles['phantomforce'] || this.volatiles['shadowforce'] || this.isSkyDropped());
    }
    isSkyDropped() {
        if (this.volatiles['skydrop'])
            return true;
        for (const foeActive of this.side.foe.active) {
            if (foeActive.volatiles['skydrop'] && foeActive.volatiles['skydrop'].source === this) {
                return true;
            }
        }
        return false;
    }
    /** Specifically: is protected against a single-target damaging move */
    isProtected() {
        return !!(this.volatiles['protect'] || this.volatiles['detect'] || this.volatiles['maxguard'] ||
            this.volatiles['kingsshield'] || this.volatiles['spikyshield'] || this.volatiles['banefulbunker'] ||
            this.volatiles['obstruct'] || this.volatiles['silktrap']);
    }
    /**
     * Like Field.effectiveWeather(), but ignores sun and rain if
     * the Utility Umbrella is active for the Pokemon.
     */
    effectiveWeather() {
        const weather = this.battle.field.effectiveWeather();
        switch (weather) {
            case 'sunnyday':
            case 'raindance':
            case 'desolateland':
            case 'primordialsea':
                if (this.hasItem('utilityumbrella'))
                    return '';
        }
        return weather;
    }
    runEffectiveness(move) {
        let totalTypeMod = 0;
        for (const type of this.getTypes()) {
            let typeMod = this.battle.dex.getEffectiveness(move, type);
            typeMod = this.battle.singleEvent('Effectiveness', move, null, this, type, move, typeMod);
            totalTypeMod += this.battle.runEvent('Effectiveness', this, type, move, typeMod);
        }
        return totalTypeMod;
    }
    /** false = immune, true = not immune */
    runImmunity(type, message) {
        if (!type || type === '???')
            return true;
        if (!this.battle.dex.types.isName(type)) {
            throw new Error("Use runStatusImmunity for " + type);
        }
        if (this.fainted)
            return false;
        const negateImmunity = !this.battle.runEvent('NegateImmunity', this, type);
        const notImmune = type === 'Ground' ?
            this.isGrounded(negateImmunity) :
            negateImmunity || this.battle.dex.getImmunity(type, this);
        if (notImmune)
            return true;
        if (!message)
            return false;
        if (notImmune === null) {
            this.battle.add('-immune', this, '[from] ability: Levitate');
        }
        else {
            this.battle.add('-immune', this);
        }
        return false;
    }
    runStatusImmunity(type, message) {
        if (this.fainted)
            return false;
        if (!type)
            return true;
        if (!this.battle.dex.getImmunity(type, this)) {
            this.battle.debug('natural status immunity');
            if (message) {
                this.battle.add('-immune', this);
            }
            return false;
        }
        const immunity = this.battle.runEvent('Immunity', this, null, null, type);
        if (!immunity) {
            this.battle.debug('artificial status immunity');
            if (message && immunity !== null) {
                this.battle.add('-immune', this);
            }
            return false;
        }
        return true;
    }
    destroy() {
        // deallocate ourself
        // get rid of some possibly-circular references
        this.battle = null;
        this.side = null;
    }
}
exports.Pokemon = Pokemon;
//# sourceMappingURL=pokemon.js.map