[Bug] Fix Inconsistency with stat boost when breaking through boss shields + tests (#3785)
* fix boss shield stats up calculation and add tests * update test to remove usage of deprecated startBattle
This commit is contained in:
parent
9c30e5b213
commit
89a1ff7b5b
|
@ -841,12 +841,13 @@ export default class BattleScene extends SceneBase {
|
|||
}
|
||||
|
||||
addEnemyPokemon(species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean = false, dataSource?: PokemonData, postProcess?: (enemyPokemon: EnemyPokemon) => void): EnemyPokemon {
|
||||
if (Overrides.OPP_LEVEL_OVERRIDE > 0) {
|
||||
level = Overrides.OPP_LEVEL_OVERRIDE;
|
||||
}
|
||||
if (Overrides.OPP_SPECIES_OVERRIDE) {
|
||||
species = getPokemonSpecies(Overrides.OPP_SPECIES_OVERRIDE);
|
||||
}
|
||||
|
||||
if (Overrides.OPP_LEVEL_OVERRIDE !== 0) {
|
||||
level = Overrides.OPP_LEVEL_OVERRIDE;
|
||||
// The fact that a Pokemon is a boss or not can change based on its Species and level
|
||||
boss = this.getEncounterBossSegments(this.currentBattle.waveIndex, level, species) > 1;
|
||||
}
|
||||
|
||||
const pokemon = new EnemyPokemon(this, species, level, trainerSlot, boss, dataSource);
|
||||
|
@ -1327,6 +1328,13 @@ export default class BattleScene extends SceneBase {
|
|||
}
|
||||
|
||||
getEncounterBossSegments(waveIndex: integer, level: integer, species?: PokemonSpecies, forceBoss: boolean = false): integer {
|
||||
if (Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE > 1) {
|
||||
return Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE;
|
||||
} else if (Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE === 1) {
|
||||
// The rest of the code expects to be returned 0 and not 1 if the enemy is not a boss
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this.gameMode.isDaily && this.gameMode.isWaveFinal(waveIndex)) {
|
||||
return 5;
|
||||
}
|
||||
|
|
|
@ -4176,7 +4176,7 @@ export class EnemyPokemon extends Pokemon {
|
|||
//console.log('damage', damage, 'segment', segmentsBypassed + 1, 'segment size', segmentSize, 'damage needed', Math.round(segmentSize * Math.pow(2, segmentsBypassed + 1)));
|
||||
}
|
||||
|
||||
damage = hpRemainder + Math.round(segmentSize * segmentsBypassed);
|
||||
damage = Utils.toDmgValue(this.hp - hpThreshold + segmentSize * segmentsBypassed);
|
||||
clearedBossSegmentIndex = s - segmentsBypassed;
|
||||
}
|
||||
break;
|
||||
|
@ -4241,17 +4241,13 @@ export class EnemyPokemon extends Pokemon {
|
|||
|
||||
let statLevels = 1;
|
||||
|
||||
switch (segmentIndex) {
|
||||
case 1:
|
||||
if (this.bossSegments >= 3) {
|
||||
statLevels++;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
if (this.bossSegments >= 5) {
|
||||
statLevels++;
|
||||
}
|
||||
break;
|
||||
// increase the boost if the boss has at least 3 segments and we passed last shield
|
||||
if (this.bossSegments >= 3 && this.bossSegmentIndex === 1) {
|
||||
statLevels++;
|
||||
}
|
||||
// increase the boost if the boss has at least 5 segments and we passed the second to last shield
|
||||
if (this.bossSegments >= 5 && this.bossSegmentIndex === 2) {
|
||||
statLevels++;
|
||||
}
|
||||
|
||||
this.scene.unshiftPhase(new StatChangePhase(this.scene, this.getBattlerIndex(), true, [ boostedStat ], statLevels, true, true));
|
||||
|
|
|
@ -116,6 +116,14 @@ class DefaultOverrides {
|
|||
readonly OPP_VARIANT_OVERRIDE: Variant = 0;
|
||||
readonly OPP_IVS_OVERRIDE: number | number[] = [];
|
||||
readonly OPP_FORM_OVERRIDES: Partial<Record<Species, number>> = {};
|
||||
/**
|
||||
* Override to give the enemy Pokemon a given amount of health segments
|
||||
*
|
||||
* 0 (default): the health segments will be handled normally based on wave, level and species
|
||||
* 1: the Pokemon will have a single health segment and therefore will not be a boss
|
||||
* 2+: the Pokemon will be a boss with the given number of health segments
|
||||
*/
|
||||
readonly OPP_HEALTH_SEGMENTS_OVERRIDE: number = 0;
|
||||
|
||||
// -------------
|
||||
// EGG OVERRIDES
|
||||
|
|
|
@ -26,6 +26,7 @@ import { ScanIvsPhase } from "./scan-ivs-phase";
|
|||
import { ShinySparklePhase } from "./shiny-sparkle-phase";
|
||||
import { SummonPhase } from "./summon-phase";
|
||||
import { ToggleDoublePositionPhase } from "./toggle-double-position-phase";
|
||||
import Overrides from "#app/overrides";
|
||||
|
||||
export class EncounterPhase extends BattlePhase {
|
||||
private loaded: boolean;
|
||||
|
@ -112,10 +113,11 @@ export class EncounterPhase extends BattlePhase {
|
|||
if (battle.battleType === BattleType.TRAINER) {
|
||||
loadEnemyAssets.push(battle.trainer?.loadAssets().then(() => battle.trainer?.initSprite())!); // TODO: is this bang correct?
|
||||
} else {
|
||||
// This block only applies for double battles to init the boss segments (idk why it's split up like this)
|
||||
if (battle.enemyParty.filter(p => p.isBoss()).length > 1) {
|
||||
const overridedBossSegments = Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE > 1;
|
||||
// for double battles, reduce the health segments for boss Pokemon unless there is an override
|
||||
if (!overridedBossSegments && battle.enemyParty.filter(p => p.isBoss()).length > 1) {
|
||||
for (const enemyPokemon of battle.enemyParty) {
|
||||
// If the enemy pokemon is a boss and wasn't populated from data source, then set it up
|
||||
// If the enemy pokemon is a boss and wasn't populated from data source, then update the number of segments
|
||||
if (enemyPokemon.isBoss() && !enemyPokemon.isPopulatedFromDataSource) {
|
||||
enemyPokemon.setBoss(true, Math.ceil(enemyPokemon.bossSegments * (enemyPokemon.getSpeciesForm().baseTotal / totalBst)));
|
||||
enemyPokemon.initBattleInfo();
|
||||
|
|
|
@ -0,0 +1,220 @@
|
|||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import GameManager from "./utils/gameManager";
|
||||
import { Species } from "#app/enums/species";
|
||||
import { getPokemonSpecies } from "#app/data/pokemon-species";
|
||||
import { SPLASH_ONLY } from "./utils/testUtils";
|
||||
import { Abilities } from "#app/enums/abilities";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { BattleStat } from "#app/data/battle-stat";
|
||||
import { EnemyPokemon } from "#app/field/pokemon";
|
||||
import { toDmgValue } from "#app/utils";
|
||||
|
||||
describe("Boss Pokemon / Shields", () => {
|
||||
const TIMEOUT = 2500;
|
||||
|
||||
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
|
||||
.battleType("single")
|
||||
.disableTrainerWaves()
|
||||
.disableCrits()
|
||||
.enemySpecies(Species.RATTATA)
|
||||
.enemyMoveset(SPLASH_ONLY)
|
||||
.enemyHeldItems([])
|
||||
.startingLevel(1000)
|
||||
.moveset([Moves.FALSE_SWIPE, Moves.SUPER_FANG, Moves.SPLASH])
|
||||
.ability(Abilities.NO_GUARD);
|
||||
});
|
||||
|
||||
it("Pokemon should get shields based on their Species and level and the current wave", async () => {
|
||||
let level = 50;
|
||||
let wave = 5;
|
||||
|
||||
// On normal waves, no shields...
|
||||
expect(game.scene.getEncounterBossSegments(wave, level, getPokemonSpecies(Species.RATTATA))).toBe(0);
|
||||
// ... expect (sub)-legendary and mythical Pokemon who always get shields
|
||||
expect(game.scene.getEncounterBossSegments(wave, level, getPokemonSpecies(Species.MEW))).toBe(2);
|
||||
// Pokemon with 670+ BST get an extra shield
|
||||
expect(game.scene.getEncounterBossSegments(wave, level, getPokemonSpecies(Species.MEWTWO))).toBe(3);
|
||||
|
||||
// Every 10 waves will always be a boss Pokemon with shield(s)
|
||||
wave = 50;
|
||||
expect(game.scene.getEncounterBossSegments(wave, level, getPokemonSpecies(Species.RATTATA))).toBe(2);
|
||||
// Every extra 250 waves adds a shield
|
||||
wave += 250;
|
||||
expect(game.scene.getEncounterBossSegments(wave, level, getPokemonSpecies(Species.RATTATA))).toBe(3);
|
||||
wave += 750;
|
||||
expect(game.scene.getEncounterBossSegments(wave, level, getPokemonSpecies(Species.RATTATA))).toBe(6);
|
||||
|
||||
// Pokemon above level 100 get an extra shield
|
||||
level = 100;
|
||||
expect(game.scene.getEncounterBossSegments(wave, level, getPokemonSpecies(Species.RATTATA))).toBe(7);
|
||||
}, TIMEOUT);
|
||||
|
||||
it("should reduce the number of shields if we are in a double battle", async () => {
|
||||
game.override
|
||||
.battleType("double")
|
||||
.startingWave(150); // Floor 150 > 2 shields / 3 health segments
|
||||
|
||||
await game.classicMode.startBattle([ Species.MEWTWO ]);
|
||||
|
||||
const boss1: EnemyPokemon = game.scene.getEnemyParty()[0]!;
|
||||
const boss2: EnemyPokemon = game.scene.getEnemyParty()[1]!;
|
||||
expect(boss1.isBoss()).toBe(true);
|
||||
expect(boss1.bossSegments).toBe(2);
|
||||
expect(boss2.isBoss()).toBe(true);
|
||||
expect(boss2.bossSegments).toBe(2);
|
||||
}, TIMEOUT);
|
||||
|
||||
it("shields should stop overflow damage and give stat boosts when broken", async () => {
|
||||
game.override.startingWave(150); // Floor 150 > 2 shields / 3 health segments
|
||||
|
||||
await game.classicMode.startBattle([ Species.MEWTWO ]);
|
||||
|
||||
const enemyPokemon = game.scene.getEnemyPokemon()!;
|
||||
const segmentHp = enemyPokemon.getMaxHp() / enemyPokemon.bossSegments;
|
||||
expect(enemyPokemon.isBoss()).toBe(true);
|
||||
expect(enemyPokemon.bossSegments).toBe(3);
|
||||
expect(getTotalStatBoosts(enemyPokemon)).toBe(0);
|
||||
|
||||
game.move.select(Moves.SUPER_FANG); // Enough to break the first shield
|
||||
await game.toNextTurn();
|
||||
|
||||
// Broke 1st of 2 shields, health at 2/3rd
|
||||
expect(enemyPokemon.bossSegmentIndex).toBe(1);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - toDmgValue(segmentHp));
|
||||
// Breaking the shield gives a +1 boost to ATK, DEF, SP ATK, SP DEF or SPD
|
||||
expect(getTotalStatBoosts(enemyPokemon)).toBe(1);
|
||||
|
||||
game.move.select(Moves.FALSE_SWIPE); // Enough to break last shield but not kill
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyPokemon.bossSegmentIndex).toBe(0);
|
||||
expect(enemyPokemon.hp).toBe(enemyPokemon.getMaxHp() - toDmgValue(2 * segmentHp));
|
||||
// Breaking the last shield gives a +2 boost to ATK, DEF, SP ATK, SP DEF or SPD
|
||||
expect(getTotalStatBoosts(enemyPokemon)).toBe(3);
|
||||
|
||||
}, TIMEOUT);
|
||||
|
||||
it("breaking multiple shields at once requires extra damage", async () => {
|
||||
game.override
|
||||
.battleType("double")
|
||||
.enemyHealthSegments(5);
|
||||
|
||||
await game.classicMode.startBattle([ Species.MEWTWO ]);
|
||||
|
||||
// In this test we want to break through 3 shields at once
|
||||
const brokenShields = 3;
|
||||
|
||||
const boss1: EnemyPokemon = game.scene.getEnemyParty()[0]!;
|
||||
const boss1SegmentHp = boss1.getMaxHp() / boss1.bossSegments;
|
||||
const requiredDamageBoss1 = boss1SegmentHp * (1 + Math.pow(2, brokenShields));
|
||||
expect(boss1.isBoss()).toBe(true);
|
||||
expect(boss1.bossSegments).toBe(5);
|
||||
expect(boss1.bossSegmentIndex).toBe(4);
|
||||
|
||||
// Not enough damage to break through all shields
|
||||
boss1.damageAndUpdate(Math.floor(requiredDamageBoss1 - 5));
|
||||
expect(boss1.bossSegmentIndex).toBe(1);
|
||||
expect(boss1.hp).toBe(boss1.getMaxHp() - toDmgValue(boss1SegmentHp * 3));
|
||||
|
||||
const boss2: EnemyPokemon = game.scene.getEnemyParty()[1]!;
|
||||
const boss2SegmentHp = boss2.getMaxHp() / boss2.bossSegments;
|
||||
const requiredDamageBoss2 = boss2SegmentHp * (1 + Math.pow(2, brokenShields));
|
||||
|
||||
expect(boss2.isBoss()).toBe(true);
|
||||
expect(boss2.bossSegments).toBe(5);
|
||||
|
||||
// Enough damage to break through all shields
|
||||
boss2.damageAndUpdate(Math.ceil(requiredDamageBoss2));
|
||||
expect(boss2.bossSegmentIndex).toBe(0);
|
||||
expect(boss2.hp).toBe(boss2.getMaxHp() - toDmgValue(boss2SegmentHp * 4));
|
||||
|
||||
}, TIMEOUT);
|
||||
|
||||
it("the number of stats boosts is consistent when several shields are broken at once", async () => {
|
||||
const shieldsToBreak = 4;
|
||||
|
||||
game.override
|
||||
.battleType("double")
|
||||
.enemyHealthSegments(shieldsToBreak + 1);
|
||||
|
||||
await game.classicMode.startBattle([ Species.MEWTWO ]);
|
||||
|
||||
const boss1: EnemyPokemon = game.scene.getEnemyParty()[0]!;
|
||||
const boss1SegmentHp = boss1.getMaxHp() / boss1.bossSegments;
|
||||
const singleShieldDamage = Math.ceil(boss1SegmentHp);
|
||||
expect(boss1.isBoss()).toBe(true);
|
||||
expect(boss1.bossSegments).toBe(shieldsToBreak + 1);
|
||||
expect(boss1.bossSegmentIndex).toBe(shieldsToBreak);
|
||||
expect(getTotalStatBoosts(boss1)).toBe(0);
|
||||
|
||||
|
||||
let totalStats = 0;
|
||||
|
||||
// Break the shields one by one
|
||||
for (let i = 1; i <= shieldsToBreak; i++) {
|
||||
boss1.damageAndUpdate(singleShieldDamage);
|
||||
expect(boss1.bossSegmentIndex).toBe(shieldsToBreak - i);
|
||||
expect(boss1.hp).toBe(boss1.getMaxHp() - toDmgValue(boss1SegmentHp * i));
|
||||
// Do nothing and go to next turn so that the StatChangePhase gets applied
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
// All broken shields give +1 stat boost, except the last two that gives +2
|
||||
totalStats += i >= shieldsToBreak -1? 2 : 1;
|
||||
expect(getTotalStatBoosts(boss1)).toBe(totalStats);
|
||||
}
|
||||
|
||||
const boss2: EnemyPokemon = game.scene.getEnemyParty()[1]!;
|
||||
const boss2SegmentHp = boss2.getMaxHp() / boss2.bossSegments;
|
||||
const requiredDamage = boss2SegmentHp * (1 + Math.pow(2, shieldsToBreak - 1));
|
||||
|
||||
expect(boss2.isBoss()).toBe(true);
|
||||
expect(boss2.bossSegments).toBe(shieldsToBreak + 1);
|
||||
expect(boss2.bossSegmentIndex).toBe(shieldsToBreak);
|
||||
expect(getTotalStatBoosts(boss2)).toBe(0);
|
||||
|
||||
// Enough damage to break all shields at once
|
||||
boss2.damageAndUpdate(Math.ceil(requiredDamage));
|
||||
expect(boss2.bossSegmentIndex).toBe(0);
|
||||
expect(boss2.hp).toBe(boss2.getMaxHp() - toDmgValue(boss2SegmentHp * shieldsToBreak));
|
||||
// Do nothing and go to next turn so that the StatChangePhase gets applied
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
expect(getTotalStatBoosts(boss2)).toBe(totalStats);
|
||||
|
||||
}, TIMEOUT);
|
||||
|
||||
/**
|
||||
* Gets the sum of the ATK, DEF, SP ATK, SP DEF and SPD boosts for the given Pokemon
|
||||
* @param enemyPokemon the pokemon to get stats from
|
||||
* @returns the total stats boosts
|
||||
*/
|
||||
function getTotalStatBoosts(enemyPokemon: EnemyPokemon): number {
|
||||
const enemyBattleStats = enemyPokemon.summonData.battleStats;
|
||||
return enemyBattleStats?.reduce(statsSum, 0);
|
||||
}
|
||||
|
||||
function statsSum(total: number, value: number, index: number) {
|
||||
if (index <= BattleStat.SPD) {
|
||||
return total + value;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
});
|
||||
|
|
@ -27,7 +27,7 @@ describe("Moves - Fusion Flare", () => {
|
|||
game.override.moveset([fusionFlare]);
|
||||
game.override.startingLevel(1);
|
||||
|
||||
game.override.enemySpecies(Species.RESHIRAM);
|
||||
game.override.enemySpecies(Species.RATTATA);
|
||||
game.override.enemyMoveset([Moves.REST, Moves.REST, Moves.REST, Moves.REST]);
|
||||
|
||||
game.override.battleType("single");
|
||||
|
|
|
@ -281,6 +281,20 @@ export class OverridesHelper extends GameManagerHelper {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the enemy (Pokemon) to have the given amount of health segments
|
||||
* @param healthSegments the number of segments to give
|
||||
* default: 0, the health segments will be handled like in the game based on wave, level and species
|
||||
* 1: the Pokemon will not be a boss
|
||||
* 2+: the Pokemon will be a boss with the given number of health segments
|
||||
* @returns this
|
||||
*/
|
||||
enemyHealthSegments(healthSegments: number) {
|
||||
vi.spyOn(Overrides, "OPP_HEALTH_SEGMENTS_OVERRIDE", "get").mockReturnValue(healthSegments);
|
||||
this.log("Enemy Pokemon health segments set to:", healthSegments);
|
||||
return this;
|
||||
}
|
||||
|
||||
private log(...params: any[]) {
|
||||
console.log("Overrides:", ...params);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue