pokemon-showdown/data/mods/linked/scripts.ts
Slayer95 7bd04f470c
Linked: Remove m.lastMoveAbsolute in favor of lastMove (#11791)
* Fix Linked last move checks

At some point between af6affb and 045fe456, Linked's concept of
last absolute move changed from using the 2nd move in a link to
using the 1st move, likely in an attempt to preserve the 1st move
information that used to be stored for debugging purposes.

However, the redefinition has impacted the mechanics of Sketch, and
other moves for a long time.

This commit restores the original mechanics, and gets rid of the
last absolute move concept, in favor of the orthodox pokemon.lastMove.
In consequence, all modded data entries that now match gen9 are
also deleted.

* Linked: Fix Grassy Glide priority calculation
2026-03-03 09:50:30 -07:00

698 lines
25 KiB
TypeScript

export const Scripts: ModdedBattleScriptsData = {
gen: 9,
getActionSpeed(action) {
if (action.choice === 'move') {
let move = action.move;
if (action.zmove) {
const zMoveName = this.actions.getZMove(action.move, action.pokemon, true);
if (zMoveName) {
const zMove = this.dex.getActiveMove(zMoveName);
if (zMove.exists && zMove.isZ) {
move = zMove;
}
}
}
if (action.maxMove) {
const maxMoveName = this.actions.getMaxMove(action.maxMove, action.pokemon);
if (maxMoveName) {
const maxMove = this.actions.getActiveMaxMove(action.move, action.pokemon);
if (maxMove.exists && maxMove.isMax) {
move = maxMove;
}
}
}
// take priority from the base move, so abilities like Prankster only apply once
// (instead of compounding every time `getActionSpeed` is called)
let priority = this.dex.moves.get(move.id).priority;
// Linked mod
const linkedMoves: [ActiveMove, ActiveMove] = action.pokemon.getLinkedMoves();
let linkIndex = -1;
if (linkedMoves.length && !move.isZ && !move.isMax &&
(linkIndex = linkedMoves.findIndex(x => x.id === this.toID(action.move))) >= 0) {
const linkedActions = action.linked || linkedMoves;
const altMove = linkedActions[1 - linkIndex];
let thisPriority = this.singleEvent('ModifyPriority', move, null, action.pokemon, null, null, priority);
thisPriority = this.runEvent('ModifyPriority', action.pokemon, null, linkedActions[linkIndex], thisPriority);
let thatPriority = this.singleEvent('ModifyPriority', altMove, null, action.pokemon, null, null, altMove.priority);
thatPriority = this.runEvent('ModifyPriority', action.pokemon, null, altMove, thatPriority);
priority = Math.min(thisPriority, thatPriority);
action.priority = priority + action.fractionalPriority;
if (this.gen > 5) {
// Gen 6+: Quick Guard blocks moves with artificially enhanced priority.
// This also applies to Psychic Terrain.
linkedActions[linkIndex].priority = priority;
altMove.priority = priority;
}
} else {
priority = this.singleEvent('ModifyPriority', move, null, action.pokemon, null, null, priority);
priority = this.runEvent('ModifyPriority', action.pokemon, null, move, priority);
action.priority = priority + action.fractionalPriority;
// In Gen 6, Quick Guard blocks moves with artificially enhanced priority.
if (this.gen > 5) action.move.priority = priority;
}
}
if (!action.pokemon) {
action.speed = 1;
} else {
action.speed = action.pokemon.getActionSpeed();
}
},
runAction(action) {
const pokemonOriginalHP = action.pokemon?.hp;
let residualPokemon: (readonly [Pokemon, number])[] = [];
// returns whether or not we ended in a callback
switch (action.choice) {
case 'start': {
for (const side of this.sides) {
if (side.pokemonLeft) side.pokemonLeft = side.pokemon.length;
this.add('teamsize', side.id, side.pokemon.length);
}
this.add('start');
for (const pokemon of this.getAllPokemon()) {
this.singleEvent('BattleStart', this.dex.conditions.getByID(pokemon.species.id), pokemon.speciesState, pokemon);
}
this.format.onBattleStart?.call(this);
for (const rule of this.ruleTable.keys()) {
if ('+*-!'.includes(rule.charAt(0))) continue;
const subFormat = this.dex.formats.get(rule);
subFormat.onBattleStart?.call(this);
}
for (const side of this.sides) {
for (let i = 0; i < side.active.length; i++) {
if (!side.pokemonLeft) {
// forfeited before starting
side.active[i] = side.pokemon[i];
side.active[i].fainted = true;
side.active[i].hp = 0;
} else {
this.actions.switchIn(side.pokemon[i], i);
}
}
}
this.midTurn = true;
break;
}
case 'move':
if (!action.pokemon.isActive) return false;
if (action.pokemon.fainted) return false;
// Linked moves
// @ts-expect-error modded
if (action.linked) {
// @ts-expect-error modded
const linkedMoves: ActiveMove[] = action.linked;
for (let i = linkedMoves.length - 1; i >= 0; i--) {
const isValidTarget = this.validTargetLoc(action.targetLoc, action.pokemon, linkedMoves[i].target);
const randomTarget = this.getRandomTarget(action.pokemon, linkedMoves[i]);
const targetLoc = isValidTarget || !randomTarget ? action.targetLoc : action.pokemon.getLocOf(randomTarget);
const pseudoAction: Action = {
choice: 'move', priority: action.priority, speed: action.speed, pokemon: action.pokemon,
targetLoc, moveid: linkedMoves[i].id, move: linkedMoves[i], mega: action.mega,
order: action.order, fractionalPriority: action.fractionalPriority, originalTarget: action.originalTarget,
};
this.queue.unshift(pseudoAction);
}
return;
}
this.actions.runMove(action.move, action.pokemon, action.targetLoc, {
sourceEffect: action.sourceEffect, zMove: action.zmove,
maxMove: action.maxMove, originalTarget: action.originalTarget,
});
break;
case 'megaEvo':
this.actions.runMegaEvo(action.pokemon);
break;
case 'megaEvoX':
this.actions.runMegaEvoX?.(action.pokemon);
break;
case 'megaEvoY':
this.actions.runMegaEvoY?.(action.pokemon);
break;
case 'runDynamax':
action.pokemon.addVolatile('dynamax');
action.pokemon.side.dynamaxUsed = true;
if (action.pokemon.side.allySide) action.pokemon.side.allySide.dynamaxUsed = true;
break;
case 'terastallize':
this.actions.terastallize(action.pokemon);
break;
case 'beforeTurnMove':
if (!action.pokemon.isActive) return false;
if (action.pokemon.fainted) return false;
this.debug('before turn callback: ' + action.move.id);
const target = this.getTarget(action.pokemon, action.move, action.targetLoc);
if (!target) return false;
if (!action.move.beforeTurnCallback) throw new Error(`beforeTurnMove has no beforeTurnCallback`);
action.move.beforeTurnCallback.call(this, action.pokemon, target);
break;
case 'priorityChargeMove':
if (!action.pokemon.isActive) return false;
if (action.pokemon.fainted) return false;
this.debug('priority charge callback: ' + action.move.id);
if (!action.move.priorityChargeCallback) throw new Error(`priorityChargeMove has no priorityChargeCallback`);
action.move.priorityChargeCallback.call(this, action.pokemon);
break;
case 'event':
this.runEvent(action.event!, action.pokemon);
break;
case 'team':
if (action.index === 0) {
action.pokemon.side.pokemon = [];
}
action.pokemon.side.pokemon.push(action.pokemon);
action.pokemon.position = action.index;
// we return here because the update event would crash since there are no active pokemon yet
return;
case 'pass':
return;
case 'instaswitch':
case 'switch':
if (action.choice === 'switch' && action.pokemon.status) {
this.singleEvent('CheckShow', this.dex.abilities.getByID('naturalcure' as ID), null, action.pokemon);
}
if (this.actions.switchIn(action.target, action.pokemon.position, action.sourceEffect) === 'pursuitfaint') {
// a pokemon fainted from Pursuit before it could switch
if (this.gen <= 4) {
// in gen 2-4, the switch still happens
this.hint("Previously chosen switches continue in Gen 2-4 after a Pursuit target faints.");
action.priority = -101;
this.queue.unshift(action);
break;
} else {
// in gen 5+, the switch is cancelled
this.hint("A Pokemon can't switch between when it runs out of HP and when it faints");
break;
}
}
break;
case 'revivalblessing':
action.pokemon.side.pokemonLeft++;
if (action.target.position < action.pokemon.side.active.length) {
this.queue.addChoice({
choice: 'instaswitch',
pokemon: action.target,
target: action.target,
});
}
action.target.fainted = false;
action.target.faintQueued = false;
action.target.subFainted = false;
action.target.status = '';
action.target.hp = 1; // Needed so hp functions works
action.target.sethp(action.target.maxhp / 2);
this.add('-heal', action.target, action.target.getHealth, '[from] move: Revival Blessing');
action.pokemon.side.removeSlotCondition(action.pokemon, 'revivalblessing');
break;
case 'runSwitch':
this.actions.runSwitch(action.pokemon);
break;
case 'shift':
if (!action.pokemon.isActive) return false;
if (action.pokemon.fainted) return false;
this.swapPosition(action.pokemon, 1);
break;
case 'beforeTurn':
this.eachEvent('BeforeTurn');
break;
case 'residual':
this.add('');
this.clearActiveMove(true);
this.updateSpeed();
residualPokemon = this.getAllActive().map(pokemon => [pokemon, pokemon.getUndynamaxedHP()] as const);
this.fieldEvent('Residual');
if (!this.ended) this.add('upkeep');
break;
}
// phazing (Roar, etc)
for (const side of this.sides) {
for (const pokemon of side.active) {
if (pokemon.forceSwitchFlag) {
if (pokemon.hp) this.actions.dragIn(pokemon.side, pokemon.position);
pokemon.forceSwitchFlag = false;
}
}
}
this.clearActiveMove();
// fainting
this.faintMessages();
if (this.ended) return true;
// switching (fainted pokemon, U-turn, Baton Pass, etc)
if (!this.queue.peek() || (this.gen <= 3 && ['move', 'residual'].includes(this.queue.peek()!.choice))) {
// in gen 3 or earlier, switching in fainted pokemon is done after
// every move, rather than only at the end of the turn.
this.checkFainted();
} else if (['megaEvo', 'megaEvoX', 'megaEvoY'].includes(action.choice) && this.gen === 7) {
this.eachEvent('Update');
// In Gen 7, the action order is recalculated for a Pokémon that mega evolves.
for (const [i, queuedAction] of this.queue.list.entries()) {
if (queuedAction.pokemon === action.pokemon && queuedAction.choice === 'move') {
this.queue.list.splice(i, 1);
queuedAction.mega = 'done';
this.queue.insertChoice(queuedAction, true);
break;
}
}
return false;
} else if (this.queue.peek()?.choice === 'instaswitch') {
return false;
}
if (this.gen >= 5 && action.choice !== 'start') {
this.eachEvent('Update');
for (const [pokemon, originalHP] of residualPokemon) {
const maxhp = pokemon.getUndynamaxedHP(pokemon.maxhp);
if (pokemon.hp && pokemon.getUndynamaxedHP() <= maxhp / 2 && originalHP > maxhp / 2) {
this.runEvent('EmergencyExit', pokemon);
}
}
}
if (action.choice === 'runSwitch') {
const pokemon = action.pokemon;
if (pokemon.hp && pokemon.hp <= pokemon.maxhp / 2 && pokemonOriginalHP! > pokemon.maxhp / 2) {
this.runEvent('EmergencyExit', pokemon);
}
}
const switches = this.sides.map(
side => side.active.some(pokemon => pokemon && !!pokemon.switchFlag)
);
for (let i = 0; i < this.sides.length; i++) {
let reviveSwitch = false; // Used to ignore the fake switch for Revival Blessing
if (switches[i] && !this.canSwitch(this.sides[i])) {
for (const pokemon of this.sides[i].active) {
if (this.sides[i].slotConditions[pokemon.position]['revivalblessing']) {
reviveSwitch = true;
continue;
}
pokemon.switchFlag = false;
}
if (!reviveSwitch) switches[i] = false;
} else if (switches[i]) {
for (const pokemon of this.sides[i].active) {
if (
pokemon.hp && pokemon.switchFlag && pokemon.switchFlag !== 'revivalblessing' &&
!pokemon.skipBeforeSwitchOutEventFlag
) {
this.runEvent('BeforeSwitchOut', pokemon);
pokemon.skipBeforeSwitchOutEventFlag = true;
this.faintMessages(); // Pokemon may have fainted in BeforeSwitchOut
if (this.ended) return true;
if (pokemon.fainted) {
switches[i] = this.sides[i].active.some(sidePokemon => sidePokemon && !!sidePokemon.switchFlag);
}
}
}
}
}
for (const playerSwitch of switches) {
if (playerSwitch) {
this.makeRequest('switch');
return true;
}
}
if (this.gen < 5) this.eachEvent('Update');
const nextAction = this.queue.peek();
if (this.gen >= 8 &&
(nextAction?.choice === 'move' || nextAction?.choice === 'runDynamax') && nextAction?.pokemon !== action.pokemon) {
// In gen 8, speed is updated dynamically so update the queue's speed properties and sort it.
this.updateSpeed();
for (const queueAction of this.queue.list) {
if (queueAction.pokemon) this.getActionSpeed(queueAction);
}
this.queue.sort();
}
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++;
const zMove = options?.zMove;
const maxMove = options?.maxMove;
const externalMove = options?.externalMove;
const originalTarget = options?.originalTarget;
let sourceEffect = options?.sourceEffect;
let target = this.battle.getTarget(pokemon, maxMove || zMove || moveOrMoveName, targetLoc, originalTarget);
let baseMove = this.dex.getActiveMove(moveOrMoveName);
const priority = baseMove.priority;
const pranksterBoosted = baseMove.pranksterBoosted;
if (baseMove.id !== 'struggle' && !zMove && !maxMove && !externalMove) {
const changedMove = this.battle.runEvent('OverrideAction', pokemon, target, baseMove);
if (changedMove && changedMove !== true) {
baseMove = this.dex.getActiveMove(changedMove);
baseMove.priority = priority;
if (pranksterBoosted) baseMove.pranksterBoosted = pranksterBoosted;
target = this.battle.getRandomTarget(pokemon, baseMove);
}
}
let move = baseMove;
if (zMove) {
move = this.getActiveZMove(baseMove, pokemon);
} else if (maxMove) {
move = this.getActiveMaxMove(baseMove, pokemon);
}
move.isExternal = externalMove;
this.battle.setActiveMove(move, pokemon, target);
/* if (pokemon.moveThisTurn) {
// THIS IS PURELY A SANITY CHECK
// DO NOT TAKE ADVANTAGE OF THIS TO PREVENT A POKEMON FROM MOVING;
// USE this.queue.cancelMove INSTEAD
this.battle.debug(`${pokemon.id} INCONSISTENT STATE, ALREADY MOVED: ${pokemon.moveThisTurn}`);
this.battle.clearActiveMove(true);
return;
} */
const willTryMove = this.battle.runEvent('BeforeMove', pokemon, target, move);
if (!willTryMove) {
this.battle.runEvent('MoveAborted', pokemon, target, move);
this.battle.clearActiveMove(true);
// The event 'BeforeMove' could have returned false or null
// false indicates that this counts as a move failing for the purpose of calculating Stomping Tantrum's base power
// null indicates the opposite, as the Pokemon didn't have an option to choose anything
pokemon.moveThisTurnResult = willTryMove;
return;
}
// Used exclusively for a hint later
if (move.flags['cantusetwice'] && pokemon.lastMove?.id === move.id) {
pokemon.addVolatile(move.id);
}
if (move.beforeMoveCallback) {
if (move.beforeMoveCallback.call(this.battle, pokemon, target, move)) {
this.battle.clearActiveMove(true);
pokemon.moveThisTurnResult = false;
return;
}
}
pokemon.lastDamage = 0;
let lockedMove;
if (!externalMove) {
lockedMove = this.battle.runEvent('LockMove', pokemon);
if (lockedMove === true) lockedMove = false;
if (!lockedMove) {
if (!pokemon.deductPP(baseMove, null, target) && (move.id !== 'struggle')) {
this.battle.add('cant', pokemon, 'nopp', move);
this.battle.clearActiveMove(true);
pokemon.moveThisTurnResult = false;
return;
}
} else {
sourceEffect = this.dex.conditions.get('lockedmove');
}
pokemon.moveUsed(move, targetLoc);
}
// Dancer Petal Dance hack
// TODO: implement properly
const noLock = externalMove && !pokemon.volatiles['lockedmove'];
if (zMove) {
if (pokemon.illusion) {
this.battle.singleEvent('End', this.dex.abilities.get('Illusion'), pokemon.abilityState, pokemon);
}
this.battle.add('-zpower', pokemon);
pokemon.side.zMoveUsed = true;
}
const oldActiveMove = move;
const moveDidSomething = this.useMove(baseMove, pokemon, { target, sourceEffect, zMove, maxMove });
this.battle.lastSuccessfulMoveThisTurn = moveDidSomething ? this.battle.activeMove && this.battle.activeMove.id : null;
if (this.battle.activeMove) move = this.battle.activeMove;
this.battle.singleEvent('AfterMove', move, null, pokemon, target, move);
this.battle.runEvent('AfterMove', pokemon, target, move);
if (move.flags['cantusetwice'] && pokemon.removeVolatile(move.id)) {
this.battle.add('-hint', `Some effects can force a Pokemon to use ${move.name} again in a row.`);
}
// TODO: Refactor to use BattleQueue#prioritizeAction in onAnyAfterMove handlers
// Dancer's activation order is completely different from any other event, so it's handled separately
if (move.flags['dance'] && moveDidSomething && !move.isExternal) {
const dancers = [];
for (const currentPoke of this.battle.getAllActive()) {
if (pokemon === currentPoke) continue;
if (currentPoke.hasAbility('dancer') && !currentPoke.isSemiInvulnerable()) {
dancers.push(currentPoke);
}
}
// Dancer activates in order of lowest speed stat to highest
// Note that the speed stat used is after any volatile replacements like Speed Swap,
// but before any multipliers like Agility or Choice Scarf
// Ties go to whichever Pokemon has had the ability for the least amount of time
dancers.sort(
(a, b) => -(b.storedStats['spe'] - a.storedStats['spe']) || b.abilityState.effectOrder - a.abilityState.effectOrder
);
const targetOf1stDance = this.battle.activeTarget!;
for (const dancer of dancers) {
if (this.battle.faintMessages()) break;
if (dancer.fainted) continue;
this.battle.add('-activate', dancer, 'ability: Dancer');
const dancersTarget = !targetOf1stDance.isAlly(dancer) && pokemon.isAlly(dancer) ?
targetOf1stDance :
pokemon;
const dancersTargetLoc = dancer.getLocOf(dancersTarget);
this.runMove(move.id, dancer, dancersTargetLoc, { sourceEffect: this.dex.abilities.get('dancer'), externalMove: true });
}
}
if (noLock && pokemon.volatiles['lockedmove']) delete pokemon.volatiles['lockedmove'];
this.battle.faintMessages();
this.battle.checkWin();
if (this.battle.gen <= 4) {
// In gen 4, the outermost move is considered the last move for Copycat
this.battle.activeMove = oldActiveMove;
}
},
},
queue: {
resolveAction(action, midTurn = false) {
if (!action) throw new Error(`Action not passed to resolveAction`);
if (action.choice === 'pass') return [];
const actions = [action];
if (!action.side && action.pokemon) action.side = action.pokemon.side;
if (!action.move && action.moveid) action.move = this.battle.dex.getActiveMove(action.moveid);
if (!action.order) {
const orders: { [choice: string]: number } = {
team: 1,
start: 2,
instaswitch: 3,
beforeTurn: 4,
beforeTurnMove: 5,
revivalblessing: 6,
runSwitch: 101,
switch: 103,
megaEvo: 104,
megaEvoX: 104,
megaEvoY: 104,
runDynamax: 105,
terastallize: 106,
priorityChargeMove: 107,
shift: 200,
// default is 200 (for moves)
residual: 300,
};
if (action.choice in orders) {
action.order = orders[action.choice];
} else {
action.order = 200;
if (!['move', 'event'].includes(action.choice)) {
throw new Error(`Unexpected orderless action ${action.choice}`);
}
}
}
if (!midTurn) {
if (action.choice === 'move') {
if (!action.maxMove && !action.zmove && action.move.beforeTurnCallback) {
actions.unshift(...this.resolveAction({
choice: 'beforeTurnMove', pokemon: action.pokemon, move: action.move, targetLoc: action.targetLoc,
}));
}
if (action.mega && !action.pokemon.isSkyDropped()) {
actions.unshift(...this.resolveAction({
choice: 'megaEvo',
pokemon: action.pokemon,
}));
}
if (action.megax && !action.pokemon.isSkyDropped()) {
actions.unshift(...this.resolveAction({
choice: 'megaEvoX',
pokemon: action.pokemon,
}));
}
if (action.megay && !action.pokemon.isSkyDropped()) {
actions.unshift(...this.resolveAction({
choice: 'megaEvoY',
pokemon: action.pokemon,
}));
}
if (action.terastallize && !action.pokemon.terastallized) {
actions.unshift(...this.resolveAction({
choice: 'terastallize',
pokemon: action.pokemon,
}));
}
if (action.maxMove && !action.pokemon.volatiles['dynamax']) {
actions.unshift(...this.resolveAction({
choice: 'runDynamax',
pokemon: action.pokemon,
}));
}
if (!action.maxMove && !action.zmove && action.move.priorityChargeCallback) {
actions.unshift(...this.resolveAction({
choice: 'priorityChargeMove',
pokemon: action.pokemon,
move: action.move,
}));
}
action.fractionalPriority = this.battle.runEvent('FractionalPriority', action.pokemon, null, action.move, 0);
const linkedMoves: [ActiveMove, ActiveMove] = action.pokemon.getLinkedMoves();
if (
linkedMoves.length &&
!(action.pokemon.getItem().isChoice || action.pokemon.hasAbility('gorillatactics')) &&
!action.zmove && !action.maxMove
) {
const decisionMove = this.battle.toID(action.move);
if (linkedMoves.some(x => x.id === decisionMove)) {
action.linked = linkedMoves;
const linkedOtherMove = action.linked[1 - linkedMoves.findIndex(x => x.id === decisionMove)];
if (linkedOtherMove.beforeTurnCallback) {
this.addChoice({
choice: 'beforeTurnMove',
pokemon: action.pokemon,
move: linkedOtherMove,
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)) {
if (typeof action.pokemon.switchFlag === 'string') {
action.sourceEffect = this.battle.dex.moves.get(action.pokemon.switchFlag as ID) as any;
}
action.pokemon.switchFlag = false;
}
}
const deferPriority = this.battle.gen === 7 && action.mega && action.mega !== 'done';
if (action.move) {
let target = null;
action.move = this.battle.dex.getActiveMove(action.move);
if (!action.targetLoc) {
target = this.battle.getRandomTarget(action.pokemon, action.move);
// TODO: what actually happens here?
if (target) action.targetLoc = action.pokemon.getLocOf(target);
}
action.originalTarget = action.pokemon.getAtLoc(action.targetLoc);
}
if (!deferPriority) this.battle.getActionSpeed(action);
return actions as any;
},
},
pokemon: {
moveUsed(move, targetLoc) {
this.lastMove = move;
this.moveThisTurn = move.id;
this.lastMoveTargetLoc = targetLoc;
},
getLinkedMoves(ignoreDisabled) {
const linkedMoves = this.moveSlots.slice(0, 2);
if (linkedMoves.length !== 2 || linkedMoves[0].pp <= 0 || linkedMoves[1].pp <= 0) return [];
const ret: [ActiveMove, ActiveMove] = [
this.battle.dex.getActiveMove(linkedMoves[0].id), this.battle.dex.getActiveMove(linkedMoves[1].id),
];
if (ignoreDisabled) return ret;
if (!this.ateBerry && ret.some(x => x.id === 'belch')) return [];
if (this.hasItem('assaultvest') && (ret[0].category === 'Status' || ret[1].category === 'Status')) {
return [];
}
return ret;
},
hasLinkedMove(move) {
// @ts-expect-error modded
const linkedMoves: [ActiveMove, ActiveMove] = this.getLinkedMoves(true);
if (!linkedMoves.length) return false;
return linkedMoves.some(x => x.id === move.id);
},
},
};