Skip to content

Commit

Permalink
[Balance] Rework Multi-Lens (#4831)
Browse files Browse the repository at this point in the history
* Rework Multi-Lens

* Multi-Lens integration tests

* Apply suggestions from code review

Co-authored-by: NightKev <[email protected]>

* Fix obsolete tests related to Multi-Lens

* Fix flaky unburden tests

* maybe fix flaky ceaseless edge test?

* Fixed Multi-Lens apply comment

* Fix ceaseless edge test for real this time

* Update locales

* Another locale update

---------

Co-authored-by: NightKev <[email protected]>
  • Loading branch information
innerthunder and DayKev authored Nov 11, 2024
1 parent e5e3926 commit cebedd2
Show file tree
Hide file tree
Showing 12 changed files with 214 additions and 177 deletions.
2 changes: 1 addition & 1 deletion public/locales
Submodule locales updated 45 files
+1 −1 de/dialogue.json
+1 −1 de/modifier-type.json
+2 −1 de/move-trigger.json
+13 −13 de/move.json
+8 −8 de/mystery-encounters/bug-type-superfan-dialogue.json
+6 −6 de/mystery-encounters/lost-at-sea-dialogue.json
+1 −1 en/modifier-type.json
+2 −1 en/move-trigger.json
+0 −3 es-ES/modifier-type.json
+1 −1 fr/modifier-type.json
+2 −1 fr/move-trigger.json
+1 −1 fr/mystery-encounters/uncommon-breed-dialogue.json
+3 −0 he/dialogue-final-boss.json
+7 −3 he/mystery-encounters/an-offer-you-cant-refuse-dialogue.json
+81 −1 he/pokemon.json
+2 −1 it/arena-flyout.json
+2 −1 it/arena-tag.json
+2 −0 it/battle.json
+3 −1 it/battler-tags.json
+27 −14 it/modifier-type.json
+2 −1 it/move-trigger.json
+18 −0 it/status-effect.json
+2 −0 ja/battle.json
+3 −1 ja/battler-tags.json
+4 −3 ja/modifier-type.json
+2 −1 ja/move-trigger.json
+1 −0 ja/splash-texts.json
+1 −1 ko/modifier-type.json
+2 −1 pt-BR/arena-flyout.json
+2 −1 pt-BR/arena-tag.json
+2 −0 pt-BR/battle.json
+3 −1 pt-BR/battler-tags.json
+4 −3 pt-BR/modifier-type.json
+2 −1 pt-BR/move-trigger.json
+1 −0 pt-BR/mystery-encounters/fun-and-games-dialogue.json
+1 −1 pt-BR/mystery-encounters/part-timer-dialogue.json
+1 −1 pt-BR/mystery-encounters/shady-vitamin-dealer-dialogue.json
+1 −1 pt-BR/mystery-encounters/training-session-dialogue.json
+2 −1 zh-CN/arena-flyout.json
+2 −1 zh-CN/arena-tag.json
+2 −0 zh-CN/battle.json
+3 −1 zh-CN/battler-tags.json
+5 −1 zh-CN/modifier-type.json
+2 −1 zh-CN/move-trigger.json
+1 −1 zh-TW/modifier-type.json
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 @@ -2642,10 +2642,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 @@ -2722,7 +2723,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

0 comments on commit cebedd2

Please sign in to comment.