Merge branch 'beta' into MergeFix

This commit is contained in:
Jesse M Chung 2024-09-01 23:54:33 -04:00 committed by GitHub
commit 0567b4db73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
42 changed files with 1261 additions and 480 deletions

101
create-test-boilerplate.js Normal file
View File

@ -0,0 +1,101 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
/**
* This script creates a test boilerplate file for a move or ability.
* @param {string} type - The type of test to create. Either "move" or "ability".
* @param {string} fileName - The name of the file to create.
* @example npm run create-test move tackle
*/
// Get the directory name of the current module file
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Get the arguments from the command line
const args = process.argv.slice(2);
const type = args[0]; // "move" or "ability"
let fileName = args[1]; // The file name
if (!type || !fileName) {
console.error('Please provide both a type ("move" or "ability") and a file name.');
process.exit(1);
}
// Convert fileName from to snake_case if camelCase is given
fileName = fileName.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
// Format the description for the test case
const formattedName = fileName
.replace(/_/g, ' ')
.replace(/\b\w/g, char => char.toUpperCase());
// Determine the directory based on the type
let dir;
let description;
if (type === 'move') {
dir = path.join(__dirname, 'src', 'test', 'moves');
description = `Moves - ${formattedName}`;
} else if (type === 'ability') {
dir = path.join(__dirname, 'src', 'test', 'abilities');
description = `Abilities - ${formattedName}`;
} else {
console.error('Invalid type. Please use "move" or "ability".');
process.exit(1);
}
// Ensure the directory exists
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// Create the file with the given name
const filePath = path.join(dir, `${fileName}.test.ts`);
if (fs.existsSync(filePath)) {
console.error(`File "${fileName}.test.ts" already exists.`);
process.exit(1);
}
// Define the content template
const content = `import { Abilities } from "#enums/abilities";
import GameManager from "#test/utils/gameManager";
import { SPLASH_ONLY } from "#test/utils/testUtils";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, it } from "vitest";
describe("${description}", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const TIMEOUT = 20 * 1000;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.battleType("single")
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(SPLASH_ONLY);
});
it("test case", async () => {
// await game.classicMode.startBattle();
// game.move.select();
}, TIMEOUT);
});
`;
// Write the template content to the file
fs.writeFileSync(filePath, content, 'utf8');
console.log(`File created at: ${filePath}`);

View File

@ -18,7 +18,8 @@
"eslint-ci": "eslint .",
"docs": "typedoc",
"depcruise": "depcruise src",
"depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg"
"depcruise:graph": "depcruise src --output-type dot | node dependency-graph.js > dependency-graph.svg",
"create-test": "node ./create-test-boilerplate.js"
},
"devDependencies": {
"@eslint/js": "^9.3.0",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -8,7 +8,7 @@ import { Weather, WeatherType } from "./weather";
import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags";
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
import { Gender } from "./gender";
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr } from "./move";
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
import { Stat, getStatName } from "./pokemon-stat";
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
@ -475,6 +475,47 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr {
}
}
/**
* Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Tera_Shell_(Ability) | Tera Shell}
* When the source is at full HP, incoming attacks will have a maximum 0.5x type effectiveness multiplier.
* @extends PreDefendAbAttr
*/
export class FullHpResistTypeAbAttr extends PreDefendAbAttr {
/**
* Reduces a type multiplier to 0.5 if the source is at full HP.
* @param pokemon {@linkcode Pokemon} the Pokemon with this ability
* @param passive n/a
* @param simulated n/a (this doesn't change game state)
* @param attacker n/a
* @param move {@linkcode Move} the move being used on the source
* @param cancelled n/a
* @param args `[0]` a container for the move's current type effectiveness multiplier
* @returns `true` if the move's effectiveness is reduced; `false` otherwise
*/
applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move | null, cancelled: Utils.BooleanHolder | null, args: any[]): boolean | Promise<boolean> {
const typeMultiplier = args[0];
if (!(typeMultiplier && typeMultiplier instanceof Utils.NumberHolder)) {
return false;
}
if (move && move.hasAttr(FixedDamageAttr)) {
return false;
}
if (pokemon.isFullHp() && typeMultiplier.value > 0.5) {
typeMultiplier.value = 0.5;
return true;
}
return false;
}
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
return i18next.t("abilityTriggers:fullHpResistType", {
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon)
});
}
}
export class PostDefendAbAttr extends AbAttr {
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean | Promise<boolean> {
return false;
@ -2387,7 +2428,7 @@ export class PostSummonWeatherSuppressedFormChangeAbAttr extends PostSummonAbAtt
/**
* Triggers weather-based form change when summoned into an active weather.
* Used by Forecast.
* Used by Forecast and Flower Gift.
* @extends PostSummonAbAttr
*/
export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr {
@ -2410,7 +2451,10 @@ export class PostSummonFormChangeByWeatherAbAttr extends PostSummonAbAttr {
* @returns whether the form change was triggered
*/
applyPostSummon(pokemon: Pokemon, passive: boolean, simulated: boolean, args: any[]): boolean {
if (pokemon.species.speciesId === Species.CASTFORM && this.ability === Abilities.FORECAST) {
const isCastformWithForecast = (pokemon.species.speciesId === Species.CASTFORM && this.ability === Abilities.FORECAST);
const isCherrimWithFlowerGift = (pokemon.species.speciesId === Species.CHERRIM && this.ability === Abilities.FLOWER_GIFT);
if (isCastformWithForecast || isCherrimWithFlowerGift) {
if (simulated) {
return simulated;
}
@ -3083,37 +3127,41 @@ export class PostWeatherChangeAbAttr extends AbAttr {
/**
* Triggers weather-based form change when weather changes.
* Used by Forecast.
* Used by Forecast and Flower Gift.
* @extends PostWeatherChangeAbAttr
*/
export class PostWeatherChangeFormChangeAbAttr extends PostWeatherChangeAbAttr {
private ability: Abilities;
private formRevertingWeathers: WeatherType[];
constructor(ability: Abilities) {
constructor(ability: Abilities, formRevertingWeathers: WeatherType[]) {
super(false);
this.ability = ability;
this.formRevertingWeathers = formRevertingWeathers;
}
/**
* Calls {@linkcode Arena.triggerWeatherBasedFormChangesToNormal | triggerWeatherBasedFormChangesToNormal} when the
* weather changed to form-reverting weather, otherwise calls {@linkcode Arena.triggerWeatherBasedFormChanges | triggerWeatherBasedFormChanges}
* @param {Pokemon} pokemon the Pokemon that changed the weather
* @param {Pokemon} pokemon the Pokemon with this ability
* @param passive n/a
* @param weather n/a
* @param args n/a
* @returns whether the form change was triggered
*/
applyPostWeatherChange(pokemon: Pokemon, passive: boolean, simulated: boolean, weather: WeatherType, args: any[]): boolean {
if (pokemon.species.speciesId === Species.CASTFORM && this.ability === Abilities.FORECAST) {
const isCastformWithForecast = (pokemon.species.speciesId === Species.CASTFORM && this.ability === Abilities.FORECAST);
const isCherrimWithFlowerGift = (pokemon.species.speciesId === Species.CHERRIM && this.ability === Abilities.FLOWER_GIFT);
if (isCastformWithForecast || isCherrimWithFlowerGift) {
if (simulated) {
return simulated;
}
const formRevertingWeathers: WeatherType[] = [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG ];
const weatherType = pokemon.scene.arena.weather?.weatherType;
if (weatherType && formRevertingWeathers.includes(weatherType)) {
if (weatherType && this.formRevertingWeathers.includes(weatherType)) {
pokemon.scene.arena.triggerWeatherBasedFormChangesToNormal();
} else {
pokemon.scene.arena.triggerWeatherBasedFormChanges();
@ -4703,7 +4751,8 @@ function setAbilityRevealed(pokemon: Pokemon): void {
*/
function getPokemonWithWeatherBasedForms(scene: BattleScene) {
return scene.getField(true).filter(p =>
p.hasAbility(Abilities.FORECAST) && p.species.speciesId === Species.CASTFORM
(p.hasAbility(Abilities.FORECAST) && p.species.speciesId === Species.CASTFORM)
|| (p.hasAbility(Abilities.FLOWER_GIFT) && p.species.speciesId === Species.CHERRIM)
);
}
@ -4902,7 +4951,7 @@ export function initAbilities() {
.attr(UncopiableAbilityAbAttr)
.attr(NoFusionAbilityAbAttr)
.attr(PostSummonFormChangeByWeatherAbAttr, Abilities.FORECAST)
.attr(PostWeatherChangeFormChangeAbAttr, Abilities.FORECAST),
.attr(PostWeatherChangeFormChangeAbAttr, Abilities.FORECAST, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG ]),
new Ability(Abilities.STICKY_HOLD, 3)
.attr(BlockItemTheftAbAttr)
.bypassFaint()
@ -5095,8 +5144,10 @@ export function initAbilities() {
.conditionalAttr(getWeatherCondition(WeatherType.SUNNY || WeatherType.HARSH_SUN), BattleStatMultiplierAbAttr, BattleStat.SPDEF, 1.5)
.attr(UncopiableAbilityAbAttr)
.attr(NoFusionAbilityAbAttr)
.ignorable()
.partial(),
.attr(PostSummonFormChangeByWeatherAbAttr, Abilities.FLOWER_GIFT)
.attr(PostWeatherChangeFormChangeAbAttr, Abilities.FLOWER_GIFT, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG, WeatherType.HAIL, WeatherType.HEAVY_RAIN, WeatherType.SNOW, WeatherType.RAIN ])
.partial() // Should also boosts stats of ally
.ignorable(),
new Ability(Abilities.BAD_DREAMS, 4)
.attr(PostTurnHurtIfSleepingAbAttr),
new Ability(Abilities.PICKPOCKET, 5)
@ -5761,10 +5812,10 @@ export function initAbilities() {
.attr(NoTransformAbilityAbAttr)
.attr(NoFusionAbilityAbAttr),
new Ability(Abilities.TERA_SHELL, 9)
.attr(FullHpResistTypeAbAttr)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)
.ignorable()
.unimplemented(),
.ignorable(),
new Ability(Abilities.TERAFORM_ZERO, 9)
.attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr)

View File

@ -788,10 +788,10 @@ export abstract class BattleAnim {
targetSprite.pipelineData["tone"] = [ 0.0, 0.0, 0.0, 0.0 ];
targetSprite.setAngle(0);
if (!this.isHideUser() && userSprite) {
userSprite.setVisible(true);
this.user?.getSprite().setVisible(true); // using this.user to fix context loss due to isOppAnim swap (#481)
}
if (!this.isHideTarget() && (targetSprite !== userSprite || !this.isHideUser())) {
targetSprite.setVisible(true);
this.target?.getSprite().setVisible(true); // using this.target to fix context loss due to isOppAnim swap (#481)
}
for (const ms of Object.values(spriteCache).flat()) {
if (ms) {

View File

@ -212,7 +212,7 @@ export class TrappedTag extends BattlerTag {
canAdd(pokemon: Pokemon): boolean {
const isGhost = pokemon.isOfType(Type.GHOST);
const isTrapped = pokemon.getTag(BattlerTagType.TRAPPED);
const isTrapped = pokemon.getTag(TrappedTag);
return !isTrapped && !isGhost;
}
@ -245,6 +245,23 @@ export class TrappedTag extends BattlerTag {
}
}
/**
* BattlerTag implementing No Retreat's trapping effect.
* This is treated separately from other trapping effects to prevent
* Ghost-type Pokemon from being able to reuse the move.
* @extends TrappedTag
*/
class NoRetreatTag extends TrappedTag {
constructor(sourceId: number) {
super(BattlerTagType.NO_RETREAT, BattlerTagLapseType.CUSTOM, 0, Moves.NO_RETREAT, sourceId);
}
/** overrides {@linkcode TrappedTag.apply}, removing the Ghost-type condition */
canAdd(pokemon: Pokemon): boolean {
return !pokemon.getTag(TrappedTag);
}
}
/**
* BattlerTag that represents the {@link https://bulbapedia.bulbagarden.net/wiki/Flinch Flinch} status condition
*/
@ -868,7 +885,7 @@ export abstract class DamagingTrapTag extends TrappedTag {
}
canAdd(pokemon: Pokemon): boolean {
return !pokemon.isOfType(Type.GHOST) && !pokemon.findTag(t => t instanceof DamagingTrapTag);
return !pokemon.getTag(TrappedTag);
}
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
@ -1511,6 +1528,25 @@ export class CritBoostTag extends BattlerTag {
}
}
/**
* Tag for the effects of Dragon Cheer, which boosts the critical hit ratio of the user's allies.
* @extends {CritBoostTag}
*/
export class DragonCheerTag extends CritBoostTag {
/** The types of the user's ally when the tag is added */
public typesOnAdd: Type[];
constructor() {
super(BattlerTagType.CRIT_BOOST, Moves.DRAGON_CHEER);
}
onAdd(pokemon: Pokemon): void {
super.onAdd(pokemon);
this.typesOnAdd = pokemon.getTypes(true);
}
}
export class SaltCuredTag extends BattlerTag {
private sourceIndex: number;
@ -1868,6 +1904,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new DrowsyTag();
case BattlerTagType.TRAPPED:
return new TrappedTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
case BattlerTagType.NO_RETREAT:
return new NoRetreatTag(sourceId);
case BattlerTagType.BIND:
return new BindTag(turnCount, sourceId);
case BattlerTagType.WRAP:
@ -1927,6 +1965,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source
return new TypeBoostTag(tagType, sourceMove, Type.FIRE, 1.5, false);
case BattlerTagType.CRIT_BOOST:
return new CritBoostTag(tagType, sourceMove);
case BattlerTagType.DRAGON_CHEER:
return new DragonCheerTag();
case BattlerTagType.ALWAYS_CRIT:
case BattlerTagType.IGNORE_ACCURACY:
return new BattlerTag(tagType, BattlerTagLapseType.TURN_END, 2, sourceMove);

View File

@ -5889,9 +5889,9 @@ export class SwitchAbilitiesAttr extends MoveEffectAttr {
target.summonData.ability = tempAbilityId;
user.scene.queueMessage(i18next.t("moveTriggers:swappedAbilitiesWithTarget", {pokemonName: getPokemonNameWithAffix(user)}));
// Swaps Forecast from Castform
// Swaps Forecast/Flower Gift from Castform/Cherrim
user.scene.arena.triggerWeatherBasedFormChangesToNormal();
// Swaps Forecast to Castform (edge case)
// Swaps Forecast/Flower Gift to Castform/Cherrim (edge case)
user.scene.arena.triggerWeatherBasedFormChanges();
return true;
@ -6038,6 +6038,57 @@ export class DestinyBondAttr extends MoveEffectAttr {
}
}
/**
* Attribute to apply a battler tag to the target if they have had their stats boosted this turn.
* @extends AddBattlerTagAttr
*/
export class AddBattlerTagIfBoostedAttr extends AddBattlerTagAttr {
constructor(tag: BattlerTagType) {
super(tag, false, false, 2, 5);
}
/**
* @param user {@linkcode Pokemon} using this move
* @param target {@linkcode Pokemon} target of this move
* @param move {@linkcode Move} being used
* @param {any[]} args N/A
* @returns true
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (target.turnData.battleStatsIncreased) {
super.apply(user, target, move, args);
}
return true;
}
}
/**
* Attribute to apply a status effect to the target if they have had their stats boosted this turn.
* @extends MoveEffectAttr
*/
export class StatusIfBoostedAttr extends MoveEffectAttr {
public effect: StatusEffect;
constructor(effect: StatusEffect) {
super(true, MoveEffectTrigger.HIT);
this.effect = effect;
}
/**
* @param user {@linkcode Pokemon} using this move
* @param target {@linkcode Pokemon} target of this move
* @param move {@linkcode Move} N/A
* @param {any[]} args N/A
* @returns true
*/
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
if (target.turnData.battleStatsIncreased) {
target.trySetStatus(this.effect, true, user);
}
return true;
}
}
export class LastResortAttr extends MoveAttr {
getCondition(): MoveConditionFunc {
return (user: Pokemon, target: Pokemon, move: Move) => {
@ -8503,7 +8554,7 @@ export function initMoves() {
.partial(),
new SelfStatusMove(Moves.NO_RETREAT, Type.FIGHTING, -1, 5, -1, 0, 8)
.attr(StatChangeAttr, [ BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD ], 1, true)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, true, false, 1)
.attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false)
.condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== Moves.NO_RETREAT), // fails if the user is currently trapped by No Retreat
new StatusMove(Moves.TAR_SHOT, Type.ROCK, 100, 15, -1, 0, 8)
.attr(StatChangeAttr, BattleStat.SPD, -1)
@ -8695,10 +8746,10 @@ export function initMoves() {
new AttackMove(Moves.SKITTER_SMACK, Type.BUG, MoveCategory.PHYSICAL, 70, 90, 10, 100, 0, 8)
.attr(StatChangeAttr, BattleStat.SPATK, -1),
new AttackMove(Moves.BURNING_JEALOUSY, Type.FIRE, MoveCategory.SPECIAL, 70, 100, 5, 100, 0, 8)
.target(MoveTarget.ALL_NEAR_ENEMIES)
.partial(),
.attr(StatusIfBoostedAttr, StatusEffect.BURN)
.target(MoveTarget.ALL_NEAR_ENEMIES),
new AttackMove(Moves.LASH_OUT, Type.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8)
.partial(),
.attr(MovePowerMultiplierAttr, (user, target, move) => user.turnData.battleStatsDecreased ? 2 : 1),
new AttackMove(Moves.POLTERGEIST, Type.GHOST, MoveCategory.PHYSICAL, 110, 90, 5, -1, 0, 8)
.attr(AttackedByItemAttr)
.makesContact(false),
@ -9144,12 +9195,11 @@ export function initMoves() {
new AttackMove(Moves.HARD_PRESS, Type.STEEL, MoveCategory.PHYSICAL, -1, 100, 10, -1, 0, 9)
.attr(OpponentHighHpPowerAttr, 100),
new StatusMove(Moves.DRAGON_CHEER, Type.DRAGON, -1, 15, -1, 0, 9)
.attr(AddBattlerTagAttr, BattlerTagType.CRIT_BOOST, false, true)
.target(MoveTarget.NEAR_ALLY)
.partial(),
.attr(AddBattlerTagAttr, BattlerTagType.DRAGON_CHEER, false, true)
.target(MoveTarget.NEAR_ALLY),
new AttackMove(Moves.ALLURING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, -1, 0, 9)
.soundBased()
.partial(),
.attr(AddBattlerTagIfBoostedAttr, BattlerTagType.CONFUSED)
.soundBased(),
new AttackMove(Moves.TEMPER_FLARE, Type.FIRE, MoveCategory.PHYSICAL, 75, 100, 10, -1, 0, 9)
.attr(MovePowerMultiplierAttr, (user, target, move) => user.getLastXMoves(2)[1]?.result === MoveResult.MISS || user.getLastXMoves(2)[1]?.result === MoveResult.FAIL ? 2 : 1),
new AttackMove(Moves.SUPERCELL_SLAM, Type.ELECTRIC, MoveCategory.PHYSICAL, 100, 95, 15, -1, 0, 9)

View File

@ -359,7 +359,7 @@ export class SpeciesDefaultFormMatchTrigger extends SpeciesFormChangeTrigger {
/**
* Class used for triggering form changes based on weather.
* Used by Castform.
* Used by Castform and Cherrim.
* @extends SpeciesFormChangeTrigger
*/
export class SpeciesFormChangeWeatherTrigger extends SpeciesFormChangeTrigger {
@ -392,7 +392,7 @@ export class SpeciesFormChangeWeatherTrigger extends SpeciesFormChangeTrigger {
/**
* Class used for reverting to the original form when the weather runs out
* or when the user loses the ability/is suppressed.
* Used by Castform.
* Used by Castform and Cherrim.
* @extends SpeciesFormChangeTrigger
*/
export class SpeciesFormChangeRevertWeatherFormTrigger extends SpeciesFormChangeTrigger {
@ -930,6 +930,11 @@ export const pokemonFormChanges: PokemonFormChanges = {
new SpeciesFormChange(Species.CASTFORM, "rainy", "", new SpeciesFormChangeActiveTrigger(), true),
new SpeciesFormChange(Species.CASTFORM, "snowy", "", new SpeciesFormChangeActiveTrigger(), true),
],
[Species.CHERRIM]: [
new SpeciesFormChange(Species.CHERRIM, "overcast", "sunshine", new SpeciesFormChangeWeatherTrigger(Abilities.FLOWER_GIFT, [ WeatherType.SUNNY, WeatherType.HARSH_SUN ]), true),
new SpeciesFormChange(Species.CHERRIM, "sunshine", "overcast", new SpeciesFormChangeRevertWeatherFormTrigger(Abilities.FLOWER_GIFT, [ WeatherType.NONE, WeatherType.SANDSTORM, WeatherType.STRONG_WINDS, WeatherType.FOG, WeatherType.HAIL, WeatherType.HEAVY_RAIN, WeatherType.SNOW, WeatherType.RAIN ]), true),
new SpeciesFormChange(Species.CHERRIM, "sunshine", "overcast", new SpeciesFormChangeActiveTrigger(), true),
],
};
export function initPokemonForms() {

View File

@ -69,5 +69,7 @@ export enum BattlerTagType {
GULP_MISSILE_ARROKUDA = "GULP_MISSILE_ARROKUDA",
GULP_MISSILE_PIKACHU = "GULP_MISSILE_PIKACHU",
BEAK_BLAST_CHARGING = "BEAK_BLAST_CHARGING",
SHELL_TRAP = "SHELL_TRAP"
SHELL_TRAP = "SHELL_TRAP",
DRAGON_CHEER = "DRAGON_CHEER",
NO_RETREAT = "NO_RETREAT",
}

View File

@ -339,7 +339,10 @@ export class Arena {
*/
triggerWeatherBasedFormChanges(): void {
this.scene.getField(true).forEach( p => {
if (p.hasAbility(Abilities.FORECAST) && p.species.speciesId === Species.CASTFORM) {
const isCastformWithForecast = (p.hasAbility(Abilities.FORECAST) && p.species.speciesId === Species.CASTFORM);
const isCherrimWithFlowerGift = (p.hasAbility(Abilities.FLOWER_GIFT) && p.species.speciesId === Species.CHERRIM);
if (isCastformWithForecast || isCherrimWithFlowerGift) {
new ShowAbilityPhase(this.scene, p.getBattlerIndex());
this.scene.triggerPokemonFormChange(p, SpeciesFormChangeWeatherTrigger);
}
@ -351,7 +354,10 @@ export class Arena {
*/
triggerWeatherBasedFormChangesToNormal(): void {
this.scene.getField(true).forEach( p => {
if (p.hasAbility(Abilities.FORECAST, false, true) && p.species.speciesId === Species.CASTFORM) {
const isCastformWithForecast = (p.hasAbility(Abilities.FORECAST, false, true) && p.species.speciesId === Species.CASTFORM);
const isCherrimWithFlowerGift = (p.hasAbility(Abilities.FLOWER_GIFT, false, true) && p.species.speciesId === Species.CHERRIM);
if (isCastformWithForecast || isCherrimWithFlowerGift) {
new ShowAbilityPhase(this.scene, p.getBattlerIndex());
return this.scene.triggerPokemonFormChange(p, SpeciesFormChangeRevertWeatherFormTrigger);
}

View File

@ -18,11 +18,11 @@ import { Status, StatusEffect, getRandomStatus } from "../data/status-effect";
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions";
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
import { BattleStat } from "../data/battle-stat";
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag } from "../data/battler-tags";
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag } from "../data/battler-tags";
import { WeatherType } from "../data/weather";
import { TempBattleStat } from "../data/temp-battle-stat";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr } from "../data/ability";
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "../data/ability";
import PokemonData from "../system/pokemon-data";
import { BattlerIndex } from "../battle";
import { Mode } from "../ui/ui";
@ -1210,6 +1210,28 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
return !!this.getTag(GroundedTag) || (!this.isOfType(Type.FLYING, true, true) && !this.hasAbility(Abilities.LEVITATE) && !this.getTag(BattlerTagType.MAGNET_RISEN) && !this.getTag(SemiInvulnerableTag));
}
/**
* Determines whether this Pokemon is prevented from running or switching due
* to effects from moves and/or abilities.
* @param trappedAbMessages `string[]` If defined, ability trigger messages
* (e.g. from Shadow Tag) are forwarded through this array.
* @param simulated `boolean` if `true`, applies abilities via simulated calls.
* @returns
*/
isTrapped(trappedAbMessages: string[] = [], simulated: boolean = true): boolean {
if (this.isOfType(Type.GHOST)) {
return false;
}
const trappedByAbility = new Utils.BooleanHolder(false);
this.scene.getEnemyField()!.forEach(enemyPokemon =>
applyCheckTrappedAbAttrs(CheckTrappedAbAttr, enemyPokemon, trappedByAbility, this, trappedAbMessages, simulated)
);
return (trappedByAbility.value || !!this.getTag(TrappedTag));
}
/**
* Calculates the type of a move when used by this Pokemon after
* type-changing move and ability attributes have applied.
@ -1276,6 +1298,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
// Apply Tera Shell's effect to attacks after all immunities are accounted for
if (!ignoreAbility && move.category !== MoveCategory.STATUS) {
applyPreDefendAbAttrs(FullHpResistTypeAbAttr, this, source, move, cancelledHolder, simulated, typeMultiplier);
}
return (!cancelledHolder.value ? typeMultiplier.value : 0) as TypeDamageMultiplier;
}
@ -2064,9 +2091,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
critLevel.value += 1;
}
}
if (source.getTag(BattlerTagType.CRIT_BOOST)) {
critLevel.value += 2;
const critBoostTag = source.getTag(CritBoostTag);
if (critBoostTag) {
if (critBoostTag instanceof DragonCheerTag) {
critLevel.value += critBoostTag.typesOnAdd.includes(Type.DRAGON) ? 2 : 1;
} else {
critLevel.value += 2;
}
}
console.log(`crit stage: +${critLevel.value}`);
const critChance = [24, 8, 2, 1][Math.max(0, Math.min(critLevel.value, 3))];
isCritical = critChance === 1 || !this.scene.randBattleSeedInt(critChance);
@ -4298,7 +4332,7 @@ export interface TurnMove {
targets?: BattlerIndex[];
result: MoveResult;
virtual?: boolean;
turn?: integer;
turn?: number;
}
export interface QueuedMove {
@ -4310,17 +4344,17 @@ export interface QueuedMove {
export interface AttackMoveResult {
move: Moves;
result: DamageResult;
damage: integer;
damage: number;
critical: boolean;
sourceId: integer;
sourceId: number;
sourceBattlerIndex: BattlerIndex;
}
export class PokemonSummonData {
public battleStats: integer[] = [ 0, 0, 0, 0, 0, 0, 0 ];
public battleStats: number[] = [ 0, 0, 0, 0, 0, 0, 0 ];
public moveQueue: QueuedMove[] = [];
public disabledMove: Moves = Moves.NONE;
public disabledTurns: integer = 0;
public disabledTurns: number = 0;
public tags: BattlerTag[] = [];
public abilitySuppressed: boolean = false;
public abilitiesApplied: Abilities[] = [];
@ -4330,14 +4364,14 @@ export class PokemonSummonData {
public ability: Abilities = Abilities.NONE;
public gender: Gender;
public fusionGender: Gender;
public stats: integer[];
public stats: number[];
public moveset: (PokemonMove | null)[];
// If not initialized this value will not be populated from save data.
public types: Type[] = [];
}
export class PokemonBattleData {
public hitCount: integer = 0;
public hitCount: number = 0;
public endured: boolean = false;
public berriesEaten: BerryType[] = [];
public abilitiesApplied: Abilities[] = [];
@ -4346,21 +4380,23 @@ export class PokemonBattleData {
export class PokemonBattleSummonData {
/** The number of turns the pokemon has passed since entering the battle */
public turnCount: integer = 1;
public turnCount: number = 1;
/** The list of moves the pokemon has used since entering the battle */
public moveHistory: TurnMove[] = [];
}
export class PokemonTurnData {
public flinched: boolean;
public acted: boolean;
public hitCount: integer;
public hitsLeft: integer;
public damageDealt: integer = 0;
public currDamageDealt: integer = 0;
public damageTaken: integer = 0;
public flinched: boolean = false;
public acted: boolean = false;
public hitCount: number;
public hitsLeft: number;
public damageDealt: number = 0;
public currDamageDealt: number = 0;
public damageTaken: number = 0;
public attacksReceived: AttackMoveResult[] = [];
public order: number;
public battleStatsIncreased: boolean = false;
public battleStatsDecreased: boolean = false;
}
export enum AiType {

View File

@ -1403,19 +1403,19 @@
"1": "Ich muss dein Potenzial als Trainer und die Stärke der Pokémon sehen, die mit dir kämpfen!",
"2": "Los geht's! Dies sind meine Gesteins-Pokémon, mein ganzer Stolz!",
"3": "Gesteins-Pokémon sind einfach die besten!",
"4": "Ich muss dein Potenzial als Trainer und die Stärke der Pokémon sehen, die mit dir kämpfen!"
"4": "Tag für Tag grabe ich hier nach Fossilien.\n$Die viele Arbeit hat meine Pokémon felsenfest gemacht\nund das wirst du jetzt im Kampf zu spüren bekommen!"
},
"victory": {
"1": "W-was? Das kann nicht sein! Meine total tranierten Pokémon!",
"2": "…Wir haben die Kontrolle verloren. Beim nächsten Mal fordere ich dich\n$zu einem Fossilien-Ausgrabungswettbewerb heraus.",
"3": "Mit deinem Können ist es nur natürlich, dass du gewinnst.",
"4": "W-was?! Das kann nicht sein! Selbst das war nicht genug?",
"5": "Ich habe es vermasselt."
"4": "W-was?! Das kann nicht sein! Selbst das war nicht genug?"
},
"defeat": {
"1": "Siehst du? Ich bin stolz auf meinen steinigen Kampfstil!",
"2": "Danke! Der Kampf hat mir Vertrauen gegeben, dass ich vielleicht meinen Vater besiegen kann!",
"3": "Ich fühle mich, als hätte ich gerade einen wirklich hartnäckigen Felsen durchbrochen!"
"3": "Na, was sagst du jetzt? Meine felsenfesten Pokémon waren hart genug für dich, was?",
"4": "Ich wusste, dass ich gewinnen würde!"
}
},
"morty": {

View File

@ -12,6 +12,7 @@
"blockItemTheft": "{{pokemonNameWithAffix}}'s {{abilityName}}\nprevents item theft!",
"typeImmunityHeal": "{{pokemonNameWithAffix}}'s {{abilityName}}\nrestored its HP a little!",
"nonSuperEffectiveImmunity": "{{pokemonNameWithAffix}} avoided damage\nwith {{abilityName}}!",
"fullHpResistType": "{{pokemonNameWithAffix}} made its shell gleam!\nIt's distorting type matchups!",
"moveImmunity": "It doesn't affect {{pokemonNameWithAffix}}!",
"reverseDrain": "{{pokemonNameWithAffix}} sucked up the liquid ooze!",
"postDefendTypeChange": "{{pokemonNameWithAffix}}'s {{abilityName}}\nmade it the {{typeName}} type!",

View File

@ -742,7 +742,7 @@
"plumeria": {
"encounter": {
"1": " ...Hmph. You don't look like anything special to me.",
"2": "It takes these dumb Grunts way too long to deal with you kids..",
"2": "It takes these dumb Grunts way too long to deal with you kids...",
"3": "Mess with anyone in Team Skull, and I'll show you how serious I can get."
},
"victory": {
@ -1479,21 +1479,21 @@
"1_female": "I need to see your potential as a Trainer. And, I'll need to see the toughness of the Pokémon that battle with you!",
"2": "Here goes! These are my rocking Pokémon, my pride and joy!",
"3": "Rock-type Pokémon are simply the best!",
"4": "I need to see your potential as a Trainer. And, I'll need to see the toughness of the Pokémon that battle with you!",
"4_female": "I need to see your potential as a Trainer. And, I'll need to see the toughness of the Pokémon that battle with you!"
"4": "Every day, I toughened up my Pokémon by digging up Fossils nonstop.\n$Could I show you how tough I made them in a battle?",
"4_female": "Every day, I toughened up my Pokémon by digging up Fossils nonstop.\n$Could I show you how tough I made them in a battle?"
},
"victory": {
"1": "W-what? That can't be! My buffed-up Pokémon!",
"2": "…We lost control there. Next time I'd like to challenge you to a Fossil-digging race underground.",
"2_female": "…We lost control there. Next time I'd like to challenge you to a Fossil-digging race underground.",
"3": "With skill like yours, it's natural for you to win.",
"4": "Wh-what?! It can't be! Even that wasn't enough?",
"5": "I blew it."
"4": "Wh-what?! It can't be! Even that wasn't enough?"
},
"defeat": {
"1": "See? I'm proud of my rocking battle style!",
"2": "Thanks! The battle gave me confidence that I may be able to beat my dad!",
"3": "I feel like I just smashed through a really stubborn boulder!"
"3": "See? These are my rocking Pokémon, my pride and joy!",
"4": "I knew I would win!"
}
},
"morty": {

View File

@ -1,3 +1,12 @@
{
"bypassSpeedChanceApply": "¡Gracias {{itemName}} {{pokemonName}} puede tener prioridad!"
}
"surviveDamageApply": "{{pokemonNameWithAffix}} ha usado {{typeName}} y ha logrado resistir!",
"turnHealApply": "{{pokemonNameWithAffix}} ha recuperado unos pocos PS gracias a {{typeName}}!",
"hitHealApply": "{{pokemonNameWithAffix}} ha recuperado unos pocos PS gracias a {{typeName}}!",
"pokemonInstantReviveApply": "{{pokemonNameWithAffix}} ha sido revivido gracias a su {{typeName}}!",
"pokemonResetNegativeStatStageApply": "Las estadísticas bajadas de {{pokemonNameWithAffix}} fueron restauradas gracias a {{typeName}}!",
"moneyInterestApply": "Recibiste intereses de ₽{{moneyAmount}}\ngracias a {{typeName}}!",
"turnHeldItemTransferApply": "{{pokemonNameWithAffix}}'s {{itemName}} fue absorbido\npor {{pokemonName}}'s {{typeName}}!",
"contactHeldItemTransferApply": "{{pokemonNameWithAffix}}'s {{itemName}} fue robado por {{pokemonName}}'s {{typeName}}!",
"enemyTurnHealApply": "¡{{pokemonNameWithAffix}}\nrecuperó algunos PS!",
"bypassSpeedChanceApply": "¡Gracias a su {{itemName}}, {{pokemonName}} puede tener prioridad!"
}

View File

@ -3,12 +3,12 @@
"joinTheDiscord": "¡Únete al Discord!",
"infiniteLevels": "¡Niveles infinitos!",
"everythingStacks": "¡Todo se acumula!",
"optionalSaveScumming": "¡Trampas guardando opcionales!)!",
"optionalSaveScumming": "¡Trampas de guardado opcionales!",
"biomes": "¡35 biomas!",
"openSource": "¡Código abierto!",
"playWithSpeed": "¡Juega a velocidad 5x!",
"liveBugTesting": Arreglo de bugs sobre la marcha!",
"heavyInfluence": Influencia Alta en RoR2!",
"liveBugTesting": Testeo de bugs en directo!",
"heavyInfluence": Mucha Influencia de RoR2!",
"pokemonRiskAndPokemonRain": "¡Pokémon Risk y Pokémon Rain!",
"nowWithMoreSalt": "¡Con un 33% más de polémica!",
"infiniteFusionAtHome": "¡Infinite Fusion en casa!",
@ -17,16 +17,16 @@
"mubstitute": "¡Mubstituto!",
"thatsCrazy": "¡De locos!",
"oranceJuice": "¡Zumo de narancia!",
"questionableBalancing": Balance cuestionable!",
"questionableBalancing": Cambios en balance cuestionables!",
"coolShaders": "¡Shaders impresionantes!",
"aiFree": "¡Libre de IA!",
"suddenDifficultySpikes": "¡Saltos de dificultad repentinos!",
"basedOnAnUnfinishedFlashGame": "¡Basado en un juego Flash inacabado!",
"moreAddictiveThanIntended": "¡Más adictivo de la cuenta!",
"moreAddictiveThanIntended": "¡Más adictivo de lo previsto!",
"mostlyConsistentSeeds": "¡Semillas CASI consistentes!",
"achievementPointsDontDoAnything": "¡Los Puntos de Logro no hacen nada!",
"youDoNotStartAtLevel": "¡No empiezas al nivel 2000!",
"dontTalkAboutTheManaphyEggIncident": "¡No hablen del incidente del Huevo Manaphy!",
"dontTalkAboutTheManaphyEggIncident": "¡No se habla del Incidente Manaphy!",
"alsoTryPokengine": "¡Prueba también Pokéngine!",
"alsoTryEmeraldRogue": "¡Prueba también Emerald Rogue!",
"alsoTryRadicalRed": "¡Prueba también Radical Red!",

View File

@ -1 +1,16 @@
{}
{
"misty": "Niebla",
"mistyStartMessage": "¡La niebla ha envuelto el terreno de combate!",
"mistyClearMessage": "La niebla se ha disipado.",
"mistyBlockMessage": "¡El campo de niebla ha protegido a {{pokemonNameWithAffix}} ",
"electric": "Eléctrico",
"electricStartMessage": "¡Se ha formado un campo de corriente eléctrica en el terreno\nde combate!",
"electricClearMessage": "El campo de corriente eléctrica ha desaparecido.\t",
"grassy": "Hierba",
"grassyStartMessage": "¡El terreno de combate se ha cubierto de hierba!",
"grassyClearMessage": "La hierba ha desaparecido.",
"psychic": "Psíquico",
"psychicStartMessage": "¡El terreno de combate se ha vuelto muy extraño!",
"psychicClearMessage": "Ha desaparecido la extraña sensación que se percibía en el terreno\nde combate.",
"defaultBlockMessage": "¡El campo {{terrainName}} ha protegido a {{pokemonNameWithAffix}} "
}

View File

@ -1,7 +1,7 @@
{
"brock": "Brock",
"misty": "Misty",
"lt_surge": "Tt. Surge",
"lt_surge": "Teniente Surge",
"erika": "Erika",
"janine": "Sachiko",
"sabrina": "Sabrina",
@ -23,7 +23,7 @@
"winona": "Alana",
"tate": "Vito",
"liza": "Leti",
"juan": "Galán",
"juan": "Galano",
"roark": "Roco",
"gardenia": "Gardenia",
"maylene": "Brega",
@ -34,7 +34,7 @@
"volkner": "Lectro",
"cilan": "Millo",
"chili": "Zeo",
"cress": "Maiz",
"cress": "Maíz",
"cheren": "Cheren",
"lenora": "Aloe",
"roxie": "Hiedra",
@ -57,7 +57,7 @@
"nessa": "Cathy",
"kabu": "Naboru",
"bea": "Judith",
"allister": "Allistair",
"allister": "Alistair",
"opal": "Sally",
"bede": "Berto",
"gordie": "Morris",
@ -123,30 +123,28 @@
"leon": "Lionel",
"rival": "Finn",
"rival_female": "Ivy",
"archer": "Archer",
"ariana": "Ariana",
"proton": "Proton",
"archer": "Atlas",
"ariana": "Atenea",
"proton": "Protón",
"petrel": "Petrel",
"tabitha": "Tabitha",
"courtney": "Courtney",
"shelly": "Shelly",
"matt": "Matt",
"mars": "Mars",
"jupiter": "Jupiter",
"saturn": "Saturn",
"zinzolin": "Zinzolin",
"rood": "Rood",
"xerosic": "Xerosic",
"bryony": "Bryony",
"tabitha": "Tatiano",
"courtney": "Carola",
"shelly": "Silvina",
"matt": "Matías",
"mars": "Venus",
"jupiter": "Ceres",
"saturn": "Saturno",
"zinzolin": "Menek",
"rood": "Ruga",
"xerosic": "Xero",
"bryony": "Begonia",
"maxie": "Magno",
"archie": "Aquiles",
"cyrus": "Helio",
"ghetsis": "Ghechis",
"lysandre": "Lysson",
"faba": "Fabio",
"maxie": "Maxie",
"archie": "Archie",
"cyrus": "Cyrus",
"ghetsis": "Ghetsis",
"lysandre": "Lysandre",
"lusamine": "Samina",
"blue_red_double": "Azul y Rojo",
"red_blue_double": "Rojo y Azul",
"tate_liza_double": "Vito y Leti",

View File

@ -13,5 +13,32 @@
"metFragment": {
"normal": "rencontré au N.{{level}},\n{{biome}}.",
"apparently": "apparemment rencontré au N.{{level}},\n{{biome}}."
},
"natureFragment": {
"Hardy": "{{nature}}",
"Lonely": "{{nature}}",
"Brave": "{{nature}}",
"Adamant": "{{nature}}",
"Naughty": "{{nature}}",
"Bold": "{{nature}}",
"Docile": "{{nature}}",
"Relaxed": "{{nature}}",
"Impish": "{{nature}}",
"Lax": "{{nature}}",
"Timid": "{{nature}}",
"Hasty": "{{nature}}",
"Serious": "{{nature}}",
"Jolly": "{{nature}}",
"Naive": "{{nature}}",
"Modest": "{{nature}}",
"Mild": "{{nature}}",
"Quiet": "{{nature}}",
"Bashful": "{{nature}}",
"Rash": "{{nature}}",
"Calm": "{{nature}}",
"Gentle": "{{nature}}",
"Sassy": "{{nature}}",
"Careful": "{{nature}}",
"Quirky": "{{nature}}"
}
}
}

View File

@ -225,7 +225,7 @@
"name": "독침붕처럼 쏴라"
},
"MONO_GHOST": {
"name": "누굴 부를 거야?"
"name": "무서운 게 딱 좋아!"
},
"MONO_STEEL": {
"name": "강철 심장"
@ -265,4 +265,4 @@
"name": "상성 전문가(였던 것)",
"description": "거꾸로 배틀 챌린지 모드 클리어."
}
}
}

View File

@ -1,21 +1,18 @@
import BattleScene from "#app/battle-scene.js";
import { TurnCommand, BattleType } from "#app/battle.js";
import { applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "#app/data/ability.js";
import { TrappedTag, EncoreTag } from "#app/data/battler-tags.js";
import { MoveTargetSet, getMoveTargets } from "#app/data/move.js";
import { speciesStarters } from "#app/data/pokemon-species.js";
import { Type } from "#app/data/type.js";
import { Abilities } from "#app/enums/abilities.js";
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
import { Biome } from "#app/enums/biome.js";
import { Moves } from "#app/enums/moves.js";
import { PokeballType } from "#app/enums/pokeball.js";
import { FieldPosition, PlayerPokemon } from "#app/field/pokemon.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { Command } from "#app/ui/command-ui-handler.js";
import { Mode } from "#app/ui/ui.js";
import BattleScene from "#app/battle-scene";
import { TurnCommand, BattleType } from "#app/battle";
import { TrappedTag, EncoreTag } from "#app/data/battler-tags";
import { MoveTargetSet, getMoveTargets } from "#app/data/move";
import { speciesStarters } from "#app/data/pokemon-species";
import { Abilities } from "#app/enums/abilities";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Biome } from "#app/enums/biome";
import { Moves } from "#app/enums/moves";
import { PokeballType } from "#app/enums/pokeball";
import { FieldPosition, PlayerPokemon } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { Command } from "#app/ui/command-ui-handler";
import { Mode } from "#app/ui/ui";
import i18next from "i18next";
import * as Utils from "#app/utils.js";
import { FieldPhase } from "./field-phase";
import { SelectTargetPhase } from "./select-target-phase";
@ -77,7 +74,6 @@ export class CommandPhase extends FieldPhase {
handleCommand(command: Command, cursor: integer, ...args: any[]): boolean {
const playerPokemon = this.scene.getPlayerField()[this.fieldIndex];
const enemyField = this.scene.getEnemyField();
let success: boolean;
switch (command) {
@ -184,14 +180,9 @@ export class CommandPhase extends FieldPhase {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}, null, true);
} else {
const trapTag = playerPokemon.findTag(t => t instanceof TrappedTag) as TrappedTag;
const trapped = new Utils.BooleanHolder(false);
const batonPass = isSwitch && args[0] as boolean;
const trappedAbMessages: string[] = [];
if (!batonPass) {
enemyField.forEach(enemyPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, enemyPokemon, trapped, playerPokemon, trappedAbMessages, true));
}
if (batonPass || (!trapTag && !trapped.value)) {
if (batonPass || !playerPokemon.isTrapped(trappedAbMessages)) {
this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch
? { command: Command.POKEMON, cursor: cursor, args: args }
: { command: Command.RUN };
@ -199,14 +190,27 @@ export class CommandPhase extends FieldPhase {
if (!isSwitch && this.fieldIndex) {
this.scene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
}
} else if (trapTag) {
if (trapTag.sourceMove === Moves.INGRAIN && trapTag.sourceId && this.scene.getPokemonById(trapTag.sourceId)?.isOfType(Type.GHOST)) {
success = true;
} else if (trappedAbMessages.length > 0) {
if (!isSwitch) {
this.scene.ui.setMode(Mode.MESSAGE);
}
this.scene.ui.showText(trappedAbMessages[0], null, () => {
this.scene.ui.showText("", 0);
if (!isSwitch) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}
}, null, true);
} else {
const trapTag = playerPokemon.getTag(TrappedTag);
// trapTag should be defined at this point, but just in case...
if (!trapTag) {
this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch
? { command: Command.POKEMON, cursor: cursor, args: args }
: { command: Command.RUN };
break;
}
if (!isSwitch) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
@ -224,16 +228,6 @@ export class CommandPhase extends FieldPhase {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}
}, null, true);
} else if (trapped.value && trappedAbMessages.length > 0) {
if (!isSwitch) {
this.scene.ui.setMode(Mode.MESSAGE);
}
this.scene.ui.showText(trappedAbMessages[0], null, () => {
this.scene.ui.showText("", 0);
if (!isSwitch) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}
}, null, true);
}
}
break;

View File

@ -1,9 +1,6 @@
import BattleScene from "#app/battle-scene.js";
import { BattlerIndex } from "#app/battle.js";
import { applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "#app/data/ability.js";
import { TrappedTag } from "#app/data/battler-tags.js";
import { Command } from "#app/ui/command-ui-handler.js";
import * as Utils from "#app/utils.js";
import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle";
import { Command } from "#app/ui/command-ui-handler";
import { FieldPhase } from "./field-phase";
/**
@ -45,10 +42,7 @@ export class EnemyCommandPhase extends FieldPhase {
if (trainer && !enemyPokemon.getMoveQueue().length) {
const opponents = enemyPokemon.getOpponents();
const trapTag = enemyPokemon.findTag(t => t instanceof TrappedTag) as TrappedTag;
const trapped = new Utils.BooleanHolder(false);
opponents.forEach(playerPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, playerPokemon, trapped, enemyPokemon, [""], true));
if (!trapTag && !trapped.value) {
if (!enemyPokemon.isTrapped()) {
const partyMemberScores = trainer.getPartyMemberMatchupScores(enemyPokemon.trainerSlot, true);
if (partyMemberScores.length) {

View File

@ -1,14 +1,14 @@
import BattleScene from "#app/battle-scene.js";
import { BattlerIndex } from "#app/battle.js";
import { applyPreStatChangeAbAttrs, ProtectStatAbAttr, applyAbAttrs, StatChangeMultiplierAbAttr, StatChangeCopyAbAttr, applyPostStatChangeAbAttrs, PostStatChangeAbAttr } from "#app/data/ability.js";
import { MistTag, ArenaTagSide } from "#app/data/arena-tag.js";
import { BattleStat, getBattleStatName, getBattleStatLevelChangeDescription } from "#app/data/battle-stat.js";
import Pokemon from "#app/field/pokemon.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { PokemonResetNegativeStatStageModifier } from "#app/modifier/modifier.js";
import { handleTutorial, Tutorial } from "#app/tutorial.js";
import { BattlerIndex } from "#app/battle";
import BattleScene from "#app/battle-scene";
import { applyAbAttrs, applyPostStatChangeAbAttrs, applyPreStatChangeAbAttrs, PostStatChangeAbAttr, ProtectStatAbAttr, StatChangeCopyAbAttr, StatChangeMultiplierAbAttr } from "#app/data/ability";
import { ArenaTagSide, MistTag } from "#app/data/arena-tag";
import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "#app/data/battle-stat";
import Pokemon from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonResetNegativeStatStageModifier } from "#app/modifier/modifier";
import { handleTutorial, Tutorial } from "#app/tutorial";
import * as Utils from "#app/utils";
import i18next from "i18next";
import * as Utils from "#app/utils.js";
import { PokemonPhase } from "./pokemon-phase";
export type StatChangeCallback = (target: Pokemon | null, changed: BattleStat[], relativeChanges: number[]) => void;
@ -72,7 +72,7 @@ export class StatChangePhase extends PokemonPhase {
}
const battleStats = this.getPokemon().summonData.battleStats;
const relLevels = filteredStats.map(stat => (levels.value >= 1 ? Math.min(battleStats![stat] + levels.value, 6) : Math.max(battleStats![stat] + levels.value, -6)) - battleStats![stat]);
const relLevels = filteredStats.map(stat => (levels.value >= 1 ? Math.min(battleStats[stat] + levels.value, 6) : Math.max(battleStats[stat] + levels.value, -6)) - battleStats[stat]);
this.onChange && this.onChange(this.getPokemon(), filteredStats, relLevels);
@ -85,6 +85,20 @@ export class StatChangePhase extends PokemonPhase {
}
for (const stat of filteredStats) {
if (levels.value > 0 && pokemon.summonData.battleStats[stat] < 6) {
if (!pokemon.turnData) {
// Temporary fix for missing turn data struct on turn 1
pokemon.resetTurnData();
}
pokemon.turnData.battleStatsIncreased = true;
} else if (levels.value < 0 && pokemon.summonData.battleStats[stat] > -6) {
if (!pokemon.turnData) {
// Temporary fix for missing turn data struct on turn 1
pokemon.resetTurnData();
}
pokemon.turnData.battleStatsDecreased = true;
}
pokemon.summonData.battleStats[stat] = Math.max(Math.min(pokemon.summonData.battleStats[stat] + levels.value, 6), -6);
}

View File

@ -1,13 +1,11 @@
import { BattleStyle } from "#app/enums/battle-style";
import { CommandPhase } from "#app/phases/command-phase";
import { MessagePhase } from "#app/phases/message-phase";
import { TurnInitPhase } from "#app/phases/turn-init-phase";
import i18next, { initI18n } from "#app/plugins/i18n";
import { Mode } from "#app/ui/ui";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import { SPLASH_ONLY } from "#test/utils/testUtils";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@ -28,19 +26,18 @@ describe("Ability Timing", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.battleType("single");
game.override.enemySpecies(Species.PIDGEY);
game.override.enemyAbility(Abilities.INTIMIDATE);
game.override.enemyMoveset(SPLASH_ONLY);
game.override.ability(Abilities.BALL_FETCH);
game.override.moveset([Moves.SPLASH, Moves.ICE_BEAM]);
game.override
.battleType("single")
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.INTIMIDATE)
.ability(Abilities.BALL_FETCH);
});
it("should trigger after switch check", async() => {
it("should trigger after switch check", async () => {
initI18n();
i18next.changeLanguage("en");
game.settings.battleStyle = BattleStyle.SWITCH;
await game.classicMode.runToSummon([Species.EEVEE, Species.FEEBAS]);
game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => {
@ -48,7 +45,7 @@ describe("Ability Timing", () => {
game.endPhase();
}, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase));
await game.phaseInterceptor.to(MessagePhase);
await game.phaseInterceptor.to("MessagePhase");
const message = game.textInterceptor.getLatestMessage();
expect(message).toContain("Attack fell");
}, 5000);

View File

@ -1,11 +1,5 @@
import { BattleStat } from "#app/data/battle-stat";
import { StatusEffect } from "#app/data/status-effect";
import { CommandPhase } from "#app/phases/command-phase";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { MoveEndPhase } from "#app/phases/move-end-phase";
import { TurnEndPhase } from "#app/phases/turn-end-phase";
import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { Mode } from "#app/ui/ui";
import { toDmgValue } from "#app/utils";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
@ -33,13 +27,12 @@ describe("Abilities - Disguise", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.battleType("single");
game.override.enemySpecies(Species.MIMIKYU);
game.override.enemyMoveset(SPLASH_ONLY);
game.override.starterSpecies(Species.REGIELEKI);
game.override.moveset([Moves.SHADOW_SNEAK, Moves.VACUUM_WAVE, Moves.TOXIC_THREAD, Moves.SPLASH]);
game.override
.battleType("single")
.enemySpecies(Species.MIMIKYU)
.enemyMoveset(SPLASH_ONLY)
.starterSpecies(Species.REGIELEKI)
.moveset([Moves.SHADOW_SNEAK, Moves.VACUUM_WAVE, Moves.TOXIC_THREAD, Moves.SPLASH]);
}, TIMEOUT);
it("takes no damage from attacking move and transforms to Busted form, takes 1/8 max HP damage from the disguise breaking", async () => {
@ -53,7 +46,7 @@ describe("Abilities - Disguise", () => {
game.move.select(Moves.SHADOW_SNEAK);
await game.phaseInterceptor.to(MoveEndPhase);
await game.phaseInterceptor.to("MoveEndPhase");
expect(mimikyu.hp).equals(maxHp - disguiseDamage);
expect(mimikyu.formIndex).toBe(bustedForm);
@ -68,7 +61,7 @@ describe("Abilities - Disguise", () => {
game.move.select(Moves.VACUUM_WAVE);
await game.phaseInterceptor.to(MoveEndPhase);
await game.phaseInterceptor.to("MoveEndPhase");
expect(mimikyu.formIndex).toBe(disguisedForm);
}, TIMEOUT);
@ -87,12 +80,12 @@ describe("Abilities - Disguise", () => {
game.move.select(Moves.SURGING_STRIKES);
// First hit
await game.phaseInterceptor.to(MoveEffectPhase);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(mimikyu.hp).equals(maxHp - disguiseDamage);
expect(mimikyu.formIndex).toBe(disguisedForm);
// Second hit
await game.phaseInterceptor.to(MoveEffectPhase);
await game.phaseInterceptor.to("MoveEffectPhase");
expect(mimikyu.hp).lessThan(maxHp - disguiseDamage);
expect(mimikyu.formIndex).toBe(bustedForm);
}, TIMEOUT);
@ -105,7 +98,7 @@ describe("Abilities - Disguise", () => {
game.move.select(Moves.TOXIC_THREAD);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(mimikyu.formIndex).toBe(disguisedForm);
expect(mimikyu.status?.effect).toBe(StatusEffect.POISON);
@ -125,7 +118,7 @@ describe("Abilities - Disguise", () => {
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(mimikyu.formIndex).toBe(bustedForm);
expect(mimikyu.hp).equals(maxHp - disguiseDamage);
@ -133,7 +126,7 @@ describe("Abilities - Disguise", () => {
await game.toNextTurn();
game.doSwitchPokemon(1);
await game.phaseInterceptor.to(TurnEndPhase);
await game.phaseInterceptor.to("TurnEndPhase");
expect(mimikyu.formIndex).toBe(bustedForm);
}, TIMEOUT);
@ -194,15 +187,6 @@ describe("Abilities - Disguise", () => {
await game.toNextTurn();
game.move.select(Moves.SPLASH);
await game.doKillOpponents();
game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => { // TODO: Make tests run in set mode instead of switch mode
game.setMode(Mode.MESSAGE);
game.endPhase();
}, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase));
game.onNextPrompt("CheckSwitchPhase", Mode.CONFIRM, () => {
game.setMode(Mode.MESSAGE);
game.endPhase();
}, () => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase));
await game.phaseInterceptor.to("PartyHealPhase");
expect(mimikyu1.formIndex).toBe(disguisedForm);

View File

@ -0,0 +1,154 @@
import { BattlerIndex } from "#app/battle";
import { Abilities } from "#app/enums/abilities";
import { Stat } from "#app/enums/stat";
import { WeatherType } from "#app/enums/weather-type";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import { SPLASH_ONLY } from "#test/utils/testUtils";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Abilities - Flower Gift", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
const OVERCAST_FORM = 0;
const SUNSHINE_FORM = 1;
/**
* Tests reverting to normal form when Cloud Nine/Air Lock is active on the field
* @param {GameManager} game The game manager instance
* @param {Abilities} ability The ability that is active on the field
*/
const testRevertFormAgainstAbility = async (game: GameManager, ability: Abilities) => {
game.override.starterForms({ [Species.CASTFORM]: SUNSHINE_FORM }).enemyAbility(ability);
await game.classicMode.startBattle([Species.CASTFORM]);
game.move.select(Moves.SPLASH);
expect(game.scene.getPlayerPokemon()?.formIndex).toBe(OVERCAST_FORM);
};
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
game.override
.moveset([Moves.SPLASH, Moves.RAIN_DANCE, Moves.SUNNY_DAY, Moves.SKILL_SWAP])
.enemySpecies(Species.MAGIKARP)
.enemyMoveset(SPLASH_ONLY)
.enemyAbility(Abilities.BALL_FETCH);
});
// TODO: Uncomment expect statements when the ability is implemented - currently does not increase stats of allies
it("increases the Attack and Special Defense stats of the Pokémon with this Ability and its allies by 1.5× during Harsh Sunlight", async () => {
game.override.battleType("double");
await game.classicMode.startBattle([Species.CHERRIM, Species.MAGIKARP]);
const [ cherrim ] = game.scene.getPlayerField();
const cherrimAtkStat = cherrim.getBattleStat(Stat.ATK);
const cherrimSpDefStat = cherrim.getBattleStat(Stat.SPDEF);
// const magikarpAtkStat = magikarp.getBattleStat(Stat.ATK);;
// const magikarpSpDefStat = magikarp.getBattleStat(Stat.SPDEF);
game.move.select(Moves.SUNNY_DAY, 0);
game.move.select(Moves.SPLASH, 1);
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
await game.phaseInterceptor.to("TurnEndPhase");
expect(cherrim.formIndex).toBe(SUNSHINE_FORM);
expect(cherrim.getBattleStat(Stat.ATK)).toBe(Math.floor(cherrimAtkStat * 1.5));
expect(cherrim.getBattleStat(Stat.SPDEF)).toBe(Math.floor(cherrimSpDefStat * 1.5));
// expect(magikarp.getBattleStat(Stat.ATK)).toBe(Math.floor(magikarpAtkStat * 1.5));
// expect(magikarp.getBattleStat(Stat.SPDEF)).toBe(Math.floor(magikarpSpDefStat * 1.5));
});
it("changes the Pokemon's form during Harsh Sunlight", async () => {
game.override.weather(WeatherType.HARSH_SUN);
await game.classicMode.startBattle([Species.CHERRIM]);
const cherrim = game.scene.getPlayerPokemon()!;
expect(cherrim.formIndex).toBe(SUNSHINE_FORM);
game.move.select(Moves.SPLASH);
});
it("reverts to Overcast Form if a Pokémon on the field has Air Lock", async () => {
await testRevertFormAgainstAbility(game, Abilities.AIR_LOCK);
});
it("reverts to Overcast Form if a Pokémon on the field has Cloud Nine", async () => {
await testRevertFormAgainstAbility(game, Abilities.CLOUD_NINE);
});
it("reverts to Overcast Form when the Pokémon loses Flower Gift, changes form under Harsh Sunlight/Sunny when it regains it", async () => {
game.override.enemyMoveset(Array(4).fill(Moves.SKILL_SWAP)).weather(WeatherType.HARSH_SUN);
await game.classicMode.startBattle([Species.CHERRIM]);
const cherrim = game.scene.getPlayerPokemon()!;
game.move.select(Moves.SKILL_SWAP);
await game.phaseInterceptor.to("TurnStartPhase");
expect(cherrim.formIndex).toBe(SUNSHINE_FORM);
await game.phaseInterceptor.to("MoveEndPhase");
expect(cherrim.formIndex).toBe(OVERCAST_FORM);
await game.phaseInterceptor.to("MoveEndPhase");
expect(cherrim.formIndex).toBe(SUNSHINE_FORM);
});
it("reverts to Overcast Form when the Flower Gift is suppressed, changes form under Harsh Sunlight/Sunny when it regains it", async () => {
game.override.enemyMoveset(Array(4).fill(Moves.GASTRO_ACID)).weather(WeatherType.HARSH_SUN);
await game.classicMode.startBattle([Species.CHERRIM, Species.MAGIKARP]);
const cherrim = game.scene.getPlayerPokemon()!;
expect(cherrim.formIndex).toBe(SUNSHINE_FORM);
game.move.select(Moves.SPLASH);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("TurnEndPhase");
expect(cherrim.summonData.abilitySuppressed).toBe(true);
expect(cherrim.formIndex).toBe(OVERCAST_FORM);
await game.toNextTurn();
game.doSwitchPokemon(1);
await game.toNextTurn();
game.doSwitchPokemon(1);
await game.phaseInterceptor.to("MovePhase");
expect(cherrim.summonData.abilitySuppressed).toBe(false);
expect(cherrim.formIndex).toBe(SUNSHINE_FORM);
});
it("should be in Overcast Form after the user is switched out", async () => {
game.override.weather(WeatherType.SUNNY);
await game.classicMode.startBattle([Species.CASTFORM, Species.MAGIKARP]);
const cherrim = game.scene.getPlayerPokemon()!;
expect(cherrim.formIndex).toBe(SUNSHINE_FORM);
game.doSwitchPokemon(1);
await game.toNextTurn();
expect(cherrim.formIndex).toBe(OVERCAST_FORM);
});
});

View File

@ -1,12 +1,8 @@
import { BattleStat } from "#app/data/battle-stat";
import { Status, StatusEffect } from "#app/data/status-effect";
import { GameModes, getGameMode } from "#app/game-mode";
import { CommandPhase } from "#app/phases/command-phase";
import { DamagePhase } from "#app/phases/damage-phase";
import { EncounterPhase } from "#app/phases/encounter-phase";
import { EnemyCommandPhase } from "#app/phases/enemy-command-phase";
import { SelectStarterPhase } from "#app/phases/select-starter-phase";
import { TurnInitPhase } from "#app/phases/turn-init-phase";
import { Mode } from "#app/ui/ui";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
@ -33,36 +29,26 @@ describe("Abilities - Intimidate", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.battleType("single");
game.override.enemySpecies(Species.RATTATA);
game.override.enemyAbility(Abilities.INTIMIDATE);
game.override.enemyPassiveAbility(Abilities.HYDRATION);
game.override.ability(Abilities.INTIMIDATE);
game.override.startingWave(3);
game.override.enemyMoveset(SPLASH_ONLY);
game.override
.battleType("single")
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.INTIMIDATE)
.ability(Abilities.INTIMIDATE)
.moveset([Moves.SPLASH, Moves.AERIAL_ACE])
.enemyMoveset(SPLASH_ONLY);
});
it("single - wild with switch", async () => {
await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]);
game.onNextPrompt(
"CheckSwitchPhase",
Mode.CONFIRM,
() => {
game.setMode(Mode.MESSAGE);
game.endPhase();
},
() => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase)
);
await game.phaseInterceptor.to(CommandPhase, false);
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.MIGHTYENA);
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
game.doSwitchPokemon(1);
await game.phaseInterceptor.run(CommandPhase);
await game.phaseInterceptor.to(CommandPhase);
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.POOCHYENA);
await game.phaseInterceptor.to("CommandPhase");
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(0);
@ -73,25 +59,16 @@ describe("Abilities - Intimidate", () => {
it("single - boss should only trigger once then switch", async () => {
game.override.startingWave(10);
await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]);
game.onNextPrompt(
"CheckSwitchPhase",
Mode.CONFIRM,
() => {
game.setMode(Mode.MESSAGE);
game.endPhase();
},
() => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase)
);
await game.phaseInterceptor.to(CommandPhase, false);
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
game.doSwitchPokemon(1);
await game.phaseInterceptor.run(CommandPhase);
await game.phaseInterceptor.to(CommandPhase);
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.POOCHYENA);
await game.phaseInterceptor.to("CommandPhase");
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(0);
@ -102,25 +79,16 @@ describe("Abilities - Intimidate", () => {
it("single - trainer should only trigger once with switch", async () => {
game.override.startingWave(5);
await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]);
game.onNextPrompt(
"CheckSwitchPhase",
Mode.CONFIRM,
() => {
game.setMode(Mode.MESSAGE);
game.endPhase();
},
() => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase)
);
await game.phaseInterceptor.to(CommandPhase, false);
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
game.doSwitchPokemon(1);
await game.phaseInterceptor.run(CommandPhase);
await game.phaseInterceptor.to(CommandPhase);
expect(game.scene.getParty()[0].species.speciesId).toBe(Species.POOCHYENA);
await game.phaseInterceptor.to("CommandPhase");
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(0);
@ -130,21 +98,14 @@ describe("Abilities - Intimidate", () => {
}, 200000);
it("double - trainer should only trigger once per pokemon", async () => {
game.override.battleType("double");
game.override.startingWave(5);
await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]);
game.onNextPrompt(
"CheckSwitchPhase",
Mode.CONFIRM,
() => {
game.setMode(Mode.MESSAGE);
game.endPhase();
},
() => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase)
);
await game.phaseInterceptor.to(CommandPhase, false);
game.override
.battleType("double")
.startingWave(5);
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2);
const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats;
expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2);
@ -157,20 +118,11 @@ describe("Abilities - Intimidate", () => {
it("double - wild: should only trigger once per pokemon", async () => {
game.override.battleType("double");
game.override.startingWave(3);
await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]);
game.onNextPrompt(
"CheckSwitchPhase",
Mode.CONFIRM,
() => {
game.setMode(Mode.MESSAGE);
game.endPhase();
},
() => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase)
);
await game.phaseInterceptor.to(CommandPhase, false);
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2);
const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats;
expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2);
@ -182,21 +134,14 @@ describe("Abilities - Intimidate", () => {
}, 20000);
it("double - boss: should only trigger once per pokemon", async () => {
game.override.battleType("double");
game.override.startingWave(10);
await game.classicMode.runToSummon([Species.MIGHTYENA, Species.POOCHYENA]);
game.onNextPrompt(
"CheckSwitchPhase",
Mode.CONFIRM,
() => {
game.setMode(Mode.MESSAGE);
game.endPhase();
},
() => game.isCurrentPhase(CommandPhase) || game.isCurrentPhase(TurnInitPhase)
);
await game.phaseInterceptor.to(CommandPhase, false);
game.override
.battleType("double")
.startingWave(10);
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-2);
const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats;
expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-2);
@ -208,104 +153,101 @@ describe("Abilities - Intimidate", () => {
}, 20000);
it("single - wild next wave opp triger once, us: none", async () => {
game.override.startingWave(2);
game.override.moveset([Moves.AERIAL_ACE]);
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
await game.startBattle([Species.MIGHTYENA]);
let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
game.move.select(Moves.AERIAL_ACE);
await game.phaseInterceptor.runFrom(EnemyCommandPhase).to(DamagePhase);
await game.killPokemon(game.scene.currentBattle.enemyParty[0]);
expect(game.scene.currentBattle.enemyParty[0].isFainted()).toBe(true);
await game.phaseInterceptor.to("DamagePhase");
await game.doKillOpponents();
await game.toNextWave();
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2);
battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(0);
}, 20000);
it("single - wild next turn - no retrigger on next turn", async () => {
game.override.startingWave(2);
game.override.moveset([Moves.SPLASH]);
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
await game.startBattle([Species.MIGHTYENA]);
let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
game.move.select(Moves.AERIAL_ACE);
console.log("===to new turn===");
game.move.select(Moves.SPLASH);
await game.toNextTurn();
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
}, 20000);
it("single - trainer should only trigger once and each time he switch", async () => {
game.override.moveset([Moves.SPLASH]);
game.override.enemyMoveset([Moves.VOLT_SWITCH, Moves.VOLT_SWITCH, Moves.VOLT_SWITCH, Moves.VOLT_SWITCH]);
game.override.startingWave(5);
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
game.override
.enemyMoveset(Array(4).fill(Moves.VOLT_SWITCH))
.startingWave(5);
await game.startBattle([Species.MIGHTYENA]);
let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
game.move.select(Moves.AERIAL_ACE);
console.log("===to new turn===");
game.move.select(Moves.SPLASH);
await game.toNextTurn();
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-2);
battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(0);
game.move.select(Moves.AERIAL_ACE);
console.log("===to new turn===");
game.move.select(Moves.SPLASH);
await game.toNextTurn();
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-3);
battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(0);
}, 200000);
it("single - trainer should only trigger once whatever turn we are", async () => {
game.override.moveset([Moves.SPLASH]);
game.override.enemyMoveset(SPLASH_ONLY);
game.override.startingWave(5);
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
let battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
await game.startBattle([Species.MIGHTYENA]);
const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
const battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
let battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
game.move.select(Moves.AERIAL_ACE);
console.log("===to new turn===");
game.move.select(Moves.SPLASH);
await game.toNextTurn();
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
game.move.select(Moves.AERIAL_ACE);
console.log("===to new turn===");
await game.toNextTurn();
battleStatsPokemon = game.scene.getParty()[0].summonData.battleStats;
expect(battleStatsPokemon[BattleStat.ATK]).toBe(-1);
battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
}, 20000);
it("double - wild vs only 1 on player side", async () => {
game.override.battleType("double");
game.override.startingWave(3);
await game.classicMode.runToSummon([Species.MIGHTYENA]);
await game.phaseInterceptor.to(CommandPhase, false);
await game.phaseInterceptor.to("CommandPhase", false);
const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats;
expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-1);
@ -315,7 +257,6 @@ describe("Abilities - Intimidate", () => {
it("double - wild vs only 1 alive on player side", async () => {
game.override.battleType("double");
game.override.startingWave(3);
await game.runToTitle();
game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
@ -330,9 +271,11 @@ describe("Abilities - Intimidate", () => {
await game.phaseInterceptor.run(EncounterPhase);
await game.phaseInterceptor.to(CommandPhase, false);
await game.phaseInterceptor.to("CommandPhase", false);
const battleStatsOpponent = game.scene.currentBattle.enemyParty[0].summonData.battleStats;
expect(battleStatsOpponent[BattleStat.ATK]).toBe(-1);
const battleStatsOpponent2 = game.scene.currentBattle.enemyParty[1].summonData.battleStats;
expect(battleStatsOpponent2[BattleStat.ATK]).toBe(-1);

View File

@ -0,0 +1,111 @@
import { Abilities } from "#app/enums/abilities";
import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species";
import { HitResult } from "#app/field/pokemon.js";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
const TIMEOUT = 10 * 1000; // 10 second timeout
describe("Abilities - Tera Shell", () => {
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")
.ability(Abilities.TERA_SHELL)
.moveset([Moves.SPLASH])
.enemySpecies(Species.SNORLAX)
.enemyAbility(Abilities.INSOMNIA)
.enemyMoveset(Array(4).fill(Moves.MACH_PUNCH))
.startingLevel(100)
.enemyLevel(100);
});
it(
"should change the effectiveness of non-resisted attacks when the source is at full HP",
async () => {
await game.classicMode.startBattle([Species.SNORLAX]);
const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "getMoveEffectiveness");
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("MoveEndPhase");
expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(0.5);
await game.toNextTurn();
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("MoveEndPhase");
expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(2);
}, TIMEOUT
);
it(
"should not override type immunities",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.SHADOW_SNEAK));
await game.classicMode.startBattle([Species.SNORLAX]);
const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "getMoveEffectiveness");
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("MoveEndPhase");
expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(0);
}, TIMEOUT
);
it(
"should not override type multipliers less than 0.5x",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.QUICK_ATTACK));
await game.classicMode.startBattle([Species.AGGRON]);
const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "getMoveEffectiveness");
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("MoveEndPhase");
expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(0.25);
}, TIMEOUT
);
it(
"should not affect the effectiveness of fixed-damage moves",
async () => {
game.override.enemyMoveset(Array(4).fill(Moves.DRAGON_RAGE));
await game.classicMode.startBattle([Species.CHARIZARD]);
const playerPokemon = game.scene.getPlayerPokemon()!;
vi.spyOn(playerPokemon, "apply");
game.move.select(Moves.SPLASH);
await game.phaseInterceptor.to("BerryPhase", false);
expect(playerPokemon.apply).toHaveLastReturnedWith(HitResult.EFFECTIVE);
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp() - 40);
}, TIMEOUT
);
});

View File

@ -32,7 +32,7 @@ describe("Endless Boss", () => {
it(`should spawn a minor boss every ${EndlessBossWave.Minor} waves in END biome in Endless`, async () => {
game.override.startingWave(EndlessBossWave.Minor);
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.ENDLESS);
await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.ENDLESS);
expect(game.scene.currentBattle.waveIndex).toBe(EndlessBossWave.Minor);
expect(game.scene.arena.biomeType).toBe(Biome.END);
@ -44,7 +44,7 @@ describe("Endless Boss", () => {
it(`should spawn a major boss every ${EndlessBossWave.Major} waves in END biome in Endless`, async () => {
game.override.startingWave(EndlessBossWave.Major);
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.ENDLESS);
await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.ENDLESS);
expect(game.scene.currentBattle.waveIndex).toBe(EndlessBossWave.Major);
expect(game.scene.arena.biomeType).toBe(Biome.END);
@ -56,7 +56,7 @@ describe("Endless Boss", () => {
it(`should spawn a minor boss every ${EndlessBossWave.Minor} waves in END biome in Spliced Endless`, async () => {
game.override.startingWave(EndlessBossWave.Minor);
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.SPLICED_ENDLESS);
await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.SPLICED_ENDLESS);
expect(game.scene.currentBattle.waveIndex).toBe(EndlessBossWave.Minor);
expect(game.scene.arena.biomeType).toBe(Biome.END);
@ -68,7 +68,7 @@ describe("Endless Boss", () => {
it(`should spawn a major boss every ${EndlessBossWave.Major} waves in END biome in Spliced Endless`, async () => {
game.override.startingWave(EndlessBossWave.Major);
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.SPLICED_ENDLESS);
await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.SPLICED_ENDLESS);
expect(game.scene.currentBattle.waveIndex).toBe(EndlessBossWave.Major);
expect(game.scene.arena.biomeType).toBe(Biome.END);
@ -80,7 +80,7 @@ describe("Endless Boss", () => {
it(`should NOT spawn major or minor boss outside wave ${EndlessBossWave.Minor}s in END biome`, async () => {
game.override.startingWave(EndlessBossWave.Minor - 1);
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.ENDLESS);
await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.ENDLESS);
expect(game.scene.currentBattle.waveIndex).not.toBe(EndlessBossWave.Minor);
expect(game.scene.getEnemyPokemon()!.species.speciesId).not.toBe(Species.ETERNATUS);

View File

@ -2,9 +2,10 @@ import { pokemonEvolutions, SpeciesFormEvolution, SpeciesWildEvolutionDelay } fr
import { Abilities } from "#app/enums/abilities";
import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species";
import * as Utils from "#app/utils";
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 { SPLASH_ONLY } from "./utils/testUtils";
describe("Evolution", () => {
@ -148,4 +149,28 @@ describe("Evolution", () => {
expect(cyndaquil.hp).toBeGreaterThan(hpBefore);
expect(cyndaquil.hp).toBeLessThan(cyndaquil.getMaxHp());
}, TIMEOUT);
it("should handle rng-based split evolution", async () => {
/* this test checks to make sure that tandemaus will
* evolve into a 3 family maushold 25% of the time
* and a 4 family maushold the other 75% of the time
* This is done by using the getEvolution method in pokemon.ts
* getEvolution will give back the form that the pokemon can evolve into
* It does this by checking the pokemon conditions in pokemon-forms.ts
* For tandemaus, the conditions are random due to a randSeedInt(4)
* If the value is 0, it's a 3 family maushold, whereas if the value is
* 1, 2 or 3, it's a 4 family maushold
*/
await game.startBattle([Species.TANDEMAUS]); // starts us off with a tandemaus
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.level = 25; // tandemaus evolves at level 25
vi.spyOn(Utils, "randSeedInt").mockReturnValue(0); // setting the random generator to be 0 to force a three family maushold
const threeForm = playerPokemon.getEvolution()!;
expect(threeForm.evoFormKey).toBe("three"); // as per pokemon-forms, the evoFormKey for 3 family mausholds is "three"
for (let f = 1; f < 4; f++) {
vi.spyOn(Utils, "randSeedInt").mockReturnValue(f); // setting the random generator to 1, 2 and 3 to force 4 family mausholds
const fourForm = playerPokemon.getEvolution()!;
expect(fourForm.evoFormKey).toBe(null); // meanwhile, according to the pokemon-forms, the evoFormKey for a 4 family maushold is null
}
}, TIMEOUT);
});

View File

@ -1,48 +0,0 @@
import * as Utils from "#app/utils";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Evolution tests", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(() => {
game = new GameManager(phaserGame);
});
it("tandemaus evolution form test", async () => {
/* this test checks to make sure that tandemaus will
* evolve into a 3 family maushold 25% of the time
* and a 4 family maushold the other 75% of the time
* This is done by using the getEvolution method in pokemon.ts
* getEvolution will give back the form that the pokemon can evolve into
* It does this by checking the pokemon conditions in pokemon-forms.ts
* For tandemaus, the conditions are random due to a randSeedInt(4)
* If the value is 0, it's a 3 family maushold, whereas if the value is
* 1, 2 or 3, it's a 4 family maushold
*/
await game.startBattle([Species.TANDEMAUS]); // starts us off with a tandemaus
const playerPokemon = game.scene.getPlayerPokemon()!;
playerPokemon.level = 25; // tandemaus evolves at level 25
vi.spyOn(Utils, "randSeedInt").mockReturnValue(0); // setting the random generator to be 0 to force a three family maushold
const threeForm = playerPokemon.getEvolution()!;
expect(threeForm.evoFormKey).toBe("three"); // as per pokemon-forms, the evoFormKey for 3 family mausholds is "three"
for (let f = 1; f < 4; f++) {
vi.spyOn(Utils, "randSeedInt").mockReturnValue(f); // setting the random generator to 1, 2 and 3 to force 4 family mausholds
const fourForm = playerPokemon.getEvolution()!;
expect(fourForm.evoFormKey).toBe(null); // meanwhile, according to the pokemon-forms, the evoFormKey for a 4 family maushold is null
}
}, 5000);
});

View File

@ -1,8 +1,13 @@
import { StatusEffect } from "#app/data/status-effect";
import { Abilities } from "#app/enums/abilities";
import { Biome } from "#app/enums/biome";
import { Moves } from "#app/enums/moves";
import { Species } from "#app/enums/species";
import { GameModes } from "#app/game-mode";
import { TurnHeldItemTransferModifier } from "#app/modifier/modifier";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
import GameManager from "./utils/gameManager";
import { SPLASH_ONLY } from "./utils/testUtils";
const FinalWave = {
Classic: 200,
@ -20,7 +25,13 @@ describe("Final Boss", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.override.startingWave(FinalWave.Classic).startingBiome(Biome.END).disableCrits();
game.override
.startingWave(FinalWave.Classic)
.startingBiome(Biome.END)
.disableCrits()
.enemyMoveset(SPLASH_ONLY)
.moveset([ Moves.SPLASH, Moves.WILL_O_WISP, Moves.DRAGON_PULSE ])
.startingLevel(10000);
});
afterEach(() => {
@ -28,7 +39,7 @@ describe("Final Boss", () => {
});
it("should spawn Eternatus on wave 200 in END biome", async () => {
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC);
await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.CLASSIC);
expect(game.scene.currentBattle.waveIndex).toBe(FinalWave.Classic);
expect(game.scene.arena.biomeType).toBe(Biome.END);
@ -37,7 +48,7 @@ describe("Final Boss", () => {
it("should NOT spawn Eternatus before wave 200 in END biome", async () => {
game.override.startingWave(FinalWave.Classic - 1);
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC);
await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.CLASSIC);
expect(game.scene.currentBattle.waveIndex).not.toBe(FinalWave.Classic);
expect(game.scene.arena.biomeType).toBe(Biome.END);
@ -46,7 +57,7 @@ describe("Final Boss", () => {
it("should NOT spawn Eternatus outside of END biome", async () => {
game.override.startingBiome(Biome.FOREST);
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC);
await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.CLASSIC);
expect(game.scene.currentBattle.waveIndex).toBe(FinalWave.Classic);
expect(game.scene.arena.biomeType).not.toBe(Biome.END);
@ -54,12 +65,81 @@ describe("Final Boss", () => {
});
it("should not have passive enabled on Eternatus", async () => {
await game.runToFinalBossEncounter(game, [Species.BIDOOF], GameModes.CLASSIC);
await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.CLASSIC);
const eternatus = game.scene.getEnemyPokemon();
expect(eternatus?.species.speciesId).toBe(Species.ETERNATUS);
expect(eternatus?.hasPassive()).toBe(false);
const eternatus = game.scene.getEnemyPokemon()!;
expect(eternatus.species.speciesId).toBe(Species.ETERNATUS);
expect(eternatus.hasPassive()).toBe(false);
});
it("should change form on direct hit down to last boss fragment", async () => {
await game.runToFinalBossEncounter([Species.KYUREM], GameModes.CLASSIC);
await game.phaseInterceptor.to("CommandPhase");
// Eternatus phase 1
const eternatus = game.scene.getEnemyPokemon()!;
const phase1Hp = eternatus.getMaxHp();
expect(eternatus.species.speciesId).toBe(Species.ETERNATUS);
expect(eternatus.formIndex).toBe(0);
expect(eternatus.bossSegments).toBe(4);
expect(eternatus.bossSegmentIndex).toBe(3);
game.move.select(Moves.DRAGON_PULSE);
await game.toNextTurn();
// Eternatus phase 2: changed form, healed and restored its shields
expect(eternatus.species.speciesId).toBe(Species.ETERNATUS);
expect(eternatus.hp).toBeGreaterThan(phase1Hp);
expect(eternatus.hp).toBe(eternatus.getMaxHp());
expect(eternatus.formIndex).toBe(1);
expect(eternatus.bossSegments).toBe(5);
expect(eternatus.bossSegmentIndex).toBe(4);
const miniBlackHole = eternatus.getHeldItems().find(m => m instanceof TurnHeldItemTransferModifier);
expect(miniBlackHole).toBeDefined();
expect(miniBlackHole?.stackCount).toBe(1);
});
it("should change form on status damage down to last boss fragment", async () => {
game.override.ability(Abilities.NO_GUARD);
await game.runToFinalBossEncounter([Species.BIDOOF], GameModes.CLASSIC);
await game.phaseInterceptor.to("CommandPhase");
// Eternatus phase 1
const eternatus = game.scene.getEnemyPokemon()!;
const phase1Hp = eternatus.getMaxHp();
expect(eternatus.species.speciesId).toBe(Species.ETERNATUS);
expect(eternatus.formIndex).toBe(0);
expect(eternatus.bossSegments).toBe(4);
expect(eternatus.bossSegmentIndex).toBe(3);
game.move.select(Moves.WILL_O_WISP);
await game.toNextTurn();
expect(eternatus.status?.effect).toBe(StatusEffect.BURN);
const tickDamage = phase1Hp - eternatus.hp;
const lastShieldHp = Math.ceil(phase1Hp / eternatus.bossSegments);
// Stall until the burn is one hit away from breaking the last shield
while (eternatus.hp - tickDamage > lastShieldHp) {
game.move.select(Moves.SPLASH);
await game.toNextTurn();
}
expect(eternatus.bossSegmentIndex).toBe(1);
game.move.select(Moves.SPLASH);
await game.toNextTurn();
// Eternatus phase 2: changed form, healed and restored its shields
expect(eternatus.hp).toBeGreaterThan(phase1Hp);
expect(eternatus.hp).toBe(eternatus.getMaxHp());
expect(eternatus.status).toBeFalsy();
expect(eternatus.formIndex).toBe(1);
expect(eternatus.bossSegments).toBe(5);
expect(eternatus.bossSegmentIndex).toBe(4);
const miniBlackHole = eternatus.getHeldItems().find(m => m instanceof TurnHeldItemTransferModifier);
expect(miniBlackHole).toBeDefined();
expect(miniBlackHole?.stackCount).toBe(1);
});
it.todo("should change form on direct hit down to last boss fragment", () => {});
});

View File

@ -30,7 +30,7 @@ describe("Test misc", () => {
return response.json();
}).then(data => {
spy(); // Call the spy function
expect(data).toEqual({"username":"greenlamp", "lastSessionSlot":0});
expect(data).toEqual({ "username": "greenlamp", "lastSessionSlot": 0 });
});
expect(spy).toHaveBeenCalled();
});
@ -43,7 +43,7 @@ describe("Test misc", () => {
return response.json();
}).then(data => {
spy(); // Call the spy function
expect(data).toEqual({"username":"greenlamp", "lastSessionSlot":0});
expect(data).toEqual({ "username": "greenlamp", "lastSessionSlot": 0 });
});
expect(spy).toHaveBeenCalled();
});
@ -54,7 +54,7 @@ describe("Test misc", () => {
expect(response.ok).toBe(true);
expect(response.status).toBe(200);
expect(data).toEqual({"username":"greenlamp", "lastSessionSlot":0});
expect(data).toEqual({ "username": "greenlamp", "lastSessionSlot": 0 });
});
it("test apifetch mock sync", async () => {

View File

@ -0,0 +1,54 @@
import { BattlerIndex } from "#app/battle";
import { Abilities } from "#app/enums/abilities";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { BerryPhase } from "#app/phases/berry-phase";
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";
const TIMEOUT = 20 * 1000;
describe("Moves - Alluring Voice", () => {
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")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.ICE_SCALES)
.enemyMoveset(Array(4).fill(Moves.HOWL))
.startingLevel(10)
.enemyLevel(10)
.starterSpecies(Species.FEEBAS)
.ability(Abilities.BALL_FETCH)
.moveset([Moves.ALLURING_VOICE]);
});
it("should confuse the opponent if their stats were raised", async () => {
await game.classicMode.startBattle();
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.ALLURING_VOICE);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to(BerryPhase);
expect(enemy.getTag(BattlerTagType.CONFUSED)?.tagType).toBe("CONFUSED");
}, TIMEOUT);
});

View File

@ -0,0 +1,103 @@
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move";
import { Abilities } from "#app/enums/abilities";
import { StatusEffect } from "#app/enums/status-effect";
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, vi } from "vitest";
import { SPLASH_ONLY } from "../utils/testUtils";
const TIMEOUT = 20 * 1000;
describe("Moves - Burning Jealousy", () => {
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")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.ICE_SCALES)
.enemyMoveset(Array(4).fill(Moves.HOWL))
.startingLevel(10)
.enemyLevel(10)
.starterSpecies(Species.FEEBAS)
.ability(Abilities.BALL_FETCH)
.moveset([Moves.BURNING_JEALOUSY, Moves.GROWL]);
});
it("should burn the opponent if their stats were raised", async () => {
await game.classicMode.startBattle();
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.BURNING_JEALOUSY);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase");
expect(enemy.status?.effect).toBe(StatusEffect.BURN);
}, TIMEOUT);
it("should still burn the opponent if their stats were both raised and lowered in the same turn", async () => {
game.override
.starterSpecies(0)
.battleType("double");
await game.classicMode.startBattle([Species.FEEBAS, Species.ABRA]);
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.BURNING_JEALOUSY);
game.move.select(Moves.GROWL, 1);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY_2]);
await game.phaseInterceptor.to("BerryPhase");
expect(enemy.status?.effect).toBe(StatusEffect.BURN);
}, TIMEOUT);
it("should ignore stats raised by imposter", async () => {
game.override
.enemySpecies(Species.DITTO)
.enemyAbility(Abilities.IMPOSTER)
.enemyMoveset(SPLASH_ONLY);
await game.classicMode.startBattle();
const enemy = game.scene.getEnemyPokemon()!;
game.move.select(Moves.BURNING_JEALOUSY);
await game.phaseInterceptor.to("BerryPhase");
expect(enemy.status?.effect).toBeUndefined();
}, TIMEOUT);
it.skip("should ignore weakness policy", async () => { // TODO: Make this test if WP is implemented
await game.classicMode.startBattle();
}, TIMEOUT);
it("should be boosted by Sheer Force even if opponent didn't raise stats", async () => {
game.override
.ability(Abilities.SHEER_FORCE)
.enemyMoveset(SPLASH_ONLY);
vi.spyOn(allMoves[Moves.BURNING_JEALOUSY], "calculateBattlePower");
await game.classicMode.startBattle();
game.move.select(Moves.BURNING_JEALOUSY);
await game.phaseInterceptor.to("BerryPhase");
expect(allMoves[Moves.BURNING_JEALOUSY].calculateBattlePower).toHaveReturnedWith(allMoves[Moves.BURNING_JEALOUSY].power * 5461 / 4096);
}, TIMEOUT);
});

View File

@ -0,0 +1,52 @@
import { BattlerIndex } from "#app/battle";
import { allMoves } from "#app/data/move";
import { Abilities } from "#app/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, vi } from "vitest";
const TIMEOUT = 20 * 1000;
describe("Moves - Lash Out", () => {
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")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.FUR_COAT)
.enemyMoveset(Array(4).fill(Moves.GROWL))
.startingLevel(10)
.enemyLevel(10)
.starterSpecies(Species.FEEBAS)
.ability(Abilities.BALL_FETCH)
.moveset([Moves.LASH_OUT]);
});
it("should deal double damage if the user's stats were lowered this turn", async () => {
vi.spyOn(allMoves[Moves.LASH_OUT], "calculateBattlePower");
await game.classicMode.startBattle();
game.move.select(Moves.LASH_OUT);
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
await game.phaseInterceptor.to("BerryPhase");
expect(allMoves[Moves.LASH_OUT].calculateBattlePower).toHaveReturnedWith(150);
}, TIMEOUT);
});

View File

@ -1,10 +1,10 @@
import { CommandPhase } from "#app/phases/command-phase";
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 { SPLASH_ONLY } from "../utils/testUtils";
describe("Moves - Spikes", () => {
@ -23,93 +23,61 @@ describe("Moves - Spikes", () => {
beforeEach(() => {
game = new GameManager(phaserGame);
game.scene.battleStyle = 1;
game.override.battleType("single");
game.override.enemySpecies(Species.RATTATA);
game.override.enemyAbility(Abilities.HYDRATION);
game.override.enemyPassiveAbility(Abilities.HYDRATION);
game.override.ability(Abilities.HYDRATION);
game.override.passiveAbility(Abilities.HYDRATION);
game.override.startingWave(3);
game.override.enemyMoveset([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
game.override.moveset([Moves.SPIKES, Moves.SPLASH, Moves.ROAR]);
game.override
.battleType("single")
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.ability(Abilities.BALL_FETCH)
.enemyMoveset(SPLASH_ONLY)
.moveset([Moves.SPIKES, Moves.SPLASH, Moves.ROAR]);
});
it("single - wild - stay on field - no damage", async () => {
await game.classicMode.runToSummon([
Species.MIGHTYENA,
Species.POOCHYENA,
]);
await game.phaseInterceptor.to(CommandPhase, true);
const initialHp = game.scene.getParty()[0].hp;
expect(game.scene.getParty()[0].hp).toBe(initialHp);
it("should not damage the team that set them", async () => {
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
game.move.select(Moves.SPIKES);
await game.toNextTurn();
game.move.select(Moves.SPLASH);
await game.toNextTurn();
expect(game.scene.getParty()[0].hp).toBe(initialHp);
}, 20000);
it("single - wild - take some damage", async () => {
// player set spikes on the field and switch back to back
// opponent do splash for 2 turns
// nobody should take damage
await game.classicMode.runToSummon([
Species.MIGHTYENA,
Species.POOCHYENA,
]);
await game.phaseInterceptor.to(CommandPhase, false);
const initialHp = game.scene.getParty()[0].hp;
game.doSwitchPokemon(1);
await game.phaseInterceptor.run(CommandPhase);
await game.phaseInterceptor.to(CommandPhase, false);
game.doSwitchPokemon(1);
await game.phaseInterceptor.run(CommandPhase);
await game.phaseInterceptor.to(CommandPhase, false);
await game.toNextTurn();
expect(game.scene.getParty()[0].hp).toBe(initialHp);
game.doSwitchPokemon(1);
await game.toNextTurn();
const player = game.scene.getParty()[0];
expect(player.hp).toBe(player.getMaxHp());
}, 20000);
it("trainer - wild - force switch opponent - should take damage", async () => {
it("should damage opposing pokemon that are forced to switch in", async () => {
game.override.startingWave(5);
// player set spikes on the field and do splash for 3 turns
// opponent do splash for 4 turns
// nobody should take damage
await game.classicMode.runToSummon([
Species.MIGHTYENA,
Species.POOCHYENA,
]);
await game.phaseInterceptor.to(CommandPhase, true);
const initialHpOpponent = game.scene.currentBattle.enemyParty[1].hp;
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
game.move.select(Moves.SPIKES);
await game.toNextTurn();
game.move.select(Moves.ROAR);
await game.toNextTurn();
expect(game.scene.currentBattle.enemyParty[0].hp).toBeLessThan(initialHpOpponent);
const enemy = game.scene.getEnemyParty()[0];
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
}, 20000);
it("trainer - wild - force switch by himself opponent - should take damage", async () => {
it("should damage opposing pokemon that choose to switch in", async () => {
game.override.startingWave(5);
game.override.startingLevel(5000);
game.override.enemySpecies(0);
// turn 1: player set spikes, opponent do splash
// turn 2: player do splash, opponent switch pokemon
// opponent pokemon should trigger spikes and lose HP
await game.classicMode.runToSummon([
Species.MIGHTYENA,
Species.POOCHYENA,
]);
await game.phaseInterceptor.to(CommandPhase, true);
const initialHpOpponent = game.scene.currentBattle.enemyParty[1].hp;
await game.startBattle([Species.MIGHTYENA, Species.POOCHYENA]);
game.move.select(Moves.SPIKES);
await game.toNextTurn();
game.forceOpponentToSwitch();
game.move.select(Moves.SPLASH);
game.forceOpponentToSwitch();
await game.toNextTurn();
expect(game.scene.currentBattle.enemyParty[0].hp).toBeLessThan(initialHpOpponent);
const enemy = game.scene.getEnemyParty()[0];
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
}, 20000);
});

View File

@ -6,6 +6,7 @@ import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
import Trainer from "#app/field/trainer";
import { GameModes, getGameMode } from "#app/game-mode";
import { ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type";
import overrides from "#app/overrides";
import { CommandPhase } from "#app/phases/command-phase";
import { EncounterPhase } from "#app/phases/encounter-phase";
import { FaintPhase } from "#app/phases/faint-phase";
@ -138,7 +139,7 @@ export default class GameManager {
this.scene.hpBarSpeed = 3;
this.scene.enableTutorials = false;
this.scene.gameData.gender = PlayerGender.MALE; // set initial player gender
this.scene.battleStyle = this.settings.battleStyle;
}
/**
@ -148,28 +149,26 @@ export default class GameManager {
* @param species
* @param mode
*/
async runToFinalBossEncounter(game: GameManager, species: Species[], mode: GameModes) {
async runToFinalBossEncounter(species: Species[], mode: GameModes) {
console.log("===to final boss encounter===");
await game.runToTitle();
await this.runToTitle();
game.onNextPrompt("TitlePhase", Mode.TITLE, () => {
game.scene.gameMode = getGameMode(mode);
const starters = generateStarter(game.scene, species);
const selectStarterPhase = new SelectStarterPhase(game.scene);
game.scene.pushPhase(new EncounterPhase(game.scene, false));
this.onNextPrompt("TitlePhase", Mode.TITLE, () => {
this.scene.gameMode = getGameMode(mode);
const starters = generateStarter(this.scene, species);
const selectStarterPhase = new SelectStarterPhase(this.scene);
this.scene.pushPhase(new EncounterPhase(this.scene, false));
selectStarterPhase.initBattle(starters);
});
game.onNextPrompt("EncounterPhase", Mode.MESSAGE, async () => {
// This will skip all entry dialogue (I can't figure out a way to sequentially handle the 8 chained messages via 1 prompt handler)
game.setMode(Mode.MESSAGE);
const encounterPhase = game.scene.getCurrentPhase() as EncounterPhase;
// This will consider all battle entry dialog as seens and skip them
vi.spyOn(this.scene.ui, "shouldSkipDialogue").mockReturnValue(true);
// No need to end phase, this will do it for you
encounterPhase.doEncounterCommon(false);
});
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0) {
this.removeEnemyHeldItems();
}
await game.phaseInterceptor.to(EncounterPhase, true);
await this.phaseInterceptor.to(EncounterPhase);
console.log("===finished run to final boss encounter===");
}

View File

@ -1,16 +1,30 @@
import { PlayerGender } from "#app/enums/player-gender";
import { BattleStyle } from "#app/enums/battle-style";
import { GameManagerHelper } from "./gameManagerHelper";
/**
* Helper to handle settings for tests
*/
export class SettingsHelper extends GameManagerHelper {
private _battleStyle: BattleStyle = BattleStyle.SET;
get battleStyle(): BattleStyle {
return this._battleStyle;
}
/**
* Change the battle style to Switch or Set mode (tests default to {@linkcode BattleStyle.SET})
* @param mode {@linkcode BattleStyle.SWITCH} or {@linkcode BattleStyle.SET}
*/
set battleStyle(mode: BattleStyle.SWITCH | BattleStyle.SET) {
this._battleStyle = mode;
}
/**
* Disable/Enable type hints settings
* @param enable true to enabled, false to disabled
*/
typeHints(enable: boolean) {
typeHints(enable: boolean): void {
this.game.scene.typeHints = enable;
this.log(`Type Hints ${enable? "enabled" : "disabled"}` );
}

View File

@ -317,10 +317,11 @@ export default class UI extends Phaser.GameObjects.Container {
if (i18next.exists(keyOrText) ) {
const i18nKey = keyOrText;
hasi18n = true;
text = i18next.t(i18nKey, { context: genderStr }); // override text with translation
// Skip dialogue if the player has enabled the option and the dialogue has been already seen
if (battleScene.skipSeenDialogues && battleScene.gameData.getSeenDialogues()[i18nKey] === true) {
if (this.shouldSkipDialogue(i18nKey)) {
console.log(`Dialogue ${i18nKey} skipped`);
callback();
return;