diff --git a/calc/src/desc.ts b/calc/src/desc.ts index bd8be6859..75ce35f2e 100644 --- a/calc/src/desc.ts +++ b/calc/src/desc.ts @@ -1,8 +1,9 @@ -import type {Generation, Weather, Terrain, TypeName, ID, AbilityName} from './data/interface'; +/* eslint-disable @typescript-eslint/prefer-for-of */ +import type {Generation, Weather, Terrain, TypeName, ID} from './data/interface'; import type {Field, Side} from './field'; import type {Move} from './move'; import type {Pokemon} from './pokemon'; -import {type Damage, damageRange} from './result'; +import {type Damage, damageRange, multiDamageRange} from './result'; import {error} from './util'; // NOTE: This needs to come last to simplify bundling import {isGrounded} from './mechanics/util'; @@ -64,9 +65,7 @@ export function display( notation = '%', err = true ) { - const [minDamage, maxDamage] = damageRange(damage); - const min = (typeof minDamage === 'number' ? minDamage : minDamage[0] + minDamage[1]); - const max = (typeof maxDamage === 'number' ? maxDamage : maxDamage[0] + maxDamage[1]); + const [min, max] = damageRange(damage); const minDisplay = toDisplay(notation, min, defender.maxHP()); const maxDisplay = toDisplay(notation, max, defender.maxHP()); @@ -87,9 +86,7 @@ export function displayMove( damage: Damage, notation = '%' ) { - const [minDamage, maxDamage] = damageRange(damage); - const min = (typeof minDamage === 'number' ? minDamage : minDamage[0] + minDamage[1]); - const max = (typeof maxDamage === 'number' ? maxDamage : maxDamage[0] + maxDamage[1]); + const [min, max] = damageRange(damage); const minDisplay = toDisplay(notation, min, defender.maxHP()); const maxDisplay = toDisplay(notation, max, defender.maxHP()); @@ -110,8 +107,14 @@ export function getRecovery( notation = '%' ) { const [minDamage, maxDamage] = damageRange(damage); - const minD = typeof minDamage === 'number' ? [minDamage] : minDamage; - const maxD = typeof maxDamage === 'number' ? [maxDamage] : maxDamage; + let minD; + let maxD; + if (move.timesUsed && move.timesUsed > 1) { + [minD, maxD] = multiDamageRange(damage) as [number[], number[]]; + } else { + minD = [minDamage]; + maxD = [maxDamage]; + } const recovery = [0, 0] as [number, number]; let text = ''; @@ -121,13 +124,16 @@ export function getRecovery( if (attacker.hasItem('Shell Bell') && !ignoresShellBell) { const max = Math.round(defender.maxHP() / 8); for (let i = 0; i < minD.length; i++) { - recovery[0] += Math.min(Math.round(minD[i] * move.hits / 8), max); - recovery[1] += Math.min(Math.round(maxD[i] * move.hits / 8), max); + const minHealed = minD[i] > 0 ? Math.max(Math.round(minD[i] * move.hits / 8), 1) : 0; + const maxHealed = maxD[i] > 0 ? Math.max(Math.round(maxD[i] * move.hits / 8), 1) : 0; + recovery[0] = Math.min(minHealed + recovery[0], max); + recovery[1] = Math.min(maxHealed + recovery[1], max); } } if (move.named('G-Max Finale')) { - recovery[0] = recovery[1] = Math.round(attacker.maxHP() / 6); + recovery[0] += Math.round(attacker.maxHP() / 6); + recovery[1] += Math.round(attacker.maxHP() / 6); } if (move.named('Pain Split')) { @@ -136,14 +142,19 @@ export function getRecovery( } if (move.drain) { + // Parental Bond counts as multiple heals for drain moves, but not for Shell Bell + // Currently no drain moves are multihit, however this covers for it. + if (attacker.hasAbility('Parental Bond') || move.hits > 1) { + [minD, maxD] = multiDamageRange(damage) as [number[], number[]]; + } const percentHealed = move.drain[0] / move.drain[1]; - const max = Math.round(defender.maxHP() * percentHealed); + const max = Math.round(defender.curHP() * percentHealed); for (let i = 0; i < minD.length; i++) { const range = [minD[i], maxD[i]]; for (const j in recovery) { - let drained = Math.round(range[j] * percentHealed); + let drained = Math.max(Math.round(range[j] * percentHealed), 1); if (attacker.hasItem('Big Root')) drained = Math.trunc(drained * 5324 / 4096); - recovery[j] += Math.min(drained * move.hits, max); + recovery[j] += Math.min(drained, max); } } } @@ -167,14 +178,12 @@ export function getRecoil( damage: Damage, notation = '%' ) { - const [minDamage, maxDamage] = damageRange(damage); - const min = (typeof minDamage === 'number' ? minDamage : minDamage[0] + minDamage[1]) * move.hits; - const max = (typeof maxDamage === 'number' ? maxDamage : maxDamage[0] + maxDamage[1]) * move.hits; + const [min, max] = damageRange(damage); let recoil: [number, number] | number = [0, 0]; let text = ''; - const damageOverflow = minDamage > defender.curHP() || maxDamage > defender.curHP(); + const damageOverflow = min > defender.curHP() || max > defender.curHP(); if (move.recoil) { const mod = (move.recoil[0] / move.recoil[1]) * 100; let minRecoilDamage, maxRecoilDamage; @@ -286,7 +295,7 @@ export function getKOChance( // multi-hit moves have too many possibilities for brute-forcing to work, so reduce it // to an approximate distribution - let qualifier = move.hits > 1 ? 'approx. ' : ''; + let qualifier = move.hits > 2 ? 'approx. ' : ''; const hazardsText = hazards.texts.length > 0 ? ' after ' + serializeText(hazards.texts) @@ -329,7 +338,7 @@ export function getKOChance( // if the move OHKOing is guaranteed even without end of turn damage } else if (chanceWithoutEot === 1) { chance = chanceWithoutEot; - if (qualifier === '') text += 'guaranteed '; + text = 'guaranteed '; text += `OHKO${hazardsText}`; } else if (chanceWithoutEot > 0) { chance = chanceWithEot; @@ -354,7 +363,7 @@ export function getKOChance( chance = chanceWithEot; // if the move KOing is not possible, but eot damage guarantees the OHKO if (chanceWithEot === 1) { - if (qualifier === '') text += 'guaranteed '; + text = 'guaranteed '; text += `${KOTurnText}${afterText}`; // if the move KOing is not possible, but eot damage might KO } else if (chanceWithEot > 0) { @@ -450,24 +459,53 @@ export function getKOChance( function combine(damage: Damage) { // Fixed Damage if (typeof damage === 'number') return [damage]; + // Standard Damage - if (damage.length > 2) { - if (damage[0] > damage[damage.length - 1]) damage = damage.slice().sort() as number[]; - return damage as number[]; + if (damage.length > 2 && typeof damage[0] === 'number') { + damage = damage as number[]; + if (damage[0] > damage[damage.length - 1]) damage = damage.slice().sort(); + return damage; } - // Fixed Parental Bond Damage + // Fixed Multi-hit Damage if (typeof damage[0] === 'number' && typeof damage[1] === 'number') { return [damage[0] + damage[1]]; } - // Parental Bond Damage - const d = damage as [number[], number[]]; - const combined = []; - for (let i = 0; i < d[0].length; i++) { // eslint-disable-line - for (let j = 0; j < d[1].length; j++) { // eslint-disable-line - combined.push(d[0][i] + d[1][j]); + // Multi-hit Damage + + // Reduce Distribution to be at most 256 elements, maintains min and max + function reduce(dist: number[]): number[] { + const MAX_LENGTH = 256; + if (dist.length <= MAX_LENGTH) { + return dist; } + const reduced = []; + reduced[0] = dist[0]; + reduced[MAX_LENGTH - 1] = dist[dist.length - 1]; + const scaleValue = dist.length / MAX_LENGTH; // Should always be 16 + for (let i = 1; i < MAX_LENGTH - 1; i++) { + reduced[i] = dist[Math.round(i * scaleValue + scaleValue / 2)]; + } + return reduced; + } + + function combineTwo(dist1: number[], dist2: number[]): number[] { + const combined = dist1.flatMap(val1 => dist2.map(val2 => val1 + val2)).sort((a, b) => a - b); + return combined; } - return combined.sort(); + + // Combine n distributions to return an approximation of sum with <= 256 elements + // Accurate for <= 2 hits, should be within 1% otherwise + function combineDistributions(dists: number[][]): number[] { + let combined = [0]; + for (let i = 0; i < dists.length; i++) { + combined = combineTwo(combined, dists[i]); + combined = reduce(combined); + } + return combined; + } + + const d = damage as number[][]; + return combineDistributions(d); } const TRAPPING = [ diff --git a/calc/src/mechanics/gen12.ts b/calc/src/mechanics/gen12.ts index 325bbc171..77234ef76 100644 --- a/calc/src/mechanics/gen12.ts +++ b/calc/src/mechanics/gen12.ts @@ -255,42 +255,42 @@ export function calculateRBYGSC( return result; } - result.damage = []; + const damage = []; for (let i = 217; i <= 255; i++) { if (gen.num === 2) { // in gen 2 damage is always rounded up to 1. TODO ADD TESTS - result.damage[i - 217] = Math.max(1, Math.floor((baseDamage * i) / 255)); + damage[i - 217] = Math.max(1, Math.floor((baseDamage * i) / 255)); } else { if (baseDamage === 1) { // in gen 1 the random factor multiplication is skipped if damage = 1 - result.damage[i - 217] = 1; + damage[i - 217] = 1; } else { - result.damage[i - 217] = Math.floor((baseDamage * i) / 255); + damage[i - 217] = Math.floor((baseDamage * i) / 255); } } } + result.damage = damage; if (move.hits > 1) { - for (let times = 0; times < move.hits; times++) { - let damageMultiplier = 217; - result.damage = result.damage.map(affectedAmount => { - if (times) { - let newFinalDamage = 0; - // in gen 2 damage is always rounded up to 1. TODO ADD TESTS - if (gen.num === 2) { - newFinalDamage = Math.max(1, Math.floor((baseDamage * damageMultiplier) / 255)); + const damageMatrix = [damage]; + for (let times = 1; times < move.hits; times++) { + const damage = []; + for (let damageMultiplier = 217; damageMultiplier <= 255; damageMultiplier++) { + let newFinalDamage = 0; + // in gen 2 damage is always rounded up to 1. TODO ADD TESTS + if (gen.num === 2) { + newFinalDamage = Math.max(1, Math.floor((baseDamage * damageMultiplier) / 255)); + } else { + // in gen 1 the random factor multiplication is skipped if damage = 1 + if (baseDamage === 1) { + newFinalDamage = 1; } else { - // in gen 1 the random factor multiplication is skipped if damage = 1 - if (baseDamage === 1) { - newFinalDamage = 1; - } else { - newFinalDamage = Math.floor((baseDamage * damageMultiplier) / 255); - } + newFinalDamage = Math.floor((baseDamage * damageMultiplier) / 255); } - damageMultiplier++; - return affectedAmount + newFinalDamage; } - return affectedAmount; - }); + damage[damageMultiplier - 217] = newFinalDamage; + } + damageMatrix[times] = damage; } + result.damage = damageMatrix; } return result; diff --git a/calc/src/mechanics/gen3.ts b/calc/src/mechanics/gen3.ts index b2d42f4a2..be4f342ad 100644 --- a/calc/src/mechanics/gen3.ts +++ b/calc/src/mechanics/gen3.ts @@ -160,10 +160,11 @@ export function calculateADV( baseDamage = calculateFinalModsADV(baseDamage, attacker, move, field, desc, isCritical); baseDamage = Math.floor(baseDamage * typeEffectiveness); - result.damage = []; + const damage = []; for (let i = 85; i <= 100; i++) { - result.damage[i - 85] = Math.max(1, Math.floor((baseDamage * i) / 100)); + damage[i - 85] = Math.max(1, Math.floor((baseDamage * i) / 100)); } + result.damage = damage; if ((move.dropsStats && move.timesUsed! > 1) || move.hits > 1) { // store boosts so intermediate boosts don't show. @@ -177,6 +178,7 @@ export function calculateADV( numAttacks = move.hits; } let usedItems = [false, false]; + const damageMatrix = [damage]; for (let times = 1; times < numAttacks; times++) { usedItems = checkMultihitBoost(gen, attacker, defender, move, field, desc, usedItems[0], usedItems[1]); @@ -189,13 +191,14 @@ export function calculateADV( 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; - }); + const damage = []; + for (let i = 85; i <= 100; i++) { + const newFinalDamage = Math.max(1, Math.floor((newBaseDmg * i) / 100)); + damage[i - 85] = newFinalDamage; + } + damageMatrix[times] = damage; } + result.damage = damageMatrix; desc.defenseBoost = origDefBoost; desc.attackBoost = origAtkBoost; } diff --git a/calc/src/mechanics/gen4.ts b/calc/src/mechanics/gen4.ts index f8b6d3318..d9a6d0eb9 100644 --- a/calc/src/mechanics/gen4.ts +++ b/calc/src/mechanics/gen4.ts @@ -291,6 +291,7 @@ export function calculateDPP( numAttacks = move.hits; } let usedItems = [false, false]; + const damageMatrix = [damage]; for (let times = 1; times < numAttacks; times++) { usedItems = checkMultihitBoost(gen, attacker, defender, move, field, desc, usedItems[0], usedItems[1]); @@ -308,10 +309,10 @@ export function calculateDPP( } baseDamage = calculateFinalModsDPP(baseDamage, attacker, move, field, desc, isCritical); - let damageMultiplier = 0; - result.damage = result.damage.map(affectedAmount => { + const damageArray = []; + for (let i = 0; i < 16; i++) { let newFinalDamage = 0; - newFinalDamage = Math.floor((baseDamage * (85 + damageMultiplier)) / 100); + newFinalDamage = Math.floor((baseDamage * (85 + i)) / 100); newFinalDamage = Math.floor(newFinalDamage * stabMod); newFinalDamage = Math.floor(newFinalDamage * type1Effectiveness); newFinalDamage = Math.floor(newFinalDamage * type2Effectiveness); @@ -319,10 +320,11 @@ export function calculateDPP( newFinalDamage = Math.floor(newFinalDamage * ebeltMod); newFinalDamage = Math.floor(newFinalDamage * tintedMod); newFinalDamage = Math.max(1, newFinalDamage); - damageMultiplier++; - return affectedAmount + newFinalDamage; - }); + damageArray[i] = newFinalDamage; + } + damageMatrix[times] = damageArray; } + result.damage = damageMatrix; desc.defenseBoost = origDefBoost; desc.attackBoost = origAtkBoost; } diff --git a/calc/src/mechanics/gen56.ts b/calc/src/mechanics/gen56.ts index 8000d4823..f71e60ef0 100644 --- a/calc/src/mechanics/gen56.ts +++ b/calc/src/mechanics/gen56.ts @@ -362,16 +362,18 @@ export function calculateBWXY( desc.attackerAbility = attacker.ability; } - let damage: number[] = []; + const damage: number[] = []; for (let i = 0; i < 16; i++) { damage[i] = getFinalDamage(baseDamage, i, typeEffectiveness, applyBurn, stabMod, finalMod); } + result.damage = childDamage ? [damage, childDamage] : damage; desc.attackBoost = move.named('Foul Play') ? defender.boosts[attackStat] : attacker.boosts[attackStat]; if ((move.dropsStats && move.timesUsed! > 1) || move.hits > 1) { + const damageMatrix = [damage]; // store boosts so intermediate boosts don't show. const origDefBoost = desc.defenseBoost; const origAtkBoost = desc.attackBoost; @@ -421,26 +423,25 @@ export function calculateBWXY( ); const newFinalMod = chainMods(newFinalMods, 41, 131072); - let damageMultiplier = 0; - damage = damage.map(affectedAmount => { + const damageArray = []; + for (let i = 0; i < 16; i++) { const newFinalDamage = getFinalDamage( newBaseDamage, - damageMultiplier, + i, typeEffectiveness, applyBurn, stabMod, newFinalMod ); - damageMultiplier++; - return affectedAmount + newFinalDamage; - }); + damageArray[i] = newFinalDamage; + } + damageMatrix[times] = damageArray; } + result.damage = damageMatrix; desc.defenseBoost = origDefBoost; desc.attackBoost = origAtkBoost; } - result.damage = childDamage ? [damage, childDamage] : damage; - // #endregion return result; diff --git a/calc/src/mechanics/gen789.ts b/calc/src/mechanics/gen789.ts index 6c063f690..06cf07e25 100644 --- a/calc/src/mechanics/gen789.ts +++ b/calc/src/mechanics/gen789.ts @@ -667,11 +667,12 @@ export function calculateSMSSSV( desc.attackerAbility = attacker.ability; } - let damage = []; + const damage = []; for (let i = 0; i < 16; i++) { damage[i] = getFinalDamage(baseDamage, i, typeEffectiveness, applyBurn, stabMod, finalMod, protect); } + result.damage = childDamage ? [damage, childDamage] : damage; desc.attackBoost = move.named('Foul Play') ? defender.boosts[attackStat] : attacker.boosts[attackStat]; @@ -689,6 +690,7 @@ export function calculateSMSSSV( numAttacks = move.hits; } let usedItems = [false, false]; + const damageMatrix = [damage]; for (let times = 1; times < numAttacks; times++) { usedItems = checkMultihitBoost(gen, attacker, defender, move, field, desc, usedItems[0], usedItems[1]); @@ -745,26 +747,26 @@ export function calculateSMSSSV( ); const newFinalMod = chainMods(newFinalMods, 41, 131072); - let damageMultiplier = 0; - damage = damage.map(affectedAmount => { + const damageArray = []; + for (let i = 0; i < 16; i++) { const newFinalDamage = getFinalDamage( newBaseDamage, - damageMultiplier, + i, typeEffectiveness, applyBurn, stabMod, newFinalMod, protect ); - damageMultiplier++; - return affectedAmount + newFinalDamage; - }); + damageArray[i] = newFinalDamage; + } + damageMatrix[times] = damageArray; } + result.damage = damageMatrix; desc.defenseBoost = origDefBoost; desc.attackBoost = origAtkBoost; } - result.damage = childDamage ? [damage, childDamage] : damage; // #endregion diff --git a/calc/src/result.ts b/calc/src/result.ts index b895bb159..5dd545ec9 100644 --- a/calc/src/result.ts +++ b/calc/src/result.ts @@ -4,7 +4,7 @@ import type {Field} from './field'; import type {Move} from './move'; import type {Pokemon} from './pokemon'; -export type Damage = number | number[] | [number, number] | [number[], number[]]; +export type Damage = number | number[] | [number, number] | number[][]; export class Result { gen: Generation; @@ -12,7 +12,7 @@ export class Result { defender: Pokemon; move: Move; field: Field; - damage: number | number[] | [number[], number[]]; + damage: number | number[] | number[][]; rawDesc: RawDesc; constructor( @@ -38,10 +38,8 @@ export class Result { } range(): [number, number] { - const range = damageRange(this.damage); - if (typeof range[0] === 'number') return range as [number, number]; - const d = range as [number[], number[]]; - return [d[0][0] + d[0][1], d[1][0] + d[1][1]]; + const [min, max] = damageRange(this.damage); + return [min, max]; } fullDesc(notation = '%', err = true) { @@ -83,24 +81,38 @@ export class Result { } } -export function damageRange( +export function damageRange(damage: Damage): [number, number] { + const range = multiDamageRange(damage); + if (typeof range[0] === 'number') return range as [number, number]; + const d = range as [number[], number[]]; + const summedRange: [number, number] = [0, 0]; + for (let i = 0; i < d[0].length; i++) { + summedRange[0] += d[0][i]; + summedRange[1] += d[1][i]; + } + return summedRange; +} + +export function multiDamageRange( damage: Damage -): [number, number] | [[number, number], [number, number]] { +): [number, number] | [number[], number[]] { // Fixed Damage if (typeof damage === 'number') return [damage, damage]; - // Standard Damage - if (damage.length > 2) { - const d = damage as number[]; - if (d[0] > d[d.length - 1]) return [Math.min(...d), Math.max(...d)]; - return [d[0], d[d.length - 1]]; + // Multihit Damage + if (typeof damage[0] !== 'number') { + damage = damage as number[][]; + const ranges: [number[], number[]] = [[], []]; + for (const damageList of damage) { + ranges[0].push(damageList[0]); + ranges[1].push(damageList[damageList.length - 1]); + } + return ranges; } - // Fixed Parental Bond Damage - if (typeof damage[0] === 'number' && typeof damage[1] === 'number') { - return [[damage[0], damage[1]], [damage[0], damage[1]]]; + const d = damage as number[]; + // Fixed Multihit + if (d.length < 16) { + return [d, d]; } - // Parental Bond Damage - const d = damage as [number[], number[]]; - if (d[0][0] > d[0][d[0].length - 1]) d[0] = d[0].slice().sort(); - if (d[1][0] > d[1][d[1].length - 1]) d[1] = d[1].slice().sort(); - return [[d[0][0], d[1][0]], [d[0][d[0].length - 1], d[1][d[1].length - 1]]]; + // Standard Damage + return [d[0], d[d.length - 1]]; } diff --git a/calc/src/test/calc.test.ts b/calc/src/test/calc.test.ts index 16efa7235..090e1bd2f 100644 --- a/calc/src/test/calc.test.ts +++ b/calc/src/test/calc.test.ts @@ -44,9 +44,9 @@ describe('calc', () => { tests('Comet Punch', ({gen, calculate, Pokemon, Move}) => { expect(calculate(Pokemon('Snorlax'), Pokemon('Vulpix'), Move('Comet Punch'))).toMatch(gen, { - 1: {range: [108, 129], desc: 'Snorlax Comet Punch (3 hits) vs. Vulpix', result: '(38.7 - 46.2%) -- approx. 3HKO'}, - 3: {range: [132, 156], desc: '0 Atk Snorlax Comet Punch (3 hits) vs. 0 HP / 0 Def Vulpix', result: '(60.8 - 71.8%) -- approx. 2HKO'}, - 4: {range: [129, 156], result: '(59.4 - 71.8%) -- approx. 2HKO'}, + 1: {range: [108, 129], desc: 'Snorlax Comet Punch (3 hits) vs. Vulpix', result: '(38.7 - 46.2%) -- guaranteed 3HKO'}, + 3: {range: [132, 156], desc: '0 Atk Snorlax Comet Punch (3 hits) vs. 0 HP / 0 Def Vulpix', result: '(60.8 - 71.8%) -- guaranteed 2HKO'}, + 4: {range: [129, 156], result: '(59.4 - 71.8%) -- guaranteed 2HKO'}, }); }); @@ -424,7 +424,7 @@ describe('calc', () => { [76, 76, 78, 78, 79, 81, 81, 82, 82, 84, 85, 85, 87, 87, 88, 90], ]); expect(result.desc()).toBe( - '152 Atk Parental Bond Kangaskhan-Mega Frustration vs. 252 HP / 152+ Def Amoonguss: 229-270 (53 - 62.5%) -- approx. 2HKO' + '152 Atk Parental Bond Kangaskhan-Mega Frustration vs. 252 HP / 152+ Def Amoonguss: 229-270 (53 - 62.5%) -- guaranteed 2HKO' ); } else { expect(result.damage).toEqual([ @@ -474,7 +474,7 @@ describe('calc', () => { [92, 96, 96, 96, 96, 100, 100, 100, 104, 104, 104, 104, 108, 108, 108, 112], ]); expect(result.desc()).toBe( - '252 Atk Parental Bond Kangaskhan-Mega Crunch vs. 0 HP / 0 Def Shadow Shield Lunala: 280-334 (67.4 - 80.4%) -- approx. 2HKO' + '252 Atk Parental Bond Kangaskhan-Mega Crunch vs. 0 HP / 0 Def Shadow Shield Lunala: 280-334 (67.4 - 80.4%) -- guaranteed 2HKO' ); }); }); @@ -496,6 +496,41 @@ describe('calc', () => { }); }); + inGens(1, 9, ({gen, calculate, Pokemon, Move}) => { + test(`Multi-hit percentage kill (gen ${gen})`, () => { + if (gen < 3) { + const result = calculate( + Pokemon('Persian', {boosts: {atk: 4}}), + Pokemon('Abra'), + Move('Fury Swipes', {hits: 2}), + ); + expect(result.range()).toEqual([218, 258]); + expect(result.desc()).toBe( + '+4 Persian Fury Swipes (2 hits) vs. Abra: 218-258 (86.1 - 101.9%) -- 2.7% chance to OHKO' + ); + } else if (gen === 3) { + const result = calculate( + Pokemon('Persian', {boosts: {atk: 3}}), + Pokemon('Abra', {boosts: {def: 1}}), + Move('Fury Swipes', {hits: 2}), + ); + expect(result.range()).toEqual([174, 206]); + expect(result.desc()).toBe( + '+3 0 Atk Persian Fury Swipes (2 hits) vs. +1 0 HP / 0 Def Abra: 174-206 (91 - 107.8%) -- 41.8% chance to OHKO' + ); + } else { + const result = calculate( + Pokemon('Persian', {boosts: {atk: 3}}), + Pokemon('Abra', {boosts: {def: 1}}), + Move('Fury Swipes', {hits: 2}), + ); + expect(result.range()).toEqual([174, 206]); + expect(result.desc()).toBe( + '+3 0 Atk Persian Fury Swipes (2 hits) vs. +1 0 HP / 0 Def Abra: 174-206 (91 - 107.8%) -- 43.8% chance to OHKO' + ); + } + }); + }); inGens(5, 9, ({gen, calculate, Pokemon, Move}) => { test(`Multi-hit interaction with Multiscale (gen ${gen})`, () => { const result = calculate( @@ -522,9 +557,6 @@ describe('calc', () => { 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'), @@ -535,9 +567,6 @@ describe('calc', () => { 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. 99.9% chance to 3HKO' - ); result = calculate( Pokemon('Mamoswine'), @@ -549,9 +578,6 @@ describe('calc', () => { 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.1% chance to 4HKO' - ); result = calculate( Pokemon('Mamoswine', { @@ -565,9 +591,6 @@ describe('calc', () => { 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' - ); }); }); @@ -583,12 +606,12 @@ describe('calc', () => { 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' + '0 Atk Aerilate Pinsir-Mega Double Hit (2 hits) vs. 0 HP / 0 Def Mummy Cofagrigus: 96-113 (37.3 - 43.9%) -- guaranteed 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' + '0 Atk Aerilate Pinsir-Mega Double Hit (2 hits) vs. 0 HP / 0 Def Mummy Cofagrigus: 91-107 (35.4 - 41.6%) -- guaranteed 3HKO' ); } }); @@ -605,7 +628,7 @@ describe('calc', () => { ); 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' + '0 SpA Greninja Water Shuriken (15 BP) (3 hits) vs. 0 HP / 0 SpD Luminous Moss Gliscor: 104-126 (35.7 - 43.2%) -- guaranteed 3HKO' ); result = calculate( @@ -617,9 +640,6 @@ describe('calc', () => { 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'), @@ -631,7 +651,7 @@ describe('calc', () => { ); 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' + '0 SpA Greninja Water Shuriken (15 BP) (3 hits) vs. 0 HP / 0 SpD Luminous Moss Contrary Gliscor: 176-210 (60.4 - 72.1%) -- guaranteed 2HKO' ); }); }); diff --git a/src/index.template.html b/src/index.template.html index c832aa94b..5fee81fd9 100644 --- a/src/index.template.html +++ b/src/index.template.html @@ -138,7 +138,11 @@ Loading...
Copied
-
(If you see this message for more than a few seconds, try enabling JavaScript.) +
+
+ (If you see this message for more than a few seconds, try enabling JavaScript.) +

+
diff --git a/src/js/index_randoms_controls.js b/src/js/index_randoms_controls.js index 127194d6a..72b9075cb 100644 --- a/src/js/index_randoms_controls.js +++ b/src/js/index_randoms_controls.js @@ -108,22 +108,54 @@ $(".result-move").change(function () { var desc = result.fullDesc(notation, false); if (desc.indexOf('--') === -1) desc += ' -- possibly the worst move ever'; $("#mainResult").text(desc); - $("#damageValues").text("Possible damage amounts: (" + displayDamageHits(result.damage) + ")"); + var summary = displayDamageHits(result.damage); + var rest = ""; + var newLine = summary.indexOf('\n'); + if (newLine > -1) { + rest = summary.substring(newLine + 1); + summary = summary.substring(0, newLine); + } + $("#firstDmgValues").text("Possible damage amounts: (" + summary + ")"); + $("#restDmgValues").text(rest); } } }); function displayDamageHits(damage) { // Fixed Damage - if (typeof damage === 'number') return damage; + if (typeof damage === 'number') return damage.toString(); // Standard Damage - if (damage.length > 2) return damage.join(', '); + if (damage.length > 2 && typeof damage[0] === 'number') + return damage.join(', '); // Fixed Parental Bond Damage if (typeof damage[0] === 'number' && typeof damage[1] === 'number') { return '1st Hit: ' + damage[0] + '; 2nd Hit: ' + damage[1]; } - // Parental Bond Damage - return '1st Hit: ' + damage[0].join(', ') + '; 2nd Hit: ' + damage[1].join(', '); + // Multihit Damage + var fullText = ""; + for (var i = 1; i <= damage.length; i++) { + var txt = toOrdinal(i) + " Hit: " + damage[i - 1].join(', '); + if (i > 1) txt += "; "; + fullText += txt; + if (i % 2 == 1 && i < damage.length) fullText += "\n"; + } + return fullText; +} + +function toOrdinal(num) { + if (typeof num !== "number" || !Number.isInteger(num)) { + return "Input must be an integer."; + } + switch (num) { + case 1: + return num + "st"; + case 2: + return num + "nd"; + case 3: + return num + "rd"; + default: + return num + "th"; + } } function findDamageResult(resultMoveObj) { diff --git a/src/oms.template.html b/src/oms.template.html index 5c57c0e3b..2b3d95ed7 100644 --- a/src/oms.template.html +++ b/src/oms.template.html @@ -138,7 +138,11 @@ Loading...
Copied
-
(If you see this message for more than a few seconds, try enabling JavaScript.) +
+
+ (If you see this message for more than a few seconds, try enabling JavaScript.) +

+
diff --git a/src/randoms.template.html b/src/randoms.template.html index 18517a2d4..7c465c0ce 100644 --- a/src/randoms.template.html +++ b/src/randoms.template.html @@ -138,7 +138,12 @@ Loading...
Copied
-
(If you see this message for more than a few seconds, try enabling JavaScript.) +
+
+ (If you see this message for more than a few seconds, try enabling JavaScript.) +

+
+