Linked: Fix bugs with Encore, Stalwart, Propeller Tail, and Snipe Shot

This commit is contained in:
Kris Johnson 2026-03-02 21:14:50 -07:00
parent bf612bf9ee
commit 6545c74486
3 changed files with 133 additions and 25 deletions

View File

@ -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);
}
}
}
},

View File

@ -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;

View File

@ -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<BattleScriptsData> {
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;