/* mods/linked/moves.js */ 'use strict'; exports.BattleMovedex = { /** * Artificial priority * */ pursuit: { inherit: true, beforeTurnCallback: function (pokemon, target) { let linkedMoves = pokemon.getLinkedMoves(); if (linkedMoves.length && !linkedMoves.disabled) { if (linkedMoves[0] === 'pursuit' && linkedMoves[1] !== 'pursuit') return; if (linkedMoves[0] !== 'pursuit' && linkedMoves[1] === 'pursuit') return; } target.side.addSideCondition('pursuit', pokemon); if (!target.side.sideConditions['pursuit'].sources) { target.side.sideConditions['pursuit'].sources = []; } target.side.sideConditions['pursuit'].sources.push(pokemon); }, }, mefirst: { inherit: true, onHit: function (target, pokemon) { let decision = this.willMove(target); if (!decision) return false; let noMeFirst = { chatter:1, counter:1, covet:1, focuspunch:1, mefirst:1, metalburst:1, mirrorcoat:1, struggle:1, thief:1, }; // Mod-specific: Me First copies the first move in the link let move = this.getMove(decision.linked ? decision.linked[0] : decision.move); if (move.category !== 'Status' && !noMeFirst[move]) { pokemon.addVolatile('mefirst'); this.useMove(move, pokemon); return; } return false; }, }, quash: { inherit: true, // Mod-specific: default mechanics }, /** * Sucker Punch * Will miss on two linked Status moves * */ suckerpunch: { inherit: true, onTry: function (source, target) { let decision = this.willMove(target); if (!decision || decision.choice !== 'move') { this.add('-fail', source); return false; } if (target.volatiles['mustrecharge'] && target.volatiles['mustrecharge'].duration < 2) { // Duration may not be lower than 2 if Sucker Punch is used as a low-priority move // i.e. if Sucker Punch is linked with a negative priority move this.add('-fail', source); return false; } if (!decision.linked) { if (decision.move.category !== 'Status' || decision.move.id === 'mefirst') return; this.add('-fail', source); return false; } for (let i = 0; i < decision.linked.length; i++) { let linkedMove = this.getMove(decision.linked[i]); if (linkedMove.category !== 'Status' || linkedMove.id === 'mefirst') return; } this.add('-fail', source); return false; }, }, /** * Pledges * If two Pledge moves are linked, the joint effect is triggered * **/ firepledge: { inherit: true, // Mod-specific: default mechanics }, grasspledge: { inherit: true, // Mod-specific: default mechanics }, waterpledge: { inherit: true, // Mod-specific: default mechanics }, /** * Mimic and Sketch * When any of them is linked, the link will get updated for the new move * They will copy the last absolute single move used by the foe. * **/ sketch: { inherit: true, onHit: function (target, source) { let disallowedMoves = {chatter:1, sketch:1, struggle:1}; let lastMove = target.getLastMoveAbsolute(); if (source.transformed || !lastMove || disallowedMoves[lastMove] || source.moves.indexOf(lastMove) !== -1) return false; let moveslot = source.moves.indexOf('sketch'); if (moveslot === -1) return false; let move = Tools.getMove(lastMove); let sketchedMove = { move: move.name, id: move.id, pp: move.pp, maxpp: move.pp, target: move.target, disabled: false, used: false, }; source.moveset[moveslot] = sketchedMove; source.baseMoveset[moveslot] = sketchedMove; source.moves[moveslot] = toId(move.name); this.add('-activate', source, 'move: Sketch', move.name); }, }, mimic: { inherit: true, onHit: function (target, source) { let disallowedMoves = {chatter:1, mimic:1, sketch:1, struggle:1, transform:1}; let lastMove = target.getLastMoveAbsolute(); if (source.transformed || !lastMove || disallowedMoves[lastMove] || source.moves.indexOf(lastMove) !== -1) return false; let moveslot = source.moves.indexOf('mimic'); if (moveslot === -1) return false; let move = Tools.getMove(lastMove); source.moveset[moveslot] = { move: move.name, id: move.id, pp: move.pp, maxpp: move.pp, target: move.target, disabled: false, used: false, }; source.moves[moveslot] = toId(move.name); this.add('-start', source, 'Mimic', move.name); }, }, /** * Copycat and Mirror Move * Copy/call the last absolute move used by the target * */ copycat: { inherit: true, onHit: function (pokemon) { let noCopycat = {assist:1, bestow:1, chatter:1, circlethrow:1, copycat:1, counter:1, covet:1, destinybond:1, detect:1, dragontail:1, endure:1, feint:1, focuspunch:1, followme:1, helpinghand:1, mefirst:1, metronome:1, mimic:1, mirrorcoat:1, mirrormove:1, naturepower:1, protect:1, ragepowder:1, roar:1, sketch:1, sleeptalk:1, snatch:1, struggle:1, switcheroo:1, thief:1, transform:1, trick:1, whirlwind:1}; let lastMove = pokemon.getLastMoveAbsolute(); if (!lastMove || noCopycat[lastMove]) return false; this.useMove(lastMove, pokemon); }, }, mirrormove: { inherit: true, onTryHit: function (target) { let lastMove = target.getLastMoveAbsolute(); if (!lastMove || !this.getMove(lastMove).flags['mirror']) { return false; } }, onHit: function (target, source) { this.useMove(target.getLastMoveAbsolute(), source); }, }, /** * Disable, Encore and Torment * Disabling effects * */ disable: { inherit: true, effect: { duration: 4, noCopy: true, // doesn't get copied by Baton Pass onStart: function (pokemon) { let lastMove = pokemon.getLastMoveAbsolute(); if (!this.willMove(pokemon)) { this.effectData.duration++; } if (!lastMove) { this.debug('pokemon hasn\'t moved yet'); return false; } let moves = pokemon.moveset; for (let i = 0; i < moves.length; i++) { if (moves[i].id === lastMove) { if (!moves[i].pp) { this.debug('Move out of PP'); return false; } else { this.add('-start', pokemon, 'Disable', moves[i].move); this.effectData.move = lastMove; return; } } } this.debug('Move doesn\'t exist ???'); return false; }, onResidualOrder: 14, onEnd: function (pokemon) { this.add('-end', pokemon, 'Disable'); }, onBeforeMovePriority: 7, onBeforeMove: function (attacker, defender, move) { if (move.id === this.effectData.move) { this.add('cant', attacker, 'Disable', move); return false; } }, onDisableMove: function (pokemon) { let moves = pokemon.moveset; for (let i = 0; i < moves.length; i++) { if (moves[i].id === this.effectData.move) { pokemon.disableMove(moves[i].id); } } }, }, }, encore: { inherit: true, effect: { duration: 3, onStart: function (target) { let noEncore = {encore:1, mimic:1, mirrormove:1, sketch:1, struggle:1, transform:1}; let lastMove = target.getLastMoveAbsolute(); let moveIndex = target.moves.indexOf(lastMove); if (!lastMove) { // it failed delete target.volatiles['encore']; return false; } if (target.hasLinkedMove(lastMove)) { // TODO: Check instead whether the last executed move was linked let linkedMoves = target.getLinkedMoves(); if (noEncore[linkedMoves[0]] || noEncore[linkedMoves[1]] || target.moveset[0].pp <= 0 || target.moveset[1].pp <= 0) { // it failed delete target.volatiles['encore']; return false; } this.effectData.move = linkedMoves; } else { if (noEncore[lastMove] || (target.moveset[moveIndex] && target.moveset[moveIndex].pp <= 0)) { // it failed delete target.volatiles['encore']; return false; } this.effectData.move = lastMove; } this.effectData.turnsActivated = {}; this.add('-start', target, 'Encore'); if (!this.willMove(target)) { this.effectData.duration++; } }, onOverrideDecision: function (pokemon, target, move) { if (!this.effectData.turnsActivated[this.turn]) { // Initialize Encore effect for this turn this.effectData.turnsActivated[this.turn] = 0; } else if (this.effectData.turnsActivated[this.turn] >= (Array.isArray(this.effectData.move) ? this.effectData.move.length : 1)) { // Finish Encore effect for this turn return; } this.effectData.turnsActivated[this.turn]++; if (!Array.isArray(this.effectData.move)) { let nextDecision = this.willMove(pokemon); if (nextDecision) this.queue.splice(this.queue.indexOf(nextDecision), 1); if (move.id !== this.effectData.move) return this.effectData.move; return; } // Locked into a link switch (this.effectData.turnsActivated[this.turn]) { case 1: { if (!this.willMove(pokemon)) { let pseudoDecision = {choice: 'move', move: this.effectData.move[1], targetLoc: this.currentDecision.targetLoc, pokemon: this.currentDecision.pokemon, targetPosition: this.currentDecision.targetPosition, targetSide: this.currentDecision.targetSide}; this.queue.unshift(pseudoDecision); } if (this.effectData.move[0] !== move.id) return this.effectData.move[0]; return; } case 2: if (this.effectData.move[1] !== move.id) return this.effectData.move[1]; return; } }, onResidualOrder: 13, onResidual: function (target) { // early termination if you run out of PP let lastMove = target.getLastMoveAbsolute(); let index = target.moves.indexOf(lastMove); if (index === -1) return; // no last move if (target.hasLinkedMove(lastMove)) { // TODO: Check instead whether the last executed move was linked if (target.moveset[0].pp <= 0 || target.moveset[1].pp <= 0) { delete target.volatiles.encore; this.add('-end', target, 'Encore'); } } else { if (target.moveset[index].pp <= 0) { delete target.volatiles.encore; this.add('-end', target, 'Encore'); } } }, onEnd: function (target) { this.add('-end', target, 'Encore'); }, onDisableMove: function (pokemon) { if (!this.effectData.move) return; // ?? if (!Array.isArray(this.effectData.move)) { if (!pokemon.hasMove(this.effectData.move)) return; for (let i = 0; i < pokemon.moveset.length; i++) { if (pokemon.moveset[i].id !== this.effectData.move) { pokemon.disableMove(pokemon.moveset[i].id); } } } else { for (let i = 0; i < this.effectData.move.length; i++) { if (!pokemon.hasMove(this.effectData.move[i])) return; } for (let i = this.effectData.move.length; i < pokemon.moveset.length; i++) { if (this.effectData.move.indexOf(pokemon.moveset[i].id) >= 0) continue; pokemon.disableMove(pokemon.moveset[i].id); } } }, }, }, torment: { inherit: true, effect: { onStart: function (pokemon) { this.add('-start', pokemon, 'Torment'); }, onEnd: function (pokemon) { this.add('-end', pokemon, 'Torment'); }, onDisableMove: function (pokemon) { let lastMove = pokemon.lastMove; if (lastMove === 'struggle') return; if (Array.isArray(lastMove)) { for (let i = 0; i < lastMove.length; i++) { pokemon.disableMove(lastMove[i]); } } else { pokemon.disableMove(lastMove); } }, }, }, /** * Spite and Grudge * Decrease the PP of the last absolute move used by the target * Also, Grudge's effect won't be removed by its linked move, if any * */ grudge: { inherit: true, effect: { onStart: function (pokemon) { this.add('-singlemove', pokemon, 'Grudge'); }, onFaint: function (target, source, effect) { this.debug('Grudge detected fainted pokemon'); if (!source || !effect) return; if (effect.effectType === 'Move') { let lastMove = source.getLastMoveAbsolute(); for (let i = 0; i < source.moveset.length; i++) { if (source.moveset[i].id === lastMove) { source.moveset[i].pp = 0; this.add('-activate', source, 'Grudge', this.getMove(lastMove).name); } } } }, onBeforeMovePriority: 100, onBeforeMove: function (pokemon) { if (pokemon.moveThisTurn) return; // Second stage of a Linked move this.debug('removing Grudge before attack'); pokemon.removeVolatile('grudge'); }, }, }, spite: { inherit: true, onHit: function (target) { let lastMove = target.getLastMoveAbsolute(); if (target.deductPP(lastMove, 4)) { this.add("-activate", target, 'move: Spite', lastMove, 4); return; } return false; }, }, /** * Rollout and Ice Ball * */ rollout: { inherit: true, // Mod-specific: default mechanics }, iceball: { inherit: true, // Mod-specific: default mechanics }, /** * Other moves that check `pokemon.lastMove` * (may behave counter-intuitively if left unmodded) * **/ conversion2: { inherit: true, onHit: function (target, source) { let lastMove = target.getLastMoveAbsolute(); if (!lastMove) return false; let possibleTypes = []; let attackType = this.getMove(lastMove).type; for (let type in this.data.TypeChart) { if (source.hasType(type) || target.hasType(type)) continue; let typeCheck = this.data.TypeChart[type].damageTaken[attackType]; if (typeCheck === 2 || typeCheck === 3) { possibleTypes.push(type); } } if (!possibleTypes.length) { return false; } let type = possibleTypes[this.random(possibleTypes.length)]; if (!source.setType(type)) return false; this.add('-start', source, 'typechange', type); }, }, destinybond: { inherit: true, effect: { onStart: function (pokemon) { this.add('-singlemove', pokemon, 'Destiny Bond'); }, onFaint: function (target, source, effect) { if (!source || !effect) return; if (effect.effectType === 'Move' && !effect.isFutureMove) { this.add('-activate', target, 'Destiny Bond'); source.faint(); } }, onBeforeMovePriority: 100, onBeforeMove: function (pokemon, target, move) { // Second stage of a Linked move does not remove Destiny Bond if (pokemon.moveThisTurn) return; this.debug('removing Destiny Bond before attack'); pokemon.removeVolatile('destinybond'); }, }, }, trumpcard: { inherit: true, basePowerCallback: function (pokemon) { let move = pokemon.getMoveData(pokemon.getLastMoveAbsolute()); // Account for calling Trump Card via other moves switch (move.pp) { case 0: return 200; case 1: return 80; case 2: return 60; case 3: return 50; default: return 40; } }, }, uproar: { inherit: true, // Mod-specific: default mechanics }, /** * Moves that check `pokemon.moveThisTurn` * (may behave counter-intuitively if left unmodded) * **/ fusionbolt: { inherit: true, onBasePower: function (basePower, pokemon) { let actives = pokemon.side.active; for (let i = 0; i < actives.length; i++) { if (actives[i] && actives[i].checkMoveThisTurn('fusionflare')) { this.debug('double power'); return this.chainModify(2); } } }, }, fusionflare: { inherit: true, onBasePower: function (basePower, pokemon) { let actives = pokemon.side.active; for (let i = 0; i < actives.length; i++) { if (actives[i] && actives[i].checkMoveThisTurn('fusionbolt')) { this.debug('double power'); return this.chainModify(2); } } }, }, /** * Moves that should clean the house after running * (but aren't doing so in standard for whatever reason) */ beatup: { inherit: true, onAfterMove: function (pokemon) { pokemon.removeVolatile('beatup'); }, }, triplekick: { inherit: true, onAfterMove: function (pokemon) { pokemon.removeVolatile('triplekick'); }, }, /** * Moves that should clean the house if they aren't run * */ furycutter: { inherit: true, effect: { duration: 2, onStart: function () { this.effectData.multiplier = 1; }, onRestart: function () { if (this.effectData.multiplier < 4) { this.effectData.multiplier <<= 1; } this.effectData.duration = 2; }, onBeforeMove: function (pokemon, target, move) { if (move.id !== 'furycutter') pokemon.removeVolatile('furycutter'); }, }, }, /** * First Law of Pokémon Simulation: * Always make sure that Sky Drop works * */ skydrop: { inherit: true, effect: { duration: 2, onDragOut: false, onSourceDragOut: false, onFoeModifyPokemon: function (defender) { if (defender !== this.effectData.source) return; defender.trapped = true; }, onFoeBeforeMovePriority: 11, onFoeBeforeMove: function (attacker, defender, move) { if (attacker === this.effectData.source) { this.debug('Sky drop nullifying.'); return null; } }, onRedirectTarget: function (target, source, source2) { if (source !== this.effectData.target) return; if (this.effectData.source.fainted) return; return this.effectData.source; }, onAnyAccuracy: function (accuracy, target, source, move) { // both user and target of Sky Drop avoid moves, yet // not moves targetting themselves (Linked) if (target !== this.effectData.target && target !== this.effectData.source) { return; } if (source === this.effectData.target && target === this.effectData.source) { return; } if (source === target) return; if (move.id === 'gust' || move.id === 'twister') { return; } if (move.id === 'skyuppercut' || move.id === 'thunder' || move.id === 'hurricane' || move.id === 'smackdown' || move.id === 'thousandarrows' || move.id === 'helpinghand') { return; } if (source.hasAbility('noguard') || target.hasAbility('noguard')) { return; } if (source.volatiles['lockon'] && target === source.volatiles['lockon'].source) return; return 0; }, onAnyBasePower: function (basePower, target, source, move) { if (target !== this.effectData.target && target !== this.effectData.source) { return; } if (source === this.effectData.target && target === this.effectData.source) { return; } if (move.id === 'gust' || move.id === 'twister') { return this.chainModify(2); } }, onFaint: function (target) { if (target.volatiles['skydrop'] && target.volatiles['twoturnmove'].source) { this.add('-end', target.volatiles['twoturnmove'].source, 'Sky Drop', '[interrupt]'); } }, }, }, };