Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Approx multihit percentages #670

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 75 additions & 35 deletions calc/src/desc.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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());
Expand All @@ -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());
Expand All @@ -110,24 +107,35 @@ 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 = '';

const ignoresShellBell =
gen.num === 3 && move.named('Doom Desire', 'Future Sight');
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);
recovery[0] += minD[i] > 0 ? Math.max(Math.round(minD[i] / 8), 1) : 0;
recovery[1] += maxD[i] > 0 ? Math.max(Math.round(maxD[i] / 8), 1) : 0;
}
// This is incorrect if the opponent heals during your damage
// Ex: Sitrus Berry procs in the middle of multi-hit move
const maxHealing = Math.round(defender.curHP() / 8);
recovery[0] = Math.min(recovery[0], maxHealing);
recovery[1] = Math.min(recovery[1], maxHealing);
}

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')) {
Expand All @@ -136,14 +144,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);
}
}
}
Expand All @@ -167,14 +180,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;
Expand Down Expand Up @@ -286,7 +297,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)
Expand Down Expand Up @@ -329,7 +340,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;
Expand All @@ -354,7 +365,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) {
Expand Down Expand Up @@ -450,24 +461,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 = [
Expand Down
44 changes: 22 additions & 22 deletions calc/src/mechanics/gen12.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
19 changes: 11 additions & 8 deletions calc/src/mechanics/gen3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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]);
Expand All @@ -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;
}
Expand Down
14 changes: 8 additions & 6 deletions calc/src/mechanics/gen4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand All @@ -308,21 +309,22 @@ 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);
newFinalDamage = Math.floor(newFinalDamage * filterMod);
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;
}
Expand Down
Loading