Merge branch 'beta' into fix-4981

This commit is contained in:
NightKev 2025-04-16 21:46:20 -07:00 committed by GitHub
commit d03038cba5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 155 additions and 94 deletions

View File

@ -6704,8 +6704,8 @@ export function initAbilities() {
.attr(PostDefendStealHeldItemAbAttr, (target, user, move) => move.hasFlag(MoveFlags.MAKES_CONTACT))
.condition(getSheerForceHitDisableAbCondition()),
new Ability(Abilities.SHEER_FORCE, 5)
.attr(MovePowerBoostAbAttr, (user, target, move) => move.chance >= 1, 5461 / 4096)
.attr(MoveEffectChanceMultiplierAbAttr, 0), // Should disable life orb, eject button, red card, kee/maranga berry if they get implemented
.attr(MovePowerBoostAbAttr, (user, target, move) => move.chance >= 1, 1.3)
.attr(MoveEffectChanceMultiplierAbAttr, 0), // This attribute does not seem to function - Should disable life orb, eject button, red card, kee/maranga berry if they get implemented
new Ability(Abilities.CONTRARY, 5)
.attr(StatStageChangeMultiplierAbAttr, -1)
.ignorable(),

View File

@ -3466,8 +3466,7 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr {
/**
* Attribute implementing the stat boosting effect of {@link https://bulbapedia.bulbagarden.net/wiki/Order_Up_(move) | Order Up}.
* If the user has a Pokemon with {@link https://bulbapedia.bulbagarden.net/wiki/Commander_(Ability) | Commander} in their mouth,
* one of the user's stats are increased by 1 stage, depending on the "commanding" Pokemon's form. This effect does not respect
* effect chance, but Order Up itself may be boosted by Sheer Force.
* one of the user's stats are increased by 1 stage, depending on the "commanding" Pokemon's form.
*/
export class OrderUpStatBoostAttr extends MoveEffectAttr {
constructor() {
@ -9726,7 +9725,7 @@ export function initMoves() {
.ignoresProtect()
.target(MoveTarget.BOTH_SIDES)
.unimplemented(),
new AttackMove(Moves.SMACK_DOWN, PokemonType.ROCK, MoveCategory.PHYSICAL, 50, 100, 15, 100, 0, 5)
new AttackMove(Moves.SMACK_DOWN, PokemonType.ROCK, MoveCategory.PHYSICAL, 50, 100, 15, -1, 0, 5)
.attr(FallDownAttr)
.attr(AddBattlerTagAttr, BattlerTagType.INTERRUPTED)
.attr(RemoveBattlerTagAttr, [ BattlerTagType.FLYING, BattlerTagType.FLOATING, BattlerTagType.TELEKINESIS ])
@ -9893,7 +9892,7 @@ export function initMoves() {
.attr(MovePowerMultiplierAttr, (user, target, move) => globalScene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() ? 0.5 : 1)
.makesContact(false)
.target(MoveTarget.ALL_NEAR_OTHERS),
new AttackMove(Moves.FROST_BREATH, PokemonType.ICE, MoveCategory.SPECIAL, 60, 90, 10, 100, 0, 5)
new AttackMove(Moves.FROST_BREATH, PokemonType.ICE, MoveCategory.SPECIAL, 60, 90, 10, -1, 0, 5)
.attr(CritOnlyAttr),
new AttackMove(Moves.DRAGON_TAIL, PokemonType.DRAGON, MoveCategory.PHYSICAL, 60, 90, 10, -1, -6, 5)
.attr(ForceSwitchOutAttr, false, SwitchType.FORCE_SWITCH)
@ -10535,7 +10534,7 @@ export function initMoves() {
.attr(AddArenaTagAttr, ArenaTagType.LIGHT_SCREEN, 5, false, true),
new AttackMove(Moves.BADDY_BAD, PokemonType.DARK, MoveCategory.SPECIAL, 80, 95, 15, -1, 0, 7)
.attr(AddArenaTagAttr, ArenaTagType.REFLECT, 5, false, true),
new AttackMove(Moves.SAPPY_SEED, PokemonType.GRASS, MoveCategory.PHYSICAL, 100, 90, 10, 100, 0, 7)
new AttackMove(Moves.SAPPY_SEED, PokemonType.GRASS, MoveCategory.PHYSICAL, 100, 90, 10, -1, 0, 7)
.attr(LeechSeedAttr)
.makesContact(false),
new AttackMove(Moves.FREEZY_FROST, PokemonType.ICE, MoveCategory.SPECIAL, 100, 90, 10, -1, 0, 7)
@ -10863,7 +10862,7 @@ export function initMoves() {
.attr(StatStageChangeAttr, [ Stat.SPD ], 1, true),
new AttackMove(Moves.BITTER_MALICE, PokemonType.GHOST, MoveCategory.SPECIAL, 75, 100, 10, 100, 0, 8)
.attr(StatStageChangeAttr, [ Stat.ATK ], -1),
new SelfStatusMove(Moves.SHELTER, PokemonType.STEEL, -1, 10, 100, 0, 8)
new SelfStatusMove(Moves.SHELTER, PokemonType.STEEL, -1, 10, -1, 0, 8)
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
new AttackMove(Moves.TRIPLE_ARROWS, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 90, 100, 10, 30, 0, 8)
.makesContact(false)
@ -11018,7 +11017,7 @@ export function initMoves() {
.makesContact(false),
new AttackMove(Moves.LUMINA_CRASH, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9)
.attr(StatStageChangeAttr, [ Stat.SPDEF ], -2),
new AttackMove(Moves.ORDER_UP, PokemonType.DRAGON, MoveCategory.PHYSICAL, 80, 100, 10, 100, 0, 9)
new AttackMove(Moves.ORDER_UP, PokemonType.DRAGON, MoveCategory.PHYSICAL, 80, 100, 10, -1, 0, 9)
.attr(OrderUpStatBoostAttr)
.makesContact(false),
new AttackMove(Moves.JET_PUNCH, PokemonType.WATER, MoveCategory.PHYSICAL, 60, 100, 15, -1, 1, 9)
@ -11072,7 +11071,7 @@ export function initMoves() {
.attr(CutHpStatStageBoostAttr, [ Stat.ATK, Stat.SPATK, Stat.SPD ], 2, 2),
new AttackMove(Moves.KOWTOW_CLEAVE, PokemonType.DARK, MoveCategory.PHYSICAL, 85, -1, 10, -1, 0, 9)
.slicingMove(),
new AttackMove(Moves.FLOWER_TRICK, PokemonType.GRASS, MoveCategory.PHYSICAL, 70, -1, 10, 100, 0, 9)
new AttackMove(Moves.FLOWER_TRICK, PokemonType.GRASS, MoveCategory.PHYSICAL, 70, -1, 10, -1, 0, 9)
.attr(CritOnlyAttr)
.makesContact(false),
new AttackMove(Moves.TORCH_SONG, PokemonType.FIRE, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9)
@ -11191,7 +11190,7 @@ export function initMoves() {
.attr(StatusEffectAttr, StatusEffect.BURN)
.target(MoveTarget.ALL_NEAR_ENEMIES)
.triageMove(),
new AttackMove(Moves.SYRUP_BOMB, PokemonType.GRASS, MoveCategory.SPECIAL, 60, 85, 10, -1, 0, 9)
new AttackMove(Moves.SYRUP_BOMB, PokemonType.GRASS, MoveCategory.SPECIAL, 60, 85, 10, 100, 0, 9)
.attr(AddBattlerTagAttr, BattlerTagType.SYRUP_BOMB, false, false, 3)
.ballBombMove(),
new AttackMove(Moves.IVY_CUDGEL, PokemonType.GRASS, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 9)
@ -11209,7 +11208,8 @@ export function initMoves() {
.partial(), /** Does not ignore abilities that affect stats, relevant in determining the move's category {@see TeraMoveCategoryAttr} */
new AttackMove(Moves.FICKLE_BEAM, PokemonType.DRAGON, MoveCategory.SPECIAL, 80, 100, 5, 30, 0, 9)
.attr(PreMoveMessageAttr, doublePowerChanceMessageFunc)
.attr(DoublePowerChanceAttr),
.attr(DoublePowerChanceAttr)
.edgeCase(), // Should not interact with Sheer Force
new SelfStatusMove(Moves.BURNING_BULWARK, PokemonType.FIRE, -1, 10, -1, 4, 9)
.attr(ProtectAttr, BattlerTagType.BURNING_BULWARK)
.condition(failIfLastCondition),
@ -11232,7 +11232,7 @@ export function initMoves() {
new StatusMove(Moves.DRAGON_CHEER, PokemonType.DRAGON, -1, 15, -1, 0, 9)
.attr(AddBattlerTagAttr, BattlerTagType.DRAGON_CHEER, false, true)
.target(MoveTarget.NEAR_ALLY),
new AttackMove(Moves.ALLURING_VOICE, PokemonType.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 9)
new AttackMove(Moves.ALLURING_VOICE, PokemonType.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9)
.attr(AddBattlerTagIfBoostedAttr, BattlerTagType.CONFUSED)
.soundBased(),
new AttackMove(Moves.TEMPER_FLARE, PokemonType.FIRE, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 9)
@ -11241,7 +11241,7 @@ export function initMoves() {
.attr(MissEffectAttr, crashDamageFunc)
.attr(NoEffectAttr, crashDamageFunc)
.recklessMove(),
new AttackMove(Moves.PSYCHIC_NOISE, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 75, 100, 10, -1, 0, 9)
new AttackMove(Moves.PSYCHIC_NOISE, PokemonType.PSYCHIC, MoveCategory.SPECIAL, 75, 100, 10, 100, 0, 9)
.soundBased()
.attr(AddBattlerTagAttr, BattlerTagType.HEAL_BLOCK, false, false, 2),
new AttackMove(Moves.UPPER_HAND, PokemonType.FIGHTING, MoveCategory.PHYSICAL, 65, 100, 15, 100, 3, 9)

View File

@ -4151,6 +4151,62 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return baseDamage;
}
/** Determine the STAB multiplier for a move used against this pokemon.
*
* @param source - The attacking {@linkcode Pokemon}
* @param move - The {@linkcode Move} used in the attack
* @param ignoreSourceAbility - If `true`, ignores the attacking Pokemon's ability effects
* @param simulated - If `true`, suppresses changes to game state during the calculation
*
* @returns The STAB multiplier for the move used against this Pokemon
*/
calculateStabMultiplier(source: Pokemon, move: Move, ignoreSourceAbility: boolean, simulated: boolean): number {
// If the move has the Typeless attribute, it doesn't get STAB (e.g. struggle)
if (move.hasAttr(TypelessAttr)) {
return 1;
}
const sourceTypes = source.getTypes();
const sourceTeraType = source.getTeraType();
const moveType = source.getMoveType(move);
const matchesSourceType = sourceTypes.includes(source.getMoveType(move));
const stabMultiplier = new NumberHolder(1);
if (matchesSourceType && moveType !== PokemonType.STELLAR) {
stabMultiplier.value += 0.5;
}
applyMoveAttrs(
CombinedPledgeStabBoostAttr,
source,
this,
move,
stabMultiplier,
);
if (!ignoreSourceAbility) {
applyAbAttrs(StabBoostAbAttr, source, null, simulated, stabMultiplier);
}
if (
source.isTerastallized &&
sourceTeraType === moveType &&
moveType !== PokemonType.STELLAR
) {
stabMultiplier.value += 0.5;
}
if (
source.isTerastallized &&
source.getTeraType() === PokemonType.STELLAR &&
(!source.stellarTypesBoosted.includes(moveType) ||
source.hasSpecies(Species.TERAPAGOS))
) {
stabMultiplier.value += matchesSourceType ? 0.5 : 0.2;
}
return Math.min(stabMultiplier.value, 2.25);
}
/**
* Calculates the damage of an attack made by another Pokemon against this Pokemon
* @param source {@linkcode Pokemon} the attacking Pokemon
@ -4333,70 +4389,29 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
? 1
: this.randSeedIntRange(85, 100) / 100;
const sourceTypes = source.getTypes();
const sourceTeraType = source.getTeraType();
const matchesSourceType = sourceTypes.includes(moveType);
/** A damage multiplier for when the attack is of the attacker's type and/or Tera type. */
const stabMultiplier = new NumberHolder(1);
if (matchesSourceType && moveType !== PokemonType.STELLAR) {
stabMultiplier.value += 0.5;
}
if (!ignoreSourceAbility) {
applyAbAttrs(StabBoostAbAttr, source, null, simulated, stabMultiplier);
}
applyMoveAttrs(
CombinedPledgeStabBoostAttr,
source,
this,
move,
stabMultiplier,
);
if (
source.isTerastallized &&
sourceTeraType === moveType &&
moveType !== PokemonType.STELLAR
) {
stabMultiplier.value += 0.5;
}
if (
source.isTerastallized &&
source.getTeraType() === PokemonType.STELLAR &&
(!source.stellarTypesBoosted.includes(moveType) ||
source.hasSpecies(Species.TERAPAGOS))
) {
if (matchesSourceType) {
stabMultiplier.value += 0.5;
} else {
stabMultiplier.value += 0.2;
}
}
stabMultiplier.value = Math.min(stabMultiplier.value, 2.25);
const stabMultiplier = this.calculateStabMultiplier(source, move, ignoreSourceAbility, simulated);
/** Halves damage if the attacker is using a physical attack while burned */
const burnMultiplier = new NumberHolder(1);
let burnMultiplier = 1;
if (
isPhysical &&
source.status &&
source.status.effect === StatusEffect.BURN
source.status.effect === StatusEffect.BURN &&
!move.hasAttr(BypassBurnDamageReductionAttr)
) {
if (!move.hasAttr(BypassBurnDamageReductionAttr)) {
const burnDamageReductionCancelled = new BooleanHolder(false);
if (!ignoreSourceAbility) {
applyAbAttrs(
BypassBurnDamageReductionAbAttr,
source,
burnDamageReductionCancelled,
simulated,
);
}
if (!burnDamageReductionCancelled.value) {
burnMultiplier.value = 0.5;
}
const burnDamageReductionCancelled = new BooleanHolder(false);
if (!ignoreSourceAbility) {
applyAbAttrs(
BypassBurnDamageReductionAbAttr,
source,
burnDamageReductionCancelled,
simulated,
);
}
if (!burnDamageReductionCancelled.value) {
burnMultiplier = 0.5;
}
}
@ -4447,9 +4462,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
glaiveRushMultiplier.value *
criticalMultiplier.value *
randomMultiplier *
stabMultiplier.value *
stabMultiplier *
typeMultiplier *
burnMultiplier.value *
burnMultiplier *
screenMultiplier.value *
hitsTagMultiplier.value *
mistyTerrainMultiplier,

View File

@ -34,7 +34,7 @@ describe("Abilities - Sheer Force", () => {
.disableCrits();
});
const SHEER_FORCE_MULT = 5461 / 4096;
const SHEER_FORCE_MULT = 1.3;
it("Sheer Force should boost the power of the move but disable secondary effects", async () => {
game.override.moveset([Moves.AIR_SLASH]);

View File

@ -89,7 +89,7 @@ describe("Moves - Burning Jealousy", () => {
await game.phaseInterceptor.to("BerryPhase");
expect(allMoves[Moves.BURNING_JEALOUSY].calculateBattlePower).toHaveReturnedWith(
(allMoves[Moves.BURNING_JEALOUSY].power * 5461) / 4096,
allMoves[Moves.BURNING_JEALOUSY].power * 1.3,
);
});
});

View File

@ -65,23 +65,4 @@ describe("Moves - Order Up", () => {
affectedStats.forEach(st => expect(dondozo.getStatStage(st)).toBe(st === stat ? 3 : 2));
},
);
it("should be boosted by Sheer Force while still applying a stat boost", async () => {
game.override.passiveAbility(Abilities.SHEER_FORCE).starterForms({ [Species.TATSUGIRI]: 0 });
await game.classicMode.startBattle([Species.TATSUGIRI, Species.DONDOZO]);
const [tatsugiri, dondozo] = game.scene.getPlayerField();
expect(game.scene.triggerPokemonBattleAnim).toHaveBeenLastCalledWith(tatsugiri, PokemonAnimType.COMMANDER_APPLY);
expect(dondozo.getTag(BattlerTagType.COMMANDED)).toBeDefined();
game.move.select(Moves.ORDER_UP, 1, BattlerIndex.ENEMY);
expect(game.scene.currentBattle.turnCommands[0]?.skip).toBeTruthy();
await game.phaseInterceptor.to("BerryPhase", false);
expect(dondozo.battleData.abilitiesApplied.includes(Abilities.SHEER_FORCE)).toBeTruthy();
expect(dondozo.getStatStage(Stat.ATK)).toBe(3);
});
});

View File

@ -0,0 +1,65 @@
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/testUtils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Moves - Struggle", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([Moves.SPLASH])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should not have its power boosted by adaptability or stab", async () => {
game.override.moveset([Moves.STRUGGLE]).ability(Abilities.ADAPTABILITY);
await game.classicMode.startBattle([Species.RATTATA]);
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.STRUGGLE);
const stabSpy = vi.spyOn(enemy, "calculateStabMultiplier");
await game.phaseInterceptor.to("BerryPhase");
expect(stabSpy).toHaveReturnedWith(1);
stabSpy.mockRestore();
});
it("should ignore type effectiveness", async () => {
game.override.moveset([Moves.STRUGGLE]);
await game.classicMode.startBattle([Species.GASTLY]);
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.STRUGGLE);
const moveEffectivenessSpy = vi.spyOn(enemy, "getMoveEffectiveness");
await game.phaseInterceptor.to("BerryPhase");
expect(moveEffectivenessSpy).toHaveReturnedWith(1);
moveEffectivenessSpy.mockRestore();
});
});