diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 1dc4972af79..c5be177e849 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "#app/battle-scene"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { variantData } from "#app/data/variant"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info"; -import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr, VariableMoveTypeChartAttr } from "#app/data/move"; +import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr, VariableMoveTypeChartAttr, TargetHalfHpDamageAttr } from "#app/data/move"; import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { starterPassiveAbilities } from "#app/data/balance/passives"; @@ -2618,10 +2618,34 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { }; } - // If the attack deals fixed damaged, return a result with that much damage - const fixedDamage = new Utils.IntegerHolder(0); + // If the attack deals fixed damage, return a result with that much damage + const fixedDamage = new Utils.NumberHolder(0); applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage); if (fixedDamage.value) { + const lensCount = source.getHeldItems().find(i => i instanceof PokemonMultiHitModifier)?.getStackCount() ?? 0; + // Apply damage fixing for hp cutting moves on multi lens hits (NOT PARENTAL BOND) + if (move.hasAttr(TargetHalfHpDamageAttr) && + (source.turnData.hitCount === source.turnData.hitsLeft || + source.turnData.hitCount - source.turnData.hitsLeft !== lensCount + 1)) { + // Do some unholy math to make the moves' damage values add up to 50% + // Values obtained courtesy of WolframAlpha and Desmos Graphing Calculator + // (https://www.desmos.com/calculator/wdngrksdfz) + let damageMulti = 0; + // NOTE: If multi lens ever gets updated (again) this switch case will NEED to be updated alongside it! + switch (lensCount) { + case 1: + damageMulti = 0.558481559888; + break; + case 2: + damageMulti = 0.60875846088; + break; + case 3: + damageMulti = 0.636414338985; + break; + } + + fixedDamage.value = this.hp * damageMulti; + } const multiLensMultiplier = new Utils.NumberHolder(1); source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, move.id, null, multiLensMultiplier); fixedDamage.value = Utils.toDmgValue(fixedDamage.value * multiLensMultiplier.value); diff --git a/src/test/items/multi_lens.test.ts b/src/test/items/multi_lens.test.ts index c5e60c9f9e5..37685e2836c 100644 --- a/src/test/items/multi_lens.test.ts +++ b/src/test/items/multi_lens.test.ts @@ -135,4 +135,78 @@ describe("Items - Multi Lens", () => { expect(damageResults[0]).toBe(Math.floor(playerPokemon.level * 0.75)); expect(damageResults[1]).toBe(Math.floor(playerPokemon.level * 0.25)); }); + + it("should result in correct damage for hp% attacks with 1 lens", async () => { + game.override.startingHeldItems([{ name: "MULTI_LENS", count: 1 }]) + .moveset(Moves.SUPER_FANG) + .ability(Abilities.COMPOUND_EYES) + .enemyLevel(100000) + .enemySpecies(Species.BLISSEY); // allows for unrealistically high levels of accuracy + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SUPER_FANG); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEndPhase", true); + expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.5, 10); // unrealistically high level of precision + }); + + it("should result in correct damage for hp% attacks with 2 lenses", async () => { + game.override.startingHeldItems([{ name: "MULTI_LENS", count: 2 }]) + .moveset(Moves.SUPER_FANG) + .ability(Abilities.COMPOUND_EYES) + .enemyMoveset(Moves.SPLASH) + .enemyLevel(100000) + .enemySpecies(Species.BLISSEY); // allows for unrealistically high levels of accuracy + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SUPER_FANG); + await game.forceEnemyMove(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEndPhase", true); + expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.5, 8); // unrealistically high level of precision + }); + + it("should result in correct damage for hp% attacks with 3 lenses", async () => { + game.override.startingHeldItems([{ name: "MULTI_LENS", count: 3 }]) + .moveset(Moves.SUPER_FANG) + .ability(Abilities.COMPOUND_EYES) + .enemyMoveset(Moves.SPLASH) + .enemyLevel(100000) + .enemySpecies(Species.BLISSEY); // allows for unrealistically high levels of accuracy + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SUPER_FANG); + await game.forceEnemyMove(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEndPhase", true); + expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.5, 8); + }); + it("should result in correct damage for hp% attacks with 3 lenses + Parental Bond", async () => { + game.override.startingHeldItems([{ name: "MULTI_LENS", count: 3 }, + { name: "WIDE_LENS", count: 2 }]) // ensures move always hits + .moveset(Moves.SUPER_FANG) + .ability(Abilities.PARENTAL_BOND) + .enemyMoveset(Moves.SPLASH) + .enemyLevel(100000) + .enemySpecies(Species.BLISSEY); // allows for unrealistically high levels of accuracy + + await game.classicMode.startBattle([ Species.MAGIKARP ]); + + const enemyPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SUPER_FANG); + await game.forceEnemyMove(Moves.SPLASH); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEndPhase", true); + expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.25, 8); + }); });