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

[Balance] Rework Multi-Lens #4831

Merged
merged 12 commits into from
Nov 11, 2024
61 changes: 13 additions & 48 deletions src/data/ability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Weather } from "#app/data/weather";
import { BattlerTag, BattlerTagLapseType, GroundedTag } from "./battler-tags";
import { getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "#app/data/status-effect";
import { Gender } from "./gender";
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
import { BerryModifier, HitHealModifier, PokemonHeldItemModifier } from "../modifier/modifier";
import { TerrainType } from "./terrain";
Expand Down Expand Up @@ -1351,65 +1351,30 @@ export class AddSecondStrikeAbAttr extends PreAttackAbAttr {
this.damageMultiplier = damageMultiplier;
}

/**
* Determines whether this attribute can apply to a given move.
* @param {Move} move the move to which this attribute may apply
* @param numTargets the number of {@linkcode Pokemon} targeted by this move
* @returns true if the attribute can apply to the move, false otherwise
*/
canApplyPreAttack(move: Move, numTargets: integer): boolean {
/**
* Parental Bond cannot apply to multi-hit moves, charging moves, or
* moves that cause the user to faint.
*/
const exceptAttrs: Constructor<MoveAttr>[] = [
MultiHitAttr,
SacrificialAttr,
SacrificialAttrOnHit
];

/** Parental Bond cannot apply to these specific moves */
const exceptMoves: Moves[] = [
Moves.FLING,
Moves.UPROAR,
Moves.ROLLOUT,
Moves.ICE_BALL,
Moves.ENDEAVOR
];

/** Also check if this move is an Attack move and if it's only targeting one Pokemon */
return numTargets === 1
&& !move.isChargingMove()
&& !exceptAttrs.some(attr => move.hasAttr(attr))
&& !exceptMoves.some(id => move.id === id)
&& move.category !== MoveCategory.STATUS;
}

/**
* If conditions are met, this doubles the move's hit count (via args[1])
* or multiplies the damage of secondary strikes (via args[2])
* @param {Pokemon} pokemon the Pokemon using the move
* @param pokemon the {@linkcode Pokemon} using the move
* @param passive n/a
* @param defender n/a
* @param {Move} move the move used by the ability source
* @param args\[0\] the number of Pokemon this move is targeting
* @param {Utils.IntegerHolder} args\[1\] the number of strikes with this move
* @param {Utils.NumberHolder} args\[2\] the damage multiplier for the current strike
* @param move the {@linkcode Move} used by the ability source
* @param args Additional arguments:
* - `[0]` the number of strikes this move currently has ({@linkcode Utils.NumberHolder})
* - `[1]` the damage multiplier for the current strike ({@linkcode Utils.NumberHolder})
* @returns
*/
applyPreAttack(pokemon: Pokemon, passive: boolean, simulated: boolean, defender: Pokemon, move: Move, args: any[]): boolean {
const numTargets = args[0] as integer;
const hitCount = args[1] as Utils.IntegerHolder;
const multiplier = args[2] as Utils.NumberHolder;
const hitCount = args[0] as Utils.NumberHolder;
const multiplier = args[1] as Utils.NumberHolder;

if (this.canApplyPreAttack(move, numTargets)) {
if (move.canBeMultiStrikeEnhanced(pokemon)) {
this.showAbility = !!hitCount?.value;
if (!!hitCount?.value) {
hitCount.value *= 2;
if (hitCount?.value) {
hitCount.value += 1;
}

if (!!multiplier?.value && pokemon.turnData.hitsLeft % 2 === 1 && pokemon.turnData.hitsLeft !== pokemon.turnData.hitCount) {
multiplier.value *= this.damageMultiplier;
if (multiplier?.value && pokemon.turnData.hitsLeft === 1) {
multiplier.value = this.damageMultiplier;
}
return true;
}
Expand Down
38 changes: 37 additions & 1 deletion src/data/move.ts
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,7 @@ export default class Move implements Localizable {

applyMoveAttrs(VariablePowerAttr, source, target, this, power);

source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, new Utils.IntegerHolder(0), power);
source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, this.id, null, power);

if (!this.hasAttr(TypelessAttr)) {
source.scene.arena.applyTags(WeakenMoveTypeTag, simulated, this.type, power);
Expand All @@ -840,6 +840,42 @@ export default class Move implements Localizable {

return priority.value;
}

/**
* Returns `true` if this move can be given additional strikes
* by enhancing effects.
* Currently used for {@link https://bulbapedia.bulbagarden.net/wiki/Parental_Bond_(Ability) | Parental Bond}
* and {@linkcode PokemonMultiHitModifier | Multi-Lens}.
*/
canBeMultiStrikeEnhanced(user: Pokemon): boolean {
// Multi-strike enhancers...

// ...cannot enhance moves that hit multiple targets
const { targets, multiple } = getMoveTargets(user, this.id);
const isMultiTarget = multiple && targets.length > 1;

// ...cannot enhance multi-hit or sacrificial moves
const exceptAttrs: Constructor<MoveAttr>[] = [
MultiHitAttr,
SacrificialAttr,
SacrificialAttrOnHit
];

// ...and cannot enhance these specific moves.
const exceptMoves: Moves[] = [
Moves.FLING,
Moves.UPROAR,
Moves.ROLLOUT,
Moves.ICE_BALL,
Moves.ENDEAVOR
];

return !isMultiTarget
&& !this.isChargingMove()
&& !exceptAttrs.some(attr => this.hasAttr(attr))
&& !exceptMoves.some(id => this.id === id)
&& this.category !== MoveCategory.STATUS;
}
}

export class AttackMove extends Move {
Expand Down
9 changes: 5 additions & 4 deletions src/field/pokemon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2640,10 +2640,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const numTargets = multiple ? targets.length : 1;
const targetMultiplier = (numTargets > 1) ? 0.75 : 1;

/** 0.25x multiplier if this is an added strike from the attacker's Parental Bond */
const parentalBondMultiplier = new Utils.NumberHolder(1);
/** Multiplier for moves enhanced by Multi-Lens and/or Parental Bond */
const multiStrikeEnhancementMultiplier = new Utils.NumberHolder(1);
source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, move.id, null, multiStrikeEnhancementMultiplier);
if (!ignoreSourceAbility) {
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, simulated, numTargets, new Utils.IntegerHolder(0), parentalBondMultiplier);
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, simulated, null, multiStrikeEnhancementMultiplier);
}

/** Doubles damage if this Pokemon's last move was Glaive Rush */
Expand Down Expand Up @@ -2720,7 +2721,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
damage.value = Utils.toDmgValue(
baseDamage
* targetMultiplier
* parentalBondMultiplier.value
* multiStrikeEnhancementMultiplier.value
* arenaAttackTypeMultiplier.value
* glaiveRushMultiplier.value
* criticalMultiplier.value
Expand Down
62 changes: 44 additions & 18 deletions src/modifier/modifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { allMoves } from "#app/data/move";
import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball";
import { type FormChangeItem, SpeciesFormChangeItemTrigger, SpeciesFormChangeLapseTeraTrigger, SpeciesFormChangeTeraTrigger } from "#app/data/pokemon-forms";
import { getStatusEffectHealText } from "#app/data/status-effect";
import { Type } from "#enums/type";
import Pokemon, { type PlayerPokemon } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides";
Expand All @@ -22,11 +21,13 @@ import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, toDmgValue }
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type";
import { Moves } from "#enums/moves";
import type { Nature } from "#enums/nature";
import type { PokeballType } from "#enums/pokeball";
import { Species } from "#enums/species";
import { type PermanentStat, type TempBattleStat, BATTLE_STATS, Stat, TEMP_BATTLE_STATS } from "#enums/stat";
import { StatusEffect } from "#enums/status-effect";
import { Type } from "#enums/type";
import i18next from "i18next";
import { type DoubleBattleChanceBoosterModifierType, type EvolutionItemModifierType, type FormChangeItemModifierType, type ModifierOverride, type ModifierType, type PokemonBaseStatTotalModifierType, type PokemonExpBoosterModifierType, type PokemonFriendshipBoosterModifierType, type PokemonMoveAccuracyBoosterModifierType, type PokemonMultiHitModifierType, type TerastallizeModifierType, type TmModifierType, getModifierType, ModifierPoolType, ModifierTypeGenerator, modifierTypes, PokemonHeldItemModifierType } from "./modifier-type";
import { Color, ShadowColor } from "#enums/color";
Expand Down Expand Up @@ -2689,32 +2690,57 @@ export class PokemonMultiHitModifier extends PokemonHeldItemModifier {
}

/**
* Applies {@linkcode PokemonMultiHitModifier}
* @param _pokemon The {@linkcode Pokemon} using the move
* @param count {@linkcode NumberHolder} holding the number of items
* @param power {@linkcode NumberHolder} holding the power of the move
* For each stack, converts 25 percent of attack damage into an additional strike.
* @param pokemon The {@linkcode Pokemon} using the move
* @param moveId The {@linkcode Moves | identifier} for the move being used
* @param count {@linkcode NumberHolder} holding the move's hit count for this turn
* @param damageMultiplier {@linkcode NumberHolder} holding a damage multiplier applied to a strike of this move
* @returns always `true`
*/
override apply(_pokemon: Pokemon, count: NumberHolder, power: NumberHolder): boolean {
count.value *= (this.getStackCount() + 1);
override apply(pokemon: Pokemon, moveId: Moves, count: NumberHolder | null = null, damageMultiplier: NumberHolder | null = null): boolean {
const move = allMoves[moveId];
/**
* The move must meet Parental Bond's restrictions for this item
* to apply. This means
* - Only attacks are boosted
* - Multi-strike moves, charge moves, and self-sacrificial moves are not boosted
* (though Multi-Lens can still affect moves boosted by Parental Bond)
* - Multi-target moves are not boosted *unless* they can only hit a single Pokemon
* - Fling, Uproar, Rollout, Ice Ball, and Endeavor are not boosted
*/
if (!move.canBeMultiStrikeEnhanced(pokemon)) {
return false;
}

switch (this.getStackCount()) {
case 1:
power.value *= 0.4;
break;
case 2:
power.value *= 0.25;
break;
case 3:
power.value *= 0.175;
break;
if (!isNullOrUndefined(count)) {
return this.applyHitCountBoost(count);
} else if (!isNullOrUndefined(damageMultiplier)) {
return this.applyPowerModifier(pokemon, damageMultiplier);
}

return false;
}

/** Adds strikes to a move equal to the number of stacked Multi-Lenses */
private applyHitCountBoost(count: NumberHolder): boolean {
count.value += this.getStackCount();
return true;
}

/**
* If applied to the first hit of a move, sets the damage multiplier
* equal to (1 - the number of stacked Multi-Lenses).
* Additional strikes beyond that are given a 0.25x damage multiplier
*/
private applyPowerModifier(pokemon: Pokemon, damageMultiplier: NumberHolder): boolean {
damageMultiplier.value = (pokemon.turnData.hitsLeft === pokemon.turnData.hitCount)
? (1 - (0.25 * this.getStackCount()))
: 0.25;
return true;
}

getMaxHeldItemCount(pokemon: Pokemon): number {
return 3;
return 2;
}
}

Expand Down
11 changes: 4 additions & 7 deletions src/phases/move-effect-phase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import {
applyMoveAttrs,
AttackMove,
DelayedAttackAttr,
FixedDamageAttr,
HitsTagAttr,
MissEffectAttr,
MoveAttr,
Expand Down Expand Up @@ -122,12 +121,10 @@ export class MoveEffectPhase extends PokemonPhase {
const hitCount = new NumberHolder(1);
// Assume single target for multi hit
applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount);
// If Parental Bond is applicable, double the hit count
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new NumberHolder(0));
// If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user
if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) {
this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new NumberHolder(0));
}
// If Parental Bond is applicable, add another hit
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, hitCount, null);
// If Multi-Lens is applicable, add hits equal to the number of held Multi-Lenses
this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, move.id, hitCount);
// Set the user's relevant turnData fields to reflect the final hit count
user.turnData.hitCount = hitCount.value;
user.turnData.hitsLeft = hitCount.value;
Expand Down
60 changes: 5 additions & 55 deletions src/test/abilities/parental_bond.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ describe("Abilities - Parental Bond", () => {
);

it(
"Moves boosted by this ability and Multi-Lens should strike 4 times",
"Moves boosted by this ability and Multi-Lens should strike 3 times",
async () => {
game.override.moveset([ Moves.TACKLE ]);
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
Expand All @@ -287,36 +287,12 @@ describe("Abilities - Parental Bond", () => {

await game.phaseInterceptor.to("DamagePhase");

expect(leadPokemon.turnData.hitCount).toBe(4);
expect(leadPokemon.turnData.hitCount).toBe(3);
}
);

it(
"Super Fang boosted by this ability and Multi-Lens should strike twice",
async () => {
game.override.moveset([ Moves.SUPER_FANG ]);
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);

await game.classicMode.startBattle([ Species.MAGIKARP ]);

const leadPokemon = game.scene.getPlayerPokemon()!;
const enemyPokemon = game.scene.getEnemyPokemon()!;

game.move.select(Moves.SUPER_FANG);
await game.move.forceHit();

await game.phaseInterceptor.to("DamagePhase");

expect(leadPokemon.turnData.hitCount).toBe(2);

await game.phaseInterceptor.to("MoveEndPhase", false);

expect(enemyPokemon.hp).toBe(Math.ceil(enemyPokemon.getMaxHp() * 0.25));
}
);

it(
"Seismic Toss boosted by this ability and Multi-Lens should strike twice",
"Seismic Toss boosted by this ability and Multi-Lens should strike 3 times",
async () => {
game.override.moveset([ Moves.SEISMIC_TOSS ]);
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);
Expand All @@ -333,11 +309,11 @@ describe("Abilities - Parental Bond", () => {

await game.phaseInterceptor.to("DamagePhase");

expect(leadPokemon.turnData.hitCount).toBe(2);
expect(leadPokemon.turnData.hitCount).toBe(3);

await game.phaseInterceptor.to("MoveEndPhase", false);

expect(enemyPokemon.hp).toBe(enemyStartingHp - 200);
expect(enemyPokemon.hp).toBe(enemyStartingHp - 300);
}
);

Expand Down Expand Up @@ -494,30 +470,4 @@ describe("Abilities - Parental Bond", () => {
expect(enemyPokemon.getStatStage(Stat.SPATK)).toBe(1);
}
);

it(
"should not apply to multi-target moves with Multi-Lens",
async () => {
game.override.battleType("double");
game.override.moveset([ Moves.EARTHQUAKE, Moves.SPLASH ]);
game.override.passiveAbility(Abilities.LEVITATE);
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]);

await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]);

const enemyPokemon = game.scene.getEnemyField();

const enemyStartingHp = enemyPokemon.map(p => p.hp);

game.move.select(Moves.EARTHQUAKE);
game.move.select(Moves.SPLASH, 1);

await game.phaseInterceptor.to("DamagePhase");
const enemyFirstHitDamage = enemyStartingHp.map((hp, i) => hp - enemyPokemon[i].hp);

await game.phaseInterceptor.to("BerryPhase", false);

enemyPokemon.forEach((p, i) => expect(enemyStartingHp[i] - p.hp).toBe(2 * enemyFirstHitDamage[i]));
}
);
});
Loading
Loading