mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-09-23 15:03:24 +02:00
[Refactor] Refactor boss health segment calculation to improve clarity (#6574)
* Refactor boss health bar * Apply Kev's suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
This commit is contained in:
parent
cffbafe4bd
commit
8d5ba221d8
@ -170,6 +170,7 @@ import {
|
|||||||
rgbToHsv,
|
rgbToHsv,
|
||||||
toDmgValue,
|
toDmgValue,
|
||||||
} from "#utils/common";
|
} from "#utils/common";
|
||||||
|
import { calculateBossSegmentDamage } from "#utils/damage";
|
||||||
import { getEnumValues } from "#utils/enums";
|
import { getEnumValues } from "#utils/enums";
|
||||||
import { getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
import { getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||||
import { argbFromRgba, QuantizerCelebi, rgbaFromArgb } from "@material/material-color-utilities";
|
import { argbFromRgba, QuantizerCelebi, rgbaFromArgb } from "@material/material-color-utilities";
|
||||||
@ -6764,32 +6765,17 @@ export class EnemyPokemon extends Pokemon {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const segmentSize = this.getMaxHp() / this.bossSegments;
|
||||||
|
|
||||||
let clearedBossSegmentIndex = this.isBoss() ? this.bossSegmentIndex + 1 : 0;
|
let clearedBossSegmentIndex = this.isBoss() ? this.bossSegmentIndex + 1 : 0;
|
||||||
|
|
||||||
if (this.isBoss() && !ignoreSegments) {
|
if (this.isBoss() && !ignoreSegments) {
|
||||||
const segmentSize = this.getMaxHp() / this.bossSegments;
|
[damage, clearedBossSegmentIndex] = calculateBossSegmentDamage(
|
||||||
for (let s = this.bossSegmentIndex; s > 0; s--) {
|
damage,
|
||||||
const hpThreshold = segmentSize * s;
|
this.hp,
|
||||||
const roundedHpThreshold = Math.round(hpThreshold);
|
segmentSize,
|
||||||
if (this.hp >= roundedHpThreshold) {
|
this.getMinimumSegmentIndex(),
|
||||||
if (this.hp - damage <= roundedHpThreshold) {
|
);
|
||||||
const hpRemainder = this.hp - roundedHpThreshold;
|
|
||||||
let segmentsBypassed = 0;
|
|
||||||
while (
|
|
||||||
segmentsBypassed < this.bossSegmentIndex
|
|
||||||
&& this.canBypassBossSegments(segmentsBypassed + 1)
|
|
||||||
&& damage - hpRemainder >= Math.round(segmentSize * Math.pow(2, segmentsBypassed + 1))
|
|
||||||
) {
|
|
||||||
segmentsBypassed++;
|
|
||||||
//console.log('damage', damage, 'segment', segmentsBypassed + 1, 'segment size', segmentSize, 'damage needed', Math.round(segmentSize * Math.pow(2, segmentsBypassed + 1)));
|
|
||||||
}
|
|
||||||
|
|
||||||
damage = toDmgValue(this.hp - hpThreshold + segmentSize * segmentsBypassed);
|
|
||||||
clearedBossSegmentIndex = s - segmentsBypassed;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (globalScene.currentBattle.battleSpec) {
|
switch (globalScene.currentBattle.battleSpec) {
|
||||||
@ -6803,7 +6789,6 @@ export class EnemyPokemon extends Pokemon {
|
|||||||
|
|
||||||
if (this.isBoss()) {
|
if (this.isBoss()) {
|
||||||
if (ignoreSegments) {
|
if (ignoreSegments) {
|
||||||
const segmentSize = this.getMaxHp() / this.bossSegments;
|
|
||||||
clearedBossSegmentIndex = Math.ceil(this.hp / segmentSize);
|
clearedBossSegmentIndex = Math.ceil(this.hp / segmentSize);
|
||||||
}
|
}
|
||||||
if (clearedBossSegmentIndex <= this.bossSegmentIndex) {
|
if (clearedBossSegmentIndex <= this.bossSegmentIndex) {
|
||||||
@ -6815,16 +6800,12 @@ export class EnemyPokemon extends Pokemon {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
private canBypassBossSegments(segmentCount = 1): boolean {
|
private getMinimumSegmentIndex(): number {
|
||||||
if (
|
if (globalScene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS && !this.formIndex) {
|
||||||
globalScene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS
|
return 1;
|
||||||
&& !this.formIndex
|
|
||||||
&& this.bossSegmentIndex - segmentCount < 1
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
65
src/utils/damage.ts
Normal file
65
src/utils/damage.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-Copyright-Text: 2025 Pagefault Games
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* Utility functions relating to damage calculations.
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { toDmgValue } from "#utils/common";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the adjusted damage and number of boss segments bypassed for a damage interaction
|
||||||
|
* @param damage - The raw damage dealt.
|
||||||
|
* @param currentHp - The target's current HP
|
||||||
|
* @param segmentHp - The HP in each segment (total HP / number of segments)
|
||||||
|
* @param minSegmentIndex - The minimum segment index that can be cleared; default `0` (all segments). Used for the final boss
|
||||||
|
* @returns A tuple consisting of the adjusted damage and index of the boss segment the target is in after damage is applied.
|
||||||
|
*/
|
||||||
|
export function calculateBossSegmentDamage(
|
||||||
|
damage: number,
|
||||||
|
currentHp: number,
|
||||||
|
segmentHp: number,
|
||||||
|
minSegmentIndex = 0,
|
||||||
|
): [adjustedDamage: number, clearedBossSegmentIndex: number] {
|
||||||
|
const segmentIndex = Math.ceil(currentHp / segmentHp) - 1;
|
||||||
|
if (segmentIndex <= 0) {
|
||||||
|
return [damage, 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The HP that the current segment ends at.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* If a Pokemon has 3 segments and 300 max HP, each segment is 100 HP.
|
||||||
|
* In the first iteration, this would be 200 HP, as the first segment ends at 200 HP.
|
||||||
|
*/
|
||||||
|
const segmentThreshold = segmentHp * segmentIndex;
|
||||||
|
const roundedSegmentThreshold = Math.round(segmentThreshold);
|
||||||
|
|
||||||
|
const remainingSegmentHp = currentHp - roundedSegmentThreshold;
|
||||||
|
|
||||||
|
const leftoverDamage = damage - remainingSegmentHp;
|
||||||
|
|
||||||
|
// Insufficient damage to get down to current segment HP, return original damage
|
||||||
|
// Segment index + 1 because this segment is not considered cleared
|
||||||
|
if (leftoverDamage < 0) {
|
||||||
|
return [damage, segmentIndex + 1];
|
||||||
|
}
|
||||||
|
if (leftoverDamage === 0) {
|
||||||
|
return [damage, segmentIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Breaking the nth segment requires dealing at least segmentHp * 2^(n-1) damage
|
||||||
|
// and must ensure at least `segmentIndex - minSegmentIndex` segments remain
|
||||||
|
const segmentsBypassed = Math.min(
|
||||||
|
Math.max(Math.floor(Math.log2(leftoverDamage / segmentHp)), 0),
|
||||||
|
segmentIndex - minSegmentIndex,
|
||||||
|
);
|
||||||
|
const adjustedDamage = toDmgValue(currentHp - segmentThreshold + segmentHp * segmentsBypassed);
|
||||||
|
const clearedBossSegmentIndex = segmentIndex - segmentsBypassed;
|
||||||
|
|
||||||
|
return [adjustedDamage, clearedBossSegmentIndex];
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { EFFECTIVE_STATS } from "#enums/stat";
|
import { EFFECTIVE_STATS, Stat } from "#enums/stat";
|
||||||
import type { EnemyPokemon } from "#field/pokemon";
|
import type { EnemyPokemon } from "#field/pokemon";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
import { toDmgValue } from "#utils/common";
|
import { toDmgValue } from "#utils/common";
|
||||||
@ -73,8 +73,9 @@ describe("Boss Pokemon / Shields", () => {
|
|||||||
expect(boss2.bossSegments).toBe(2);
|
expect(boss2.bossSegments).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shields should stop overflow damage and give stat stage boosts when broken", async () => {
|
// TODO: This test is flaky. It passes when run individually, but not in tandem with others
|
||||||
game.override.startingWave(150); // Floor 150 > 2 shields / 3 health segments
|
it.todo("shields should stop overflow damage and give stat stage boosts when broken", async () => {
|
||||||
|
game.override.startingWave(150).startingLevel(5000); // Floor 150 > 2 shields / 3 health segments
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MEWTWO]);
|
await game.classicMode.startBattle([SpeciesId.MEWTWO]);
|
||||||
|
|
||||||
@ -137,14 +138,17 @@ describe("Boss Pokemon / Shields", () => {
|
|||||||
|
|
||||||
it("the number of stat stage boosts is consistent when several shields are broken at once", async () => {
|
it("the number of stat stage boosts is consistent when several shields are broken at once", async () => {
|
||||||
const shieldsToBreak = 4;
|
const shieldsToBreak = 4;
|
||||||
|
const segmentHp = 100;
|
||||||
|
|
||||||
game.override.battleStyle("double").enemyHealthSegments(shieldsToBreak + 1);
|
game.override.battleStyle("double").enemyHealthSegments(shieldsToBreak + 1);
|
||||||
|
|
||||||
await game.classicMode.startBattle([SpeciesId.MEWTWO]);
|
await game.classicMode.startBattle([SpeciesId.MEWTWO]);
|
||||||
|
|
||||||
const boss1 = game.field.getEnemyPokemon();
|
const boss1 = game.field.getEnemyPokemon();
|
||||||
const boss1SegmentHp = boss1.getMaxHp() / boss1.bossSegments;
|
boss1.setStat(Stat.HP, (shieldsToBreak + 1) * segmentHp); // Set HP to a known value for easier calculations
|
||||||
const singleShieldDamage = Math.ceil(boss1SegmentHp);
|
boss1.hp = boss1.getMaxHp();
|
||||||
|
const boss1SegmentHp = segmentHp;
|
||||||
|
const singleShieldDamage = boss1SegmentHp; // Damage to break a single shield
|
||||||
expect(boss1.isBoss()).toBe(true);
|
expect(boss1.isBoss()).toBe(true);
|
||||||
expect(boss1.bossSegments).toBe(shieldsToBreak + 1);
|
expect(boss1.bossSegments).toBe(shieldsToBreak + 1);
|
||||||
expect(boss1.bossSegmentIndex).toBe(shieldsToBreak);
|
expect(boss1.bossSegmentIndex).toBe(shieldsToBreak);
|
||||||
|
57
test/utils/damage.test.ts
Normal file
57
test/utils/damage.test.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
import { calculateBossSegmentDamage } from "#utils/damage";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
describe("Unit Test - calculateBossSegmentDamage", () => {
|
||||||
|
it("Allows multiple segments to be cleared", () => {
|
||||||
|
// Deal 850 damage to a target with 800 max HP that has 8 segments.
|
||||||
|
const [adjusted, clearedIndex] = calculateBossSegmentDamage(850, 800, 100);
|
||||||
|
expect(adjusted).toEqual(300);
|
||||||
|
expect(clearedIndex).toEqual(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns original damage and next segment index if damage is insufficient to clear current segment", () => {
|
||||||
|
// segmentHp = 100, currentHp = 250 (segmentIndex = 2), damage = 30
|
||||||
|
// remainingSegmentHp = 250 - 200 = 50, leftoverDamage = 30 - 50 = -20
|
||||||
|
const [adjusted, clearedIndex] = calculateBossSegmentDamage(30, 250, 100);
|
||||||
|
expect(adjusted).toBe(30);
|
||||||
|
expect(clearedIndex).toBe(3); // segmentIndex + 1
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns adjusted damage and decremented segment index when damage clears one segment", () => {
|
||||||
|
// segmentHp = 100, currentHp = 250 (segmentIndex = 2), damage = 60
|
||||||
|
// remainingSegmentHp = 50, leftoverDamage = 10
|
||||||
|
const [adjusted, clearedIndex] = calculateBossSegmentDamage(60, 250, 100);
|
||||||
|
expect(adjusted).toBeGreaterThanOrEqual(50); // at least remainingSegmentHp
|
||||||
|
expect(clearedIndex).toBe(2); // segmentIndex - segmentsBypassed (0)
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles exact segment boundary", () => {
|
||||||
|
// segmentHp = 100, currentHp = 200 (segmentIndex = 1), damage = 150
|
||||||
|
// remainingSegmentHp = 200 = 100, leftoverDamage = 0
|
||||||
|
const [adjusted, clearedIndex] = calculateBossSegmentDamage(150, 200, 100);
|
||||||
|
expect(adjusted).toBe(100);
|
||||||
|
expect(clearedIndex).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles exact segment boundary for small damage", () => {
|
||||||
|
// segmentHp = 100, currentHp = 200 (segmentIndex = 1), damage = 150
|
||||||
|
// remainingSegmentHp = 200 = 100, leftoverDamage = 0
|
||||||
|
const [adjusted, clearedIndex] = calculateBossSegmentDamage(50, 200, 100);
|
||||||
|
expect(adjusted).toBe(50);
|
||||||
|
expect(clearedIndex).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles single segment case", () => {
|
||||||
|
// segmentHp = 100, currentHp = 100, damage = 50
|
||||||
|
const [adjusted, clearedIndex] = calculateBossSegmentDamage(50, 100, 100);
|
||||||
|
expect(adjusted).toBe(50);
|
||||||
|
expect(clearedIndex).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles zero damage", () => {
|
||||||
|
// segmentHp = 100, currentHp = 250, damage = 0
|
||||||
|
const [adjusted, clearedIndex] = calculateBossSegmentDamage(0, 250, 100);
|
||||||
|
expect(adjusted).toBe(0);
|
||||||
|
expect(clearedIndex).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user