mirror of
https://github.com/smogon/pokemon-showdown.git
synced 2026-06-02 22:08:36 -05:00
Some checks are pending
Node.js CI / build (18.x) (push) Waiting to run
* Fix Piercing Drill and Unseen Fist * Change variable name * Fix * Rename onBypassProtect to onHitProtect * Fix
506 lines
18 KiB
TypeScript
506 lines
18 KiB
TypeScript
export const Scripts: ModdedBattleScriptsData = {
|
|
gen: 9,
|
|
init() {
|
|
for (const i in this.data.Moves) {
|
|
if (this.data.Moves[i].pp > 20) {
|
|
this.modData('Moves', i).pp = 20;
|
|
}
|
|
}
|
|
},
|
|
statModify(baseStats, set, statName) {
|
|
const tr = this.trunc;
|
|
let stat = baseStats[statName];
|
|
const evs = set.evs[statName];
|
|
if (statName === 'hp') {
|
|
return stat + evs + 75;
|
|
}
|
|
stat = stat + evs + 20;
|
|
const nature = this.dex.natures.get(set.nature);
|
|
// Natures are calculated with 16-bit truncation.
|
|
// This only affects Eternatus-Eternamax in Pure Hackmons.
|
|
if (nature.plus === statName) {
|
|
stat = this.ruleTable.has('overflowstatmod') ? Math.min(stat, 595) : stat;
|
|
stat = tr(tr(stat * 110, 16) / 100);
|
|
} else if (nature.minus === statName) {
|
|
stat = this.ruleTable.has('overflowstatmod') ? Math.min(stat, 728) : stat;
|
|
stat = tr(tr(stat * 90, 16) / 100);
|
|
}
|
|
return stat;
|
|
},
|
|
calculatePP(move, ppUps) {
|
|
return move.noPPBoosts ? move.pp : (move.pp / 5 + 1) * 4;
|
|
},
|
|
pokemon: {
|
|
// Remove Trick Room underflow
|
|
getActionSpeed() {
|
|
let speed = this.getStat('spe', false, false);
|
|
const trickRoomCheck = this.battle.ruleTable.has('twisteddimensionmod') ?
|
|
!this.battle.field.getPseudoWeather('trickroom') : this.battle.field.getPseudoWeather('trickroom');
|
|
if (trickRoomCheck) {
|
|
speed = -speed;
|
|
}
|
|
return speed;
|
|
},
|
|
// Don't revert Mega Evolutions after fainting
|
|
// TODO: confirm interaction with Revival Blessing
|
|
formeChange(speciesId, source, isPermanent, abilitySlot = '0', message) {
|
|
const rawSpecies = this.battle.dex.species.get(speciesId);
|
|
|
|
const species = this.setSpecies(rawSpecies, source);
|
|
if (!species) return false;
|
|
|
|
if (this.battle.gen <= 2) return true;
|
|
|
|
// The species the opponent sees
|
|
const apparentSpecies =
|
|
this.illusion ? this.illusion.species.name : species.baseSpecies;
|
|
if (isPermanent) {
|
|
this.baseSpecies = rawSpecies;
|
|
this.details = this.getUpdatedDetails();
|
|
let details = (this.illusion || this).details;
|
|
if (this.terastallized) details += `, tera:${this.terastallized}`;
|
|
this.battle.add('detailschange', this, details);
|
|
this.updateMaxHp();
|
|
if (!source) {
|
|
// Tera forme
|
|
// Ogerpon/Terapagos text goes here
|
|
this.formeRegression = true;
|
|
} else if (source.effectType === 'Item') {
|
|
this.canTerastallize = null; // National Dex behavior
|
|
if (source.zMove) {
|
|
this.battle.add('-burst', this, apparentSpecies, species.requiredItem);
|
|
this.moveThisTurnResult = true; // Ultra Burst counts as an action for Truant
|
|
} else if (source.isPrimalOrb) {
|
|
if (this.illusion) {
|
|
this.ability = '';
|
|
this.battle.add('-primal', this.illusion, species.requiredItem);
|
|
} else {
|
|
this.battle.add('-primal', this, species.requiredItem);
|
|
}
|
|
} else {
|
|
this.battle.add('-mega', this, apparentSpecies, species.requiredItem);
|
|
this.moveThisTurnResult = true; // Mega Evolution counts as an action for Truant
|
|
}
|
|
} else if (source.effectType === 'Status') {
|
|
// Shaymin-Sky -> Shaymin
|
|
this.battle.add('-formechange', this, species.name, message);
|
|
}
|
|
} else {
|
|
if (source?.effectType === 'Ability') {
|
|
this.battle.add('-formechange', this, species.name, message, `[from] ability: ${source.name}`);
|
|
} else {
|
|
this.battle.add('-formechange', this, this.illusion ? this.illusion.species.name : species.name, message);
|
|
}
|
|
}
|
|
if (isPermanent && (!source || !['disguise', 'iceface'].includes(source.id))) {
|
|
if (this.illusion && source) {
|
|
// Tera forme by Ogerpon or Terapagos breaks the Illusion
|
|
this.ability = ''; // Don't allow Illusion to wear off
|
|
}
|
|
const ability = species.abilities[abilitySlot] || species.abilities['0'];
|
|
// Ogerpon's forme change doesn't override permanent abilities
|
|
if (source || !this.getAbility().flags['cantsuppress']) this.setAbility(ability, null, null, true);
|
|
// However, its ability does reset upon switching out
|
|
this.baseAbility = this.battle.toID(ability);
|
|
}
|
|
if (this.terastallized) {
|
|
this.knownType = true;
|
|
this.apparentType = this.terastallized;
|
|
}
|
|
return true;
|
|
},
|
|
// Announce status immunities from abilities without revealing the ability
|
|
// TODO: check if this happens to other abilities besides Spicy Spray (Static, Poison Touch, etc.)
|
|
setStatus(status, source, sourceEffect, ignoreImmunities) {
|
|
if (!this.hp) return false;
|
|
status = this.battle.dex.conditions.get(status);
|
|
if (this.battle.event) {
|
|
if (!source) source = this.battle.event.source;
|
|
if (!sourceEffect) sourceEffect = this.battle.effect;
|
|
}
|
|
if (!source) source = this;
|
|
|
|
if (this.status === status.id) {
|
|
if ((sourceEffect as Move)?.status === this.status) {
|
|
this.battle.add('-fail', this, this.status);
|
|
} else if ((sourceEffect as Move)?.status) {
|
|
this.battle.add('-fail', source);
|
|
this.battle.attrLastMove('[still]');
|
|
}
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
!ignoreImmunities && status.id && !(source?.hasAbility('corrosion') && ['tox', 'psn'].includes(status.id))
|
|
) {
|
|
// the game currently never ignores immunities
|
|
if (!this.runStatusImmunity(status.id === 'tox' ? 'psn' : status.id)) {
|
|
this.battle.debug('immune to status');
|
|
if ((sourceEffect as Move)?.status || sourceEffect?.effectType === 'Ability') {
|
|
this.battle.add('-immune', this);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
const prevStatus = this.status;
|
|
const prevStatusState = this.statusState;
|
|
if (status.id) {
|
|
const result: boolean = this.battle.runEvent('SetStatus', this, source, sourceEffect, status);
|
|
if (!result) {
|
|
this.battle.debug('set status [' + status.id + '] interrupted');
|
|
return result;
|
|
}
|
|
}
|
|
|
|
this.status = status.id;
|
|
this.statusState = this.battle.initEffectState({ id: status.id, target: this });
|
|
if (source) this.statusState.source = source;
|
|
if (status.duration) this.statusState.duration = status.duration;
|
|
if (status.durationCallback) {
|
|
this.statusState.duration = status.durationCallback.call(this.battle, this, source, sourceEffect);
|
|
}
|
|
|
|
if (status.id && !this.battle.singleEvent('Start', status, this.statusState, this, source, sourceEffect)) {
|
|
this.battle.debug('status start [' + status.id + '] interrupted');
|
|
// cancel the setstatus
|
|
this.status = prevStatus;
|
|
this.statusState = prevStatusState;
|
|
return false;
|
|
}
|
|
if (status.id && !this.battle.runEvent('AfterSetStatus', this, source, sourceEffect, status)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
// Disable Fake Out if the user has already acted since switching in
|
|
getMoves(lockedMove, restrictData) {
|
|
if (lockedMove) {
|
|
lockedMove = this.battle.toID(lockedMove);
|
|
if (lockedMove === 'recharge') {
|
|
return [{
|
|
move: 'Recharge',
|
|
id: 'recharge' as ID,
|
|
}];
|
|
}
|
|
for (const moveSlot of this.moveSlots) {
|
|
if (moveSlot.id !== lockedMove) continue;
|
|
return [{
|
|
move: moveSlot.move,
|
|
id: moveSlot.id,
|
|
}];
|
|
}
|
|
// does this happen?
|
|
return [{
|
|
move: this.battle.dex.moves.get(lockedMove).name,
|
|
id: lockedMove,
|
|
}];
|
|
}
|
|
const moves = [];
|
|
let hasValidMove = false;
|
|
for (const moveSlot of this.moveSlots) {
|
|
let moveName = moveSlot.move;
|
|
if (moveSlot.id === 'hiddenpower') {
|
|
moveName = `Hidden Power ${this.hpType}`;
|
|
if (this.battle.gen < 6) moveName += ` ${this.hpPower}`;
|
|
} else if (moveSlot.id === 'return' || moveSlot.id === 'frustration') {
|
|
const basePowerCallback = this.battle.dex.moves.get(moveSlot.id).basePowerCallback as (pokemon: Pokemon) => number;
|
|
moveName += ` ${basePowerCallback(this)}`;
|
|
}
|
|
let target = moveSlot.target;
|
|
switch (moveSlot.id) {
|
|
case 'curse':
|
|
if (!this.hasType('Ghost')) {
|
|
target = this.battle.dex.moves.get('curse').nonGhostTarget;
|
|
}
|
|
break;
|
|
case 'pollenpuff':
|
|
// Heal Block only prevents Pollen Puff from targeting an ally when the user has Heal Block
|
|
if (this.volatiles['healblock']) {
|
|
target = 'adjacentFoe';
|
|
}
|
|
break;
|
|
case 'terastarstorm':
|
|
if (this.species.name === 'Terapagos-Stellar') {
|
|
target = 'allAdjacentFoes';
|
|
}
|
|
break;
|
|
}
|
|
let disabled = moveSlot.disabled;
|
|
if (this.volatiles['dynamax']) {
|
|
// if each of a Pokemon's base moves are disabled by one of these effects, it will Struggle
|
|
const canCauseStruggle = ['Encore', 'Disable', 'Taunt', 'Assault Vest', 'Belch', 'Stuff Cheeks'];
|
|
disabled = this.maxMoveDisabled(moveSlot.id) || disabled && canCauseStruggle.includes(moveSlot.disabledSource!);
|
|
} else if (moveSlot.pp <= 0 || (moveSlot.id === 'fakeout' && this.activeMoveActions > 0)) {
|
|
disabled = true;
|
|
}
|
|
|
|
if (disabled === 'hidden') {
|
|
disabled = !restrictData;
|
|
}
|
|
if (!disabled) {
|
|
hasValidMove = true;
|
|
}
|
|
|
|
moves.push({
|
|
move: moveName,
|
|
id: moveSlot.id,
|
|
pp: moveSlot.pp,
|
|
maxpp: moveSlot.maxpp,
|
|
target,
|
|
disabled,
|
|
});
|
|
}
|
|
return hasValidMove ? moves : [];
|
|
},
|
|
},
|
|
actions: {
|
|
canTerastallize(pokemon) {
|
|
return null;
|
|
},
|
|
canMegaEvo(pokemon: Pokemon) {
|
|
const species = pokemon.baseSpecies;
|
|
const altForme = species.otherFormes && this.dex.species.get(species.otherFormes[0]);
|
|
const item = pokemon.getItem();
|
|
// Mega Rayquaza
|
|
if ((this.battle.gen <= 7 || this.battle.ruleTable.has('+pokemontag:past') ||
|
|
this.battle.ruleTable.has('+pokemontag:future')) &&
|
|
altForme?.isMega && altForme?.requiredMove &&
|
|
pokemon.baseMoves.includes(toID(altForme.requiredMove)) && !item.zMove) {
|
|
return altForme.name;
|
|
}
|
|
return item.megaStone?.[species.name] || null;
|
|
},
|
|
// Announce 4x and 0.25x effectiveness
|
|
modifyDamage(baseDamage, pokemon, target, move, suppressMessages) {
|
|
const tr = this.battle.trunc;
|
|
if (!move.type) move.type = '???';
|
|
const type = move.type;
|
|
|
|
baseDamage += 2;
|
|
|
|
if (move.spreadHit) {
|
|
// multi-target modifier (doubles only)
|
|
const spreadModifier = this.battle.gameType === 'freeforall' ? 0.5 : 0.75;
|
|
this.battle.debug(`Spread modifier: ${spreadModifier}`);
|
|
baseDamage = this.battle.modify(baseDamage, spreadModifier);
|
|
} else if (move.multihitType === 'parentalbond' && move.hit > 1) {
|
|
// Parental Bond modifier
|
|
const bondModifier = this.battle.gen > 6 ? 0.25 : 0.5;
|
|
this.battle.debug(`Parental Bond modifier: ${bondModifier}`);
|
|
baseDamage = this.battle.modify(baseDamage, bondModifier);
|
|
}
|
|
|
|
// weather modifier
|
|
baseDamage = this.battle.runEvent('WeatherModifyDamage', pokemon, target, move, baseDamage);
|
|
|
|
// crit - not a modifier
|
|
const isCrit = target.getMoveHitData(move).crit;
|
|
if (isCrit) {
|
|
baseDamage = tr(baseDamage * (move.critModifier || (this.battle.gen >= 6 ? 1.5 : 2)));
|
|
}
|
|
|
|
// random factor - also not a modifier
|
|
baseDamage = this.battle.randomizer(baseDamage);
|
|
|
|
// STAB
|
|
// The "???" type never gets STAB
|
|
// Not even if you Roost in Gen 4 and somehow manage to use
|
|
// Struggle in the same turn.
|
|
// (On second thought, it might be easier to get a MissingNo.)
|
|
if (type !== '???') {
|
|
let stab: number | [number, number] = 1;
|
|
|
|
const isSTAB = move.forceSTAB || pokemon.hasType(type) || pokemon.getTypes(false, true).includes(type);
|
|
if (isSTAB) {
|
|
stab = 1.5;
|
|
}
|
|
|
|
// The Stellar tera type makes this incredibly confusing
|
|
// If the move's type does not match one of the user's base types,
|
|
// the Stellar tera type applies a one-time 1.2x damage boost for that type.
|
|
//
|
|
// If the move's type does match one of the user's base types,
|
|
// then the Stellar tera type applies a one-time 2x STAB boost for that type,
|
|
// and then goes back to using the regular 1.5x STAB boost for those types.
|
|
if (pokemon.terastallized === 'Stellar') {
|
|
if (!pokemon.stellarBoostedTypes.includes(type) || move.stellarBoosted) {
|
|
stab = isSTAB ? 2 : [4915, 4096];
|
|
move.stellarBoosted = true;
|
|
if (pokemon.species.name !== 'Terapagos-Stellar') {
|
|
pokemon.stellarBoostedTypes.push(type);
|
|
}
|
|
}
|
|
} else {
|
|
if (pokemon.terastallized === type && pokemon.getTypes(false, true).includes(type)) {
|
|
stab = 2;
|
|
}
|
|
stab = this.battle.runEvent('ModifySTAB', pokemon, target, move, stab);
|
|
}
|
|
|
|
baseDamage = this.battle.modify(baseDamage, stab);
|
|
}
|
|
|
|
// types
|
|
let typeMod = target.runEffectiveness(move);
|
|
typeMod = this.battle.clampIntRange(typeMod, -6, 6);
|
|
target.getMoveHitData(move).typeMod = typeMod;
|
|
if (typeMod > 0) {
|
|
if (!suppressMessages) this.battle.add('-supereffective', target, Math.min(typeMod, 2));
|
|
|
|
for (let i = 0; i < typeMod; i++) {
|
|
baseDamage *= 2;
|
|
}
|
|
}
|
|
if (typeMod < 0) {
|
|
if (!suppressMessages) this.battle.add('-resisted', target, Math.min(-typeMod, 2));
|
|
|
|
for (let i = 0; i > typeMod; i--) {
|
|
baseDamage = tr(baseDamage / 2);
|
|
}
|
|
}
|
|
|
|
if (isCrit && !suppressMessages) this.battle.add('-crit', target);
|
|
|
|
if (pokemon.status === 'brn' && move.category === 'Physical' && !pokemon.hasAbility('guts')) {
|
|
if (this.battle.gen < 6 || move.id !== 'facade') {
|
|
baseDamage = this.battle.modify(baseDamage, 0.5);
|
|
}
|
|
}
|
|
|
|
// Generation 5, but nothing later, sets damage to 1 before the final damage modifiers
|
|
if (this.battle.gen === 5 && !baseDamage) baseDamage = 1;
|
|
|
|
// Final modifier. Modifiers that modify damage after min damage check, such as Life Orb.
|
|
baseDamage = this.battle.runEvent('ModifyDamage', pokemon, target, move, baseDamage);
|
|
|
|
const bypassProtect = target.getMoveHitData(move).bypassProtect;
|
|
if (bypassProtect) {
|
|
baseDamage = this.battle.modify(baseDamage, 0.25);
|
|
if (bypassProtect !== true && bypassProtect.effectType === 'Ability') {
|
|
this.battle.add('-ability', pokemon, bypassProtect.name);
|
|
}
|
|
this.battle.add('-zbroken', target);
|
|
}
|
|
|
|
// Generation 6-7 moves the check for minimum 1 damage after the final modifier...
|
|
if (this.battle.gen !== 5 && !baseDamage) return 1;
|
|
|
|
// ...but 16-bit truncation happens even later, and can truncate to 0
|
|
return tr(baseDamage, 16);
|
|
},
|
|
// Run `AfterHit` events even if the source fainted
|
|
spreadMoveHit(targets, pokemon, moveOrMoveName, hitEffect?, isSecondary?, isSelf?) {
|
|
// Hardcoded for single-target purposes
|
|
// (no spread moves have any kind of onTryHit handler)
|
|
const target = targets[0];
|
|
let damage: (number | boolean | undefined)[] = [];
|
|
for (const i of targets.keys()) {
|
|
damage[i] = true;
|
|
}
|
|
const move = this.dex.getActiveMove(moveOrMoveName);
|
|
let hitResult: boolean | number | null = true;
|
|
let moveData = hitEffect!;
|
|
if (!moveData) moveData = move;
|
|
if (!moveData.flags) moveData.flags = {};
|
|
if (move.target === 'all' && !isSelf) {
|
|
hitResult = this.battle.singleEvent('TryHitField', moveData, {}, target || null, pokemon, move);
|
|
} else if ((move.target === 'foeSide' || move.target === 'allySide' || move.target === 'allyTeam') && !isSelf) {
|
|
hitResult = this.battle.singleEvent('TryHitSide', moveData, {}, target || null, pokemon, move);
|
|
} else if (target) {
|
|
hitResult = this.battle.singleEvent('TryHit', moveData, {}, target, pokemon, move);
|
|
}
|
|
if (!hitResult) {
|
|
if (hitResult === false) {
|
|
this.battle.add('-fail', pokemon);
|
|
this.battle.attrLastMove('[still]');
|
|
}
|
|
return [[false], targets]; // single-target only
|
|
}
|
|
|
|
// 0. check for substitute
|
|
if (!isSecondary && !isSelf) {
|
|
if (move.target !== 'all' && move.target !== 'allyTeam' && move.target !== 'allySide' && move.target !== 'foeSide') {
|
|
damage = this.tryPrimaryHitEvent(damage, targets, pokemon, move, moveData, isSecondary);
|
|
}
|
|
}
|
|
|
|
for (const i of targets.keys()) {
|
|
if (damage[i] === this.battle.HIT_SUBSTITUTE) {
|
|
damage[i] = true;
|
|
targets[i] = null;
|
|
}
|
|
if (targets[i] && isSecondary && !moveData.self) {
|
|
damage[i] = true;
|
|
}
|
|
if (!damage[i]) targets[i] = false;
|
|
}
|
|
// 1. call to this.battle.getDamage
|
|
damage = this.getSpreadDamage(damage, targets, pokemon, move, moveData, isSecondary, isSelf);
|
|
|
|
for (const i of targets.keys()) {
|
|
if (damage[i] === false) targets[i] = false;
|
|
}
|
|
|
|
// 2. call to this.battle.spreadDamage
|
|
damage = this.battle.spreadDamage(damage, targets, pokemon, move);
|
|
|
|
for (const i of targets.keys()) {
|
|
if (damage[i] === false) targets[i] = false;
|
|
}
|
|
|
|
// 3. onHit event happens here
|
|
damage = this.runMoveEffects(damage, targets, pokemon, move, moveData, isSecondary, isSelf);
|
|
|
|
for (const i of targets.keys()) {
|
|
if (!damage[i] && damage[i] !== 0) targets[i] = false;
|
|
}
|
|
|
|
// steps 4 and 5 can mess with this.battle.activeTarget, which needs to be preserved for Dancer
|
|
const activeTarget = this.battle.activeTarget;
|
|
|
|
// 4. self drops (start checking for targets[i] === false here)
|
|
if (moveData.self && !move.selfDropped) this.selfDrops(targets, pokemon, move, moveData, isSecondary);
|
|
|
|
// 5. secondary effects
|
|
if (moveData.secondaries) this.secondaries(targets, pokemon, move, moveData, isSelf);
|
|
|
|
this.battle.activeTarget = activeTarget;
|
|
|
|
// 6. force switch
|
|
if (moveData.forceSwitch) damage = this.forceSwitch(damage, targets, pokemon, move);
|
|
|
|
for (const i of targets.keys()) {
|
|
if (!damage[i] && damage[i] !== 0) targets[i] = false;
|
|
}
|
|
|
|
const damagedTargets: Pokemon[] = [];
|
|
const damagedDamage = [];
|
|
for (const [i, t] of targets.entries()) {
|
|
if (typeof damage[i] === 'number' && t) {
|
|
damagedTargets.push(t);
|
|
damagedDamage.push(damage[i]);
|
|
}
|
|
}
|
|
const pokemonOriginalHP = pokemon.hp;
|
|
if (damagedDamage.length && !isSecondary && !isSelf) {
|
|
if (this.battle.gen >= 5) {
|
|
this.battle.runEvent('DamagingHit', damagedTargets, pokemon, move, damagedDamage);
|
|
}
|
|
if (moveData.onAfterHit) {
|
|
for (const t of damagedTargets) {
|
|
this.battle.singleEvent('AfterHit', moveData, {}, t, pokemon, move);
|
|
}
|
|
}
|
|
if (this.battle.gen < 5) {
|
|
this.battle.runEvent('DamagingHit', damagedTargets, pokemon, move, damagedDamage);
|
|
}
|
|
if (pokemon.hp && pokemon.hp <= pokemon.maxhp / 2 && pokemonOriginalHP > pokemon.maxhp / 2) {
|
|
this.battle.runEvent('EmergencyExit', pokemon);
|
|
}
|
|
}
|
|
|
|
return [damage, targets];
|
|
},
|
|
},
|
|
};
|