Merge branch 'beta' into rest

This commit is contained in:
Bertie690 2025-07-29 22:48:50 -04:00 committed by GitHub
commit 8b1b1cd38a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1243 additions and 936 deletions

View File

@ -11,6 +11,7 @@ on:
- beta - beta
merge_group: merge_group:
types: [checks_requested] types: [checks_requested]
workflow_dispatch:
jobs: jobs:
check-path-change-filter: check-path-change-filter:

View File

@ -30,19 +30,19 @@
"@biomejs/biome": "2.0.0", "@biomejs/biome": "2.0.0",
"@ls-lint/ls-lint": "2.3.1", "@ls-lint/ls-lint": "2.3.1",
"@types/jsdom": "^21.1.7", "@types/jsdom": "^21.1.7",
"@types/node": "^22.16.3", "@types/node": "^22.16.5",
"@vitest/coverage-istanbul": "^3.2.4", "@vitest/coverage-istanbul": "^3.2.4",
"@vitest/expect": "^3.2.4", "@vitest/expect": "^3.2.4",
"chalk": "^5.4.1", "chalk": "^5.4.1",
"dependency-cruiser": "^16.10.4", "dependency-cruiser": "^16.10.4",
"inquirer": "^12.7.0", "inquirer": "^12.8.2",
"jsdom": "^26.1.0", "jsdom": "^26.1.0",
"lefthook": "^1.12.2", "lefthook": "^1.12.2",
"msw": "^2.10.4", "msw": "^2.10.4",
"phaser3spectorjs": "^0.0.8", "phaser3spectorjs": "^0.0.8",
"typedoc": "^0.28.7", "typedoc": "^0.28.8",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"vite": "^6.3.5", "vite": "^7.0.6",
"vite-tsconfig-paths": "^5.1.4", "vite-tsconfig-paths": "^5.1.4",
"vitest": "^3.2.4", "vitest": "^3.2.4",
"vitest-canvas-mock": "^0.3.3" "vitest-canvas-mock": "^0.3.3"

File diff suppressed because it is too large Load Diff

View File

@ -17,15 +17,20 @@ const version = "2.0.1";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const projectRoot = path.join(__dirname, "..", ".."); const projectRoot = path.join(__dirname, "..", "..");
const boilerplateFilePath = path.join(__dirname, "test-boilerplate.ts");
const choices = [ const choices = /** @type {const} */ (["Move", "Ability", "Item", "Reward", "Mystery Encounter", "Utils", "UI"]);
{ label: "Move", dir: "moves" }, /** @typedef {choices[number]} choiceType */
{ label: "Ability", dir: "abilities" },
{ label: "Item", dir: "items" }, /** @satisfies {{[k in choiceType]: string}} */
{ label: "Mystery Encounter", dir: "mystery-encounter/encounters" }, const choicesToDirs = /** @type {const} */ ({
{ label: "Utils", dir: "utils" }, Move: "moves",
{ label: "UI", dir: "ui" }, Ability: "abilities",
]; Item: "items",
Reward: "rewards",
"Mystery Encounter": "mystery-encounter/encounters",
Utils: "utils",
UI: "ui",
});
//#endregion //#endregion
//#region Functions //#region Functions
@ -41,48 +46,47 @@ function getTestFolderPath(...folders) {
/** /**
* Prompts the user to select a type via list. * Prompts the user to select a type via list.
* @returns {Promise<{selectedOption: {label: string, dir: string}}>} the selected type * @returns {Promise<choiceType>} the selected type
*/ */
async function promptTestType() { async function promptTestType() {
const typeAnswer = await inquirer /** @type {choiceType | "EXIT"} */
const choice = await inquirer
.prompt([ .prompt([
{ {
type: "list", type: "list",
name: "selectedOption", name: "selectedOption",
message: "What type of test would you like to create?", message: "What type of test would you like to create?",
choices: [...choices.map(choice => ({ name: choice.label, value: choice })), { name: "EXIT", value: "N/A" }], choices: [...choices, "EXIT"],
}, },
]) ])
.then(ans => ans.selectedOption); .then(ta => ta.selectedOption);
if (typeAnswer.name === "EXIT") { if (choice === "EXIT") {
console.log("Exiting..."); console.log("Exiting...");
return process.exit(0); return process.exit(0);
} }
if (!choices.some(choice => choice.dir === typeAnswer.dir)) {
console.error(`Please provide a valid type: (${choices.map(choice => choice.label).join(", ")})!`);
return await promptTestType();
}
return typeAnswer; return choice;
} }
/** /**
* Prompts the user to provide a file name. * Prompts the user to provide a file name.
* @param {string} selectedType * @param {choiceType} selectedType The chosen string (used to display console logs)
* @returns {Promise<{userInput: string}>} the selected file name * @returns {Promise<string>} the selected file name
*/ */
async function promptFileName(selectedType) { async function promptFileName(selectedType) {
/** @type {{userInput: string}} */ /** @type {string} */
const fileNameAnswer = await inquirer.prompt([ const fileNameAnswer = await inquirer
.prompt([
{ {
type: "input", type: "input",
name: "userInput", name: "userInput",
message: `Please provide the name of the ${selectedType}:`, message: `Please provide the name of the ${selectedType}.`,
}, },
]); ])
.then(fa => fa.userInput);
if (!fileNameAnswer.userInput || fileNameAnswer.userInput.trim().length === 0) { if (fileNameAnswer.trim().length === 0) {
console.error("Please provide a valid file name!"); console.error("Please provide a valid file name!");
return await promptFileName(selectedType); return await promptFileName(selectedType);
} }
@ -90,51 +94,66 @@ async function promptFileName(selectedType) {
return fileNameAnswer; return fileNameAnswer;
} }
/**
* Obtain the path to the boilerplate file based on the current option.
* @param {choiceType} choiceType The choice selected
* @returns {string} The path to the boilerplate file
*/
function getBoilerplatePath(choiceType) {
switch (choiceType) {
// case "Reward":
// return path.join(__dirname, "boilerplates/reward.ts");
default:
return path.join(__dirname, "boilerplates/default.ts");
}
}
/** /**
* Runs the interactive test:create "CLI" * Runs the interactive test:create "CLI"
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function runInteractive() { async function runInteractive() {
console.group(chalk.grey(`Create Test - v${version}\n`)); console.group(chalk.grey(`🧪 Create Test - v${version}\n`));
try { try {
const typeAnswer = await promptTestType(); const choice = await promptTestType();
const fileNameAnswer = await promptFileName(typeAnswer.selectedOption.label); const fileNameAnswer = await promptFileName(choice);
const type = typeAnswer.selectedOption;
// Convert fileName from snake_case or camelCase to kebab-case // Convert fileName from snake_case or camelCase to kebab-case
const fileName = fileNameAnswer.userInput const fileName = fileNameAnswer
.replace(/_+/g, "-") // Convert snake_case (underscore) to kebab-case (dashes) .replace(/_+/g, "-") // Convert snake_case (underscore) to kebab-case (dashes)
.replace(/([a-z])([A-Z])/g, "$1-$2") // Convert camelCase to kebab-case .replace(/([a-z])([A-Z])/g, "$1-$2") // Convert camelCase to kebab-case
.replace(/\s+/g, "-") // Replace spaces with dashes .replace(/\s+/g, "-") // Replace spaces with dashes
.toLowerCase(); // Ensure all lowercase .toLowerCase(); // Ensure all lowercase
// Format the description for the test case
// Format the description for the test case in Title Case
const formattedName = fileName.replace(/-/g, " ").replace(/\b\w/g, char => char.toUpperCase()); const formattedName = fileName.replace(/-/g, " ").replace(/\b\w/g, char => char.toUpperCase());
const description = `${choice} - ${formattedName}`;
// Determine the directory based on the type // Determine the directory based on the type
const dir = getTestFolderPath(type.dir); const localDir = choicesToDirs[choice];
const description = `${type.label} - ${formattedName}`; const absoluteDir = getTestFolderPath(localDir);
// Define the content template // Define the content template
const content = fs.readFileSync(boilerplateFilePath, "utf8").replace("{{description}}", description); const content = fs.readFileSync(getBoilerplatePath(choice), "utf8").replace("{{description}}", description);
// Ensure the directory exists // Ensure the directory exists
if (!fs.existsSync(dir)) { if (!fs.existsSync(absoluteDir)) {
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(absoluteDir, { recursive: true });
} }
// Create the file with the given name // Create the file with the given name
const filePath = path.join(dir, `${fileName}.test.ts`); const filePath = path.join(absoluteDir, `${fileName}.test.ts`);
if (fs.existsSync(filePath)) { if (fs.existsSync(filePath)) {
console.error(chalk.red.bold(`\n✗ File "${fileName}.test.ts" already exists!\n`)); console.error(chalk.red.bold(`✗ File "${fileName}.test.ts" already exists!\n`));
process.exit(1); process.exit(1);
} }
// Write the template content to the file // Write the template content to the file
fs.writeFileSync(filePath, content, "utf8"); fs.writeFileSync(filePath, content, "utf8");
console.log(chalk.green.bold(`\n✔ File created at: test/${type.dir}/${fileName}.test.ts\n`)); console.log(chalk.green.bold(`✔ File created at: test/${localDir}/${fileName}.test.ts\n`));
console.groupEnd(); console.groupEnd();
} catch (err) { } catch (err) {
console.error(chalk.red("✗ Error: ", err.message)); console.error(chalk.red("✗ Error: ", err.message));

119
src/@types/battler-tags.ts Normal file
View File

@ -0,0 +1,119 @@
// biome-ignore-start lint/correctness/noUnusedImports: Used in a TSDoc comment
import type { AbilityBattlerTag, BattlerTagTypeMap, SerializableBattlerTag, TypeBoostTag } from "#data/battler-tags";
import type { AbilityId } from "#enums/ability-id";
// biome-ignore-end lint/correctness/noUnusedImports: end
import type { BattlerTagType } from "#enums/battler-tag-type";
/**
* Subset of {@linkcode BattlerTagType}s that restrict the use of moves.
*/
export type MoveRestrictionBattlerTagType =
| BattlerTagType.THROAT_CHOPPED
| BattlerTagType.TORMENT
| BattlerTagType.TAUNT
| BattlerTagType.IMPRISON
| BattlerTagType.HEAL_BLOCK
| BattlerTagType.ENCORE
| BattlerTagType.DISABLED
| BattlerTagType.GORILLA_TACTICS;
/**
* Subset of {@linkcode BattlerTagType}s that block damage from moves.
*/
export type FormBlockDamageBattlerTagType = BattlerTagType.ICE_FACE | BattlerTagType.DISGUISE;
/**
* Subset of {@linkcode BattlerTagType}s that are related to trapping effects.
*/
export type TrappingBattlerTagType =
| BattlerTagType.BIND
| BattlerTagType.WRAP
| BattlerTagType.FIRE_SPIN
| BattlerTagType.WHIRLPOOL
| BattlerTagType.CLAMP
| BattlerTagType.SAND_TOMB
| BattlerTagType.MAGMA_STORM
| BattlerTagType.SNAP_TRAP
| BattlerTagType.THUNDER_CAGE
| BattlerTagType.INFESTATION
| BattlerTagType.INGRAIN
| BattlerTagType.OCTOLOCK
| BattlerTagType.NO_RETREAT;
/**
* Subset of {@linkcode BattlerTagType}s that are related to protection effects.
*/
export type ProtectionBattlerTagType = BattlerTagType.PROTECTED | BattlerTagType.SPIKY_SHIELD | DamageProtectedTagType;
/**
* Subset of {@linkcode BattlerTagType}s related to protection effects that block damage but not status moves.
*/
export type DamageProtectedTagType = ContactSetStatusProtectedTagType | ContactStatStageChangeProtectedTagType;
/**
* Subset of {@linkcode BattlerTagType}s related to protection effects that set a status effect on the attacker.
*/
export type ContactSetStatusProtectedTagType = BattlerTagType.BANEFUL_BUNKER | BattlerTagType.BURNING_BULWARK;
/**
* Subset of {@linkcode BattlerTagType}s related to protection effects that change stat stages of the attacker.
*/
export type ContactStatStageChangeProtectedTagType =
| BattlerTagType.KINGS_SHIELD
| BattlerTagType.SILK_TRAP
| BattlerTagType.OBSTRUCT;
/** Subset of {@linkcode BattlerTagType}s that provide the Endure effect */
export type EndureTagType = BattlerTagType.ENDURE_TOKEN | BattlerTagType.ENDURING;
/**
* Subset of {@linkcode BattlerTagType}s that are related to semi-invulnerable states.
*/
export type SemiInvulnerableTagType =
| BattlerTagType.FLYING
| BattlerTagType.UNDERGROUND
| BattlerTagType.UNDERWATER
| BattlerTagType.HIDDEN;
/**
* Subset of {@linkcode BattlerTagType}s corresponding to {@linkcode AbilityBattlerTag}s
*
* @remarks
* {@linkcode AbilityId.FLASH_FIRE | Flash Fire}'s {@linkcode BattlerTagType.FIRE_BOOST} is not included as it
* subclasses {@linkcode TypeBoostTag} and not `AbilityBattlerTag`.
*/
export type AbilityBattlerTagType =
| BattlerTagType.PROTOSYNTHESIS
| BattlerTagType.QUARK_DRIVE
| BattlerTagType.UNBURDEN
| BattlerTagType.SLOW_START
| BattlerTagType.TRUANT;
/**
* Subset of {@linkcode BattlerTagType}s related to abilities that boost the highest stat.
*/
export type HighestStatBoostTagType =
| BattlerTagType.QUARK_DRIVE // formatting
| BattlerTagType.PROTOSYNTHESIS;
/**
* Subset of {@linkcode BattlerTagType}s that are able to persist between turns and should therefore be serialized
*/
export type SerializableBattlerTagType = keyof {
[K in keyof BattlerTagTypeMap as BattlerTagTypeMap[K] extends SerializableBattlerTag
? K
: never]: BattlerTagTypeMap[K];
};
/**
* Subset of {@linkcode BattlerTagType}s that are not able to persist across waves and should therefore not be serialized
*/
export type NonSerializableBattlerTagType = Exclude<BattlerTagType, SerializableBattlerTagType>;
/**
* Dummy, typescript-only declaration to ensure that
* {@linkcode BattlerTagTypeMap} has an entry for all `BattlerTagType`s.
*
* If a battler tag is missing from the map, Typescript will throw an error on this statement.
*
* Does not actually exist at runtime, so it must not be used!
*/
declare const EnsureAllBattlerTagTypesAreMapped: BattlerTagTypeMap[BattlerTagType] & never;

View File

@ -11,7 +11,7 @@ export interface IllusionData {
/** The name of pokemon featured in the illusion */ /** The name of pokemon featured in the illusion */
name: string; name: string;
/** The nickname of the pokemon featured in the illusion */ /** The nickname of the pokemon featured in the illusion */
nickname: string; nickname?: string;
/** Whether the pokemon featured in the illusion is shiny or not */ /** Whether the pokemon featured in the illusion is shiny or not */
shiny: boolean; shiny: boolean;
/** The variant of the pokemon featured in the illusion */ /** The variant of the pokemon featured in the illusion */

File diff suppressed because it is too large Load Diff

View File

@ -10859,7 +10859,7 @@ export function initMoves() {
new SelfStatusMove(MoveId.NO_RETREAT, PokemonType.FIGHTING, -1, 5, -1, 0, 8) new SelfStatusMove(MoveId.NO_RETREAT, PokemonType.FIGHTING, -1, 5, -1, 0, 8)
.attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD ], 1, true)
.attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false) .attr(AddBattlerTagAttr, BattlerTagType.NO_RETREAT, true, false)
.condition((user, target, move) => user.getTag(TrappedTag)?.sourceMove !== MoveId.NO_RETREAT), // fails if the user is currently trapped by No Retreat .condition((user, target, move) => user.getTag(TrappedTag)?.tagType !== BattlerTagType.NO_RETREAT), // fails if the user is currently trapped by No Retreat
new StatusMove(MoveId.TAR_SHOT, PokemonType.ROCK, 100, 15, -1, 0, 8) new StatusMove(MoveId.TAR_SHOT, PokemonType.ROCK, 100, 15, -1, 0, 8)
.attr(StatStageChangeAttr, [ Stat.SPD ], -1) .attr(StatStageChangeAttr, [ Stat.SPD ], -1)
.attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false) .attr(AddBattlerTagAttr, BattlerTagType.TAR_SHOT, false)

View File

@ -39,6 +39,7 @@ import { addPokemonDataToDexAndValidateAchievements } from "#mystery-encounters/
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterBuilder } from "#mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#mystery-encounters/mystery-encounter-option";
import { PartySizeRequirement } from "#mystery-encounters/mystery-encounter-requirements";
import { PokemonData } from "#system/pokemon-data"; import { PokemonData } from "#system/pokemon-data";
import { MusicPreference } from "#system/settings"; import { MusicPreference } from "#system/settings";
import type { OptionSelectItem } from "#ui/abstact-option-select-ui-handler"; import type { OptionSelectItem } from "#ui/abstact-option-select-ui-handler";
@ -151,7 +152,8 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
return true; return true;
}) })
.withOption( .withOption(
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
.withSceneRequirement(new PartySizeRequirement([2, 6], true)) // Requires 2 valid party members
.withHasDexProgress(true) .withHasDexProgress(true)
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.1.label`, buttonLabel: `${namespace}:option.1.label`,
@ -257,7 +259,8 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = MysteryEncounterBuil
.build(), .build(),
) )
.withOption( .withOption(
MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DEFAULT) MysteryEncounterOptionBuilder.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
.withSceneRequirement(new PartySizeRequirement([2, 6], true)) // Requires 2 valid party members
.withHasDexProgress(true) .withHasDexProgress(true)
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option.2.label`, buttonLabel: `${namespace}:option.2.label`,

View File

@ -1,4 +1,5 @@
import { type BattlerTag, loadBattlerTag } from "#data/battler-tags"; import type { BattlerTag } from "#data/battler-tags";
import { loadBattlerTag, SerializableBattlerTag } from "#data/battler-tags";
import { allSpecies } from "#data/data-lists"; import { allSpecies } from "#data/data-lists";
import type { Gender } from "#data/gender"; import type { Gender } from "#data/gender";
import { PokemonMove } from "#data/moves/pokemon-move"; import { PokemonMove } from "#data/moves/pokemon-move";
@ -187,9 +188,11 @@ export class PokemonSummonData {
continue; continue;
} }
if (key === "tags") { if (key === "tags" && Array.isArray(value)) {
// load battler tags // load battler tags, discarding any that are not serializable
this.tags = value.map((t: BattlerTag) => loadBattlerTag(t)); this.tags = value
.map((t: SerializableBattlerTag) => loadBattlerTag(t))
.filter((t): t is SerializableBattlerTag => t instanceof SerializableBattlerTag);
continue; continue;
} }
this[key] = value; this[key] = value;

View File

@ -1,5 +1,4 @@
export enum BattlerTagType { export enum BattlerTagType {
NONE = "NONE",
RECHARGING = "RECHARGING", RECHARGING = "RECHARGING",
FLINCHED = "FLINCHED", FLINCHED = "FLINCHED",
INTERRUPTED = "INTERRUPTED", INTERRUPTED = "INTERRUPTED",

View File

@ -213,7 +213,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
* TODO: Stop treating this like a unique ID and stop treating 0 as no pokemon * TODO: Stop treating this like a unique ID and stop treating 0 as no pokemon
*/ */
public id: number; public id: number;
public nickname: string; /**
* The Pokemon's current nickname, or `undefined` if it currently lacks one.
* If omitted, references to this should refer to the default name for this Pokemon's species.
*/
public nickname?: string;
public species: PokemonSpecies; public species: PokemonSpecies;
public formIndex: number; public formIndex: number;
public abilityIndex: number; public abilityIndex: number;
@ -448,7 +452,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
getNameToRender(useIllusion = true) { getNameToRender(useIllusion = true) {
const illusion = this.summonData.illusion; const illusion = this.summonData.illusion;
const name = useIllusion ? (illusion?.name ?? this.name) : this.name; const name = useIllusion ? (illusion?.name ?? this.name) : this.name;
const nickname: string = useIllusion ? (illusion?.nickname ?? this.nickname) : this.nickname; const nickname: string | undefined = useIllusion ? illusion?.nickname : this.nickname;
try { try {
if (nickname) { if (nickname) {
return decodeURIComponent(escape(atob(nickname))); // TODO: Remove `atob` and `escape`... eventually... return decodeURIComponent(escape(atob(nickname))); // TODO: Remove `atob` and `escape`... eventually...

View File

@ -9,6 +9,7 @@ import { AttemptCapturePhase } from "#phases/attempt-capture-phase";
import { AttemptRunPhase } from "#phases/attempt-run-phase"; import { AttemptRunPhase } from "#phases/attempt-run-phase";
import { BattleEndPhase } from "#phases/battle-end-phase"; import { BattleEndPhase } from "#phases/battle-end-phase";
import { BerryPhase } from "#phases/berry-phase"; import { BerryPhase } from "#phases/berry-phase";
import { CheckInterludePhase } from "#phases/check-interlude-phase";
import { CheckStatusEffectPhase } from "#phases/check-status-effect-phase"; import { CheckStatusEffectPhase } from "#phases/check-status-effect-phase";
import { CheckSwitchPhase } from "#phases/check-switch-phase"; import { CheckSwitchPhase } from "#phases/check-switch-phase";
import { CommandPhase } from "#phases/command-phase"; import { CommandPhase } from "#phases/command-phase";
@ -121,6 +122,7 @@ const PHASES = Object.freeze({
AttemptRunPhase, AttemptRunPhase,
BattleEndPhase, BattleEndPhase,
BerryPhase, BerryPhase,
CheckInterludePhase,
CheckStatusEffectPhase, CheckStatusEffectPhase,
CheckSwitchPhase, CheckSwitchPhase,
CommandPhase, CommandPhase,
@ -665,4 +667,15 @@ export class PhaseManager {
): void { ): void {
this.startDynamicPhase(this.create(phase, ...args)); this.startDynamicPhase(this.create(phase, ...args));
} }
/** Prevents end of turn effects from triggering when transitioning to a new biome on a X0 wave */
public onInterlude(): void {
const phasesToRemove = ["WeatherEffectPhase", "BerryPhase", "CheckStatusEffectPhase"];
this.phaseQueue = this.phaseQueue.filter(p => !phasesToRemove.includes(p.phaseName));
const turnEndPhase = this.findPhase<TurnEndPhase>(p => p.phaseName === "TurnEndPhase");
if (turnEndPhase) {
turnEndPhase.upcomingInterlude = true;
}
}
} }

View File

@ -0,0 +1,18 @@
import { globalScene } from "#app/global-scene";
import { Phase } from "#app/phase";
export class CheckInterludePhase extends Phase {
public override readonly phaseName = "CheckInterludePhase";
public override start(): void {
super.start();
const { phaseManager } = globalScene;
const { waveIndex } = globalScene.currentBattle;
if (waveIndex % 10 === 0 && globalScene.getEnemyParty().every(p => p.isFainted())) {
phaseManager.onInterlude();
}
this.end();
}
}

View File

@ -18,6 +18,8 @@ import i18next from "i18next";
export class TurnEndPhase extends FieldPhase { export class TurnEndPhase extends FieldPhase {
public readonly phaseName = "TurnEndPhase"; public readonly phaseName = "TurnEndPhase";
public upcomingInterlude = false;
start() { start() {
super.start(); super.start();
@ -59,9 +61,11 @@ export class TurnEndPhase extends FieldPhase {
pokemon.tempSummonData.waveTurnCount++; pokemon.tempSummonData.waveTurnCount++;
}; };
if (!this.upcomingInterlude) {
this.executeForAll(handlePokemon); this.executeForAll(handlePokemon);
globalScene.arena.lapseTags(); globalScene.arena.lapseTags();
}
if (globalScene.arena.weather && !globalScene.arena.weather.lapse()) { if (globalScene.arena.weather && !globalScene.arena.weather.lapse()) {
globalScene.arena.trySetWeather(WeatherType.NONE); globalScene.arena.trySetWeather(WeatherType.NONE);

View File

@ -218,6 +218,7 @@ export class TurnStartPhase extends FieldPhase {
break; break;
} }
} }
phaseManager.pushNew("CheckInterludePhase");
phaseManager.pushNew("WeatherEffectPhase"); phaseManager.pushNew("WeatherEffectPhase");
phaseManager.pushNew("BerryPhase"); phaseManager.pushNew("BerryPhase");
@ -227,10 +228,10 @@ export class TurnStartPhase extends FieldPhase {
phaseManager.pushNew("TurnEndPhase"); phaseManager.pushNew("TurnEndPhase");
/** /*
* this.end() will call shiftPhase(), which dumps everything from PrependQueue (aka everything that is unshifted()) to the front * `this.end()` will call `PhaseManager#shiftPhase()`, which dumps everything from `phaseQueuePrepend`
* of the queue and dequeues to start the next phase * (aka everything that is queued via `unshift()`) to the front of the queue and dequeues to start the next phase.
* this is important since stuff like SwitchSummon, AttemptRun, AttemptCapture Phases break the "flow" and should take precedence * This is important since stuff like `SwitchSummonPhase`, `AttemptRunPhase`, and `AttemptCapturePhase` break the "flow" and should take precedence
*/ */
this.end(); this.end();
} }

View File

@ -93,7 +93,7 @@ describe("Global Trade System - Mystery Encounter", () => {
describe("Option 1 - Check Trade Offers", () => { describe("Option 1 - Check Trade Offers", () => {
it("should have the correct properties", () => { it("should have the correct properties", () => {
const option = GlobalTradeSystemEncounter.options[0]; const option = GlobalTradeSystemEncounter.options[0];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT);
expect(option.dialogue).toBeDefined(); expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({ expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}:option.1.label`, buttonLabel: `${namespace}:option.1.label`,
@ -154,7 +154,7 @@ describe("Global Trade System - Mystery Encounter", () => {
describe("Option 2 - Wonder Trade", () => { describe("Option 2 - Wonder Trade", () => {
it("should have the correct properties", () => { it("should have the correct properties", () => {
const option = GlobalTradeSystemEncounter.options[1]; const option = GlobalTradeSystemEncounter.options[1];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT);
expect(option.dialogue).toBeDefined(); expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({ expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}:option.2.label`, buttonLabel: `${namespace}:option.2.label`,

View File

@ -0,0 +1,62 @@
import { AbilityId } from "#enums/ability-id";
import { BerryType } from "#enums/berry-type";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { WeatherType } from "#enums/weather-type";
import { GameManager } from "#test/test-utils/game-manager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Check Biome End Phase", () => {
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
.enemySpecies(SpeciesId.MAGIKARP)
.enemyMoveset(MoveId.SPLASH)
.enemyAbility(AbilityId.BALL_FETCH)
.ability(AbilityId.BALL_FETCH)
.startingLevel(100);
});
it("should not trigger end of turn effects when defeating the final pokemon of a biome in classic", async () => {
game.override
.startingWave(10)
.weather(WeatherType.SANDSTORM)
.startingHeldItems([{ name: "BERRY", type: BerryType.SITRUS }]);
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const player = game.field.getPlayerPokemon();
player.hp = 1;
game.move.use(MoveId.EXTREME_SPEED);
await game.toEndOfTurn();
expect(player.hp).toBe(1);
});
it("should not prevent end of turn effects when transitioning waves within a biome", async () => {
game.override.weather(WeatherType.SANDSTORM);
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const player = game.field.getPlayerPokemon();
game.move.use(MoveId.EXTREME_SPEED);
await game.toEndOfTurn();
expect(player.hp).toBeLessThan(player.getMaxHp());
});
});