Skip to content

Commit

Permalink
Implement multihit moves properly
Browse files Browse the repository at this point in the history
Credits to longhiep341 & pacmanboss256 for coming up with cases where
the damage of multihit moves differ from hit x to hit y.
  • Loading branch information
thejetou committed Sep 22, 2023
1 parent ec49f1c commit d34d249
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 83 deletions.
18 changes: 7 additions & 11 deletions calc/src/desc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ export function display(
err = true
) {
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 = (typeof minDamage === 'number' ? minDamage : minDamage[0] + minDamage[1]);
const max = (typeof maxDamage === 'number' ? maxDamage : maxDamage[0] + maxDamage[1]);

const minDisplay = toDisplay(notation, min, defender.maxHP());
const maxDisplay = toDisplay(notation, max, defender.maxHP());
Expand All @@ -86,8 +86,8 @@ export function displayMove(
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 = (typeof minDamage === 'number' ? minDamage : minDamage[0] + minDamage[1]);
const max = (typeof maxDamage === 'number' ? maxDamage : maxDamage[0] + maxDamage[1]);

const minDisplay = toDisplay(notation, min, defender.maxHP());
const maxDisplay = toDisplay(notation, max, defender.maxHP());
Expand Down Expand Up @@ -277,11 +277,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 = '';
if (move.hits > 1) {
qualifier = 'approx. ';
damage = squashMultihit(gen, damage, move.hits, err);
}
let qualifier = move.hits > 1 ? 'approx. ' : '';

const hazardsText = hazards.texts.length > 0
? ' after ' + serializeText(hazards.texts)
Expand Down Expand Up @@ -372,7 +368,7 @@ export function getKOChance(
if (predictTotal(
damage[0],
eot.damage,
move.hits,
1,
move.timesUsed,
toxicCounter,
defender.maxHP()
Expand All @@ -388,7 +384,7 @@ export function getKOChance(
predictTotal(
damage[damage.length - 1],
eot.damage,
move.hits,
1,
move.timesUsed,
toxicCounter,
defender.maxHP()
Expand Down
25 changes: 25 additions & 0 deletions calc/src/mechanics/gen12.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,5 +257,30 @@ export function calculateRBYGSC(
}
}

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));
} 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);
}
}
damageMultiplier++;
return affectedAmount + newFinalDamage;
}
return affectedAmount;
});
}
}

return result;
}
14 changes: 14 additions & 0 deletions calc/src/mechanics/gen3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,5 +329,19 @@ export function calculateADV(
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;
}
22 changes: 22 additions & 0 deletions calc/src/mechanics/gen4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,28 @@ export function calculateDPP(
}
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;
Expand Down
195 changes: 127 additions & 68 deletions calc/src/mechanics/gen56.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Generation, AbilityName} from '../data/interface';
import {Generation, AbilityName} from '../data/interface';
import {toID} from '../util';
import {
getItemBoostType,
Expand Down Expand Up @@ -791,73 +791,16 @@ export function calculateBWXY(
!(move.named('Facade') && gen.num === 6);
desc.isBurned = applyBurn;

const finalMods = [];

if (field.defenderSide.isReflect && move.category === 'Physical' && !isCritical) {
finalMods.push(field.gameType !== 'Singles' ? (gen.num > 5 ? 2732 : 2703) : 2048);
desc.isReflect = true;
} else if (field.defenderSide.isLightScreen && move.category === 'Special' && !isCritical) {
finalMods.push(field.gameType !== 'Singles' ? (gen.num > 5 ? 2732 : 2703) : 2048);
desc.isLightScreen = true;
}

if (defender.hasAbility('Multiscale') && defender.curHP() === defender.maxHP() &&
!field.defenderSide.isSR && (!field.defenderSide.spikes || defender.hasType('Flying')) &&
!attacker.hasAbility('Parental Bond (Child)')) {
finalMods.push(2048);
desc.defenderAbility = defender.ability;
}

if (attacker.hasAbility('Tinted Lens') && typeEffectiveness < 1) {
finalMods.push(8192);
desc.attackerAbility = attacker.ability;
}

if (field.defenderSide.isFriendGuard) {
finalMods.push(3072);
desc.isFriendGuard = true;
}

if (attacker.hasAbility('Sniper') && isCritical) {
finalMods.push(6144);
desc.attackerAbility = attacker.ability;
}

if (defender.hasAbility('Solid Rock', 'Filter') && typeEffectiveness > 1) {
finalMods.push(3072);
desc.defenderAbility = defender.ability;
}

if (attacker.hasItem('Metronome') && move.timesUsedWithMetronome! >= 1) {
const timesUsedWithMetronome = Math.floor(move.timesUsedWithMetronome!);
if (timesUsedWithMetronome <= 4) {
finalMods.push(4096 + timesUsedWithMetronome * 819);
} else {
finalMods.push(8192);
}
desc.attackerItem = attacker.item;
}

if (attacker.hasItem('Expert Belt') && typeEffectiveness > 1 && !move.isZ) {
finalMods.push(4915);
desc.attackerItem = attacker.item;
} else if (attacker.hasItem('Life Orb')) {
finalMods.push(5324);
desc.attackerItem = attacker.item;
}

if (move.hasType(getBerryResistType(defender.item)) &&
(typeEffectiveness > 1 || move.hasType('Normal')) &&
!attacker.hasAbility('Unnerve')) {
finalMods.push(2048);
desc.defenderItem = defender.item;
}

if (field.defenderSide.isProtected && move.isZ && attacker.item && attacker.item.includes(' Z')) {
finalMods.push(1024);
desc.isProtected = true;
}

const finalMods = calculateFinalModsBWXY(
gen,
attacker,
defender,
move,
field,
desc,
isCritical,
typeEffectiveness
);
const finalMod = chainMods(finalMods, 41, 131072);

let childDamage: number[] | undefined;
Expand Down Expand Up @@ -921,6 +864,45 @@ export function calculateBWXY(
}
}

if (move.hits > 1) {
let defenderDefenseBoost = defender.boosts['def'];
for (let times = 0; times < move.hits; times++) {
let damageMultiplier = 0;
damage = damage.map(affectedAmount => {
if (times) {
const newDefense = getModifiedStat(defense, defenderDefenseBoost);
const newFinalMods = calculateFinalModsBWXY(
gen,
attacker,
defender,
move,
field,
desc,
isCritical,
typeEffectiveness,
times
);
const newFinalMod = chainMods(newFinalMods, 41, 131072);
const newBaseDamage = getBaseDamage(attacker.level, basePower, attack, newDefense);
const newFinalDamage = getFinalDamage(
newBaseDamage,
damageMultiplier,
typeEffectiveness,
applyBurn,
stabMod,
newFinalMod,
);
damageMultiplier++;
return affectedAmount + newFinalDamage;
}
return affectedAmount;
});
if (hitsPhysical && defender.ability === 'Weak Armor') {
defenderDefenseBoost = Math.max(-6, defenderDefenseBoost - 1);
}
}
}

desc.attackBoost =
move.named('Foul Play') ? defender.boosts[attackStat] : attacker.boosts[attackStat];

Expand All @@ -930,3 +912,80 @@ export function calculateBWXY(

return result;
}

function calculateFinalModsBWXY(
gen: Generation,
attacker: Pokemon,
defender: Pokemon,
move: Move,
field: Field,
desc: RawDesc,
isCritical = false,
typeEffectiveness: number,
hitCount = 0
) {
const finalMods = [];

if (field.defenderSide.isReflect && move.category === 'Physical' && !isCritical) {
finalMods.push(field.gameType !== 'Singles' ? (gen.num > 5 ? 2732 : 2703) : 2048);
desc.isReflect = true;
} else if (field.defenderSide.isLightScreen && move.category === 'Special' && !isCritical) {
finalMods.push(field.gameType !== 'Singles' ? (gen.num > 5 ? 2732 : 2703) : 2048);
desc.isLightScreen = true;
}

if (defender.hasAbility('Multiscale') && defender.curHP() === defender.maxHP() &&
hitCount === 0 &&
!field.defenderSide.isSR && (!field.defenderSide.spikes || defender.hasType('Flying')) &&
!attacker.hasAbility('Parental Bond (Child)')) {
finalMods.push(2048);
desc.defenderAbility = defender.ability;
}

if (attacker.hasAbility('Tinted Lens') && typeEffectiveness < 1) {
finalMods.push(8192);
desc.attackerAbility = attacker.ability;
}

if (field.defenderSide.isFriendGuard) {
finalMods.push(3072);
desc.isFriendGuard = true;
}

if (attacker.hasAbility('Sniper') && isCritical) {
finalMods.push(6144);
desc.attackerAbility = attacker.ability;
}

if (defender.hasAbility('Solid Rock', 'Filter') && typeEffectiveness > 1) {
finalMods.push(3072);
desc.defenderAbility = defender.ability;
}

if (attacker.hasItem('Metronome') && move.timesUsedWithMetronome! >= 1) {
const timesUsedWithMetronome = Math.floor(move.timesUsedWithMetronome!);
if (timesUsedWithMetronome <= 4) {
finalMods.push(4096 + timesUsedWithMetronome * 819);
} else {
finalMods.push(8192);
}
desc.attackerItem = attacker.item;
}

if (attacker.hasItem('Expert Belt') && typeEffectiveness > 1 && !move.isZ) {
finalMods.push(4915);
desc.attackerItem = attacker.item;
} else if (attacker.hasItem('Life Orb')) {
finalMods.push(5324);
desc.attackerItem = attacker.item;
}

if (move.hasType(getBerryResistType(defender.item)) &&
(typeEffectiveness > 1 || move.hasType('Normal')) &&
!attacker.hasAbility('Unnerve')) {
finalMods.push(2048);
desc.defenderItem = defender.item;
}

return finalMods;
}
Loading

0 comments on commit d34d249

Please sign in to comment.