diff --git a/data/mods/linked/moves.ts b/data/mods/linked/moves.ts index b92c9016e0..4f2fa7be55 100644 --- a/data/mods/linked/moves.ts +++ b/data/mods/linked/moves.ts @@ -279,8 +279,8 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { // @ts-expect-error modded const linkedMoves: [ActiveMove, ActiveMove] = target.getLinkedMoves(true); const moveSlot = target.getMoveData(move.id); - const hasLinkedMove = linkedMoves.some(x => x.id === move.id); - if (hasLinkedMove && linkedMoves.every(m => !!m.flags['failencore'])) { + const isLinkedMove = linkedMoves.some(x => x.id === move.id); + if (isLinkedMove && linkedMoves.every(m => !!m.flags['failencore'])) { // both moves cannot be encored delete target.volatiles['encore']; return false; @@ -292,7 +292,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { this.effectState.timesActivated = {}; this.effectState.move = move.id; this.add('-start', target, 'Encore'); - if (hasLinkedMove) { + if (isLinkedMove) { this.effectState.move = linkedMoves; } if (!this.queue.willMove(target)) { @@ -313,7 +313,7 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { this.queue.cancelAction(pokemon); if (move.id !== this.effectState.move) return this.effectState.move; } else { - // Locked into a link + // Locked into a link switch (this.effectState.timesActivated[this.turn]) { case 1: { if (this.effectState.move[0] !== move.id) return this.effectState.move[0]; @@ -327,22 +327,13 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { }, onResidualOrder: 13, onResidual(target) { - // early termination if you run out of PP - const lastMove = target.m.lastMoveAbsolute; - const moveSlot = target.getMoveData(lastMove); - if (!moveSlot) { - target.removeVolatile('encore'); - return; // no last move - } - - // @ts-expect-error modded - if (target.hasLinkedMove(lastMove)) { - // TODO: Check instead whether the last executed move was linked - if (target.moveSlots[0].pp <= 0 || target.moveSlots[1].pp <= 0) { + if (Array.isArray(this.effectState.move)) { + if (this.effectState.move.map(move => target.getMoveData(move)).some(moveSlot => !moveSlot || moveSlot.pp <= 0)) { target.removeVolatile('encore'); } } else { - if (moveSlot.pp <= 0) { + const moveSlot = target.getMoveData(this.effectState.move); + if (!moveSlot || moveSlot.pp <= 0) { target.removeVolatile('encore'); } } @@ -351,19 +342,20 @@ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { this.add('-end', target, 'Encore'); }, onDisableMove(pokemon) { + if (!this.effectState.move) return; if (Array.isArray(this.effectState.move)) { + if (this.effectState.move.every(move => !pokemon.hasMove(move))) return; for (const moveSlot of pokemon.moveSlots) { - if (moveSlot.id !== this.effectState.move[0] && moveSlot.id !== this.effectState.move[1]) { + if (!this.effectState.move.map(move => move.id).includes(moveSlot.id)) { pokemon.disableMove(moveSlot.id); } } - } - if (!this.effectState.move || !pokemon.hasMove(this.effectState.move)) { - return; - } - for (const moveSlot of pokemon.moveSlots) { - if (moveSlot.id !== this.effectState.move) { - pokemon.disableMove(moveSlot.id); + } else { + if (!pokemon.hasMove(this.effectState.move)) return; + for (const moveSlot of pokemon.moveSlots) { + if (moveSlot.id !== this.effectState.move) { + pokemon.disableMove(moveSlot.id); + } } } }, diff --git a/data/mods/linked/scripts.ts b/data/mods/linked/scripts.ts index 7940632bd0..90b806fd95 100644 --- a/data/mods/linked/scripts.ts +++ b/data/mods/linked/scripts.ts @@ -300,6 +300,51 @@ export const Scripts: ModdedBattleScriptsData = { return false; }, + getTarget(pokemon, move, targetLoc, originalTarget) { + move = this.dex.moves.get(move); + + // Delete tracksTarget stuff because it's useless in Linked anyway + + // banning Dragon Darts from directly targeting itself is done in side.ts, but + // Dragon Darts can target itself if Ally Switch is used afterwards + if (move.smartTarget) { + const curTarget = pokemon.getAtLoc(targetLoc); + return curTarget && !curTarget.fainted ? curTarget : this.getRandomTarget(pokemon, move); + } + + // Fails if the target is the user and the move can't target its own position + const selfLoc = pokemon.getLocOf(pokemon); + if ( + ['adjacentAlly', 'any', 'normal'].includes(move.target) && targetLoc === selfLoc && + !pokemon.volatiles['twoturnmove'] && !pokemon.volatiles['iceball'] && !pokemon.volatiles['rollout'] + ) { + return move.flags['futuremove'] ? pokemon : null; + } + if (move.target !== 'randomNormal' && this.validTargetLoc(targetLoc, pokemon, move.target)) { + const target = pokemon.getAtLoc(targetLoc); + if (target?.fainted) { + if (this.gameType === 'freeforall') { + // Target is a fainted opponent in a free-for-all battle; attack shouldn't retarget + return target; + } + if (target.isAlly(pokemon)) { + if (move.target === 'adjacentAllyOrSelf' && this.gen !== 5) { + return pokemon; + } + // Target is a fainted ally: attack shouldn't retarget + return target; + } + } + if (target && !target.fainted) { + // Target is unfainted: use selected target location + return target; + } + + // Chosen target not valid, + // retarget randomly with getRandomTarget + } + return this.getRandomTarget(pokemon, move); + }, actions: { runMove(moveOrMoveName, pokemon, targetLoc, options) { pokemon.activeMoveActions++; @@ -546,6 +591,14 @@ export const Scripts: ModdedBattleScriptsData = { targetLoc: action.targetLoc, }); } + if (linkedOtherMove.priorityChargeCallback) { + this.addChoice({ + choice: 'priorityChargeMove', + pokemon: action.pokemon, + move: linkedOtherMove, + targetLoc: action.targetLoc, + }); + } } } } else if (['switch', 'instaswitch'].includes(action.choice)) { @@ -573,6 +626,67 @@ export const Scripts: ModdedBattleScriptsData = { }, }, pokemon: { + 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' as ID) && !this.transformed) { + const moveslot = this.baseMoves.indexOf('mimic' as ID); + 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.m.lastMoveAbsolute = null; + this.lastMove = null; + if (this.battle.gen === 2) this.lastMoveEncore = null; + this.lastMoveUsed = null; + this.moveThisTurn = ''; + this.moveLastTurnResult = undefined; + this.moveThisTurnResult = undefined; + + this.lastDamage = 0; + this.attackedBy = []; + this.hurtThisTurn = null; + this.newlySwitched = true; + this.beingCalledBack = false; + + this.volatileStaleness = undefined; + + delete this.abilityState.started; + delete this.itemState.started; + + this.setSpecies(this.baseSpecies); + }, moveUsed(move, targetLoc) { if (!this.moveThisTurn) this.m.lastMoveAbsolute = move; this.lastMove = move; diff --git a/sim/global-types.ts b/sim/global-types.ts index 075d1eb48c..a8870de3d0 100644 --- a/sim/global-types.ts +++ b/sim/global-types.ts @@ -272,6 +272,7 @@ interface ModdedBattlePokemon { lostItemForDelibird?: Item | null; boostBy?: (this: Pokemon, boost: SparseBoostsTable) => boolean | number; clearBoosts?: (this: Pokemon) => void; + clearVolatile?: (this: Pokemon, includeSwitchFlags?: boolean) => void; calculateStat?: (this: Pokemon, statName: StatIDExceptHP, boost: number, modifier?: number) => number; cureStatus?: (this: Pokemon, silent?: boolean) => boolean; deductPP?: ( @@ -383,6 +384,7 @@ interface ModdedBattleScriptsData extends Partial { checkWin?: (this: Battle, faintQueue?: Battle['faintQueue'][0]) => true | undefined; fieldEvent?: (this: Battle, eventid: string, targets?: Pokemon[]) => void; getAllActive?: (this: Battle, includeFainted?: boolean, includeCommanding?: boolean) => Pokemon[]; + getTarget?: (this: Battle, pokemon: Pokemon, move: string | Move, targetLoc: number, originalTarget?: Pokemon) => Pokemon | null; } type TypeInfo = import('./dex-data').TypeInfo;