diff --git a/calc/src/mechanics/gen3.ts b/calc/src/mechanics/gen3.ts index f3f7474ee..fedf823da 100644 --- a/calc/src/mechanics/gen3.ts +++ b/calc/src/mechanics/gen3.ts @@ -13,6 +13,7 @@ import { checkAirLock, checkForecast, checkIntimidate, + checkMultihitBoost, handleFixedDamageMoves, } from './util'; @@ -132,6 +133,73 @@ export function calculateADV( desc.hits = move.hits; } + let bp = calculateBasePowerADV(attacker, defender, move, desc); + + if (bp === 0) { + return result; + } + bp = calculateBPModsADV(attacker, move, desc, bp); + + const isCritical = move.isCrit && !defender.hasAbility('Battle Armor', 'Shell Armor'); + const at = calculateAttackADV(gen, attacker, defender, move, desc, isCritical); + const df = calculateDefenseADV(gen, defender, move, desc, isCritical); + + const lv = attacker.level; + let baseDamage = Math.floor(Math.floor((Math.floor((2 * lv) / 5 + 2) * at * bp) / df) / 50); + + baseDamage = calculateFinalModsADV(baseDamage, attacker, move, field, desc, isCritical); + + baseDamage = Math.floor(baseDamage * typeEffectiveness); + result.damage = []; + for (let i = 85; i <= 100; i++) { + result.damage[i - 85] = Math.max(1, Math.floor((baseDamage * i) / 100)); + } + + if ((move.dropsStats && move.timesUsed! > 1) || move.hits > 1) { + // store boosts so intermediate boosts don't show. + const origDefBoost = desc.defenseBoost; + const origAtkBoost = desc.attackBoost; + let numAttacks = 1; + if (move.dropsStats && move.timesUsed! > 1) { + desc.moveTurns = `over ${move.timesUsed} turns`; + numAttacks = move.timesUsed!; + } else { + numAttacks = move.hits; + } + let usedItems = [false, false]; + for (let times = 1; times < numAttacks; times++) { + usedItems = checkMultihitBoost(gen, attacker, defender, move, + field, desc, usedItems[0], usedItems[1]); + const newAt = calculateAttackADV(gen, attacker, defender, move, desc, isCritical); + let newBp = calculateBasePowerADV(attacker, defender, move, desc); + newBp = calculateBPModsADV(attacker, move, desc, newBp); + let newBaseDmg = Math.floor( + Math.floor((Math.floor((2 * lv) / 5 + 2) * newAt * newBp) / df) / 50 + ); + newBaseDmg = calculateFinalModsADV(newBaseDmg, attacker, move, field, desc, isCritical); + newBaseDmg = Math.floor(newBaseDmg * typeEffectiveness); + + let damageMultiplier = 85; + result.damage = result.damage.map(affectedAmount => { + const newFinalDamage = Math.max(1, Math.floor((newBaseDmg * damageMultiplier) / 100)); + damageMultiplier++; + return affectedAmount + newFinalDamage; + }); + } + desc.defenseBoost = origDefBoost; + desc.attackBoost = origAtkBoost; + } + + return result; +} + +export function calculateBasePowerADV( + attacker: Pokemon, + defender: Pokemon, + move: Move, + desc: RawDesc, + hit = 1, +) { let bp = move.bp; switch (move.name) { case 'Flail': @@ -161,22 +229,47 @@ export function calculateADV( bp = 60; desc.moveName = 'Swift'; break; + case 'Triple Kick': + bp = hit * 10; + desc.moveBP = move.hits === 2 ? 30 : move.hits === 3 ? 60 : 10; + break; default: bp = move.bp; } + return bp; +} - if (bp === 0) { - return result; +export function calculateBPModsADV( + attacker: Pokemon, + move: Move, + desc: RawDesc, + basePower: number, +) { + if (attacker.curHP() <= attacker.maxHP() / 3 && + ((attacker.hasAbility('Overgrow') && move.hasType('Grass')) || + (attacker.hasAbility('Blaze') && move.hasType('Fire')) || + (attacker.hasAbility('Torrent') && move.hasType('Water')) || + (attacker.hasAbility('Swarm') && move.hasType('Bug'))) + ) { + basePower = Math.floor(basePower * 1.5); + desc.attackerAbility = attacker.ability; } + return basePower; +} +export function calculateAttackADV( + gen: Generation, + attacker: Pokemon, + defender: Pokemon, + move: Move, + desc: RawDesc, + isCritical = false +) { const isPhysical = move.category === 'Physical'; const attackStat = isPhysical ? 'atk' : 'spa'; desc.attackEVs = getEVDescriptionText(gen, attacker, attackStat, attacker.nature); - const defenseStat = isPhysical ? 'def' : 'spd'; - desc.defenseEVs = getEVDescriptionText(gen, defender, defenseStat, defender.nature); let at = attacker.rawStats[attackStat]; - let df = defender.rawStats[defenseStat]; if (isPhysical && attacker.hasAbility('Huge Power', 'Pure Power')) { at *= 2; @@ -204,6 +297,40 @@ export function calculateADV( desc.attackerItem = attacker.item; } + if (defender.hasAbility('Thick Fat') && (move.hasType('Fire', 'Ice'))) { + at = Math.floor(at / 2); + desc.defenderAbility = defender.ability; + } + + if ((isPhysical && + (attacker.hasAbility('Hustle') || (attacker.hasAbility('Guts') && attacker.status))) || + (!isPhysical && attacker.abilityOn && attacker.hasAbility('Plus', 'Minus')) + ) { + at = Math.floor(at * 1.5); + desc.attackerAbility = attacker.ability; + } + + const attackBoost = attacker.boosts[attackStat]; + if (attackBoost > 0 || (!isCritical && attackBoost < 0)) { + at = getModifiedStat(at, attackBoost); + desc.attackBoost = attackBoost; + } + return at; +} + +export function calculateDefenseADV( + gen: Generation, + defender: Pokemon, + move: Move, + desc: RawDesc, + isCritical = false +) { + const isPhysical = move.category === 'Physical'; + const defenseStat = isPhysical ? 'def' : 'spd'; + desc.defenseEVs = getEVDescriptionText(gen, defender, defenseStat, defender.nature); + + let df = defender.rawStats[defenseStat]; + if (!isPhysical && defender.hasItem('Soul Dew') && defender.named('Latios', 'Latias')) { df = Math.floor(df * 1.5); desc.defenderItem = defender.item; @@ -215,50 +342,32 @@ export function calculateADV( desc.defenderItem = defender.item; } - if (defender.hasAbility('Thick Fat') && (move.hasType('Fire', 'Ice'))) { - at = Math.floor(at / 2); - desc.defenderAbility = defender.ability; - } else if (isPhysical && defender.hasAbility('Marvel Scale') && defender.status) { + if (isPhysical && defender.hasAbility('Marvel Scale') && defender.status) { df = Math.floor(df * 1.5); desc.defenderAbility = defender.ability; } - if ((isPhysical && - (attacker.hasAbility('Hustle') || (attacker.hasAbility('Guts') && attacker.status))) || - (!isPhysical && attacker.abilityOn && attacker.hasAbility('Plus', 'Minus')) - ) { - at = Math.floor(at * 1.5); - desc.attackerAbility = attacker.ability; - } else if (attacker.curHP() <= attacker.maxHP() / 3 && - ((attacker.hasAbility('Overgrow') && move.hasType('Grass')) || - (attacker.hasAbility('Blaze') && move.hasType('Fire')) || - (attacker.hasAbility('Torrent') && move.hasType('Water')) || - (attacker.hasAbility('Swarm') && move.hasType('Bug'))) - ) { - bp = Math.floor(bp * 1.5); - desc.attackerAbility = attacker.ability; - } - if (move.named('Explosion', 'Self-Destruct')) { df = Math.floor(df / 2); } - const isCritical = move.isCrit && !defender.hasAbility('Battle Armor', 'Shell Armor'); - - const attackBoost = attacker.boosts[attackStat]; const defenseBoost = defender.boosts[defenseStat]; - if (attackBoost > 0 || (!isCritical && attackBoost < 0)) { - at = getModifiedStat(at, attackBoost); - desc.attackBoost = attackBoost; - } if (defenseBoost < 0 || (!isCritical && defenseBoost > 0)) { df = getModifiedStat(df, defenseBoost); desc.defenseBoost = defenseBoost; } + return df; +} - const lv = attacker.level; - let baseDamage = Math.floor(Math.floor((Math.floor((2 * lv) / 5 + 2) * at * bp) / df) / 50); - +function calculateFinalModsADV( + baseDamage: number, + attacker: Pokemon, + move: Move, + field: Field, + desc: RawDesc, + isCritical = false, +) { + const isPhysical = move.category === 'Physical'; if (attacker.hasStatus('brn') && isPhysical && !attacker.hasAbility('Guts')) { baseDamage = Math.floor(baseDamage / 2); desc.isBurned = true; @@ -303,7 +412,6 @@ export function calculateADV( } baseDamage = (move.category === 'Physical' ? Math.max(1, baseDamage) : baseDamage) + 2; - if (isCritical) { baseDamage *= 2; desc.isCritical = true; @@ -311,7 +419,7 @@ export function calculateADV( if (move.named('Weather Ball') && field.weather) { baseDamage *= 2; - desc.moveBP = bp * 2; + desc.moveBP = move.bp * 2; } if (field.attackerSide.isHelpingHand) { @@ -322,26 +430,5 @@ export function calculateADV( if (move.hasType(...attacker.types)) { baseDamage = Math.floor(baseDamage * 1.5); } - - baseDamage = Math.floor(baseDamage * typeEffectiveness); - result.damage = []; - for (let i = 85; i <= 100; i++) { - result.damage[i - 85] = Math.max(1, Math.floor((baseDamage * i) / 100)); - } - - if (move.hits > 1) { - for (let times = 0; times < move.hits; times++) { - let damageMultiplier = 85; - result.damage = result.damage.map(affectedAmount => { - if (times) { - const newFinalDamage = Math.max(1, Math.floor((baseDamage * damageMultiplier) / 100)); - damageMultiplier++; - return affectedAmount + newFinalDamage; - } - return affectedAmount; - }); - } - } - - return result; + return baseDamage; } diff --git a/calc/src/mechanics/gen4.ts b/calc/src/mechanics/gen4.ts index 14cbc828a..8f9e43c64 100644 --- a/calc/src/mechanics/gen4.ts +++ b/calc/src/mechanics/gen4.ts @@ -15,6 +15,7 @@ import { checkItem, checkIntimidate, checkDownload, + checkMultihitBoost, countBoosts, handleFixedDamageMoves, } from './util'; @@ -65,27 +66,15 @@ export function calculateDPP( const isCritical = move.isCrit && !defender.hasAbility('Battle Armor', 'Shell Armor'); - let basePower = move.bp; if (move.named('Weather Ball')) { - if (field.hasWeather('Sun')) { - move.type = 'Fire'; - basePower *= 2; - } else if (field.hasWeather('Rain')) { - move.type = 'Water'; - basePower *= 2; - } else if (field.hasWeather('Sand')) { - move.type = 'Rock'; - basePower *= 2; - } else if (field.hasWeather('Hail')) { - move.type = 'Ice'; - basePower *= 2; - } else { - move.type = 'Normal'; - } - + move.type = + field.hasWeather('Sun') ? 'Fire' + : field.hasWeather('Rain') ? 'Water' + : field.hasWeather('Sand') ? 'Rock' + : field.hasWeather('Hail') ? 'Ice' + : 'Normal'; desc.weather = field.weather; desc.moveType = move.type; - desc.moveBP = basePower; } else if (move.named('Judgment') && attacker.item && attacker.item.includes('Plate')) { move.type = getItemBoostType(attacker.item)!; } else if (move.named('Natural Gift') && attacker.item?.endsWith('Berry')) { @@ -183,11 +172,152 @@ export function calculateDPP( if (move.hits > 1) { desc.hits = move.hits; } - const turnOrder = attacker.stats.spe > defender.stats.spe ? 'first' : 'last'; + + const isPhysical = move.category === 'Physical'; // #endregion // #region Base Power + let basePower = calculateBasePowerDPP(gen, attacker, defender, move, field, desc); + if (basePower === 0) { + return result; + } + basePower = calculateBPModsDPP(attacker, defender, move, field, desc, basePower); + + // #endregion + // #region (Special) Attack + const attack = calculateAttackDPP(gen, attacker, defender, move, field, desc, isCritical); + + // #endregion + // #region (Special) Defense + const defense = calculateDefenseDPP(gen, attacker, defender, move, field, desc, isCritical); + + // #endregion + // #region Damage + + let baseDamage = Math.floor( + Math.floor((Math.floor((2 * attacker.level) / 5 + 2) * basePower * attack) / 50) / defense + ); + + if (attacker.hasStatus('brn') && isPhysical && !attacker.hasAbility('Guts')) { + baseDamage = Math.floor(baseDamage * 0.5); + desc.isBurned = true; + } + + baseDamage = calculateFinalModsDPP(baseDamage, attacker, move, field, desc, isCritical); + + // the random factor is applied between the LO mod and the STAB mod, so don't apply anything + // below this until we're inside the loop + let stabMod = 1; + if (move.hasType(...attacker.types)) { + if (attacker.hasAbility('Adaptability')) { + stabMod = 2; + desc.attackerAbility = attacker.ability; + } else { + stabMod = 1.5; + } + } + + let filterMod = 1; + if (defender.hasAbility('Filter', 'Solid Rock') && typeEffectiveness > 1) { + filterMod = 0.75; + desc.defenderAbility = defender.ability; + } + let ebeltMod = 1; + if (attacker.hasItem('Expert Belt') && typeEffectiveness > 1) { + ebeltMod = 1.2; + desc.attackerItem = attacker.item; + } + let tintedMod = 1; + if (attacker.hasAbility('Tinted Lens') && typeEffectiveness < 1) { + tintedMod = 2; + desc.attackerAbility = attacker.ability; + } + let berryMod = 1; + if (move.hasType(getBerryResistType(defender.item)) && + (typeEffectiveness > 1 || move.hasType('Normal'))) { + berryMod = 0.5; + desc.defenderItem = defender.item; + } + + const damage: number[] = []; + for (let i = 0; i < 16; i++) { + damage[i] = Math.floor((baseDamage * (85 + i)) / 100); + damage[i] = Math.floor(damage[i] * stabMod); + damage[i] = Math.floor(damage[i] * type1Effectiveness); + damage[i] = Math.floor(damage[i] * type2Effectiveness); + damage[i] = Math.floor(damage[i] * filterMod); + damage[i] = Math.floor(damage[i] * ebeltMod); + damage[i] = Math.floor(damage[i] * tintedMod); + damage[i] = Math.floor(damage[i] * berryMod); + damage[i] = Math.max(1, damage[i]); + } + result.damage = damage; + + if ((move.dropsStats && move.timesUsed! > 1) || move.hits > 1) { + // store boosts so intermediate boosts don't show. + const origDefBoost = desc.defenseBoost; + const origAtkBoost = desc.attackBoost; + let numAttacks = 1; + if (move.dropsStats && move.timesUsed! > 1) { + desc.moveTurns = `over ${move.timesUsed} turns`; + numAttacks = move.timesUsed!; + } else { + numAttacks = move.hits; + } + let usedItems = [false, false]; + for (let times = 1; times < numAttacks; times++) { + usedItems = checkMultihitBoost(gen, attacker, defender, move, + field, desc, usedItems[0], usedItems[1]); + let newBasePower = calculateBasePowerDPP(gen, attacker, defender, move, field, desc); + newBasePower = calculateBPModsDPP(attacker, defender, move, field, desc, newBasePower); + const newAtk = calculateAttackDPP(gen, attacker, defender, move, field, desc, isCritical); + let baseDamage = Math.floor( + Math.floor( + (Math.floor((2 * attacker.level) / 5 + 2) * newBasePower * newAtk) / 50 + ) / defense + ); + if (attacker.hasStatus('brn') && isPhysical && !attacker.hasAbility('Guts')) { + baseDamage = Math.floor(baseDamage * 0.5); + desc.isBurned = true; + } + baseDamage = calculateFinalModsDPP(baseDamage, attacker, move, field, desc, isCritical); + + let damageMultiplier = 0; + result.damage = result.damage.map(affectedAmount => { + let newFinalDamage = 0; + newFinalDamage = Math.floor((baseDamage * (85 + damageMultiplier)) / 100); + newFinalDamage = Math.floor(newFinalDamage * stabMod); + newFinalDamage = Math.floor(newFinalDamage * type1Effectiveness); + newFinalDamage = Math.floor(newFinalDamage * type2Effectiveness); + newFinalDamage = Math.floor(newFinalDamage * filterMod); + newFinalDamage = Math.floor(newFinalDamage * ebeltMod); + newFinalDamage = Math.floor(newFinalDamage * tintedMod); + newFinalDamage = Math.max(1, newFinalDamage); + damageMultiplier++; + return affectedAmount + newFinalDamage; + }); + } + desc.defenseBoost = origDefBoost; + desc.attackBoost = origAtkBoost; + } + + // #endregion + + return result; +} + +export function calculateBasePowerDPP( + gen: Generation, + attacker: Pokemon, + defender: Pokemon, + move: Move, + field: Field, + desc: RawDesc, + hit = 1, +) { + let basePower = move.bp; + const turnOrder = attacker.stats.spe > defender.stats.spe ? 'first' : 'last'; switch (move.name) { case 'Brine': if (defender.curHP() <= defender.maxHP() / 2) { @@ -254,14 +384,28 @@ export function calculateDPP( basePower = Math.floor((defender.curHP() * 120) / defender.maxHP()) + 1; desc.moveBP = basePower; break; + case 'Triple Kick': + basePower = hit * 10; + desc.moveBP = move.hits === 2 ? 30 : move.hits === 3 ? 60 : 10; + break; + case 'Weather Ball': + basePower = move.bp * (field.weather ? 2 : 1); + desc.moveBP = basePower; + break; default: basePower = move.bp; } + return basePower; +} - if (basePower === 0) { - return result; - } - +export function calculateBPModsDPP( + attacker: Pokemon, + defender: Pokemon, + move: Move, + field: Field, + desc: RawDesc, + basePower: number, +) { if (field.attackerSide.isHelpingHand) { basePower = Math.floor(basePower * 1.5); desc.isHelpingHand = true; @@ -310,10 +454,19 @@ export function calculateDPP( basePower = Math.floor(basePower * 1.25); desc.defenderAbility = defender.ability; } + return basePower; +} - // #endregion - // #region (Special) Attack - +export function calculateAttackDPP( + gen: Generation, + attacker: Pokemon, + defender: Pokemon, + move: Move, + field: Field, + desc: RawDesc, + isCritical = false +) { + const isPhysical = move.category === 'Physical'; const attackStat = isPhysical ? 'atk' : 'spa'; desc.attackEVs = getEVDescriptionText(gen, attacker, attackStat, attacker.nature); let attack: number; @@ -370,10 +523,19 @@ export function calculateDPP( attack *= 2; desc.attackerItem = attacker.item; } + return attack; +} - // #endregion - // #region (Special) Defense - +export function calculateDefenseDPP( + gen: Generation, + attacker: Pokemon, + defender: Pokemon, + move: Move, + field: Field, + desc: RawDesc, + isCritical = false +) { + const isPhysical = move.category === 'Physical'; const defenseStat = isPhysical ? 'def' : 'spd'; desc.defenseEVs = getEVDescriptionText(gen, defender, defenseStat, defender.nature); let defense: number; @@ -429,19 +591,18 @@ export function calculateDPP( if (defense < 1) { defense = 1; } + return defense; +} - // #endregion - // #region Damage - - let baseDamage = Math.floor( - Math.floor((Math.floor((2 * attacker.level) / 5 + 2) * basePower * attack) / 50) / defense - ); - - if (attacker.hasStatus('brn') && isPhysical && !attacker.hasAbility('Guts')) { - baseDamage = Math.floor(baseDamage * 0.5); - desc.isBurned = true; - } - +function calculateFinalModsDPP( + baseDamage: number, + attacker: Pokemon, + move: Move, + field: Field, + desc: RawDesc, + isCritical = false, +) { + const isPhysical = move.category === 'Physical'; if (!isCritical) { const screenMultiplier = field.gameType !== 'Singles' ? 2 / 3 : 1 / 2; if (isPhysical && field.defenderSide.isReflect) { @@ -502,80 +663,7 @@ export function calculateDPP( desc.isSwitching = 'out'; } } - - // the random factor is applied between the LO mod and the STAB mod, so don't apply anything - // below this until we're inside the loop - let stabMod = 1; - if (move.hasType(...attacker.types)) { - if (attacker.hasAbility('Adaptability')) { - stabMod = 2; - desc.attackerAbility = attacker.ability; - } else { - stabMod = 1.5; - } - } - - let filterMod = 1; - if (defender.hasAbility('Filter', 'Solid Rock') && typeEffectiveness > 1) { - filterMod = 0.75; - desc.defenderAbility = defender.ability; - } - let ebeltMod = 1; - if (attacker.hasItem('Expert Belt') && typeEffectiveness > 1) { - ebeltMod = 1.2; - desc.attackerItem = attacker.item; - } - let tintedMod = 1; - if (attacker.hasAbility('Tinted Lens') && typeEffectiveness < 1) { - tintedMod = 2; - desc.attackerAbility = attacker.ability; - } - let berryMod = 1; - if (move.hasType(getBerryResistType(defender.item)) && - (typeEffectiveness > 1 || move.hasType('Normal'))) { - berryMod = 0.5; - desc.defenderItem = defender.item; - } - - const damage: number[] = []; - for (let i = 0; i < 16; i++) { - damage[i] = Math.floor((baseDamage * (85 + i)) / 100); - damage[i] = Math.floor(damage[i] * stabMod); - damage[i] = Math.floor(damage[i] * type1Effectiveness); - damage[i] = Math.floor(damage[i] * type2Effectiveness); - damage[i] = Math.floor(damage[i] * filterMod); - damage[i] = Math.floor(damage[i] * ebeltMod); - damage[i] = Math.floor(damage[i] * tintedMod); - damage[i] = Math.floor(damage[i] * berryMod); - damage[i] = Math.max(1, damage[i]); - } - result.damage = damage; - - if (move.hits > 1) { - for (let times = 0; times < move.hits; times++) { - let damageMultiplier = 0; - result.damage = result.damage.map(affectedAmount => { - if (times) { - let newFinalDamage = 0; - newFinalDamage = Math.floor((baseDamage * (85 + damageMultiplier)) / 100); - newFinalDamage = Math.floor(newFinalDamage * stabMod); - newFinalDamage = Math.floor(newFinalDamage * type1Effectiveness); - newFinalDamage = Math.floor(newFinalDamage * type2Effectiveness); - newFinalDamage = Math.floor(newFinalDamage * filterMod); - newFinalDamage = Math.floor(newFinalDamage * ebeltMod); - newFinalDamage = Math.floor(newFinalDamage * tintedMod); - newFinalDamage = Math.max(1, newFinalDamage); - damageMultiplier++; - return affectedAmount + newFinalDamage; - } - return affectedAmount; - }); - } - } - - // #endregion - - return result; + return baseDamage; } function getSimpleModifiedStat(stat: number, mod: number) { diff --git a/calc/src/mechanics/gen56.ts b/calc/src/mechanics/gen56.ts index cf18a5ea4..67dbe0fe2 100644 --- a/calc/src/mechanics/gen56.ts +++ b/calc/src/mechanics/gen56.ts @@ -30,6 +30,7 @@ import { getFinalDamage, getModifiedStat, getMoveEffectiveness, + getStabMod, getWeightFactor, handleFixedDamageMoves, isGrounded, @@ -127,6 +128,7 @@ export function calculateBWXY( } } + let hasAteAbilityTypeChange = false; let isAerilate = false; let isPixilate = false; let isRefrigerate = false; @@ -149,6 +151,9 @@ export function calculateBWXY( if (isPixilate || isRefrigerate || isAerilate || isNormalize) { desc.attackerAbility = attacker.ability; } + if (isPixilate || isRefrigerate || isAerilate) { + hasAteAbilityTypeChange = true; + } } if (attacker.hasAbility('Gale Wings') && move.hasType('Flying')) { @@ -164,21 +169,6 @@ export function calculateBWXY( : 1; let typeEffectiveness = type1Effectiveness * type2Effectiveness; - let resistedKnockOffDamage = - !defender.item || - (defender.named('Giratina-Origin') && defender.hasItem('Griseous Orb')) || - (defender.name.includes('Arceus') && defender.item.includes('Plate')) || - (defender.name.includes('Genesect') && defender.item.includes('Drive')) || - (defender.named('Groudon', 'Groudon-Primal') && defender.hasItem('Red Orb')) || - (defender.named('Kyogre', 'Kyogre-Primal') && defender.hasItem('Blue Orb')); - - // The last case only applies when the Pokemon is holding the Mega Stone that matches its species - // (or when it's already a Mega-Evolution) - if (!resistedKnockOffDamage && defender.item) { - const item = gen.items.get(toID(defender.item))!; - resistedKnockOffDamage = !!(item.megaEvolves && defender.name.includes(item.megaEvolves)); - } - if (typeEffectiveness === 0 && move.named('Thousand Arrows')) { typeEffectiveness = 1; } else if (typeEffectiveness === 0 && move.hasType('Ground') && @@ -269,12 +259,179 @@ export function calculateBWXY( desc.hits = move.hits; } - const turnOrder = attacker.stats.spe > defender.stats.spe ? 'first' : 'last'; - // #endregion // #region Base Power + const basePower = calculateBasePowerBWXY( + gen, + attacker, + defender, + move, + field, + hasAteAbilityTypeChange, + desc + ); + if (basePower === 0) { + return result; + } + + // #endregion + // #region (Special) Attack + + const attack = calculateAttackBWXY(gen, attacker, defender, move, field, desc, isCritical); + const attackStat = move.category === 'Special' ? 'spa' : 'atk'; + + // #endregion + // #region (Special) Defense + + const defense = calculateDefenseBWXY(gen, attacker, defender, move, field, desc, isCritical); + + // #endregion + // #region Damage + + const baseDamage = calculateBaseDamageBWXY( + gen, + attacker, + basePower, + attack, + defense, + move, + field, + desc, + isCritical + ); + + // the random factor is applied between the crit mod and the stab mod, so don't apply anything + // below this until we're inside the loop + let stabMod = getStabMod(attacker, move, desc); + + const applyBurn = + attacker.hasStatus('brn') && + move.category === 'Physical' && + !attacker.hasAbility('Guts') && + !(move.named('Facade') && gen.num === 6); + desc.isBurned = applyBurn; + + const finalMods = calculateFinalModsBWXY( + gen, + attacker, + defender, + move, + field, + desc, + isCritical, + typeEffectiveness + ); + const finalMod = chainMods(finalMods, 41, 131072); + + const isSpread = field.gameType !== 'Singles' && + ['allAdjacent', 'allAdjacentFoes'].includes(move.target); + + let childDamage: number[] | undefined; + if (attacker.hasAbility('Parental Bond') && move.hits === 1 && !isSpread) { + const child = attacker.clone(); + child.ability = 'Parental Bond (Child)' as AbilityName; + checkMultihitBoost(gen, child, defender, move, field, desc); + childDamage = calculateBWXY(gen, child, defender, move, field).damage as number[]; + desc.attackerAbility = attacker.ability; + } + + let damage: number[] = []; + for (let i = 0; i < 16; i++) { + damage[i] = + getFinalDamage(baseDamage, i, typeEffectiveness, applyBurn, stabMod, finalMod); + } + + desc.attackBoost = + move.named('Foul Play') ? defender.boosts[attackStat] : attacker.boosts[attackStat]; + + if ((move.dropsStats && move.timesUsed! > 1) || move.hits > 1) { + // store boosts so intermediate boosts don't show. + const origDefBoost = desc.defenseBoost; + const origAtkBoost = desc.attackBoost; + let numAttacks = 1; + if (move.dropsStats && move.timesUsed! > 1) { + desc.moveTurns = `over ${move.timesUsed} turns`; + numAttacks = move.timesUsed!; + } else { + numAttacks = move.hits; + } + let usedItems = [false, false]; + for (let times = 1; times < numAttacks; times++) { + usedItems = checkMultihitBoost(gen, attacker, defender, move, + field, desc, usedItems[0], usedItems[1]); + const newAtk = calculateAttackBWXY(gen, attacker, defender, move, field, desc, isCritical); + const newDef = calculateDefenseBWXY(gen, attacker, defender, move, field, desc, isCritical); + // Check if lost -ate ability. Typing stays the same, only boost is lost + // Cannot be regained during multihit move and no Normal moves with stat drawbacks + hasAteAbilityTypeChange = hasAteAbilityTypeChange && + attacker.hasAbility('Aerilate', 'Galvanize', 'Pixilate', 'Refrigerate'); + + if ((move.dropsStats && move.timesUsed! > 1)) { + // Adaptability does not change between hits of a multihit, only between turns + stabMod = getStabMod(attacker, move, desc); + } + + const newBasePower = calculateBasePowerBWXY( + gen, + attacker, + defender, + move, + field, + hasAteAbilityTypeChange, + desc + ); + const newBaseDamage = getBaseDamage(attacker.level, newBasePower, newAtk, newDef); + const newFinalMods = calculateFinalModsBWXY( + gen, + attacker, + defender, + move, + field, + desc, + isCritical, + typeEffectiveness, + times + ); + const newFinalMod = chainMods(newFinalMods, 41, 131072); + + let damageMultiplier = 0; + damage = damage.map(affectedAmount => { + const newFinalDamage = getFinalDamage( + newBaseDamage, + damageMultiplier, + typeEffectiveness, + applyBurn, + stabMod, + newFinalMod + ); + damageMultiplier++; + return affectedAmount + newFinalDamage; + }); + } + desc.defenseBoost = origDefBoost; + desc.attackBoost = origAtkBoost; + } + + result.damage = childDamage ? [damage, childDamage] : damage; + + // #endregion + + return result; +} + +export function calculateBasePowerBWXY( + gen: Generation, + attacker: Pokemon, + defender: Pokemon, + move: Move, + field: Field, + hasAteAbilityTypeChange: boolean, + desc: RawDesc, + hit = 1, +) { let basePower: number; + const turnOrder = attacker.stats.spe > defender.stats.spe ? 'first' : 'last'; switch (move.name) { case 'Payback': @@ -389,10 +546,10 @@ export function calculateBWXY( } } break; - // Triple Kick's damage doubles after each consecutive hit (10, 20, 30), this is a hack + // Triple Kick's damage increases after each consecutive hit (10, 20, 30) case 'Triple Kick': - basePower = move.hits === 2 ? 15 : move.hits === 3 ? 30 : 10; - desc.moveBP = basePower; + basePower = hit * 10; + desc.moveBP = move.hits === 2 ? 30 : move.hits === 3 ? 60 : 10; break; case 'Crush Grip': case 'Wring Out': @@ -404,12 +561,51 @@ export function calculateBWXY( basePower = move.bp; } - if (basePower === 0) { - return result; - } + const bpMods = calculateBPModsBWXY( + gen, + attacker, + defender, + move, + field, + desc, + basePower, + hasAteAbilityTypeChange, + turnOrder + ); + + basePower = OF16(Math.max(1, pokeRound((basePower * chainMods(bpMods, 41, 2097152)) / 4096))); + return basePower; +} +export function calculateBPModsBWXY( + gen: Generation, + attacker: Pokemon, + defender: Pokemon, + move: Move, + field: Field, + desc: RawDesc, + basePower: number, + hasAteAbilityTypeChange: boolean, + turnOrder: string +) { const bpMods = []; + let resistedKnockOffDamage = + !defender.item || + (defender.named('Giratina-Origin') && defender.hasItem('Griseous Orb')) || + (defender.name.includes('Arceus') && defender.item.includes('Plate')) || + (defender.name.includes('Genesect') && defender.item.includes('Drive')) || + (defender.named('Groudon', 'Groudon-Primal') && defender.hasItem('Red Orb')) || + (defender.named('Kyogre', 'Kyogre-Primal') && defender.hasItem('Blue Orb')); + + // The last case only applies when the Pokemon is holding the Mega Stone that matches its species + // (or when it's already a Mega-Evolution) + if (!resistedKnockOffDamage && defender.item) { + const item = gen.items.get(toID(defender.item))!; + resistedKnockOffDamage = !!(item.megaEvolves && defender.name.includes(item.megaEvolves)); + } + + // Use BasePower after moves with custom BP to determine if Technician should boost if ((attacker.hasAbility('Technician') && basePower <= 60) || (attacker.hasAbility('Flare Boost') && @@ -508,7 +704,7 @@ export function calculateBWXY( desc.isHelpingHand = true; } - if (isAerilate || isPixilate || isRefrigerate || isNormalize) { + if (hasAteAbilityTypeChange) { bpMods.push(5325); desc.attackerAbility = attacker.ability; } else if ( @@ -562,11 +758,18 @@ export function calculateBWXY( } } - basePower = OF16(Math.max(1, pokeRound((basePower * chainMods(bpMods, 41, 2097152)) / 4096))); - - // #endregion - // #region (Special) Attack + return bpMods; +} +export function calculateAttackBWXY( + gen: Generation, + attacker: Pokemon, + defender: Pokemon, + move: Move, + field: Field, + desc: RawDesc, + isCritical = false +) { let attack: number; const attackSource = move.named('Foul Play') ? defender : attacker; const attackStat = move.category === 'Special' ? 'spa' : 'atk'; @@ -582,7 +785,7 @@ export function calculateBWXY( attack = attackSource.rawStats[attackStat]; desc.defenderAbility = defender.ability; } else { - attack = attackSource.stats[attackStat]; + attack = getModifiedStat(attackSource.rawStats[attackStat]!, attackSource.boosts[attackStat]!); desc.attackBoost = attackSource.boosts[attackStat]; } @@ -592,6 +795,18 @@ export function calculateBWXY( desc.attackerAbility = attacker.ability; } + const atMods = calculateAtModsBWXY(attacker, defender, move, field, desc); + attack = OF16(Math.max(1, pokeRound((attack * chainMods(atMods, 410, 131072)) / 4096))); + return attack; +} + +export function calculateAtModsBWXY( + attacker: Pokemon, + defender: Pokemon, + move: Move, + field: Field, + desc: RawDesc +) { const atMods = []; if (defender.hasAbility('Thick Fat') && move.hasType('Fire', 'Ice')) { atMods.push(2048); @@ -661,12 +876,18 @@ export function calculateBWXY( atMods.push(6144); desc.attackerItem = attacker.item; } + return atMods; +} - attack = OF16(Math.max(1, pokeRound((attack * chainMods(atMods, 410, 131072)) / 4096))); - - // #endregion - // #region (Special) Defense - +export function calculateDefenseBWXY( + gen: Generation, + attacker: Pokemon, + defender: Pokemon, + move: Move, + field: Field, + desc: RawDesc, + isCritical = false +) { let defense: number; const defenseStat = move.overrideDefensiveStat || move.category === 'Physical' ? 'def' : 'spd'; const hitsPhysical = defenseStat === 'def'; @@ -679,7 +900,7 @@ export function calculateBWXY( defense = defender.rawStats[defenseStat]; desc.attackerAbility = attacker.ability; } else { - defense = defender.stats[defenseStat]; + defense = getModifiedStat(defender.rawStats[defenseStat]!, defender.boosts[defenseStat]!); desc.defenseBoost = defender.boosts[defenseStat]; } @@ -689,6 +910,24 @@ export function calculateBWXY( desc.weather = field.weather; } + const dfMods = calculateDfModsBWXY( + gen, + defender, + field, + desc, + hitsPhysical + ); + defense = OF16(Math.max(1, pokeRound((defense * chainMods(dfMods, 410, 131072)) / 4096))); + return defense; +} + +export function calculateDfModsBWXY( + gen: Generation, + defender: Pokemon, + field: Field, + desc: RawDesc, + hitsPhysical = false +) { const dfMods = []; if (defender.hasAbility('Marvel Scale') && defender.status && hitsPhysical) { dfMods.push(6144); @@ -734,182 +973,10 @@ export function calculateBWXY( dfMods.push(8192); desc.defenderAbility = defender.ability; } - - defense = OF16(Math.max(1, pokeRound((defense * chainMods(dfMods, 410, 131072)) / 4096))); - - // #endregion - // #region Damage - - const baseDamage = calculateBaseDamageBWXY( - gen, - attacker, - basePower, - attack, - defense, - move, - field, - desc, - isCritical - ); - - // the random factor is applied between the crit mod and the stab mod, so don't apply anything - // below this until we're inside the loop - let stabMod = 4096; - if (attacker.hasType(move.type)) { - if (attacker.hasAbility('Adaptability')) { - stabMod = 8192; - desc.attackerAbility = attacker.ability; - } else { - stabMod = 6144; - } - } else if (attacker.hasAbility('Protean')) { - stabMod = 6144; - desc.attackerAbility = attacker.ability; - } - - const applyBurn = - attacker.hasStatus('brn') && - move.category === 'Physical' && - !attacker.hasAbility('Guts') && - !(move.named('Facade') && gen.num === 6); - desc.isBurned = applyBurn; - - const finalMods = calculateFinalModsBWXY( - gen, - attacker, - defender, - move, - field, - desc, - isCritical, - typeEffectiveness - ); - const finalMod = chainMods(finalMods, 41, 131072); - - const isSpread = field.gameType !== 'Singles' && - ['allAdjacent', 'allAdjacentFoes'].includes(move.target); - - let childDamage: number[] | undefined; - if (attacker.hasAbility('Parental Bond') && move.hits === 1 && !isSpread) { - const child = attacker.clone(); - child.ability = 'Parental Bond (Child)' as AbilityName; - checkMultihitBoost(gen, child, defender, move, field, desc); - childDamage = calculateBWXY(gen, child, defender, move, field).damage as number[]; - desc.attackerAbility = attacker.ability; - } - - let damage: number[] = []; - for (let i = 0; i < 16; i++) { - damage[i] = - getFinalDamage(baseDamage, i, typeEffectiveness, applyBurn, stabMod, finalMod); - } - - if (move.dropsStats && (move.timesUsed || 0) > 1) { - const simpleMultiplier = attacker.hasAbility('Simple') ? 2 : 1; - - desc.moveTurns = `over ${move.timesUsed} turns`; - const hasWhiteHerb = attacker.hasItem('White Herb'); - let usedWhiteHerb = false; - let dropCount = attacker.boosts[attackStat]; - for (let times = 0; times < move.timesUsed!; times++) { - const newAttack = getModifiedStat(attack, dropCount); - let damageMultiplier = 0; - damage = damage.map(affectedAmount => { - if (times) { - const newBaseDamage = getBaseDamage(attacker.level, basePower, newAttack, defense); - const newFinalDamage = getFinalDamage( - newBaseDamage, - damageMultiplier, - typeEffectiveness, - applyBurn, - stabMod, - finalMod - ); - damageMultiplier++; - return affectedAmount + newFinalDamage; - } - return affectedAmount; - }); - - if (attacker.hasAbility('Contrary')) { - dropCount = Math.min(6, dropCount + move.dropsStats); - desc.attackerAbility = attacker.ability; - } else { - dropCount = Math.max(-6, dropCount - move.dropsStats * simpleMultiplier); - if (attacker.hasAbility('Simple')) { - desc.attackerAbility = attacker.ability; - } - } - - // the Pokémon hits THEN the stat rises / lowers - if (hasWhiteHerb && attacker.boosts[attackStat] < 0 && !usedWhiteHerb) { - dropCount += move.dropsStats * simpleMultiplier; - usedWhiteHerb = true; - desc.attackerItem = attacker.item; - } - } - } - - if (move.hits > 1) { - let defenderDefBoost = defender.boosts['def']; - for (let times = 0; times < move.hits; times++) { - let damageMultiplier = 0; - damage = damage.map(affectedAmount => { - if (times) { - const newFinalMods = calculateFinalModsBWXY( - gen, - attacker, - defender, - move, - field, - desc, - isCritical, - typeEffectiveness, - times - ); - const newFinalMod = chainMods(newFinalMods, 41, 131072); - const newDefense = getModifiedStat(defense, defenderDefBoost); - const newBaseDamage = calculateBaseDamageBWXY( - gen, - attacker, - basePower, - attack, - newDefense, - move, - field, - desc, - isCritical - ); - const newFinalDamage = getFinalDamage( - newBaseDamage, - damageMultiplier, - typeEffectiveness, - applyBurn, - stabMod, - newFinalMod, - ); - damageMultiplier++; - return affectedAmount + newFinalDamage; - } - return affectedAmount; - }); - if (hitsPhysical && defender.ability === 'Weak Armor') { - defenderDefBoost = Math.max(-6, defenderDefBoost - 1); - desc.defenderAbility = 'Weak Armor'; - } - } - } - - desc.attackBoost = - move.named('Foul Play') ? defender.boosts[attackStat] : attacker.boosts[attackStat]; - - result.damage = childDamage ? [damage, childDamage] : damage; - - // #endregion - - return result; + return dfMods; } + function calculateBaseDamageBWXY( gen: Generation, attacker: Pokemon, @@ -1022,6 +1089,7 @@ function calculateFinalModsBWXY( if (move.hasType(getBerryResistType(defender.item)) && (typeEffectiveness > 1 || move.hasType('Normal')) && + hitCount === 0 && !attacker.hasAbility('Unnerve')) { finalMods.push(2048); desc.defenderItem = defender.item; diff --git a/calc/src/mechanics/gen789.ts b/calc/src/mechanics/gen789.ts index 02524c3c9..fb3d74fb1 100644 --- a/calc/src/mechanics/gen789.ts +++ b/calc/src/mechanics/gen789.ts @@ -44,6 +44,8 @@ import { OF16, OF32, pokeRound, isQPActive, + getStabMod, + getStellarStabMod, } from './util'; export function calculateSMSSSV( @@ -354,6 +356,8 @@ export function calculateSMSSSV( typeEffectiveness = !defender.teraType ? 1 : 2; } + const turn2typeEffectiveness = typeEffectiveness; + // Tera Shell works only at full HP, but for all hits of multi-hit moves if (defender.hasAbility('Tera Shell') && defender.curHP() === defender.maxHP() && @@ -520,34 +524,8 @@ export function calculateSMSSSV( // the random factor is applied between the crit mod and the stab mod, so don't apply anything // below this until we're inside the loop - let stabMod = 4096; - if (attacker.hasOriginalType(move.type)) { - stabMod += 2048; - } else if (attacker.hasAbility('Protean', 'Libero') && !attacker.teraType) { - stabMod += 2048; - desc.attackerAbility = attacker.ability; - } - const teraType = attacker.teraType; - if (teraType === move.type && teraType !== 'Stellar') { - stabMod += 2048; - desc.attackerTera = teraType; - } - if (attacker.hasAbility('Adaptability') && attacker.hasType(move.type)) { - stabMod += teraType && attacker.hasOriginalType(teraType) ? 1024 : 2048; - desc.attackerAbility = attacker.ability; - } - - // TODO: For now all moves are always boosted - const isStellarBoosted = - attacker.teraType === 'Stellar' && - (move.isStellarFirstUse || attacker.named('Terapagos-Stellar')); - if (isStellarBoosted) { - if (attacker.hasOriginalType(move.type)) { - stabMod += 2048; - } else { - stabMod = 4915; - } - } + let preStellarStabMod = getStabMod(attacker, move, desc); + let stabMod = getStellarStabMod(attacker, move, preStellarStabMod); const applyBurn = attacker.hasStatus('brn') && @@ -593,111 +571,97 @@ export function calculateSMSSSV( getFinalDamage(baseDamage, i, typeEffectiveness, applyBurn, stabMod, finalMod, protect); } - if (move.dropsStats && move.timesUsed! > 1) { - const simpleMultiplier = attacker.hasAbility('Simple') ? 2 : 1; + desc.attackBoost = + move.named('Foul Play') ? defender.boosts[attackStat] : attacker.boosts[attackStat]; - desc.moveTurns = `over ${move.timesUsed} turns`; - const hasWhiteHerb = attacker.hasItem('White Herb'); - let usedWhiteHerb = false; - let dropCount = 0; - for (let times = 0; times < move.timesUsed!; times++) { - const newAttack = getModifiedStat(attack, dropCount); - let damageMultiplier = 0; - damage = damage.map(affectedAmount => { - if (times) { - const newBaseDamage = getBaseDamage(attacker.level, basePower, newAttack, defense); - const newFinalDamage = getFinalDamage( - newBaseDamage, - damageMultiplier, - typeEffectiveness, - applyBurn, - stabMod, - finalMod, - protect - ); - damageMultiplier++; - return affectedAmount + newFinalDamage; - } - return affectedAmount; - }); + if ((move.dropsStats && move.timesUsed! > 1) || move.hits > 1) { + // store boosts so intermediate boosts don't show. + const origDefBoost = desc.defenseBoost; + const origAtkBoost = desc.attackBoost; - if (attacker.hasAbility('Contrary')) { - dropCount = Math.min(6, dropCount + move.dropsStats); - desc.attackerAbility = attacker.ability; - } else { - dropCount = Math.max(-6, dropCount - move.dropsStats * simpleMultiplier); - if (attacker.hasAbility('Simple')) { - desc.attackerAbility = attacker.ability; - } + let numAttacks = 1; + if (move.dropsStats && move.timesUsed! > 1) { + desc.moveTurns = `over ${move.timesUsed} turns`; + numAttacks = move.timesUsed!; + } else { + numAttacks = move.hits; + } + let usedItems = [false, false]; + for (let times = 1; times < numAttacks; times++) { + usedItems = checkMultihitBoost(gen, attacker, defender, move, + field, desc, usedItems[0], usedItems[1]); + const newAttack = calculateAttackSMSSSV(gen, attacker, defender, move, + field, desc, isCritical); + const newDefense = calculateDefenseSMSSSV(gen, attacker, defender, move, + field, desc, isCritical); + // Check if lost -ate ability. Typing stays the same, only boost is lost + // Cannot be regained during multihit move and no Normal moves with stat drawbacks + hasAteAbilityTypeChange = hasAteAbilityTypeChange && + attacker.hasAbility('Aerilate', 'Galvanize', 'Pixilate', 'Refrigerate', 'Normalize'); + + if ((move.dropsStats && move.timesUsed! > 1)) { + // Adaptability does not change between hits of a multihit, only between turns + preStellarStabMod = getStabMod(attacker, move, desc); + // Hack to make Tera Shell with multihit moves, but not over multiple turns + typeEffectiveness = turn2typeEffectiveness; + // Stellar damage boost applies for 1 turn, but all hits of multihit. + stabMod = getStellarStabMod(attacker, move, preStellarStabMod, times); } - // the Pokémon hits THEN the stat rises / lowers - if (hasWhiteHerb && attacker.boosts[attackStat] < 0 && !usedWhiteHerb) { - dropCount += move.dropsStats * simpleMultiplier; - usedWhiteHerb = true; - desc.attackerItem = attacker.item; - } - } - } + const newBasePower = calculateBasePowerSMSSSV( + gen, + attacker, + defender, + move, + field, + hasAteAbilityTypeChange, + desc, + times + 1 + ); + const newBaseDamage = calculateBaseDamageSMSSSV( + gen, + attacker, + defender, + newBasePower, + newAttack, + newDefense, + move, + field, + desc, + isCritical + ); + const newFinalMods = calculateFinalModsSMSSSV( + gen, + attacker, + defender, + move, + field, + desc, + isCritical, + typeEffectiveness, + times + ); + const newFinalMod = chainMods(newFinalMods, 41, 131072); - if (move.hits > 1) { - let defenderDefBoost = 0; - for (let times = 0; times < move.hits; times++) { - const newDefense = getModifiedStat(defense, defenderDefBoost); let damageMultiplier = 0; damage = damage.map(affectedAmount => { - if (times) { - const newFinalMods = calculateFinalModsSMSSSV( - gen, - attacker, - defender, - move, - field, - desc, - isCritical, - typeEffectiveness, - times - ); - const newFinalMod = chainMods(newFinalMods, 41, 131072); - const newBaseDamage = calculateBaseDamageSMSSSV( - gen, - attacker, - defender, - basePower, - attack, - newDefense, - move, - field, - desc, - isCritical - ); - const newFinalDamage = getFinalDamage( - newBaseDamage, - damageMultiplier, - typeEffectiveness, - applyBurn, - stabMod, - newFinalMod, - protect - ); - damageMultiplier++; - return affectedAmount + newFinalDamage; - } - return affectedAmount; + const newFinalDamage = getFinalDamage( + newBaseDamage, + damageMultiplier, + typeEffectiveness, + applyBurn, + stabMod, + newFinalMod, + protect + ); + damageMultiplier++; + return affectedAmount + newFinalDamage; }); - if (hitsPhysical && defender.ability === 'Stamina') { - defenderDefBoost = Math.min(6, defenderDefBoost + 1); - desc.defenderAbility = 'Stamina'; - } else if (hitsPhysical && defender.ability === 'Weak Armor') { - defenderDefBoost = Math.max(-6, defenderDefBoost - 1); - desc.defenderAbility = 'Weak Armor'; - } } + desc.defenseBoost = origDefBoost; + desc.attackBoost = origAtkBoost; } - desc.attackBoost = - move.named('Foul Play') ? defender.boosts[attackStat] : attacker.boosts[attackStat]; - result.damage = childDamage ? [damage, childDamage] : damage; // #endregion @@ -712,7 +676,8 @@ export function calculateBasePowerSMSSSV( move: Move, field: Field, hasAteAbilityTypeChange: boolean, - desc: RawDesc + desc: RawDesc, + hit = 1, ) { const turnOrder = attacker.stats.spe > defender.stats.spe ? 'first' : 'last'; @@ -873,15 +838,15 @@ export function calculateBasePowerSMSSSV( basePower = attacker.named('Greninja-Ash') && attacker.hasAbility('Battle Bond') ? 20 : 15; desc.moveBP = basePower; break; - // Triple Axel's damage doubles after each consecutive hit (20, 40, 60), this is a hack + // Triple Axel's damage increases after each consecutive hit (20, 40, 60) case 'Triple Axel': - basePower = move.hits === 2 ? 30 : move.hits === 3 ? 40 : 20; - desc.moveBP = basePower; + basePower = hit * 20; + desc.moveBP = move.hits === 2 ? 60 : move.hits === 3 ? 120 : 20; break; - // Triple Kick's damage doubles after each consecutive hit (10, 20, 30), this is a hack + // Triple Kick's damage increases after each consecutive hit (10, 20, 30) case 'Triple Kick': - basePower = move.hits === 2 ? 15 : move.hits === 3 ? 30 : 10; - desc.moveBP = basePower; + basePower = hit * 10; + desc.moveBP = move.hits === 2 ? 30 : move.hits === 3 ? 60 : 10; break; case 'Crush Grip': case 'Wring Out': @@ -1244,7 +1209,7 @@ export function calculateAttackSMSSSV( attack = attackSource.rawStats[attackStat]; desc.defenderAbility = defender.ability; } else { - attack = attackSource.stats[attackStat]; + attack = getModifiedStat(attackSource.rawStats[attackStat]!, attackSource.boosts[attackStat]!); desc.attackBoost = attackSource.boosts[attackStat]; } @@ -1425,7 +1390,7 @@ export function calculateDefenseSMSSSV( defense = defender.rawStats[defenseStat]; desc.attackerAbility = attacker.ability; } else { - defense = defender.stats[defenseStat]; + defense = getModifiedStat(defender.rawStats[defenseStat]!, defender.boosts[defenseStat]!); desc.defenseBoost = defender.boosts[defenseStat]; } diff --git a/calc/src/mechanics/util.ts b/calc/src/mechanics/util.ts index 51d8d0490..9f17aae89 100644 --- a/calc/src/mechanics/util.ts +++ b/calc/src/mechanics/util.ts @@ -305,15 +305,16 @@ export function checkMultihitBoost( move: Move, field: Field, desc: RawDesc, - usedWhiteHerb = false + attackerUsedItem = false, + defenderUsedItem = false ) { // NOTE: attacker.ability must be Parental Bond for these moves to be multi-hit if (move.named('Gyro Ball', 'Electro Ball') && defender.hasAbility('Gooey', 'Tangling Hair')) { // Gyro Ball (etc) makes contact into Gooey (etc) whenever its inflicting multiple hits because // this can only happen if the attacker ability is Parental Bond (and thus can't be Long Reach) - if (attacker.hasItem('White Herb') && !usedWhiteHerb) { + if (attacker.hasItem('White Herb') && !attackerUsedItem) { desc.attackerItem = attacker.item; - usedWhiteHerb = true; + attackerUsedItem = true; } else { attacker.boosts.spe = Math.max(attacker.boosts.spe - 1, -6); attacker.stats.spe = getFinalSpeed(gen, attacker, field, field.attackerSide); @@ -326,6 +327,44 @@ export function checkMultihitBoost( attacker.stats.atk = getModifiedStat(attacker.rawStats.atk, attacker.boosts.atk, gen); } + const atkSimple = attacker.hasAbility('Simple') ? 2 : 1; + const defSimple = defender.hasAbility('Simple') ? 2 : 1; + + if ((!defenderUsedItem) && + (defender.hasItem('Luminous Moss') && move.hasType('Water')) || + (defender.hasItem('Maranga Berry') && move.category === 'Special') || + (defender.hasItem('Kee Berry') && move.category === 'Physical')) { + const defStat = defender.hasItem('Kee Berry') ? 'def' : 'spd'; + if (attacker.hasAbility('Unaware')) { + desc.attackerAbility = attacker.ability; + } else { + if (defender.hasAbility('Contrary')) { + desc.defenderAbility = defender.ability; + if (defender.hasItem('White Herb') && !defenderUsedItem) { + desc.defenderItem = defender.item; + defenderUsedItem = true; + } else { + defender.boosts[defStat] = Math.max(-6, defender.boosts[defStat] - defSimple); + } + } else { + defender.boosts[defStat] = Math.min(6, defender.boosts[defStat] + defSimple); + } + if (defSimple === 2) desc.defenderAbility = defender.ability; + defender.stats[defStat] = getModifiedStat(defender.rawStats[defStat], + defender.boosts[defStat], + gen); + desc.defenderItem = defender.item; + defenderUsedItem = true; + } + } + + if (defender.hasAbility('Seed Sower')) { + field.terrain = 'Grassy'; + } + if (defender.hasAbility('Sand Spit')) { + field.weather = 'Sand'; + } + if (defender.hasAbility('Stamina')) { if (attacker.hasAbility('Unaware')) { desc.attackerAbility = attacker.ability; @@ -334,24 +373,31 @@ export function checkMultihitBoost( defender.stats.def = getModifiedStat(defender.rawStats.def, defender.boosts.def, gen); desc.defenderAbility = defender.ability; } + } else if (defender.hasAbility('Water Compaction') && move.hasType('Water')) { + if (attacker.hasAbility('Unaware')) { + desc.attackerAbility = attacker.ability; + } else { + defender.boosts.def = Math.min(defender.boosts.def + 2, 6); + defender.stats.def = getModifiedStat(defender.rawStats.def, defender.boosts.def, gen); + desc.defenderAbility = defender.ability; + } } else if (defender.hasAbility('Weak Armor')) { if (attacker.hasAbility('Unaware')) { desc.attackerAbility = attacker.ability; } else { - if (defender.hasItem('White Herb') && !usedWhiteHerb) { + if (defender.hasItem('White Herb') && !defenderUsedItem && defender.boosts.def === 0) { desc.defenderItem = defender.item; - usedWhiteHerb = true; + defenderUsedItem = true; } else { defender.boosts.def = Math.max(defender.boosts.def - 1, -6); defender.stats.def = getModifiedStat(defender.rawStats.def, defender.boosts.def, gen); } + desc.defenderAbility = defender.ability; } defender.boosts.spe = Math.min(defender.boosts.spe + 2, 6); defender.stats.spe = getFinalSpeed(gen, defender, field, field.defenderSide); - desc.defenderAbility = defender.ability; } - const simple = attacker.hasAbility('Simple') ? 2 : 1; if (move.dropsStats) { if (attacker.hasAbility('Unaware')) { desc.attackerAbility = attacker.ability; @@ -364,14 +410,14 @@ export function checkMultihitBoost( boosts = Math.min(6, boosts + move.dropsStats); desc.attackerAbility = attacker.ability; } else { - boosts = Math.max(-6, boosts - move.dropsStats * simple); - if (simple > 1) desc.attackerAbility = attacker.ability; + boosts = Math.max(-6, boosts - move.dropsStats * atkSimple); } + if (atkSimple === 2) desc.attackerAbility = attacker.ability; - if (attacker.hasItem('White Herb') && attacker.boosts[stat] < 0 && !usedWhiteHerb) { - boosts += move.dropsStats * simple; + if (attacker.hasItem('White Herb') && attacker.boosts[stat] < 0 && !attackerUsedItem) { + boosts += move.dropsStats * atkSimple; desc.attackerItem = attacker.item; - usedWhiteHerb = true; + attackerUsedItem = true; } attacker.boosts[stat] = boosts; @@ -379,7 +425,20 @@ export function checkMultihitBoost( } } - return usedWhiteHerb; + // Do ability swap after all other effects + if (defender.hasAbility('Mummy', 'Wandering Spirit', 'Lingering Aroma') && move.flags.contact) { + const oldAttackerAbility = attacker.ability; + attacker.ability = defender.ability; + // If attacker ability is notable, then ability swap is notable. + if (desc.attackerAbility) { + desc.defenderAbility = defender.ability; + } + if (defender.hasAbility('Wandering Spirit')) { + defender.ability = oldAttackerAbility; + } + } + + return [attackerUsedItem, defenderUsedItem]; } export function chainMods(mods: number[], lowerBound: number, upperBound: number) { @@ -498,6 +557,40 @@ export function getWeightFactor(pokemon: Pokemon) { : (pokemon.hasAbility('Light Metal') || pokemon.hasItem('Float Stone')) ? 0.5 : 1; } +export function getStabMod(pokemon: Pokemon, move: Move, desc: RawDesc) { + let stabMod = 4096; + if (pokemon.hasOriginalType(move.type)) { + stabMod += 2048; + } else if (pokemon.hasAbility('Protean', 'Libero') && !pokemon.teraType) { + stabMod += 2048; + desc.attackerAbility = pokemon.ability; + } + const teraType = pokemon.teraType; + if (teraType === move.type && teraType !== 'Stellar') { + stabMod += 2048; + desc.attackerTera = teraType; + } + if (pokemon.hasAbility('Adaptability') && pokemon.hasType(move.type)) { + stabMod += teraType && pokemon.hasOriginalType(teraType) ? 1024 : 2048; + desc.attackerAbility = pokemon.ability; + } + return stabMod; +} + +export function getStellarStabMod(pokemon: Pokemon, move: Move, stabMod = 1, turns = 0) { + const isStellarBoosted = + pokemon.teraType === 'Stellar' && + ((move.isStellarFirstUse && turns === 0) || pokemon.named('Terapagos-Stellar')); + if (isStellarBoosted) { + if (pokemon.hasOriginalType(move.type)) { + stabMod += 2048; + } else { + stabMod = 4915; + } + } + return stabMod; +} + export function countBoosts(gen: Generation, boosts: StatsTable) { let sum = 0; diff --git a/calc/src/test/calc.test.ts b/calc/src/test/calc.test.ts index 69b693a9d..edd471b2b 100644 --- a/calc/src/test/calc.test.ts +++ b/calc/src/test/calc.test.ts @@ -135,14 +135,14 @@ describe('calc', () => { { weather: 'Sun', type: 'Fire', damage: { adv: {range: [346, 408], desc: '(149.7 - 176.6%) -- guaranteed OHKO'}, - dpp: {range: [170, 204], desc: '(73.5 - 88.3%) -- guaranteed 2HKO'}, + dpp: {range: [342, 404], desc: '(148 - 174.8%) -- guaranteed OHKO'}, modern: {range: [344, 408], desc: '(148.9 - 176.6%) -- guaranteed OHKO'}, }, }, { weather: 'Rain', type: 'Water', damage: { adv: {range: [86, 102], desc: '(37.2 - 44.1%) -- guaranteed 3HKO'}, - dpp: {range: [42, 51], desc: '(18.1 - 22%) -- possible 5HKO'}, + dpp: {range: [85, 101], desc: '(36.7 - 43.7%) -- guaranteed 3HKO'}, modern: {range: [86, 102], desc: '(37.2 - 44.1%) -- guaranteed 3HKO'}, }, }, @@ -153,8 +153,8 @@ describe('calc', () => { desc: '(41.5 - 49.3%) -- 20.7% chance to 2HKO after sandstorm damage', }, dpp: { - range: [39, 46], - desc: '(16.8 - 19.9%) -- guaranteed 5HKO after sandstorm damage', + range: [77, 91], + desc: '(33.3 - 39.3%) -- guaranteed 3HKO after sandstorm damage', }, modern: { range: [77, 91], @@ -169,8 +169,8 @@ describe('calc', () => { desc: '(101.2 - 119.4%) -- guaranteed OHKO', }, dpp: { - range: [116, 138], - desc: '(50.2 - 59.7%) -- guaranteed 2HKO after hail damage', + range: [230, 272], + desc: '(99.5 - 117.7%) -- 93.8% chance to OHKO', }, modern: { range: [230, 272], @@ -376,6 +376,146 @@ describe('calc', () => { ); }); }); + + inGens(5, 9, ({gen, calculate, Pokemon, Move}) => { + test(`Multi-hit interaction with Multiscale (gen ${gen})`, () => { + const result = calculate( + Pokemon('Mamoswine'), + Pokemon('Dragonite', { + ability: 'Multiscale', + }), + Move('Icicle Spear'), + ); + expect(result.range()).toEqual([360, 430]); + expect(result.desc()).toBe( + '0 Atk Mamoswine Icicle Spear (3 hits) vs. 0 HP / 0 Def Multiscale Dragonite: 360-430 (111.4 - 133.1%) -- guaranteed OHKO' + ); + }); + }); + + inGens(5, 9, ({gen, calculate, Pokemon, Move}) => { + test(`Multi-hit interaction with Weak Armor (gen ${gen})`, () => { + let result = calculate( + Pokemon('Mamoswine'), + Pokemon('Skarmory', { + ability: 'Weak Armor', + }), + Move('Icicle Spear'), + ); + expect(result.range()).toEqual([115, 138]); + expect(result.desc()).toBe( + '0 Atk Mamoswine Icicle Spear (3 hits) vs. 0 HP / 0 Def Weak Armor Skarmory: 115-138 (42.4 - 50.9%) -- approx. 2.7% chance to 2HKO' + ); + + result = calculate( + Pokemon('Mamoswine'), + Pokemon('Skarmory', { + ability: 'Weak Armor', + item: 'White Herb', + }), + Move('Icicle Spear'), + ); + expect(result.range()).toEqual([89, 108]); + expect(result.desc()).toBe( + '0 Atk Mamoswine Icicle Spear (3 hits) vs. 0 HP / 0 Def White Herb Weak Armor Skarmory: 89-108 (32.8 - 39.8%) -- approx. 100% chance to 3HKO' + ); + + result = calculate( + Pokemon('Mamoswine'), + Pokemon('Skarmory', { + ability: 'Weak Armor', + item: 'White Herb', + boosts: {def: 2}, + }), + Move('Icicle Spear'), + ); + expect(result.range()).toEqual([56, 69]); + expect(result.desc()).toBe( + '0 Atk Mamoswine Icicle Spear (3 hits) vs. +2 0 HP / 0 Def Weak Armor Skarmory: 56-69 (20.6 - 25.4%) -- approx. 0% chance to 4HKO' + ); + + result = calculate( + Pokemon('Mamoswine', { + ability: 'Unaware', + }), + Pokemon('Skarmory', { + ability: 'Weak Armor', + item: 'White Herb', + boosts: {def: 2}, + }), + Move('Icicle Spear'), + ); + expect(result.range()).toEqual([75, 93]); + expect(result.desc()).toBe( + '0 Atk Unaware Mamoswine Icicle Spear (3 hits) vs. 0 HP / 0 Def Skarmory: 75-93 (27.6 - 34.3%) -- approx. 1.5% chance to 3HKO' + ); + }); + }); + + inGens(6, 9, ({gen, calculate, Pokemon, Move}) => { + test(`Multi-hit interaction with Mummy (gen ${gen})`, () => { + const result = calculate( + Pokemon('Pinsir-Mega'), + Pokemon('Cofagrigus', { + ability: 'Mummy', + }), + Move('Double Hit'), + ); + if (gen === 6) { + expect(result.range()).toEqual([96, 113]); + expect(result.desc()).toBe( + '0 Atk Aerilate Pinsir-Mega Double Hit (2 hits) vs. 0 HP / 0 Def Mummy Cofagrigus: 96-113 (37.3 - 43.9%) -- approx. 3HKO' + ); + } else { + expect(result.range()).toEqual([91, 107]); + expect(result.desc()).toBe( + '0 Atk Aerilate Pinsir-Mega Double Hit (2 hits) vs. 0 HP / 0 Def Mummy Cofagrigus: 91-107 (35.4 - 41.6%) -- approx. 3HKO' + ); + } + }); + }); + + inGens(7, 9, ({gen, calculate, Pokemon, Move}) => { + test(`Multi-hit interaction with Items (gen ${gen})`, () => { + let result = calculate( + Pokemon('Greninja'), + Pokemon('Gliscor', { + item: 'Luminous Moss', + }), + Move('Water Shuriken'), + ); + expect(result.range()).toEqual([104, 126]); + expect(result.desc()).toBe( + '0 SpA Greninja Water Shuriken (15 BP) (3 hits) vs. 0 HP / 0 SpD Luminous Moss Gliscor: 104-126 (35.7 - 43.2%) -- approx. 3HKO' + ); + + result = calculate( + Pokemon('Greninja'), + Pokemon('Gliscor', { + ability: 'Simple', + item: 'Luminous Moss', + }), + Move('Water Shuriken'), + ); + expect(result.range()).toEqual([92, 114]); + expect(result.desc()).toBe( + '0 SpA Greninja Water Shuriken (15 BP) (3 hits) vs. 0 HP / 0 SpD Luminous Moss Simple Gliscor: 92-114 (31.6 - 39.1%) -- approx. 79.4% chance to 3HKO' + ); + + result = calculate( + Pokemon('Greninja'), + Pokemon('Gliscor', { + ability: 'Contrary', + item: 'Luminous Moss', + }), + Move('Water Shuriken'), + ); + expect(result.range()).toEqual([176, 210]); + expect(result.desc()).toBe( + '0 SpA Greninja Water Shuriken (15 BP) (3 hits) vs. 0 HP / 0 SpD Luminous Moss Contrary Gliscor: 176-210 (60.4 - 72.1%) -- approx. 2HKO' + ); + }); + }); }); @@ -908,131 +1048,131 @@ describe('calc', () => { ); }); }); + }); - describe('Gen 9', () => { - inGen(9, ({calculate, Pokemon, Move, Field}) => { - test('Supreme Overlord', () => { - const kingambit = Pokemon('Kingambit', {level: 100, ability: 'Supreme Overlord', alliesFainted: 0}); - const aggron = Pokemon('Aggron', {level: 100}); - let result = calculate(kingambit, aggron, Move('Iron Head')); - expect(result.range()).toEqual([67, 79]); - expect(result.desc()).toBe( - '0 Atk Kingambit Iron Head vs. 0 HP / 0 Def Aggron: 67-79 (23.8 - 28.1%) -- 91.2% chance to 4HKO' - ); - kingambit.alliesFainted = 5; - result = calculate(kingambit, aggron, Move('Iron Head')); - expect(result.range()).toEqual([100, 118]); - expect(result.desc()).toBe( - '0 Atk Supreme Overlord 5 allies fainted Kingambit Iron Head vs. 0 HP / 0 Def Aggron: 100-118 (35.5 - 41.9%) -- guaranteed 3HKO' - ); - kingambit.alliesFainted = 10; - result = calculate(kingambit, aggron, Move('Iron Head')); - expect(result.range()).toEqual([100, 118]); - expect(result.desc()).toBe( - '0 Atk Supreme Overlord 5 allies fainted Kingambit Iron Head vs. 0 HP / 0 Def Aggron: 100-118 (35.5 - 41.9%) -- guaranteed 3HKO' - ); - }); - test('Electro Drift/Collision Course boost on Super Effective hits', () => { - const attacker = Pokemon('Arceus'); // same stats in each offense, does not get stab on fighting or electric - let defender = Pokemon('Mew'); // neutral to both - const calc = (move = Move('Electro Drift')) => calculate(attacker, defender, move).range(); - // 1x effectiveness should be identical to just using a 100 BP move - const neutral = calc(); - const fusionBolt = Move('Fusion Bolt'); - expect(calc(fusionBolt)).toEqual(neutral); - // 2x effectiveness - defender = Pokemon('Manaphy'); - const se = calc(); - // expect some sort of boost compared to the control - expect(calc(fusionBolt)).not.toEqual(se); - // tera should be able to revoke the boost - defender.teraType = 'Normal'; - expect(calc()).toEqual(neutral); - // check if secondary type resist is handled - const cc = Move('Collision Course'); // Fighting type - defender = Pokemon('Jirachi'); // Steel / Psychic is neutral to fighting, so no boost - expect(calc(cc)).toEqual(neutral); - // tera should cause the boost to be applied - defender.teraType = 'Normal'; - expect(calc(cc)).toEqual(se); - }); - function testQP(ability: string, field?: {weather?: Weather; terrain?: Terrain}) { - test(`${ability} should take into account boosted stats by default`, () => { - const attacker = Pokemon('Iron Leaves', {ability, boostedStat: 'auto', boosts: {spa: 6}}); - // highest stat = defense - const defender = Pokemon('Iron Treads', {ability, boostedStat: 'auto', boosts: {spd: 6}}); - - let result = calculate(attacker, defender, Move('Leaf Storm'), Field(field)).rawDesc; - expect(result.attackerAbility).toBe(ability); - expect(result.defenderAbility).toBe(ability); - - result = calculate(attacker, defender, Move('Psyblade'), Field(field)).rawDesc; - expect(result.attackerAbility).toBeUndefined(); - expect(result.defenderAbility).toBeUndefined(); - }); - } - function testQPOverride(ability: string, field?: {weather?: Weather; terrain?: Terrain}) { - test(`${ability} should be able to be overridden with boostedStat`, () => { - const attacker = Pokemon('Flutter Mane', {ability, boostedStat: 'atk', boosts: {spa: 6}}); - // highest stat = defense - const defender = Pokemon('Walking Wake', {ability, boostedStat: 'def', boosts: {spd: 6}}); - - let result = calculate(attacker, defender, Move('Leaf Storm'), Field(field)).rawDesc; - expect(result.attackerAbility).toBeUndefined(); - expect(result.defenderAbility).toBeUndefined(); - - result = calculate(attacker, defender, Move('Psyblade'), Field(field)).rawDesc; - expect(result.attackerAbility).toBe(ability); - expect(result.defenderAbility).toBe(ability); - }); - } - testQP('Quark Drive', {terrain: 'Electric'}); - testQP('Protosynthesis', {weather: 'Sun'}); - testQPOverride('Quark Drive', {terrain: 'Electric'}); - testQPOverride('Protosynthesis', {weather: 'Sun'}); - test('Meteor Beam/Electro Shot', () => { - const defender = Pokemon('Arceus'); - const testCase = (options: {[k: string]: any}, expected: number) => { - let result = calculate(Pokemon('Archaludon', options), defender, Move('Meteor Beam')); - expect(result.attacker.boosts.spa).toBe(expected); - result = calculate(Pokemon('Archaludon', options), defender, Move('Electro Shot')); - expect(result.attacker.boosts.spa).toBe(expected); - }; - testCase({}, 1); // raises by 1 - testCase({boosts: {spa: 6}}, 6); // caps at +6 - testCase({ability: 'Simple'}, 2); - testCase({ability: 'Contrary'}, -1); + describe('Gen 9', () => { + inGen(9, ({calculate, Pokemon, Move, Field}) => { + test('Supreme Overlord', () => { + const kingambit = Pokemon('Kingambit', {level: 100, ability: 'Supreme Overlord', alliesFainted: 0}); + const aggron = Pokemon('Aggron', {level: 100}); + let result = calculate(kingambit, aggron, Move('Iron Head')); + expect(result.range()).toEqual([67, 79]); + expect(result.desc()).toBe( + '0 Atk Kingambit Iron Head vs. 0 HP / 0 Def Aggron: 67-79 (23.8 - 28.1%) -- 91.2% chance to 4HKO' + ); + kingambit.alliesFainted = 5; + result = calculate(kingambit, aggron, Move('Iron Head')); + expect(result.range()).toEqual([100, 118]); + expect(result.desc()).toBe( + '0 Atk Supreme Overlord 5 allies fainted Kingambit Iron Head vs. 0 HP / 0 Def Aggron: 100-118 (35.5 - 41.9%) -- guaranteed 3HKO' + ); + kingambit.alliesFainted = 10; + result = calculate(kingambit, aggron, Move('Iron Head')); + expect(result.range()).toEqual([100, 118]); + expect(result.desc()).toBe( + '0 Atk Supreme Overlord 5 allies fainted Kingambit Iron Head vs. 0 HP / 0 Def Aggron: 100-118 (35.5 - 41.9%) -- guaranteed 3HKO' + ); + }); + test('Electro Drift/Collision Course boost on Super Effective hits', () => { + const attacker = Pokemon('Arceus'); // same stats in each offense, does not get stab on fighting or electric + let defender = Pokemon('Mew'); // neutral to both + const calc = (move = Move('Electro Drift')) => calculate(attacker, defender, move).range(); + // 1x effectiveness should be identical to just using a 100 BP move + const neutral = calc(); + const fusionBolt = Move('Fusion Bolt'); + expect(calc(fusionBolt)).toEqual(neutral); + // 2x effectiveness + defender = Pokemon('Manaphy'); + const se = calc(); + // expect some sort of boost compared to the control + expect(calc(fusionBolt)).not.toEqual(se); + // tera should be able to revoke the boost + defender.teraType = 'Normal'; + expect(calc()).toEqual(neutral); + // check if secondary type resist is handled + const cc = Move('Collision Course'); // Fighting type + defender = Pokemon('Jirachi'); // Steel / Psychic is neutral to fighting, so no boost + expect(calc(cc)).toEqual(neutral); + // tera should cause the boost to be applied + defender.teraType = 'Normal'; + expect(calc(cc)).toEqual(se); + }); + function testQP(ability: string, field?: {weather?: Weather; terrain?: Terrain}) { + test(`${ability} should take into account boosted stats by default`, () => { + const attacker = Pokemon('Iron Leaves', {ability, boostedStat: 'auto', boosts: {spa: 6}}); + // highest stat = defense + const defender = Pokemon('Iron Treads', {ability, boostedStat: 'auto', boosts: {spd: 6}}); + + let result = calculate(attacker, defender, Move('Leaf Storm'), Field(field)).rawDesc; + expect(result.attackerAbility).toBe(ability); + expect(result.defenderAbility).toBe(ability); + + result = calculate(attacker, defender, Move('Psyblade'), Field(field)).rawDesc; + expect(result.attackerAbility).toBeUndefined(); + expect(result.defenderAbility).toBeUndefined(); }); - test('Revelation Dance should change type if Pokemon Terastallized', () => { - const attacker = Pokemon('Oricorio-Pom-Pom'); - const defender = Pokemon('Sandaconda'); - let result = calculate(attacker, defender, Move('Revelation Dance')); - expect(result.move.type).toBe('Electric'); - - attacker.teraType = 'Water'; - result = calculate(attacker, defender, Move('Revelation Dance')); - expect(result.move.type).toBe('Water'); + } + function testQPOverride(ability: string, field?: {weather?: Weather; terrain?: Terrain}) { + test(`${ability} should be able to be overridden with boostedStat`, () => { + const attacker = Pokemon('Flutter Mane', {ability, boostedStat: 'atk', boosts: {spa: 6}}); + // highest stat = defense + const defender = Pokemon('Walking Wake', {ability, boostedStat: 'def', boosts: {spd: 6}}); + + let result = calculate(attacker, defender, Move('Leaf Storm'), Field(field)).rawDesc; + expect(result.attackerAbility).toBeUndefined(); + expect(result.defenderAbility).toBeUndefined(); + + result = calculate(attacker, defender, Move('Psyblade'), Field(field)).rawDesc; + expect(result.attackerAbility).toBe(ability); + expect(result.defenderAbility).toBe(ability); }); + } + testQP('Quark Drive', {terrain: 'Electric'}); + testQP('Protosynthesis', {weather: 'Sun'}); + testQPOverride('Quark Drive', {terrain: 'Electric'}); + testQPOverride('Protosynthesis', {weather: 'Sun'}); + test('Meteor Beam/Electro Shot', () => { + const defender = Pokemon('Arceus'); + const testCase = (options: {[k: string]: any}, expected: number) => { + let result = calculate(Pokemon('Archaludon', options), defender, Move('Meteor Beam')); + expect(result.attacker.boosts.spa).toBe(expected); + result = calculate(Pokemon('Archaludon', options), defender, Move('Electro Shot')); + expect(result.attacker.boosts.spa).toBe(expected); + }; + testCase({}, 1); // raises by 1 + testCase({boosts: {spa: 6}}, 6); // caps at +6 + testCase({ability: 'Simple'}, 2); + testCase({ability: 'Contrary'}, -1); + }); + test('Revelation Dance should change type if Pokemon Terastallized', () => { + const attacker = Pokemon('Oricorio-Pom-Pom'); + const defender = Pokemon('Sandaconda'); + let result = calculate(attacker, defender, Move('Revelation Dance')); + expect(result.move.type).toBe('Electric'); + + attacker.teraType = 'Water'; + result = calculate(attacker, defender, Move('Revelation Dance')); + expect(result.move.type).toBe('Water'); + }); - test('Flower Gift, Power Spot, Battery, and switching boosts shouldn\'t have double spaces', () => { - const attacker = Pokemon('Weavile'); - const defender = Pokemon('Vulpix'); - const field = Field({ - weather: 'Sun', - attackerSide: { - isFlowerGift: true, - isPowerSpot: true, - }, - defenderSide: { - isSwitching: 'out', - }, - }); - const result = calculate(attacker, defender, Move('Pursuit'), field); - - expect(result.desc()).toBe( - "0 Atk Weavile with an ally's Flower Gift Power Spot boosted switching boosted Pursuit (80 BP) vs. 0 HP / 0 Def Vulpix in Sun: 399-469 (183.8 - 216.1%) -- guaranteed OHKO" - ); + test('Flower Gift, Power Spot, Battery, and switching boosts shouldn\'t have double spaces', () => { + const attacker = Pokemon('Weavile'); + const defender = Pokemon('Vulpix'); + const field = Field({ + weather: 'Sun', + attackerSide: { + isFlowerGift: true, + isPowerSpot: true, + }, + defenderSide: { + isSwitching: 'out', + }, }); + const result = calculate(attacker, defender, Move('Pursuit'), field); + + expect(result.desc()).toBe( + "0 Atk Weavile with an ally's Flower Gift Power Spot boosted switching boosted Pursuit (80 BP) vs. 0 HP / 0 Def Vulpix in Sun: 399-469 (183.8 - 216.1%) -- guaranteed OHKO" + ); }); }); });