mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-19 22:09:27 +02:00
Merge branch 'beta' into candy-friendship-changes
This commit is contained in:
commit
2a4f97b756
@ -1385,16 +1385,40 @@ export class UserHpDamageAttr extends FixedDamageAttr {
|
||||
}
|
||||
|
||||
export class TargetHalfHpDamageAttr extends FixedDamageAttr {
|
||||
// the initial amount of hp the target had before the first hit
|
||||
// used for multi lens
|
||||
private initialHp: number;
|
||||
constructor() {
|
||||
super(0);
|
||||
}
|
||||
|
||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||
(args[0] as Utils.IntegerHolder).value = Utils.toDmgValue(target.hp / 2);
|
||||
|
||||
// first, determine if the hit is coming from multi lens or not
|
||||
const lensCount = user.getHeldItems().find(i => i instanceof PokemonMultiHitModifier)?.getStackCount() ?? 0;
|
||||
if (lensCount <= 0) {
|
||||
// no multi lenses; we can just halve the target's hp and call it a day
|
||||
(args[0] as Utils.NumberHolder).value = Utils.toDmgValue(target.hp / 2);
|
||||
return true;
|
||||
}
|
||||
|
||||
// figure out what hit # we're on
|
||||
switch (user.turnData.hitCount - user.turnData.hitsLeft) {
|
||||
case 0:
|
||||
// first hit of move; update initialHp tracker
|
||||
this.initialHp = target.hp;
|
||||
default:
|
||||
// multi lens added hit; use initialHp tracker to ensure correct damage
|
||||
(args[0] as Utils.NumberHolder).value = Utils.toDmgValue(this.initialHp / 2);
|
||||
return true;
|
||||
break;
|
||||
case lensCount + 1:
|
||||
// parental bond added hit; calc damage as normal
|
||||
(args[0] as Utils.NumberHolder).value = Utils.toDmgValue(target.hp / 2);
|
||||
return true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||
return target.getHpRatio() > 0.5 ? Math.floor(((target.getHpRatio() - 0.5) * -24) + 4) : -20;
|
||||
}
|
||||
@ -5975,14 +5999,22 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
}
|
||||
}
|
||||
|
||||
if (switchOutTarget.scene.getPlayerParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
|
||||
// Find indices of off-field Pokemon that are eligible to be switched into
|
||||
const eligibleNewIndices: number[] = [];
|
||||
switchOutTarget.scene.getPlayerParty().forEach((pokemon, index) => {
|
||||
if (pokemon.isAllowedInBattle() && !pokemon.isOnField()) {
|
||||
eligibleNewIndices.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
if (eligibleNewIndices.length < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (switchOutTarget.hp > 0) {
|
||||
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
||||
switchOutTarget.leaveField(true);
|
||||
const slotIndex = Utils.randIntRange(user.scene.currentBattle.getBattlerCount(), user.scene.getPlayerParty().length);
|
||||
const slotIndex = eligibleNewIndices[user.randSeedInt(eligibleNewIndices.length)];
|
||||
user.scene.prependToPhase(
|
||||
new SwitchSummonPhase(
|
||||
user.scene,
|
||||
@ -6011,14 +6043,22 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
||||
}
|
||||
return false;
|
||||
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) { // Switch out logic for enemy trainers
|
||||
if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) {
|
||||
// Find indices of off-field Pokemon that are eligible to be switched into
|
||||
const eligibleNewIndices: number[] = [];
|
||||
switchOutTarget.scene.getEnemyParty().forEach((pokemon, index) => {
|
||||
if (pokemon.isAllowedInBattle() && !pokemon.isOnField()) {
|
||||
eligibleNewIndices.push(index);
|
||||
}
|
||||
});
|
||||
|
||||
if (eligibleNewIndices.length < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (switchOutTarget.hp > 0) {
|
||||
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
||||
switchOutTarget.leaveField(true);
|
||||
const slotIndex = Utils.randIntRange(user.scene.currentBattle.getBattlerCount(), user.scene.getEnemyParty().length);
|
||||
const slotIndex = eligibleNewIndices[user.randSeedInt(eligibleNewIndices.length)];
|
||||
user.scene.prependToPhase(
|
||||
new SwitchSummonPhase(
|
||||
user.scene,
|
||||
|
@ -2618,8 +2618,8 @@ 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 multiLensMultiplier = new Utils.NumberHolder(1);
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { Mode } from "#app/ui/ui";
|
||||
import i18next from "i18next";
|
||||
import BattleScene from "../../battle-scene";
|
||||
import { hasTouchscreen } from "../../touch-controls";
|
||||
import { updateWindowType } from "../../ui/ui-theme";
|
||||
import { CandyUpgradeNotificationChangedEvent } from "../../events/battle-scene";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { hasTouchscreen } from "#app/touch-controls";
|
||||
import { updateWindowType } from "#app/ui/ui-theme";
|
||||
import { CandyUpgradeNotificationChangedEvent } from "#app/events/battle-scene";
|
||||
import SettingsUiHandler from "#app/ui/settings/settings-ui-handler";
|
||||
import { EaseType } from "#enums/ease-type";
|
||||
import { MoneyFormat } from "#enums/money-format";
|
||||
@ -44,6 +44,7 @@ const OFF_ON: SettingOption[] = [
|
||||
label: i18next.t("settings:on")
|
||||
}
|
||||
];
|
||||
|
||||
const AUTO_DISABLED: SettingOption[] = [
|
||||
{
|
||||
value: "Auto",
|
||||
@ -55,6 +56,19 @@ const AUTO_DISABLED: SettingOption[] = [
|
||||
}
|
||||
];
|
||||
|
||||
const TOUCH_CONTROLS_OPTIONS: SettingOption[] = [
|
||||
{
|
||||
value: "Auto",
|
||||
label: i18next.t("settings:auto")
|
||||
},
|
||||
{
|
||||
value: "Disabled",
|
||||
label: i18next.t("settings:disabled"),
|
||||
needConfirmation: true,
|
||||
confirmationMessage: i18next.t("settings:confirmDisableTouch")
|
||||
}
|
||||
];
|
||||
|
||||
const SHOP_CURSOR_TARGET_OPTIONS: SettingOption[] = [
|
||||
{
|
||||
value: "Rewards",
|
||||
@ -100,7 +114,9 @@ export enum SettingType {
|
||||
|
||||
type SettingOption = {
|
||||
value: string,
|
||||
label: string
|
||||
label: string,
|
||||
needConfirmation?: boolean,
|
||||
confirmationMessage?: string
|
||||
};
|
||||
|
||||
export interface Setting {
|
||||
@ -344,13 +360,6 @@ export const Setting: Array<Setting> = [
|
||||
default: 1,
|
||||
type: SettingType.GENERAL
|
||||
},
|
||||
{
|
||||
key: SettingKeys.Touch_Controls,
|
||||
label: i18next.t("settings:touchControls"),
|
||||
options: AUTO_DISABLED,
|
||||
default: 0,
|
||||
type: SettingType.GENERAL
|
||||
},
|
||||
{
|
||||
key: SettingKeys.Vibration,
|
||||
label: i18next.t("settings:vibrations"),
|
||||
@ -358,6 +367,28 @@ export const Setting: Array<Setting> = [
|
||||
default: 0,
|
||||
type: SettingType.GENERAL
|
||||
},
|
||||
{
|
||||
key: SettingKeys.Touch_Controls,
|
||||
label: i18next.t("settings:touchControls"),
|
||||
options: TOUCH_CONTROLS_OPTIONS,
|
||||
default: 0,
|
||||
type: SettingType.GENERAL,
|
||||
isHidden: () => !hasTouchscreen()
|
||||
},
|
||||
{
|
||||
key: SettingKeys.Move_Touch_Controls,
|
||||
label: i18next.t("settings:moveTouchControls"),
|
||||
options: [
|
||||
{
|
||||
value: "Configure",
|
||||
label: i18next.t("settings:change")
|
||||
}
|
||||
],
|
||||
default: 0,
|
||||
type: SettingType.GENERAL,
|
||||
activatable: true,
|
||||
isHidden: () => !hasTouchscreen()
|
||||
},
|
||||
{
|
||||
key: SettingKeys.Language,
|
||||
label: i18next.t("settings:language"),
|
||||
@ -643,20 +674,6 @@ export const Setting: Array<Setting> = [
|
||||
type: SettingType.AUDIO,
|
||||
requireReload: true
|
||||
},
|
||||
{
|
||||
key: SettingKeys.Move_Touch_Controls,
|
||||
label: i18next.t("settings:moveTouchControls"),
|
||||
options: [
|
||||
{
|
||||
value: "Configure",
|
||||
label: i18next.t("settings:change")
|
||||
}
|
||||
],
|
||||
default: 0,
|
||||
type: SettingType.GENERAL,
|
||||
activatable: true,
|
||||
isHidden: () => !hasTouchscreen()
|
||||
},
|
||||
{
|
||||
key: SettingKeys.Shop_Cursor_Target,
|
||||
label: i18next.t("settings:shopCursorTarget"),
|
||||
@ -849,7 +866,7 @@ export function setSetting(scene: BattleScene, setting: string, value: integer):
|
||||
if (scene.ui) {
|
||||
const cancelHandler = () => {
|
||||
scene.ui.revertMode();
|
||||
(scene.ui.getHandler() as SettingsUiHandler).setOptionCursor(0, 0, true);
|
||||
(scene.ui.getHandler() as SettingsUiHandler).setOptionCursor(-1, 0, true);
|
||||
};
|
||||
const changeLocaleHandler = (locale: string): boolean => {
|
||||
try {
|
||||
|
@ -135,4 +135,57 @@ 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(1000)
|
||||
.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");
|
||||
expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.5, 5);
|
||||
});
|
||||
|
||||
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(1000)
|
||||
.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");
|
||||
expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.5, 5);
|
||||
});
|
||||
it("should result in correct damage for hp% attacks with 2 lenses + Parental Bond", async () => {
|
||||
game.override.startingHeldItems([{ name: "MULTI_LENS", count: 2 }])
|
||||
.moveset(Moves.SUPER_FANG)
|
||||
.ability(Abilities.PARENTAL_BOND)
|
||||
.passiveAbility(Abilities.COMPOUND_EYES)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.enemyLevel(1000)
|
||||
.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");
|
||||
expect(enemyPokemon.getHpRatio()).toBeCloseTo(0.25, 5);
|
||||
});
|
||||
});
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { allMoves } from "#app/data/move";
|
||||
import { Status } from "#app/data/status-effect";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { Type } from "#enums/type";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
@ -193,4 +197,122 @@ describe("Moves - Dragon Tail", () => {
|
||||
expect(dratini.hp).toBe(Math.floor(dratini.getMaxHp() / 2));
|
||||
expect(game.scene.getPlayerField().length).toBe(1);
|
||||
});
|
||||
|
||||
it("should force switches randomly", async () => {
|
||||
game.override.enemyMoveset(Moves.DRAGON_TAIL)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(1);
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
|
||||
|
||||
const [ bulbasaur, charmander, squirtle ] = game.scene.getPlayerParty();
|
||||
|
||||
// Turn 1: Mock an RNG call that calls for switching to 1st backup Pokemon (Charmander)
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||
return min;
|
||||
});
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.DRAGON_TAIL);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(bulbasaur.isOnField()).toBe(false);
|
||||
expect(charmander.isOnField()).toBe(true);
|
||||
expect(squirtle.isOnField()).toBe(false);
|
||||
expect(bulbasaur.getInverseHp()).toBeGreaterThan(0);
|
||||
|
||||
// Turn 2: Mock an RNG call that calls for switching to 2nd backup Pokemon (Squirtle)
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||
return min + 1;
|
||||
});
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(bulbasaur.isOnField()).toBe(false);
|
||||
expect(charmander.isOnField()).toBe(false);
|
||||
expect(squirtle.isOnField()).toBe(true);
|
||||
expect(charmander.getInverseHp()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should not force a switch to a challenge-ineligible Pokemon", async () => {
|
||||
game.override.enemyMoveset(Moves.DRAGON_TAIL)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(1);
|
||||
// Mono-Water challenge, Eevee is ineligible
|
||||
game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, Type.WATER + 1, 0);
|
||||
await game.challengeMode.startBattle([ Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA ]);
|
||||
|
||||
const [ lapras, eevee, toxapex, primarina ] = game.scene.getPlayerParty();
|
||||
|
||||
// Turn 1: Mock an RNG call that would normally call for switching to Eevee, but it is ineligible
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||
return min;
|
||||
});
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(lapras.isOnField()).toBe(false);
|
||||
expect(eevee.isOnField()).toBe(false);
|
||||
expect(toxapex.isOnField()).toBe(true);
|
||||
expect(primarina.isOnField()).toBe(false);
|
||||
expect(lapras.getInverseHp()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should not force a switch to a fainted Pokemon", async () => {
|
||||
game.override.enemyMoveset([ Moves.SPLASH, Moves.DRAGON_TAIL ])
|
||||
.startingLevel(100)
|
||||
.enemyLevel(1);
|
||||
await game.classicMode.startBattle([ Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA ]);
|
||||
|
||||
const [ lapras, eevee, toxapex, primarina ] = game.scene.getPlayerParty();
|
||||
|
||||
// Turn 1: Eevee faints
|
||||
eevee.hp = 0;
|
||||
eevee.status = new Status(StatusEffect.FAINT);
|
||||
expect(eevee.isFainted()).toBe(true);
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||
return min;
|
||||
});
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.DRAGON_TAIL);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(lapras.isOnField()).toBe(false);
|
||||
expect(eevee.isOnField()).toBe(false);
|
||||
expect(toxapex.isOnField()).toBe(true);
|
||||
expect(primarina.isOnField()).toBe(false);
|
||||
expect(lapras.getInverseHp()).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should not force a switch if there are no available Pokemon to switch into", async () => {
|
||||
game.override.enemyMoveset([ Moves.SPLASH, Moves.DRAGON_TAIL ])
|
||||
.startingLevel(100)
|
||||
.enemyLevel(1);
|
||||
await game.classicMode.startBattle([ Species.LAPRAS, Species.EEVEE ]);
|
||||
|
||||
const [ lapras, eevee ] = game.scene.getPlayerParty();
|
||||
|
||||
// Turn 1: Eevee faints
|
||||
eevee.hp = 0;
|
||||
eevee.status = new Status(StatusEffect.FAINT);
|
||||
expect(eevee.isFainted()).toBe(true);
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||
return min;
|
||||
});
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.DRAGON_TAIL);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(lapras.isOnField()).toBe(true);
|
||||
expect(eevee.isOnField()).toBe(false);
|
||||
expect(lapras.getInverseHp()).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
@ -1,11 +1,15 @@
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { Type } from "#enums/type";
|
||||
import { MoveResult } from "#app/field/pokemon";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { Status } from "#app/data/status-effect";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
|
||||
describe("Moves - Whirlwind", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
@ -25,8 +29,9 @@ describe("Moves - Whirlwind", () => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleType("single")
|
||||
.moveset(Moves.SPLASH)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset(Moves.WHIRLWIND)
|
||||
.enemyMoveset([ Moves.SPLASH, Moves.WHIRLWIND ])
|
||||
.enemySpecies(Species.PIDGEY);
|
||||
});
|
||||
|
||||
@ -41,10 +46,114 @@ describe("Moves - Whirlwind", () => {
|
||||
const staraptor = game.scene.getPlayerPokemon()!;
|
||||
|
||||
game.move.select(move);
|
||||
await game.forceEnemyMove(Moves.WHIRLWIND);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
|
||||
expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined();
|
||||
expect(game.scene.getEnemyPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
|
||||
});
|
||||
|
||||
it("should force switches randomly", async () => {
|
||||
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE ]);
|
||||
|
||||
const [ bulbasaur, charmander, squirtle ] = game.scene.getPlayerParty();
|
||||
|
||||
// Turn 1: Mock an RNG call that calls for switching to 1st backup Pokemon (Charmander)
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||
return min;
|
||||
});
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.WHIRLWIND);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(bulbasaur.isOnField()).toBe(false);
|
||||
expect(charmander.isOnField()).toBe(true);
|
||||
expect(squirtle.isOnField()).toBe(false);
|
||||
|
||||
// Turn 2: Mock an RNG call that calls for switching to 2nd backup Pokemon (Squirtle)
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||
return min + 1;
|
||||
});
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.WHIRLWIND);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(bulbasaur.isOnField()).toBe(false);
|
||||
expect(charmander.isOnField()).toBe(false);
|
||||
expect(squirtle.isOnField()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not force a switch to a challenge-ineligible Pokemon", async () => {
|
||||
// Mono-Water challenge, Eevee is ineligible
|
||||
game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, Type.WATER + 1, 0);
|
||||
await game.challengeMode.startBattle([ Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA ]);
|
||||
|
||||
const [ lapras, eevee, toxapex, primarina ] = game.scene.getPlayerParty();
|
||||
|
||||
// Turn 1: Mock an RNG call that would normally call for switching to Eevee, but it is ineligible
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||
return min;
|
||||
});
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.WHIRLWIND);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(lapras.isOnField()).toBe(false);
|
||||
expect(eevee.isOnField()).toBe(false);
|
||||
expect(toxapex.isOnField()).toBe(true);
|
||||
expect(primarina.isOnField()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not force a switch to a fainted Pokemon", async () => {
|
||||
await game.classicMode.startBattle([ Species.LAPRAS, Species.EEVEE, Species.TOXAPEX, Species.PRIMARINA ]);
|
||||
|
||||
const [ lapras, eevee, toxapex, primarina ] = game.scene.getPlayerParty();
|
||||
|
||||
// Turn 1: Eevee faints
|
||||
eevee.hp = 0;
|
||||
eevee.status = new Status(StatusEffect.FAINT);
|
||||
expect(eevee.isFainted()).toBe(true);
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||
return min;
|
||||
});
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.WHIRLWIND);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(lapras.isOnField()).toBe(false);
|
||||
expect(eevee.isOnField()).toBe(false);
|
||||
expect(toxapex.isOnField()).toBe(true);
|
||||
expect(primarina.isOnField()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not force a switch if there are no available Pokemon to switch into", async () => {
|
||||
await game.classicMode.startBattle([ Species.LAPRAS, Species.EEVEE ]);
|
||||
|
||||
const [ lapras, eevee ] = game.scene.getPlayerParty();
|
||||
|
||||
// Turn 1: Eevee faints
|
||||
eevee.hp = 0;
|
||||
eevee.status = new Status(StatusEffect.FAINT);
|
||||
expect(eevee.isFainted()).toBe(true);
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// Turn 2: Mock an RNG call that would normally call for switching to Eevee, but it is fainted
|
||||
vi.spyOn(game.scene, "randBattleSeedInt").mockImplementation((range, min: number = 0) => {
|
||||
return min;
|
||||
});
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.WHIRLWIND);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(lapras.isOnField()).toBe(true);
|
||||
expect(eevee.isOnField()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
@ -313,6 +313,11 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container {
|
||||
this.pokemonShinyNewIcon.setShadowColor(getTextColor(TextStyle.SUMMARY_BLUE, true, this.scene.uiTheme));
|
||||
const newShinyOrVariant = ((newShiny & caughtAttr) === BigInt(0)) || ((newVariant & caughtAttr) === BigInt(0));
|
||||
this.pokemonShinyNewIcon.setVisible(!!newShinyOrVariant);
|
||||
} else if ((caughtAttr & DexAttr.NON_SHINY) === BigInt(0) && ((caughtAttr & DexAttr.SHINY) === DexAttr.SHINY)) { //If the player has *only* caught any shiny variant of this species, not a non-shiny
|
||||
this.pokemonShinyNewIcon.setVisible(true);
|
||||
this.pokemonShinyNewIcon.setText("(+)");
|
||||
this.pokemonShinyNewIcon.setColor(getTextColor(TextStyle.SUMMARY_BLUE, false, this.scene.uiTheme));
|
||||
this.pokemonShinyNewIcon.setShadowColor(getTextColor(TextStyle.SUMMARY_BLUE, true, this.scene.uiTheme));
|
||||
} else {
|
||||
this.pokemonShinyNewIcon.setVisible(false);
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { hasTouchscreen, isMobile } from "#app/touch-controls";
|
||||
import { TextStyle, addTextObject } from "#app/ui/text";
|
||||
import { Mode } from "#app/ui/ui";
|
||||
import UiHandler from "#app/ui/ui-handler";
|
||||
import MessageUiHandler from "#app/ui/message-ui-handler";
|
||||
import { addWindow } from "#app/ui/ui-theme";
|
||||
import { ScrollBar } from "#app/ui/scroll-bar";
|
||||
import { Button } from "#enums/buttons";
|
||||
@ -15,9 +14,10 @@ import i18next from "i18next";
|
||||
/**
|
||||
* Abstract class for handling UI elements related to settings.
|
||||
*/
|
||||
export default class AbstractSettingsUiHandler extends UiHandler {
|
||||
export default class AbstractSettingsUiHandler extends MessageUiHandler {
|
||||
private settingsContainer: Phaser.GameObjects.Container;
|
||||
private optionsContainer: Phaser.GameObjects.Container;
|
||||
private messageBoxContainer: Phaser.GameObjects.Container;
|
||||
private navigationContainer: NavigationMenu;
|
||||
|
||||
private scrollCursor: number;
|
||||
@ -135,6 +135,23 @@ export default class AbstractSettingsUiHandler extends UiHandler {
|
||||
this.scrollBar = new ScrollBar(this.scene, this.optionsBg.width - 9, this.optionsBg.y + 5, 4, this.optionsBg.height - 11, this.rowsToDisplay);
|
||||
this.scrollBar.setTotalRows(this.settings.length);
|
||||
|
||||
// Two-lines message box
|
||||
this.messageBoxContainer = this.scene.add.container(0, this.scene.scaledCanvas.height);
|
||||
this.messageBoxContainer.setName("settings-message-box");
|
||||
this.messageBoxContainer.setVisible(false);
|
||||
|
||||
const settingsMessageBox = addWindow(this.scene, 0, -1, this.scene.scaledCanvas.width - 2, 48);
|
||||
settingsMessageBox.setOrigin(0, 1);
|
||||
this.messageBoxContainer.add(settingsMessageBox);
|
||||
|
||||
const messageText = addTextObject(this.scene, 8, -40, "", TextStyle.WINDOW, { maxLines: 2 });
|
||||
messageText.setWordWrapWidth(this.scene.game.canvas.width - 60);
|
||||
messageText.setName("settings-message");
|
||||
messageText.setOrigin(0, 0);
|
||||
|
||||
this.messageBoxContainer.add(messageText);
|
||||
this.message = messageText;
|
||||
|
||||
this.settingsContainer.add(this.optionsBg);
|
||||
this.settingsContainer.add(this.scrollBar);
|
||||
this.settingsContainer.add(this.navigationContainer);
|
||||
@ -144,6 +161,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
|
||||
this.settingsContainer.add(iconCancel);
|
||||
this.settingsContainer.add(actionText);
|
||||
this.settingsContainer.add(cancelText);
|
||||
this.settingsContainer.add(this.messageBoxContainer);
|
||||
|
||||
ui.add(this.settingsContainer);
|
||||
|
||||
@ -326,18 +344,16 @@ export default class AbstractSettingsUiHandler extends UiHandler {
|
||||
/**
|
||||
* Set the option cursor to the specified position.
|
||||
*
|
||||
* @param settingIndex - The index of the setting.
|
||||
* @param settingIndex - The index of the setting or -1 to change the current setting
|
||||
* @param cursor - The cursor position to set.
|
||||
* @param save - Whether to save the setting to local storage.
|
||||
* @returns `true` if the option cursor was set successfully.
|
||||
*/
|
||||
setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean {
|
||||
const setting = this.settings[settingIndex];
|
||||
|
||||
if (setting.key === SettingKeys.Touch_Controls && cursor && hasTouchscreen() && isMobile()) {
|
||||
this.getUi().playError();
|
||||
return false;
|
||||
if (settingIndex === -1) {
|
||||
settingIndex = this.cursor + this.scrollCursor;
|
||||
}
|
||||
const setting = this.settings[settingIndex];
|
||||
|
||||
const lastCursor = this.optionCursors[settingIndex];
|
||||
|
||||
@ -352,10 +368,34 @@ export default class AbstractSettingsUiHandler extends UiHandler {
|
||||
newValueLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true));
|
||||
|
||||
if (save) {
|
||||
const saveSetting = () => {
|
||||
this.scene.gameData.saveSetting(setting.key, cursor);
|
||||
if (this.reloadSettings.includes(setting)) {
|
||||
if (setting.requireReload) {
|
||||
this.reloadRequired = true;
|
||||
}
|
||||
};
|
||||
|
||||
// For settings that ask for confirmation, display confirmation message and a Yes/No prompt before saving the setting
|
||||
if (setting.options[cursor].needConfirmation) {
|
||||
const confirmUpdateSetting = () => {
|
||||
this.scene.ui.revertMode();
|
||||
this.showText("");
|
||||
saveSetting();
|
||||
};
|
||||
const cancelUpdateSetting = () => {
|
||||
this.scene.ui.revertMode();
|
||||
this.showText("");
|
||||
// Put the cursor back to its previous position without saving or asking for confirmation again
|
||||
this.setOptionCursor(settingIndex, lastCursor, false);
|
||||
};
|
||||
|
||||
const confirmationMessage = setting.options[cursor].confirmationMessage ?? i18next.t("settings:defaultConfirmMessage");
|
||||
this.scene.ui.showText(confirmationMessage, null, () => {
|
||||
this.scene.ui.setOverlayMode(Mode.CONFIRM, confirmUpdateSetting, cancelUpdateSetting, null, null, 1, 750);
|
||||
});
|
||||
} else {
|
||||
saveSetting();
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
@ -421,4 +461,9 @@ export default class AbstractSettingsUiHandler extends UiHandler {
|
||||
}
|
||||
this.cursorObj = null;
|
||||
}
|
||||
|
||||
override showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer) {
|
||||
this.messageBoxContainer.setVisible(!!text?.length);
|
||||
super.showText(text, delay, callback, callbackDelay, prompt, promptDelay);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user