mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-20 06:19:29 +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 {
|
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() {
|
constructor() {
|
||||||
super(0);
|
super(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
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;
|
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 {
|
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||||
return target.getHpRatio() > 0.5 ? Math.floor(((target.getHpRatio() - 0.5) * -24) + 4) : -20;
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (switchOutTarget.hp > 0) {
|
if (switchOutTarget.hp > 0) {
|
||||||
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
||||||
switchOutTarget.leaveField(true);
|
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(
|
user.scene.prependToPhase(
|
||||||
new SwitchSummonPhase(
|
new SwitchSummonPhase(
|
||||||
user.scene,
|
user.scene,
|
||||||
@ -6011,14 +6043,22 @@ export class ForceSwitchOutAttr extends MoveEffectAttr {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
} else if (user.scene.currentBattle.battleType !== BattleType.WILD) { // Switch out logic for enemy trainers
|
} 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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (switchOutTarget.hp > 0) {
|
if (switchOutTarget.hp > 0) {
|
||||||
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
if (this.switchType === SwitchType.FORCE_SWITCH) {
|
||||||
switchOutTarget.leaveField(true);
|
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(
|
user.scene.prependToPhase(
|
||||||
new SwitchSummonPhase(
|
new SwitchSummonPhase(
|
||||||
user.scene,
|
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
|
// If the attack deals fixed damage, return a result with that much damage
|
||||||
const fixedDamage = new Utils.IntegerHolder(0);
|
const fixedDamage = new Utils.NumberHolder(0);
|
||||||
applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage);
|
applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage);
|
||||||
if (fixedDamage.value) {
|
if (fixedDamage.value) {
|
||||||
const multiLensMultiplier = new Utils.NumberHolder(1);
|
const multiLensMultiplier = new Utils.NumberHolder(1);
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { Mode } from "#app/ui/ui";
|
import { Mode } from "#app/ui/ui";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import BattleScene from "../../battle-scene";
|
import BattleScene from "#app/battle-scene";
|
||||||
import { hasTouchscreen } from "../../touch-controls";
|
import { hasTouchscreen } from "#app/touch-controls";
|
||||||
import { updateWindowType } from "../../ui/ui-theme";
|
import { updateWindowType } from "#app/ui/ui-theme";
|
||||||
import { CandyUpgradeNotificationChangedEvent } from "../../events/battle-scene";
|
import { CandyUpgradeNotificationChangedEvent } from "#app/events/battle-scene";
|
||||||
import SettingsUiHandler from "#app/ui/settings/settings-ui-handler";
|
import SettingsUiHandler from "#app/ui/settings/settings-ui-handler";
|
||||||
import { EaseType } from "#enums/ease-type";
|
import { EaseType } from "#enums/ease-type";
|
||||||
import { MoneyFormat } from "#enums/money-format";
|
import { MoneyFormat } from "#enums/money-format";
|
||||||
@ -44,6 +44,7 @@ const OFF_ON: SettingOption[] = [
|
|||||||
label: i18next.t("settings:on")
|
label: i18next.t("settings:on")
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const AUTO_DISABLED: SettingOption[] = [
|
const AUTO_DISABLED: SettingOption[] = [
|
||||||
{
|
{
|
||||||
value: "Auto",
|
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[] = [
|
const SHOP_CURSOR_TARGET_OPTIONS: SettingOption[] = [
|
||||||
{
|
{
|
||||||
value: "Rewards",
|
value: "Rewards",
|
||||||
@ -100,7 +114,9 @@ export enum SettingType {
|
|||||||
|
|
||||||
type SettingOption = {
|
type SettingOption = {
|
||||||
value: string,
|
value: string,
|
||||||
label: string
|
label: string,
|
||||||
|
needConfirmation?: boolean,
|
||||||
|
confirmationMessage?: string
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface Setting {
|
export interface Setting {
|
||||||
@ -344,13 +360,6 @@ export const Setting: Array<Setting> = [
|
|||||||
default: 1,
|
default: 1,
|
||||||
type: SettingType.GENERAL
|
type: SettingType.GENERAL
|
||||||
},
|
},
|
||||||
{
|
|
||||||
key: SettingKeys.Touch_Controls,
|
|
||||||
label: i18next.t("settings:touchControls"),
|
|
||||||
options: AUTO_DISABLED,
|
|
||||||
default: 0,
|
|
||||||
type: SettingType.GENERAL
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
key: SettingKeys.Vibration,
|
key: SettingKeys.Vibration,
|
||||||
label: i18next.t("settings:vibrations"),
|
label: i18next.t("settings:vibrations"),
|
||||||
@ -358,6 +367,28 @@ export const Setting: Array<Setting> = [
|
|||||||
default: 0,
|
default: 0,
|
||||||
type: SettingType.GENERAL
|
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,
|
key: SettingKeys.Language,
|
||||||
label: i18next.t("settings:language"),
|
label: i18next.t("settings:language"),
|
||||||
@ -643,20 +674,6 @@ export const Setting: Array<Setting> = [
|
|||||||
type: SettingType.AUDIO,
|
type: SettingType.AUDIO,
|
||||||
requireReload: true
|
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,
|
key: SettingKeys.Shop_Cursor_Target,
|
||||||
label: i18next.t("settings:shopCursorTarget"),
|
label: i18next.t("settings:shopCursorTarget"),
|
||||||
@ -849,7 +866,7 @@ export function setSetting(scene: BattleScene, setting: string, value: integer):
|
|||||||
if (scene.ui) {
|
if (scene.ui) {
|
||||||
const cancelHandler = () => {
|
const cancelHandler = () => {
|
||||||
scene.ui.revertMode();
|
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 => {
|
const changeLocaleHandler = (locale: string): boolean => {
|
||||||
try {
|
try {
|
||||||
|
@ -135,4 +135,57 @@ describe("Items - Multi Lens", () => {
|
|||||||
expect(damageResults[0]).toBe(Math.floor(playerPokemon.level * 0.75));
|
expect(damageResults[0]).toBe(Math.floor(playerPokemon.level * 0.75));
|
||||||
expect(damageResults[1]).toBe(Math.floor(playerPokemon.level * 0.25));
|
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 { BattlerIndex } from "#app/battle";
|
||||||
import { allMoves } from "#app/data/move";
|
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 { Abilities } from "#enums/abilities";
|
||||||
import { Moves } from "#enums/moves";
|
import { Moves } from "#enums/moves";
|
||||||
import { Species } from "#enums/species";
|
import { Species } from "#enums/species";
|
||||||
@ -193,4 +197,122 @@ describe("Moves - Dragon Tail", () => {
|
|||||||
expect(dratini.hp).toBe(Math.floor(dratini.getMaxHp() / 2));
|
expect(dratini.hp).toBe(Math.floor(dratini.getMaxHp() / 2));
|
||||||
expect(game.scene.getPlayerField().length).toBe(1);
|
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 { MoveResult } from "#app/field/pokemon";
|
||||||
import { Abilities } from "#enums/abilities";
|
import { Abilities } from "#enums/abilities";
|
||||||
import { Moves } from "#enums/moves";
|
import { Moves } from "#enums/moves";
|
||||||
import { Species } from "#enums/species";
|
import { Species } from "#enums/species";
|
||||||
import GameManager from "#test/utils/gameManager";
|
import GameManager from "#test/utils/gameManager";
|
||||||
import Phaser from "phaser";
|
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", () => {
|
describe("Moves - Whirlwind", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
@ -25,8 +29,9 @@ describe("Moves - Whirlwind", () => {
|
|||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
game.override
|
game.override
|
||||||
.battleType("single")
|
.battleType("single")
|
||||||
|
.moveset(Moves.SPLASH)
|
||||||
.enemyAbility(Abilities.BALL_FETCH)
|
.enemyAbility(Abilities.BALL_FETCH)
|
||||||
.enemyMoveset(Moves.WHIRLWIND)
|
.enemyMoveset([ Moves.SPLASH, Moves.WHIRLWIND ])
|
||||||
.enemySpecies(Species.PIDGEY);
|
.enemySpecies(Species.PIDGEY);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -41,10 +46,114 @@ describe("Moves - Whirlwind", () => {
|
|||||||
const staraptor = game.scene.getPlayerPokemon()!;
|
const staraptor = game.scene.getPlayerPokemon()!;
|
||||||
|
|
||||||
game.move.select(move);
|
game.move.select(move);
|
||||||
|
await game.forceEnemyMove(Moves.WHIRLWIND);
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase", false);
|
await game.phaseInterceptor.to("BerryPhase", false);
|
||||||
|
|
||||||
expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined();
|
expect(staraptor.findTag((t) => t.tagType === BattlerTagType.FLYING)).toBeDefined();
|
||||||
expect(game.scene.getEnemyPokemon()!.getLastXMoves(1)[0].result).toBe(MoveResult.MISS);
|
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));
|
this.pokemonShinyNewIcon.setShadowColor(getTextColor(TextStyle.SUMMARY_BLUE, true, this.scene.uiTheme));
|
||||||
const newShinyOrVariant = ((newShiny & caughtAttr) === BigInt(0)) || ((newVariant & caughtAttr) === BigInt(0));
|
const newShinyOrVariant = ((newShiny & caughtAttr) === BigInt(0)) || ((newVariant & caughtAttr) === BigInt(0));
|
||||||
this.pokemonShinyNewIcon.setVisible(!!newShinyOrVariant);
|
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 {
|
} else {
|
||||||
this.pokemonShinyNewIcon.setVisible(false);
|
this.pokemonShinyNewIcon.setVisible(false);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import BattleScene from "#app/battle-scene";
|
import BattleScene from "#app/battle-scene";
|
||||||
import { hasTouchscreen, isMobile } from "#app/touch-controls";
|
|
||||||
import { TextStyle, addTextObject } from "#app/ui/text";
|
import { TextStyle, addTextObject } from "#app/ui/text";
|
||||||
import { Mode } from "#app/ui/ui";
|
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 { addWindow } from "#app/ui/ui-theme";
|
||||||
import { ScrollBar } from "#app/ui/scroll-bar";
|
import { ScrollBar } from "#app/ui/scroll-bar";
|
||||||
import { Button } from "#enums/buttons";
|
import { Button } from "#enums/buttons";
|
||||||
@ -15,9 +14,10 @@ import i18next from "i18next";
|
|||||||
/**
|
/**
|
||||||
* Abstract class for handling UI elements related to settings.
|
* 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 settingsContainer: Phaser.GameObjects.Container;
|
||||||
private optionsContainer: Phaser.GameObjects.Container;
|
private optionsContainer: Phaser.GameObjects.Container;
|
||||||
|
private messageBoxContainer: Phaser.GameObjects.Container;
|
||||||
private navigationContainer: NavigationMenu;
|
private navigationContainer: NavigationMenu;
|
||||||
|
|
||||||
private scrollCursor: number;
|
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 = 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);
|
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.optionsBg);
|
||||||
this.settingsContainer.add(this.scrollBar);
|
this.settingsContainer.add(this.scrollBar);
|
||||||
this.settingsContainer.add(this.navigationContainer);
|
this.settingsContainer.add(this.navigationContainer);
|
||||||
@ -144,6 +161,7 @@ export default class AbstractSettingsUiHandler extends UiHandler {
|
|||||||
this.settingsContainer.add(iconCancel);
|
this.settingsContainer.add(iconCancel);
|
||||||
this.settingsContainer.add(actionText);
|
this.settingsContainer.add(actionText);
|
||||||
this.settingsContainer.add(cancelText);
|
this.settingsContainer.add(cancelText);
|
||||||
|
this.settingsContainer.add(this.messageBoxContainer);
|
||||||
|
|
||||||
ui.add(this.settingsContainer);
|
ui.add(this.settingsContainer);
|
||||||
|
|
||||||
@ -326,18 +344,16 @@ export default class AbstractSettingsUiHandler extends UiHandler {
|
|||||||
/**
|
/**
|
||||||
* Set the option cursor to the specified position.
|
* 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 cursor - The cursor position to set.
|
||||||
* @param save - Whether to save the setting to local storage.
|
* @param save - Whether to save the setting to local storage.
|
||||||
* @returns `true` if the option cursor was set successfully.
|
* @returns `true` if the option cursor was set successfully.
|
||||||
*/
|
*/
|
||||||
setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean {
|
setOptionCursor(settingIndex: number, cursor: number, save?: boolean): boolean {
|
||||||
const setting = this.settings[settingIndex];
|
if (settingIndex === -1) {
|
||||||
|
settingIndex = this.cursor + this.scrollCursor;
|
||||||
if (setting.key === SettingKeys.Touch_Controls && cursor && hasTouchscreen() && isMobile()) {
|
|
||||||
this.getUi().playError();
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
const setting = this.settings[settingIndex];
|
||||||
|
|
||||||
const lastCursor = this.optionCursors[settingIndex];
|
const lastCursor = this.optionCursors[settingIndex];
|
||||||
|
|
||||||
@ -352,10 +368,34 @@ export default class AbstractSettingsUiHandler extends UiHandler {
|
|||||||
newValueLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true));
|
newValueLabel.setShadowColor(this.getTextColor(TextStyle.SETTINGS_SELECTED, true));
|
||||||
|
|
||||||
if (save) {
|
if (save) {
|
||||||
|
const saveSetting = () => {
|
||||||
this.scene.gameData.saveSetting(setting.key, cursor);
|
this.scene.gameData.saveSetting(setting.key, cursor);
|
||||||
if (this.reloadSettings.includes(setting)) {
|
if (setting.requireReload) {
|
||||||
this.reloadRequired = true;
|
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;
|
return true;
|
||||||
@ -421,4 +461,9 @@ export default class AbstractSettingsUiHandler extends UiHandler {
|
|||||||
}
|
}
|
||||||
this.cursorObj = null;
|
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