/** * A lot of Gen 1 moves have to be updated due to different mechanics. * Some moves have had major changes, such as Bite's typing. */ export const Moves: import('../../../sim/dex-moves').ModdedMoveDataTable = { acid: { inherit: true, secondary: { chance: 33, boosts: { def: -1, }, }, target: "normal", }, amnesia: { inherit: true, boosts: { spa: 2, spd: 2, }, }, aurorabeam: { inherit: true, secondary: { chance: 33, boosts: { atk: -1, }, }, }, bide: { inherit: true, priority: 0, accuracy: true, condition: { onStart(pokemon) { this.effectState.damage = 0; this.effectState.time = this.random(2, 4); this.add('-start', pokemon, 'Bide'); }, onBeforeMove(pokemon, t, move) { const currentMove = this.dex.getActiveMove('bide'); this.effectState.damage += this.lastDamage; this.effectState.time--; if (!this.effectState.time) { this.add('-end', pokemon, currentMove); if (!this.effectState.damage) { this.debug("Bide failed because no damage was stored"); this.add('-fail', pokemon); pokemon.removeVolatile('bide'); return false; } const target = this.getRandomTarget(pokemon, 'Pound'); this.actions.moveHit(target, pokemon, currentMove, { damage: this.effectState.damage * 2 } as ActiveMove); pokemon.removeVolatile('bide'); return false; } this.add('-activate', pokemon, 'Bide'); return false; }, onDisableMove(pokemon) { if (!pokemon.hasMove('bide')) { return; } for (const moveSlot of pokemon.moveSlots) { if (moveSlot.id !== 'bide') { pokemon.disableMove(moveSlot.id); } } }, }, type: "???", // Will look as Normal but it's STAB-less }, bind: { inherit: true, ignoreImmunity: true, volatileStatus: 'partiallytrapped', self: { volatileStatus: 'partialtrappinglock', }, onTryMove(source, target) { if (target.volatiles['mustrecharge']) { target.removeVolatile('mustrecharge'); this.hint("In Gen 1, partial trapping moves negate the recharge turn of Hyper Beam, even if they miss.", true); } }, onHit(target, source) { /** * The duration of the partially trapped must be always renewed to 2 * so target doesn't move on trapper switch out as happens in gen 1. * However, this won't happen if there's no switch and the trapper is * about to end its partial trapping. **/ if (target.volatiles['partiallytrapped']) { if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration! > 1) { target.volatiles['partiallytrapped'].duration = 2; } } }, }, bite: { inherit: true, category: "Physical", secondary: { chance: 10, volatileStatus: 'flinch', }, type: "Normal", }, blizzard: { inherit: true, accuracy: 90, target: "normal", }, bubble: { inherit: true, secondary: { chance: 33, boosts: { spe: -1, }, }, target: "normal", }, bubblebeam: { inherit: true, secondary: { chance: 33, boosts: { spe: -1, }, }, }, clamp: { inherit: true, accuracy: 75, pp: 10, volatileStatus: 'partiallytrapped', self: { volatileStatus: 'partialtrappinglock', }, onTryMove(source, target) { if (target.volatiles['mustrecharge']) { target.removeVolatile('mustrecharge'); this.hint("In Gen 1, partial trapping moves negate the recharge turn of Hyper Beam, even if they miss.", true); } }, onHit(target, source) { /** * The duration of the partially trapped must be always renewed to 2 * so target doesn't move on trapper switch out as happens in gen 1. * However, this won't happen if there's no switch and the trapper is * about to end its partial trapping. **/ if (target.volatiles['partiallytrapped']) { if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration! > 1) { target.volatiles['partiallytrapped'].duration = 2; } } }, }, constrict: { inherit: true, secondary: { chance: 33, boosts: { spe: -1, }, }, }, conversion: { inherit: true, target: "normal", onHit(target, source) { source.setType(target.getTypes(true)); this.add('-start', source, 'typechange', source.types.join('/'), '[from] move: Conversion', `[of] ${target}`); }, }, counter: { inherit: true, ignoreImmunity: true, willCrit: false, basePower: 1, damageCallback(pokemon, target) { // Counter mechanics in gen 1: // - a move is Counterable if it is Normal or Fighting type, has nonzero Base Power, and is not Counter // - if Counter is used by the player, it will succeed if the opponent's last used move is Counterable // - if Counter is used by the opponent, it will succeed if the player's last selected move is Counterable // - (Counter will thus desync if the target's last used move is not as counterable as the target's last selected move) // - if Counter succeeds it will deal twice the last move damage dealt in battle (even if it's from a different pokemon because of a switch) const lastMove = target.side.lastMove && this.dex.moves.get(target.side.lastMove.id); const lastMoveIsCounterable = lastMove && lastMove.basePower > 0 && ['Normal', 'Fighting'].includes(lastMove.type) && lastMove.id !== 'counter'; const lastSelectedMove = target.side.lastSelectedMove && this.dex.moves.get(target.side.lastSelectedMove); const lastSelectedMoveIsCounterable = lastSelectedMove && lastSelectedMove.basePower > 0 && ['Normal', 'Fighting'].includes(lastSelectedMove.type) && lastSelectedMove.id !== 'counter'; if (!lastMoveIsCounterable && !lastSelectedMoveIsCounterable) { this.debug("Gen 1 Counter: last move was not Counterable"); this.add('-fail', pokemon); return false; } if (this.lastDamage <= 0) { this.debug("Gen 1 Counter: no previous damage exists"); this.add('-fail', pokemon); return false; } if (!lastMoveIsCounterable || !lastSelectedMoveIsCounterable) { this.hint("Desync Clause Mod activated!"); this.add('-fail', pokemon); return false; } return 2 * this.lastDamage; }, flags: { contact: 1, protect: 1, metronome: 1 }, }, crabhammer: { inherit: true, critRatio: 2, }, dig: { inherit: true, basePower: 100, condition: {}, onTryMove(attacker, defender, move) { if (attacker.removeVolatile('twoturnmove')) { attacker.removeVolatile('invulnerability'); return; } this.add('-prepare', attacker, move.name); attacker.addVolatile('twoturnmove', defender); attacker.addVolatile('invulnerability', defender); return null; }, }, disable: { num: 50, accuracy: 55, basePower: 0, category: "Status", name: "Disable", pp: 20, priority: 0, flags: { protect: 1, mirror: 1, bypasssub: 1, metronome: 1 }, volatileStatus: 'disable', onTryHit(target) { // This function should not return if the checks are met. Adding && undefined ensures this happens. return target.moveSlots.some(ms => ms.pp > 0) && !('disable' in target.volatiles) && undefined; }, condition: { onStart(pokemon) { // disable can only select moves that have pp > 0, hence the onTryHit modification const moveSlot = this.sample(pokemon.moveSlots.filter(ms => ms.pp > 0)); this.add('-start', pokemon, 'Disable', moveSlot.move); this.effectState.move = moveSlot.id; // 1-8 turns (which will in effect translate to 0-7 missed turns for the target) this.effectState.time = this.random(1, 9); }, onEnd(pokemon) { this.add('-end', pokemon, 'Disable'); }, onBeforeMovePriority: 6, onBeforeMove(pokemon, target, move) { pokemon.volatiles['disable'].time--; if (!pokemon.volatiles['disable'].time) { pokemon.removeVolatile('disable'); return; } if (pokemon.volatiles['bide']) move = this.dex.getActiveMove('bide'); if (move.id === this.effectState.move) { this.add('cant', pokemon, 'Disable', move); pokemon.removeVolatile('twoturnmove'); return false; } }, onDisableMove(pokemon) { for (const moveSlot of pokemon.moveSlots) { if (moveSlot.id === this.effectState.move) { pokemon.disableMove(moveSlot.id); } } }, }, secondary: null, target: "normal", type: "Normal", }, dizzypunch: { inherit: true, secondary: null, }, doubleedge: { inherit: true, basePower: 100, }, dragonrage: { inherit: true, basePower: 1, }, explosion: { inherit: true, basePower: 170, target: "normal", }, fireblast: { inherit: true, secondary: { chance: 30, status: 'brn', }, }, firespin: { inherit: true, accuracy: 70, basePower: 15, volatileStatus: 'partiallytrapped', self: { volatileStatus: 'partialtrappinglock', }, onTryMove(source, target) { if (target.volatiles['mustrecharge']) { target.removeVolatile('mustrecharge'); this.hint("In Gen 1, partial trapping moves negate the recharge turn of Hyper Beam, even if they miss.", true); } }, onHit(target, source) { /** * The duration of the partially trapped must be always renewed to 2 * so target doesn't move on trapper switch out as happens in gen 1. * However, this won't happen if there's no switch and the trapper is * about to end its partial trapping. **/ if (target.volatiles['partiallytrapped']) { if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration! > 1) { target.volatiles['partiallytrapped'].duration = 2; } } }, }, fly: { inherit: true, condition: {}, onTryMove(attacker, defender, move) { if (attacker.removeVolatile('twoturnmove')) { attacker.removeVolatile('invulnerability'); return; } this.add('-prepare', attacker, move.name); attacker.addVolatile('twoturnmove', defender); attacker.addVolatile('invulnerability', defender); return null; }, }, focusenergy: { inherit: true, condition: { onStart(pokemon) { this.add('-start', pokemon, 'move: Focus Energy'); }, // This does nothing as it's dealt with on critical hit calculation. onModifyMove() {}, }, }, glare: { inherit: true, ignoreImmunity: true, }, growth: { inherit: true, boosts: { spa: 1, spd: 1, }, }, gust: { inherit: true, type: "Normal", }, haze: { inherit: true, onHit(target, source) { this.add('-activate', target, 'move: Haze'); this.add('-clearallboost', '[silent]'); for (const pokemon of this.getAllActive()) { pokemon.clearBoosts(); if (pokemon !== source) { pokemon.cureStatus(true); } if (pokemon.status === 'tox') { pokemon.setStatus('psn', null, null, true); } pokemon.updateSpeed(); // should only clear a specific set of volatiles // while technically the toxic counter shouldn't be cleared, the preserved toxic counter is never used again // in-game, so it is equivalent to just clear it. const silentHack = '|[silent]'; const silentHackVolatiles = ['disable', 'confusion']; const hazeVolatiles: { [key: string]: string } = { 'disable': '', 'confusion': '', 'mist': 'Mist', 'focusenergy': 'move: Focus Energy', 'leechseed': 'move: Leech Seed', 'lightscreen': 'Light Screen', 'reflect': 'Reflect', 'residualdmg': 'Toxic counter', }; for (const v in hazeVolatiles) { if (!pokemon.removeVolatile(v)) { continue; } if (silentHackVolatiles.includes(v)) { // these volatiles have their own onEnd method that prints, so to avoid // double printing and ensure they are still silent, we need to tack on a // silent attribute at the end this.log[this.log.length - 1] += silentHack; } else { this.add('-end', pokemon, hazeVolatiles[v], '[silent]'); } } } }, target: "self", }, highjumpkick: { inherit: true, onMoveFail(target, source, move) { this.directDamage(1, source, target); }, }, jumpkick: { inherit: true, onMoveFail(target, source, move) { this.directDamage(1, source, target); }, }, karatechop: { inherit: true, critRatio: 2, type: "Normal", }, leechseed: { inherit: true, onHit() {}, condition: { onStart(target) { this.add('-start', target, 'move: Leech Seed'); }, onAfterMoveSelfPriority: 1, onAfterMoveSelf(pokemon) { const leecher = this.getAtSlot(pokemon.volatiles['leechseed'].sourceSlot); if (!leecher || leecher.fainted || leecher.hp <= 0) { this.debug('Nothing to leech into'); return; } // We check if leeched Pokémon has Toxic to increase leeched damage. let toxicCounter = 1; const residualdmg = pokemon.volatiles['residualdmg']; if (residualdmg) { residualdmg.counter++; toxicCounter = residualdmg.counter; } const toLeech = this.clampIntRange(Math.floor(pokemon.baseMaxhp / 16), 1) * toxicCounter; const damage = this.damage(toLeech, pokemon, leecher); if (residualdmg) this.hint("In Gen 1, Leech Seed's damage is affected by Toxic's counter.", true); if (!damage || toLeech > damage) { this.hint("In Gen 1, Leech Seed recovery is not limited by the remaining HP of the seeded Pokemon.", true); } this.heal(toLeech, leecher, pokemon); }, }, }, lightscreen: { num: 113, accuracy: true, basePower: 0, category: "Status", name: "Light Screen", pp: 30, priority: 0, flags: { metronome: 1 }, volatileStatus: 'lightscreen', onTryHit(pokemon) { if (pokemon.volatiles['lightscreen']) { return false; } }, condition: { onStart(pokemon) { this.add('-start', pokemon, 'Light Screen'); }, }, target: "self", type: "Psychic", }, mimic: { inherit: true, flags: { protect: 1, bypasssub: 1, metronome: 1 }, onHit(target, source) { const moveslot = source.moves.indexOf('mimic'); if (moveslot < 0) return false; const moves = target.moves; const moveid = this.sample(moves); if (!moveid) return false; const move = this.dex.moves.get(moveid); source.moveSlots[moveslot] = { move: move.name, id: move.id, pp: source.moveSlots[moveslot].pp, maxpp: move.pp * 8 / 5, target: move.target, disabled: false, used: false, virtual: true, }; this.add('-start', source, 'Mimic', move.name); }, }, mirrormove: { inherit: true, onHit(pokemon) { const foe = pokemon.side.foe.active[0]; if (!foe?.lastMove || foe.lastMove.id === 'mirrormove') { return false; } pokemon.side.lastSelectedMove = foe.lastMove.id; this.actions.useMove(foe.lastMove.id, pokemon); }, }, mist: { inherit: true, condition: { onStart(pokemon) { this.add('-start', pokemon, 'Mist'); }, onTryBoost(boost, target, source, effect) { if (effect.effectType === 'Move' && effect.category !== 'Status') return; if (source && target !== source) { let showMsg = false; let i: BoostID; for (i in boost) { if (boost[i]! < 0) { delete boost[i]; showMsg = true; } } if (showMsg && !(effect as ActiveMove).secondaries) { this.add('-activate', target, 'move: Mist'); } } }, }, }, nightshade: { inherit: true, ignoreImmunity: true, basePower: 1, }, petaldance: { inherit: true, onMoveFail() {}, }, poisonsting: { inherit: true, secondary: { chance: 20, status: 'psn', }, }, psychic: { inherit: true, secondary: { chance: 33, boosts: { spa: -1, spd: -1, }, }, }, psywave: { inherit: true, basePower: 1, damageCallback(pokemon) { const psywaveDamage = (this.random(0, this.trunc(1.5 * pokemon.level))); if (psywaveDamage <= 0) { this.hint("Desync Clause Mod activated!"); return false; } return psywaveDamage; }, }, rage: { inherit: true, self: { volatileStatus: 'rage', }, condition: { // Rage lock onStart(target, source, effect) { this.effectState.move = 'rage'; this.effectState.accuracy = 255; }, onLockMove: 'rage', onHit(target, source, move) { // Disable and exploding moves boost Rage even if they miss/fail, so they are dealt with separately. if (target.boosts.atk < 6 && (move.category !== 'Status' && !move.selfdestruct)) { this.boost({ atk: 1 }); } }, }, }, razorleaf: { inherit: true, critRatio: 2, target: "normal", }, razorwind: { inherit: true, critRatio: 1, target: "normal", onTryMove(attacker, defender, move) { if (attacker.removeVolatile('twoturnmove')) { attacker.removeVolatile('invulnerability'); return; } this.add('-prepare', attacker, move.name); attacker.addVolatile('twoturnmove', defender); return null; }, }, recover: { inherit: true, heal: null, onHit(target) { if (target.hp === target.maxhp) return false; // Fail when health is 255 or 511 less than max, unless it is divisible by 256 if ( target.hp === target.maxhp || ((target.hp === (target.maxhp - 255) || target.hp === (target.maxhp - 511)) && target.hp % 256 !== 0) ) { this.hint( "In Gen 1, recovery moves fail if (user's maximum HP - user's current HP + 1) is divisible by 256, " + "unless the current hp is also divisible by 256." ); return false; } this.heal(Math.floor(target.maxhp / 2), target, target); }, }, reflect: { num: 115, accuracy: true, basePower: 0, category: "Status", name: "Reflect", pp: 20, priority: 0, flags: { metronome: 1 }, volatileStatus: 'reflect', onTryHit(pokemon) { if (pokemon.volatiles['reflect']) { return false; } }, condition: { onStart(pokemon) { this.add('-start', pokemon, 'Reflect'); }, }, secondary: null, target: "self", type: "Psychic", }, rest: { inherit: true, onTry() {}, onHit(target, source, move) { if (target.hp === target.maxhp) return false; // Fail when health is 255 or 511 less than max, unless it is divisible by 256 if ( target.hp === target.maxhp || ((target.hp === (target.maxhp - 255) || target.hp === (target.maxhp - 511)) && target.hp % 256 !== 0) ) { this.hint( "In Gen 1, recovery moves fail if (user's maximum HP - user's current HP + 1) is divisible by 256, " + "unless the current hp is also divisible by 256." ); return false; } if (!target.setStatus('slp', source, move)) return false; target.statusState.time = 2; target.statusState.startTime = 2; this.heal(target.maxhp); // Aesthetic only as the healing happens after you fall asleep in-game }, }, roar: { inherit: true, forceSwitch: false, onTryHit() {}, priority: 0, }, rockslide: { inherit: true, secondary: null, target: "normal", }, rockthrow: { inherit: true, accuracy: 65, }, sandattack: { inherit: true, ignoreImmunity: true, type: "Normal", }, seismictoss: { inherit: true, ignoreImmunity: true, basePower: 1, }, selfdestruct: { inherit: true, basePower: 130, target: "normal", }, skullbash: { inherit: true, onTryMove(attacker, defender, move) { if (attacker.removeVolatile('twoturnmove')) { attacker.removeVolatile('invulnerability'); return; } this.add('-prepare', attacker, move.name); attacker.addVolatile('twoturnmove', defender); return null; }, }, skyattack: { inherit: true, onTryMove(attacker, defender, move) { if (attacker.removeVolatile('twoturnmove')) { attacker.removeVolatile('invulnerability'); return; } this.add('-prepare', attacker, move.name); attacker.addVolatile('twoturnmove', defender); return null; }, }, slash: { inherit: true, critRatio: 2, }, sludge: { inherit: true, secondary: { chance: 40, status: 'psn', }, }, solarbeam: { inherit: true, onTryMove(attacker, defender, move) { if (attacker.removeVolatile('twoturnmove')) { attacker.removeVolatile('invulnerability'); return; } this.add('-prepare', attacker, move.name); attacker.addVolatile('twoturnmove', defender); return null; }, }, sonicboom: { inherit: true, ignoreImmunity: true, basePower: 1, }, softboiled: { inherit: true, heal: null, onHit(target) { if (target.hp === target.maxhp) return false; // Fail when health is 255 or 511 less than max, unless it is divisible by 256 if ( target.hp === target.maxhp || ((target.hp === (target.maxhp - 255) || target.hp === (target.maxhp - 511)) && target.hp % 256 !== 0) ) { this.hint( "In Gen 1, recovery moves fail if (user's maximum HP - user's current HP + 1) is divisible by 256, " + "unless the current hp is also divisible by 256." ); return false; } this.heal(Math.floor(target.maxhp / 2), target, target); }, }, struggle: { inherit: true, pp: 10, recoil: [1, 2], onModifyMove() {}, }, substitute: { num: 164, accuracy: true, basePower: 0, category: "Status", name: "Substitute", pp: 10, priority: 0, flags: { metronome: 1 }, volatileStatus: 'substitute', onTryHit(target) { if (target.volatiles['substitute']) { this.add('-fail', target, 'move: Substitute'); return null; } // We only prevent when hp is less than one quarter. // If you use substitute at exactly one quarter, you faint. if (target.hp < target.maxhp / 4) { this.add('-fail', target, 'move: Substitute', '[weak]'); return null; } }, onHit(target) { // If max HP is 3 or less substitute makes no damage if (target.maxhp > 3) { this.directDamage(target.maxhp / 4, target, target); } }, condition: { onStart(target) { this.add('-start', target, 'Substitute'); this.effectState.hp = Math.floor(target.maxhp / 4) + 1; delete target.volatiles['partiallytrapped']; }, onTryHitPriority: -1, onTryHit(target, source, move) { if (move.category === 'Status') { // In gen 1 it only blocks: // poison, confusion, secondary effect confusion, stat reducing moves and Leech Seed. const SubBlocked = ['lockon', 'meanlook', 'mindreader', 'nightmare']; if ( move.status === 'psn' || move.status === 'tox' || (move.boosts && target !== source) || move.volatileStatus === 'confusion' || SubBlocked.includes(move.id) ) { return false; } return; } if (move.volatileStatus && target === source) return; // NOTE: In future generations the damage is capped to the remaining HP of the // Substitute, here we deliberately use the uncapped damage when tracking lastDamage etc. // Also, multi-hit moves must always deal the same damage as the first hit for any subsequent hits let uncappedDamage = move.hit > 1 ? this.lastDamage : this.actions.getDamage(source, target, move); if (move.id === 'bide') uncappedDamage = source.volatiles['bide'].damage * 2; if (!uncappedDamage && uncappedDamage !== 0) return null; uncappedDamage = this.runEvent('SubDamage', target, source, move, uncappedDamage); if (!uncappedDamage && uncappedDamage !== 0) return uncappedDamage; this.lastDamage = uncappedDamage; target.volatiles['substitute'].hp -= uncappedDamage > target.volatiles['substitute'].hp ? target.volatiles['substitute'].hp : uncappedDamage; if (target.volatiles['substitute'].hp <= 0) { target.removeVolatile('substitute'); target.subFainted = true; } else { this.add('-activate', target, 'Substitute', '[damage]'); } // Drain/recoil/secondary effect confusion do not happen if the substitute breaks if (target.volatiles['substitute']) { if (move.recoil) { this.damage(this.clampIntRange(Math.floor(uncappedDamage * move.recoil[0] / move.recoil[1]), 1), source, target, 'recoil'); } if (move.drain) { const amount = this.clampIntRange(Math.floor(uncappedDamage * move.drain[0] / move.drain[1]), 1); this.lastDamage = amount; this.heal(amount, source, target, 'drain'); } if (move.secondary?.volatileStatus === 'confusion') { const secondary = move.secondary; if (secondary.chance === undefined || this.randomChance(Math.ceil(secondary.chance * 256 / 100) - 1, 256)) { target.addVolatile(move.secondary.volatileStatus, source, move); this.hint( "In Gen 1, moves that inflict confusion as a secondary effect can confuse targets with a Substitute, " + "as long as the move does not break the Substitute." ); } } } this.runEvent('AfterSubDamage', target, source, move, uncappedDamage); // Add here counter damage const lastAttackedBy = target.getLastAttackedBy(); if (!lastAttackedBy) { target.attackedBy.push({ source, move: move.id, damage: uncappedDamage, slot: source.getSlot(), thisTurn: true }); } else { lastAttackedBy.move = move.id; lastAttackedBy.damage = uncappedDamage; } return 0; }, onEnd(target) { this.add('-end', target, 'Substitute'); }, }, secondary: null, target: "self", type: "Normal", }, superfang: { inherit: true, ignoreImmunity: true, basePower: 1, }, thrash: { inherit: true, onMoveFail() {}, }, thunder: { inherit: true, secondary: { chance: 10, status: 'par', }, }, triattack: { inherit: true, onHit() {}, secondary: null, }, whirlwind: { inherit: true, accuracy: 85, forceSwitch: false, onTryHit() {}, priority: 0, }, wingattack: { inherit: true, basePower: 35, }, wrap: { inherit: true, accuracy: 85, ignoreImmunity: true, volatileStatus: 'partiallytrapped', self: { volatileStatus: 'partialtrappinglock', }, onTryMove(source, target) { if (target.volatiles['mustrecharge']) { target.removeVolatile('mustrecharge'); this.hint("In Gen 1, partial trapping moves negate the recharge turn of Hyper Beam, even if they miss.", true); } }, onHit(target, source) { /** * The duration of the partially trapped must be always renewed to 2 * so target doesn't move on trapper switch out as happens in gen 1. * However, this won't happen if there's no switch and the trapper is * about to end its partial trapping. **/ if (target.volatiles['partiallytrapped']) { if (source.volatiles['partialtrappinglock'] && source.volatiles['partialtrappinglock'].duration! > 1) { target.volatiles['partiallytrapped'].duration = 2; } } }, }, };