Spaces:
Paused
Paused
| /** | |
| * Stadium 2 mechanics inherit from gen 2 mechanics, but fixes some bugs. | |
| */ | |
| export const Scripts: ModdedBattleScriptsData = { | |
| inherit: 'gen2', | |
| gen: 2, | |
| pokemon: { | |
| inherit: true, | |
| getStat(statName, unboosted, unmodified, fastReturn) { | |
| // @ts-expect-error type checking prevents 'hp' from being passed, but we're paranoid | |
| if (statName === 'hp') throw new Error("Please read `maxhp` directly"); | |
| // base stat | |
| let stat = this.storedStats[statName]; | |
| // Stat boosts. | |
| if (!unboosted) { | |
| let boost = this.boosts[statName]; | |
| if (boost > 6) boost = 6; | |
| if (boost < -6) boost = -6; | |
| if (boost >= 0) { | |
| const boostTable = [1, 1.5, 2, 2.5, 3, 3.5, 4]; | |
| stat = Math.floor(stat * boostTable[boost]); | |
| } else { | |
| const numerators = [100, 66, 50, 40, 33, 28, 25]; | |
| stat = Math.floor(stat * numerators[-boost] / 100); | |
| } | |
| } | |
| if (this.status === 'par' && statName === 'spe' && this.volatiles['parspeeddrop']) { | |
| stat = Math.floor(stat / 4); | |
| } | |
| if (!unmodified) { | |
| if (this.status === 'brn' && statName === 'atk' && this.volatiles['brnattackdrop']) { | |
| stat = Math.floor(stat / 2); | |
| } | |
| } | |
| // Gen 2 caps stats at 999 and min is 1. | |
| stat = this.battle.clampIntRange(stat, 1, 999); | |
| if (fastReturn) return stat; | |
| // Screens | |
| if (!unboosted) { | |
| if ( | |
| (statName === 'def' && this.side.sideConditions['reflect']) || | |
| (statName === 'spd' && this.side.sideConditions['lightscreen']) | |
| ) { | |
| stat *= 2; | |
| } | |
| } | |
| // Handle boosting items | |
| if ( | |
| (['Cubone', 'Marowak'].includes(this.species.name) && this.item === 'thickclub' && statName === 'atk') || | |
| (this.species.name === 'Pikachu' && this.item === 'lightball' && statName === 'spa') | |
| ) { | |
| stat *= 2; | |
| } else if (this.species.name === 'Ditto' && this.item === 'metalpowder' && ['def', 'spd'].includes(statName)) { | |
| stat = Math.floor(stat * 1.5); | |
| } | |
| return stat; | |
| }, | |
| }, | |
| // Stadium 2 shares gen 2 code but it fixes some problems with it. | |
| actions: { | |
| inherit: true, | |
| tryMoveHit(target, pokemon, move) { | |
| const positiveBoostTable = [1, 1.33, 1.66, 2, 2.33, 2.66, 3]; | |
| const negativeBoostTable = [1, 0.75, 0.6, 0.5, 0.43, 0.36, 0.33]; | |
| const doSelfDestruct = true; | |
| let damage: number | false | undefined = 0; | |
| if (move.selfdestruct && doSelfDestruct) { | |
| this.battle.faint(pokemon, pokemon, move); | |
| /** | |
| * Keeping track of the last move used for self-ko clause, | |
| * making sure to clear the opponents last move so that self-destruct and explosion | |
| * does not persist between Pokemon, preventing problems caused by situations, | |
| * such as a player from blowing up both they and their opponents second last Pokemon | |
| * and their opponent blowing up their last Pokemon. If we did not clear here, there would be a problem. | |
| */ | |
| target.side.lastMove = null; | |
| pokemon.side.lastMove = move; | |
| } | |
| let hitResult = this.battle.singleEvent('PrepareHit', move, {}, target, pokemon, move); | |
| if (!hitResult) { | |
| if (hitResult === false) this.battle.add('-fail', target); | |
| return false; | |
| } | |
| this.battle.runEvent('PrepareHit', pokemon, target, move); | |
| if (!this.battle.singleEvent('Try', move, null, pokemon, target, move)) { | |
| return false; | |
| } | |
| if (move.target === 'all' || move.target === 'foeSide' || move.target === 'allySide' || move.target === 'allyTeam') { | |
| if (move.target === 'all') { | |
| hitResult = this.battle.runEvent('TryHitField', target, pokemon, move); | |
| } else { | |
| hitResult = this.battle.runEvent('TryHitSide', target, pokemon, move); | |
| } | |
| if (!hitResult) { | |
| if (hitResult === false) { | |
| this.battle.add('-fail', pokemon); | |
| this.battle.attrLastMove('[still]'); | |
| } | |
| return false; | |
| } | |
| return this.moveHit(target, pokemon, move); | |
| } | |
| hitResult = this.battle.runEvent('Invulnerability', target, pokemon, move); | |
| if (hitResult === false) { | |
| this.battle.attrLastMove('[miss]'); | |
| this.battle.add('-miss', pokemon); | |
| return false; | |
| } | |
| if (move.ignoreImmunity === undefined) { | |
| move.ignoreImmunity = (move.category === 'Status'); | |
| } | |
| if ( | |
| (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) && | |
| !target.runImmunity(move.type, true) | |
| ) { | |
| return false; | |
| } | |
| hitResult = this.battle.singleEvent('TryImmunity', move, {}, target, pokemon, move); | |
| if (hitResult === false) { | |
| this.battle.add('-immune', target); | |
| return false; | |
| } | |
| hitResult = this.battle.runEvent('TryHit', target, pokemon, move); | |
| if (!hitResult) { | |
| if (hitResult === false) this.battle.add('-fail', target); | |
| return false; | |
| } | |
| let accuracy = move.accuracy; | |
| if (move.alwaysHit) { | |
| accuracy = true; | |
| } else { | |
| accuracy = this.battle.runEvent('Accuracy', target, pokemon, move, accuracy); | |
| } | |
| // Now, let's calculate the accuracy. | |
| if (accuracy !== true) { | |
| accuracy = Math.floor(accuracy * 255 / 100); | |
| if (move.ohko) { | |
| if (pokemon.level >= target.level) { | |
| accuracy += (pokemon.level - target.level) * 2; | |
| accuracy = Math.min(accuracy, 255); | |
| } else { | |
| this.battle.add('-immune', target, '[ohko]'); | |
| return false; | |
| } | |
| } | |
| if (!move.ignoreAccuracy) { | |
| if (pokemon.boosts.accuracy > 0) { | |
| accuracy *= positiveBoostTable[pokemon.boosts.accuracy]; | |
| } else { | |
| accuracy *= negativeBoostTable[-pokemon.boosts.accuracy]; | |
| } | |
| } | |
| if (!move.ignoreEvasion) { | |
| if (target.boosts.evasion > 0 && !move.ignorePositiveEvasion) { | |
| accuracy *= negativeBoostTable[target.boosts.evasion]; | |
| } else if (target.boosts.evasion < 0) { | |
| accuracy *= positiveBoostTable[-target.boosts.evasion]; | |
| } | |
| } | |
| accuracy = Math.min(Math.floor(accuracy), 255); | |
| accuracy = Math.max(accuracy, 1); | |
| } else { | |
| accuracy = this.battle.runEvent('Accuracy', target, pokemon, move, accuracy); | |
| } | |
| accuracy = this.battle.runEvent('ModifyAccuracy', target, pokemon, move, accuracy); | |
| if (accuracy !== true) accuracy = Math.max(accuracy, 0); | |
| if (move.alwaysHit) { | |
| accuracy = true; | |
| } else { | |
| accuracy = this.battle.runEvent('Accuracy', target, pokemon, move, accuracy); | |
| } | |
| if (accuracy !== true && accuracy !== 255 && !this.battle.randomChance(accuracy, 256)) { | |
| this.battle.attrLastMove('[miss]'); | |
| this.battle.add('-miss', pokemon); | |
| damage = false; | |
| return damage; | |
| } | |
| move.totalDamage = 0; | |
| pokemon.lastDamage = 0; | |
| if (move.multihit) { | |
| let hits = move.multihit; | |
| if (Array.isArray(hits)) { | |
| if (hits[0] === 2 && hits[1] === 5) { | |
| hits = this.battle.sample([2, 2, 2, 3, 3, 3, 4, 5]); | |
| } else { | |
| hits = this.battle.random(hits[0], hits[1] + 1); | |
| } | |
| } | |
| hits = Math.floor(hits); | |
| let nullDamage = true; | |
| let moveDamage: number | undefined | false; | |
| const isSleepUsable = move.sleepUsable || this.dex.moves.get(move.sourceEffect).sleepUsable; | |
| let i: number; | |
| for (i = 0; i < hits && target.hp && pokemon.hp; i++) { | |
| if (pokemon.status === 'slp' && !isSleepUsable) break; | |
| move.hit = i + 1; | |
| if (move.hit === hits) move.lastHit = true; | |
| moveDamage = this.moveHit(target, pokemon, move); | |
| if (moveDamage === false) break; | |
| if (nullDamage && (moveDamage || moveDamage === 0 || moveDamage === undefined)) nullDamage = false; | |
| damage = (moveDamage || 0); | |
| move.totalDamage += damage; | |
| this.battle.eachEvent('Update'); | |
| } | |
| if (i === 0) return 1; | |
| if (nullDamage) damage = false; | |
| this.battle.add('-hitcount', target, i); | |
| } else { | |
| damage = this.moveHit(target, pokemon, move); | |
| move.totalDamage = damage; | |
| } | |
| if (move.category !== 'Status') { | |
| target.gotAttacked(move, damage, pokemon); | |
| } | |
| if (move.ohko) this.battle.add('-ohko'); | |
| if (!move.negateSecondary) { | |
| this.battle.singleEvent('AfterMoveSecondary', move, null, target, pokemon, move); | |
| this.battle.runEvent('AfterMoveSecondary', target, pokemon, move); | |
| } | |
| // Implementing Recoil mechanics from Stadium 2. | |
| // If a pokemon caused the other to faint with a recoil move and only one pokemon remains on both sides, | |
| // recoil damage will not be taken. | |
| if (move.recoil && move.totalDamage && (pokemon.side.pokemonLeft > 1 || target.side.pokemonLeft > 1 || target.hp)) { | |
| this.battle.damage(this.calcRecoilDamage(move.totalDamage, move, pokemon), pokemon, target, 'recoil'); | |
| } | |
| return damage; | |
| }, | |
| getDamage(source, target, move, suppressMessages) { | |
| // First of all, we get the move. | |
| if (typeof move === 'string') { | |
| move = this.dex.getActiveMove(move); | |
| } else if (typeof move === 'number') { | |
| move = { | |
| basePower: move, | |
| type: '???', | |
| category: 'Physical', | |
| willCrit: false, | |
| flags: {}, | |
| } as unknown as ActiveMove; | |
| } | |
| // Let's test for immunities. | |
| if (!move.ignoreImmunity || (move.ignoreImmunity !== true && !move.ignoreImmunity[move.type])) { | |
| if (!target.runImmunity(move.type, true)) { | |
| return false; | |
| } | |
| } | |
| // Is it an OHKO move? | |
| if (move.ohko) { | |
| return target.maxhp; | |
| } | |
| // We edit the damage through move's damage callback | |
| if (move.damageCallback) { | |
| return move.damageCallback.call(this.battle, source, target); | |
| } | |
| // We take damage from damage=level moves | |
| if (move.damage === 'level') { | |
| return source.level; | |
| } | |
| // If there's a fix move damage, we run it | |
| if (move.damage) { | |
| return move.damage; | |
| } | |
| // We check the category and typing to calculate later on the damage | |
| move.category = this.battle.getCategory(move); | |
| // '???' is typeless damage: used for Struggle and Confusion etc | |
| if (!move.type) move.type = '???'; | |
| const type = move.type; | |
| // We get the base power and apply basePowerCallback if necessary | |
| let basePower: number | false | null | undefined = move.basePower; | |
| if (move.basePowerCallback) { | |
| basePower = move.basePowerCallback.call(this.battle, source, target, move); | |
| } | |
| // We check for Base Power | |
| if (!basePower) { | |
| if (basePower === 0) return; // Returning undefined means not dealing damage | |
| return basePower; | |
| } | |
| basePower = this.battle.clampIntRange(basePower, 1); | |
| // Checking for the move's Critical Hit ratio | |
| let critRatio = this.battle.runEvent('ModifyCritRatio', source, target, move, move.critRatio || 0); | |
| critRatio = this.battle.clampIntRange(critRatio, 0, 5); | |
| const critMult = [0, 16, 8, 4, 3, 2]; | |
| let isCrit = move.willCrit || false; | |
| if (typeof move.willCrit === 'undefined') { | |
| if (critRatio) { | |
| isCrit = this.battle.randomChance(1, critMult[critRatio]); | |
| } | |
| } | |
| if (isCrit && this.battle.runEvent('CriticalHit', target, null, move)) { | |
| target.getMoveHitData(move).crit = true; | |
| } | |
| // Happens after crit calculation | |
| if (basePower) { | |
| // confusion damage | |
| if (move.isConfusionSelfHit) { | |
| move.type = move.baseMoveType!; | |
| basePower = this.battle.runEvent('BasePower', source, target, move, basePower, true); | |
| move.type = '???'; | |
| } else { | |
| basePower = this.battle.runEvent('BasePower', source, target, move, basePower, true); | |
| } | |
| if (basePower && move.basePowerModifier) { | |
| basePower *= move.basePowerModifier; | |
| } | |
| } | |
| if (!basePower) return 0; | |
| basePower = this.battle.clampIntRange(basePower, 1); | |
| // We now check for attacker and defender | |
| let level = source.level; | |
| // Using Beat Up | |
| if (move.allies) { | |
| this.battle.add('-activate', source, 'move: Beat Up', '[of] ' + move.allies[0].name); | |
| level = move.allies[0].level; | |
| } | |
| const attacker = move.overrideOffensivePokemon === 'target' ? target : source; | |
| const defender = move.overrideDefensivePokemon === 'source' ? source : target; | |
| const isPhysical = move.category === 'Physical'; | |
| const atkType: StatIDExceptHP = move.overrideOffensiveStat || (isPhysical ? 'atk' : 'spa'); | |
| const defType: StatIDExceptHP = move.overrideDefensiveStat || (isPhysical ? 'def' : 'spd'); | |
| let unboosted = false; | |
| let noburndrop = false; | |
| if (isCrit) { | |
| if (!suppressMessages) this.battle.add('-crit', target); | |
| // Stat level modifications are ignored if they are neutral to or favour the defender. | |
| // Reflect and Light Screen defensive boosts are only ignored if stat level modifications were also ignored as a result of that. | |
| if (attacker.boosts[atkType] <= defender.boosts[defType]) { | |
| unboosted = true; | |
| noburndrop = true; | |
| } | |
| } | |
| let attack = attacker.getStat(atkType, unboosted, noburndrop); | |
| let defense = defender.getStat(defType, unboosted); | |
| // Using Beat Up | |
| if (move.allies) { | |
| attack = move.allies[0].species.baseStats.atk; | |
| move.allies.shift(); | |
| defense = defender.species.baseStats.def; | |
| } | |
| // Moves that ignore offense and defense respectively. | |
| if (move.ignoreOffensive) { | |
| this.battle.debug('Negating (sp)atk boost/penalty.'); | |
| // The attack drop from the burn is only applied when attacker's attack level is higher than defender's defense level. | |
| attack = attacker.getStat(atkType, true, true); | |
| } | |
| if (move.ignoreDefensive) { | |
| this.battle.debug('Negating (sp)def boost/penalty.'); | |
| defense = target.getStat(defType, true, true); | |
| } | |
| if (attack >= 256 || defense >= 256) { | |
| attack = this.battle.clampIntRange(Math.floor(this.battle.clampIntRange(attack, 1, 999) / 4), 1); | |
| defense = this.battle.clampIntRange(Math.floor(this.battle.clampIntRange(defense, 1, 999) / 4), 1); | |
| } | |
| // Self destruct moves halve defense at this point. | |
| if (move.selfdestruct && defType === 'def') { | |
| defense = this.battle.clampIntRange(Math.floor(defense / 2), 1); | |
| } | |
| // Let's go with the calculation now that we have what we need. | |
| // We do it step by step just like the game does. | |
| let damage = level * 2; | |
| damage = Math.floor(damage / 5); | |
| damage += 2; | |
| damage *= basePower; | |
| damage *= attack; | |
| damage = Math.floor(damage / defense); | |
| damage = Math.floor(damage / 50); | |
| if (isCrit) damage *= 2; | |
| damage = Math.floor(this.battle.runEvent('ModifyDamage', attacker, defender, move, damage)); | |
| damage = this.battle.clampIntRange(damage, 1, 997); | |
| damage += 2; | |
| // Weather modifiers | |
| if ( | |
| (type === 'Water' && this.battle.field.isWeather('raindance')) || | |
| (type === 'Fire' && this.battle.field.isWeather('sunnyday')) | |
| ) { | |
| damage = Math.floor(damage * 1.5); | |
| } else if ( | |
| ((type === 'Fire' || move.id === 'solarbeam') && this.battle.field.isWeather('raindance')) || | |
| (type === 'Water' && this.battle.field.isWeather('sunnyday')) | |
| ) { | |
| damage = Math.floor(damage / 2); | |
| } | |
| // STAB damage bonus, the "???" type never gets STAB | |
| if (type !== '???' && source.hasType(type)) { | |
| damage += Math.floor(damage / 2); | |
| } | |
| // Type effectiveness | |
| const totalTypeMod = target.runEffectiveness(move); | |
| // Super effective attack | |
| if (totalTypeMod > 0) { | |
| if (!suppressMessages) this.battle.add('-supereffective', target); | |
| damage *= 2; | |
| if (totalTypeMod >= 2) { | |
| damage *= 2; | |
| } | |
| } | |
| // Resisted attack | |
| if (totalTypeMod < 0) { | |
| if (!suppressMessages) this.battle.add('-resisted', target); | |
| damage = Math.floor(damage / 2); | |
| if (totalTypeMod <= -2) { | |
| damage = Math.floor(damage / 2); | |
| } | |
| } | |
| // Apply random factor if damage is greater than 1, except for Flail and Reversal | |
| if (!move.noDamageVariance && damage > 1) { | |
| damage *= this.battle.random(217, 256); | |
| damage = Math.floor(damage / 255); | |
| } | |
| // If damage is less than 1, we return 1 | |
| if (basePower && !Math.floor(damage)) { | |
| return 1; | |
| } | |
| // We are done, this is the final damage | |
| return damage; | |
| }, | |
| }, | |
| /** | |
| * Stadium 2 ignores stat drops due to status ailments upon boosting the dropped stat. | |
| * For example: if a burned Snorlax uses Curse then it will ignore the attack drop from | |
| * burn when it is recalculating its attack stat. This is why volatiles are added to status | |
| * conditions, so that we can keep track of whether or not to apply the stat drop from | |
| * statuses. | |
| */ | |
| boost(boost, target, source = null, effect = null) { | |
| if (this.event) { | |
| if (!target) target = this.event.target; | |
| if (!source) source = this.event.source; | |
| if (!effect) effect = this.effect; | |
| } | |
| if (typeof effect === 'string') effect = this.dex.conditions.get(effect); | |
| if (!target?.hp) return 0; | |
| let success = null; | |
| boost = this.runEvent('TryBoost', target, source, effect, { ...boost }); | |
| let i: BoostID; | |
| for (i in boost) { | |
| const currentBoost: SparseBoostsTable = {}; | |
| currentBoost[i] = boost[i]; | |
| let boostBy = target.boostBy(currentBoost); | |
| let msg = '-boost'; | |
| if (boost[i]! < 0) { | |
| msg = '-unboost'; | |
| boostBy = -boostBy; | |
| } | |
| if (boostBy) { | |
| success = true; | |
| // Check for boost increases deleting attack or speed drops | |
| if (i === 'atk' && target.status === 'brn' && target.volatiles['brnattackdrop']) { | |
| target.removeVolatile('brnattackdrop'); | |
| } | |
| if (i === 'spe' && target.status === 'par' && target.volatiles['parspeeddrop']) { | |
| target.removeVolatile('parspeeddrop'); | |
| } | |
| if (!effect || effect.effectType === 'Move') { | |
| this.add(msg, target, i, boostBy); | |
| } else { | |
| this.add(msg, target, i, boostBy, '[from] ' + effect.fullname); | |
| } | |
| this.runEvent('AfterEachBoost', target, source, effect, currentBoost); | |
| } | |
| } | |
| this.runEvent('AfterBoost', target, source, effect, boost); | |
| return success; | |
| }, | |
| /** | |
| * Implementing Self-KO Clause by having it check what the last move used by the players were | |
| * in the case both Pokemon faint. Since the only way this can happen in Stadium 2 is if a player | |
| * uses self-destruct or explosion, I can use this to determine who should win. | |
| */ | |
| faintMessages(lastFirst) { | |
| if (this.ended) return; | |
| const length = this.faintQueue.length; | |
| if (!length) return false; | |
| if (lastFirst) { | |
| this.faintQueue.unshift(this.faintQueue[this.faintQueue.length - 1]); | |
| this.faintQueue.pop(); | |
| } | |
| let faintData; | |
| while (this.faintQueue.length) { | |
| faintData = this.faintQueue.shift()!; | |
| const pokemon: Pokemon = faintData.target; | |
| if (!pokemon.fainted && | |
| this.runEvent('BeforeFaint', pokemon, faintData.source, faintData.effect)) { | |
| this.add('faint', pokemon); | |
| pokemon.side.pokemonLeft--; | |
| if (pokemon.side.totalFainted < 100) pokemon.side.totalFainted++; | |
| this.runEvent('Faint', pokemon, faintData.source, faintData.effect); | |
| this.singleEvent('End', pokemon.getAbility(), pokemon.abilityState, pokemon); | |
| pokemon.clearVolatile(false); | |
| pokemon.fainted = true; | |
| pokemon.isActive = false; | |
| pokemon.isStarted = false; | |
| pokemon.side.faintedThisTurn = pokemon; | |
| } | |
| } | |
| if (this.gen <= 1) { | |
| // in gen 1, fainting skips the rest of the turn | |
| // residuals don't exist in gen 1 | |
| this.queue.clear(); | |
| // Fainting clears accumulated Bide damage | |
| for (const pokemon of this.getAllActive()) { | |
| if (pokemon.volatiles['bide']?.damage) { | |
| pokemon.volatiles['bide'].damage = 0; | |
| this.hint("Desync Clause Mod activated!"); | |
| this.hint("In Gen 1, Bide's accumulated damage is reset to 0 when a Pokemon faints."); | |
| } | |
| } | |
| } else if (this.gen <= 3 && this.gameType === 'singles') { | |
| // in gen 3 or earlier, fainting in singles skips to residuals | |
| for (const pokemon of this.getAllActive()) { | |
| if (this.gen <= 2) { | |
| // in gen 2, fainting skips moves only | |
| this.queue.cancelMove(pokemon); | |
| } else { | |
| // in gen 3, fainting skips all moves and switches | |
| this.queue.cancelAction(pokemon); | |
| } | |
| } | |
| } | |
| if (!this.p1.pokemonLeft && !this.p2.pokemonLeft) { | |
| if (this.p1.lastMove !== null && this.p2.lastMove === null) { | |
| this.win(this.p2); | |
| return true; | |
| } else if (this.p2.lastMove !== null && this.p1.lastMove === null) { | |
| this.win(this.p1); | |
| return true; | |
| } | |
| this.win(faintData ? faintData.target.side.foe : null); | |
| return true; | |
| } | |
| if (!this.p1.pokemonLeft) { | |
| this.win(this.p2); | |
| return true; | |
| } | |
| if (!this.p2.pokemonLeft) { | |
| this.win(this.p1); | |
| return true; | |
| } | |
| if (faintData) { | |
| this.runEvent('AfterFaint', faintData.target, faintData.source, faintData.effect, length); | |
| } | |
| return false; | |
| }, | |
| }; | |