mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-09-24 07:23:24 +02:00
Merge branch 'beta' into reduce-gameData
This commit is contained in:
commit
2d4cfdbf67
30
.github/workflows/github-pages.yml
vendored
30
.github/workflows/github-pages.yml
vendored
@ -23,7 +23,9 @@ jobs:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
api-dir: ./
|
||||
docs-dir: ./pokerogue_docs
|
||||
# Only push docs when running on pushes to main/beta
|
||||
DRY_RUN: ${{github.event_name != 'push' || (github.ref_name != 'beta' && github.ref_name != 'main')}}
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@ -45,7 +47,7 @@ jobs:
|
||||
with:
|
||||
version: 10
|
||||
|
||||
- name: Setup Node 22.14.1
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: "pokerogue_docs/.nvmrc"
|
||||
@ -58,26 +60,26 @@ jobs:
|
||||
ref: gh-pages
|
||||
|
||||
- name: Install Node.js dependencies
|
||||
working-directory: ${{env.api-dir}}
|
||||
run: |
|
||||
cd pokerogue_docs
|
||||
pnpm i
|
||||
working-directory: ${{env.docs-dir}}
|
||||
run: pnpm i
|
||||
|
||||
- name: Generate Typedoc docs
|
||||
working-directory: ${{env.api-dir}}
|
||||
run: |
|
||||
cd pokerogue_docs
|
||||
pnpm exec typedoc --out /tmp/docs --githubPages false --entryPoints ./src/
|
||||
working-directory: ${{env.docs-dir}}
|
||||
env:
|
||||
REF_NAME: ${{github.ref_name}}
|
||||
DRY_RUN: ${{env.DRY_RUN}}
|
||||
run: pnpm typedoc
|
||||
|
||||
- name: Commit & Push docs
|
||||
if: github.event_name == 'push' && (github.ref_name == 'beta' || github.ref_name == 'main')
|
||||
# env vars are stored as strings instead of booleans (hence why an explicit check is required)
|
||||
if: ${{ env.DRY_RUN == 'false'}}
|
||||
run: |
|
||||
cd pokerogue_gh
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config user.name "github-actions[bot]"
|
||||
mkdir -p $GITHUB_REF_NAME
|
||||
rm -rf $GITHUB_REF_NAME/*
|
||||
cp -r /tmp/docs/. $GITHUB_REF_NAME
|
||||
cp -r /tmp/docs $GITHUB_REF_NAME
|
||||
git add $GITHUB_REF_NAME
|
||||
git commit --allow-empty -m "[skip ci] Deploy docs"
|
||||
git push
|
||||
git commit -m "[skip ci] Deploy docs"
|
||||
git push
|
||||
|
@ -1,5 +1,10 @@
|
||||
<picture><img src="./public/images/logo.png" width="300" alt="PokéRogue"></picture>
|
||||
|
||||

|
||||
[](https://pagefaultgames.github.io/pokerogue/beta)
|
||||
[](https://github.com/pagefaultgames/pokerogue/actions/workflows/tests.yml)
|
||||
[](https://www.gnu.org/licenses/agpl-3.0)
|
||||
|
||||
PokéRogue is a browser based Pokémon fangame heavily inspired by the roguelite genre. Battle endlessly while gathering stacking items, exploring many different biomes, fighting trainers, bosses, and more!
|
||||
|
||||
# Contributing
|
||||
@ -7,7 +12,7 @@ PokéRogue is a browser based Pokémon fangame heavily inspired by the roguelite
|
||||
See [CONTRIBUTING.md](./CONTRIBUTING.md), this includes instructions on how to set up the game locally.
|
||||
|
||||
# 📝 Credits
|
||||
>
|
||||
|
||||
> If this project contains assets you have produced and you do not see your name, **please** reach out, either [here on GitHub](https://github.com/pagefaultgames/pokerogue/issues/new) or via [Discord](https://discord.gg/pokerogue).
|
||||
|
||||
Thank you to all the wonderful people that have contributed to the PokéRogue project! You can find the credits [here](./CREDITS.md).
|
||||
|
11
global.d.ts
vendored
11
global.d.ts
vendored
@ -18,3 +18,14 @@ declare global {
|
||||
call<T extends AnyFn>(this: T, thisArg: ThisParameterType<T>, ...argArray: Parameters<T>): ReturnType<T>;
|
||||
}
|
||||
}
|
||||
|
||||
// Global augments for `typedoc` to prevent TS from erroring when editing the config JS file
|
||||
declare module "typedoc" {
|
||||
export interface TypeDocOptionMap {
|
||||
coverageLabel: string;
|
||||
coverageColor: string;
|
||||
coverageOutputPath: string;
|
||||
coverageOutputType: "svg" | "json" | "all";
|
||||
coverageSvgWidth: number;
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"biome": "biome check --write --changed --no-errors-on-unmatched --diagnostic-level=error",
|
||||
"biome-ci": "biome ci --diagnostic-level=error --reporter=github --no-errors-on-unmatched",
|
||||
"docs": "typedoc",
|
||||
"typedoc": "typedoc",
|
||||
"depcruise": "depcruise src test",
|
||||
"postinstall": "lefthook install; git submodule update --init --recursive",
|
||||
"update-version:patch": "pnpm version patch --force --no-git-tag-version",
|
||||
@ -42,6 +42,9 @@
|
||||
"msw": "^2.10.4",
|
||||
"phaser3spectorjs": "^0.0.8",
|
||||
"typedoc": "^0.28.8",
|
||||
"typedoc-github-theme": "^0.3.1",
|
||||
"typedoc-plugin-coverage": "^4.0.1",
|
||||
"typedoc-plugin-mdn-links": "^5.0.9",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^7.0.6",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
|
@ -87,6 +87,15 @@ importers:
|
||||
typedoc:
|
||||
specifier: ^0.28.8
|
||||
version: 0.28.8(typescript@5.8.3)
|
||||
typedoc-github-theme:
|
||||
specifier: ^0.3.1
|
||||
version: 0.3.1(typedoc@0.28.8(typescript@5.8.3))
|
||||
typedoc-plugin-coverage:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1(typedoc@0.28.8(typescript@5.8.3))
|
||||
typedoc-plugin-mdn-links:
|
||||
specifier: ^5.0.9
|
||||
version: 5.0.9(typedoc@0.28.8(typescript@5.8.3))
|
||||
typescript:
|
||||
specifier: ^5.8.3
|
||||
version: 5.8.3
|
||||
@ -1789,6 +1798,23 @@ packages:
|
||||
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
typedoc-github-theme@0.3.1:
|
||||
resolution: {integrity: sha512-j6PmkAGmf/MGCzYjQcUH6jS9djPsNl/IoTXooxC+MoeMkBhbmPyKJlpR6Lw12BLoe2OYpYA2J1KMktUJXp/8Sw==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
peerDependencies:
|
||||
typedoc: ~0.28.0
|
||||
|
||||
typedoc-plugin-coverage@4.0.1:
|
||||
resolution: {integrity: sha512-P1QBR5GJSfW3fDrpz4Vkd8z8lzWaBYjaHebRLk0u2Uga0oSFlPaqrCyiHzItBXxZX28aMlNlZwrUnsLgUgqA7g==}
|
||||
engines: {node: '>= 18'}
|
||||
peerDependencies:
|
||||
typedoc: 0.28.x
|
||||
|
||||
typedoc-plugin-mdn-links@5.0.9:
|
||||
resolution: {integrity: sha512-kXssRKBhUd0JeHzFmxWVsGWVFR9WXafe70Y8Ed+MYH2Nu2647cqfGQN1OBKgvXpmAT8MTpACmUIQ7GnQnh1/iw==}
|
||||
peerDependencies:
|
||||
typedoc: 0.27.x || 0.28.x
|
||||
|
||||
typedoc@0.28.8:
|
||||
resolution: {integrity: sha512-16GfLopc8icHfdvqZDqdGBoS2AieIRP2rpf9mU+MgN+gGLyEQvAO0QgOa6NJ5QNmQi0LFrDY9in4F2fUNKgJKA==}
|
||||
engines: {node: '>= 18', pnpm: '>= 10'}
|
||||
@ -3634,6 +3660,18 @@ snapshots:
|
||||
|
||||
type-fest@4.41.0: {}
|
||||
|
||||
typedoc-github-theme@0.3.1(typedoc@0.28.8(typescript@5.8.3)):
|
||||
dependencies:
|
||||
typedoc: 0.28.8(typescript@5.8.3)
|
||||
|
||||
typedoc-plugin-coverage@4.0.1(typedoc@0.28.8(typescript@5.8.3)):
|
||||
dependencies:
|
||||
typedoc: 0.28.8(typescript@5.8.3)
|
||||
|
||||
typedoc-plugin-mdn-links@5.0.9(typedoc@0.28.8(typescript@5.8.3)):
|
||||
dependencies:
|
||||
typedoc: 0.28.8(typescript@5.8.3)
|
||||
|
||||
typedoc@0.28.8(typescript@5.8.3):
|
||||
dependencies:
|
||||
'@gerrit0/mini-shiki': 3.8.1
|
||||
|
@ -2,6 +2,7 @@ import type { ArenaTagTypeMap } from "#data/arena-tag";
|
||||
import type { ArenaTagType } from "#enums/arena-tag-type";
|
||||
// biome-ignore lint/correctness/noUnusedImports: TSDocs
|
||||
import type { SessionSaveData } from "#types/data-types";
|
||||
import type { ObjectValues } from "#types/type-helpers";
|
||||
|
||||
/** Subset of {@linkcode ArenaTagType}s that apply some negative effect to pokemon that switch in ({@link https://bulbapedia.bulbagarden.net/wiki/List_of_moves_that_cause_entry_hazards#List_of_traps | entry hazards} and Imprison. */
|
||||
export type EntryHazardTagType =
|
||||
@ -24,22 +25,32 @@ export type TurnProtectArenaTagType =
|
||||
/** Subset of {@linkcode ArenaTagType}s that create Trick Room-like effects which are removed upon overlap. */
|
||||
export type RoomArenaTagType = ArenaTagType.TRICK_ROOM;
|
||||
|
||||
/** Subset of {@linkcode ArenaTagType}s that cannot persist across turns, and thus should not be serialized in {@linkcode SessionSaveData}. */
|
||||
/** Subset of {@linkcode ArenaTagType}s that are **not** able to persist across turns, and should therefore not be serialized in {@linkcode SessionSaveData}. */
|
||||
export type NonSerializableArenaTagType = ArenaTagType.NONE | TurnProtectArenaTagType | ArenaTagType.ION_DELUGE;
|
||||
|
||||
/** Subset of {@linkcode ArenaTagType}s that may persist across turns, and thus must be serialized in {@linkcode SessionSaveData}. */
|
||||
export type SerializableArenaTagType = Exclude<ArenaTagType, NonSerializableArenaTagType>;
|
||||
|
||||
/**
|
||||
* Type-safe representation of an arbitrary, serialized Arena Tag
|
||||
* Utility type containing all entries of {@linkcode ArenaTagTypeMap} corresponding to serializable tags.
|
||||
*/
|
||||
export type ArenaTagTypeData = Parameters<
|
||||
ArenaTagTypeMap[keyof {
|
||||
[K in keyof ArenaTagTypeMap as K extends SerializableArenaTagType ? K : never]: ArenaTagTypeMap[K];
|
||||
}]["loadTag"]
|
||||
>[0];
|
||||
type SerializableArenaTagTypeMap = Pick<ArenaTagTypeMap, SerializableArenaTagType>;
|
||||
|
||||
/** Dummy, typescript-only declaration to ensure that
|
||||
/**
|
||||
* Type mapping all `ArenaTag`s to type-safe representations of their serialized forms.
|
||||
* @interface
|
||||
*/
|
||||
export type ArenaTagDataMap = {
|
||||
[k in keyof SerializableArenaTagTypeMap]: Parameters<SerializableArenaTagTypeMap[k]["loadTag"]>[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Type-safe representation of an arbitrary, serialized `ArenaTag`.
|
||||
*/
|
||||
export type ArenaTagData = ObjectValues<ArenaTagDataMap>;
|
||||
|
||||
/**
|
||||
* Dummy, typescript-only declaration to ensure that
|
||||
* {@linkcode ArenaTagTypeMap} has a map for all ArenaTagTypes.
|
||||
*
|
||||
* If an arena tag is missing from the map, typescript will throw an error on this statement.
|
||||
|
@ -1,8 +1,11 @@
|
||||
// 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 { SessionSaveData } from "#system/game-data";
|
||||
// biome-ignore-end lint/correctness/noUnusedImports: Used in a TSDoc comment
|
||||
|
||||
import type { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import type { InferKeys, ObjectValues } from "#types/type-helpers";
|
||||
|
||||
/**
|
||||
* Subset of {@linkcode BattlerTagType}s that restrict the use of moves.
|
||||
@ -103,28 +106,35 @@ export type RemovedTypeTagType = BattlerTagType.DOUBLE_SHOCKED | BattlerTagType.
|
||||
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
|
||||
* Subset of {@linkcode BattlerTagType}s that are able to persist between turns, and should therefore be serialized.
|
||||
*/
|
||||
export type SerializableBattlerTagType = InferKeys<BattlerTagTypeMap, SerializableBattlerTag>;
|
||||
|
||||
/**
|
||||
* Subset of {@linkcode BattlerTagType}s that are **not** able to persist between turns,
|
||||
* and should therefore not be serialized in {@linkcode SessionSaveData}.
|
||||
*/
|
||||
export type NonSerializableBattlerTagType = Exclude<BattlerTagType, SerializableBattlerTagType>;
|
||||
|
||||
/**
|
||||
* Type-safe representation of an arbitrary, serialized Battler Tag
|
||||
* Utility type containing all entries of {@linkcode BattlerTagTypeMap} corresponding to serializable tags.
|
||||
*/
|
||||
export type BattlerTagTypeData = Parameters<
|
||||
BattlerTagTypeMap[keyof {
|
||||
[K in keyof BattlerTagTypeMap as K extends SerializableBattlerTagType ? K : never]: BattlerTagTypeMap[K];
|
||||
}]["loadTag"]
|
||||
>[0];
|
||||
type SerializableBattlerTagTypeMap = Pick<BattlerTagTypeMap, SerializableBattlerTagType>;
|
||||
|
||||
/**
|
||||
* Type mapping all `BattlerTag`s to type-safe representations of their serialized forms.
|
||||
* @interface
|
||||
*/
|
||||
export type BattlerTagDataMap = {
|
||||
[k in keyof SerializableBattlerTagTypeMap]: Parameters<SerializableBattlerTagTypeMap[k]["loadTag"]>[0];
|
||||
};
|
||||
|
||||
/**
|
||||
* Type-safe representation of an arbitrary, serialized `BattlerTag`.
|
||||
*/
|
||||
export type BattlerTagData = ObjectValues<BattlerTagDataMap>;
|
||||
|
||||
/**
|
||||
* Dummy, typescript-only declaration to ensure that
|
||||
|
@ -36,15 +36,18 @@ export type Mutable<T> = {
|
||||
|
||||
/**
|
||||
* Type helper to obtain the keys associated with a given value inside an object.
|
||||
* Acts similar to {@linkcode Pick}, except checking the object's values instead of its keys.
|
||||
* @typeParam O - The type of the object
|
||||
* @typeParam V - The type of one of O's values
|
||||
* @typeParam V - The type of one of O's values.
|
||||
*/
|
||||
export type InferKeys<O extends object, V extends ObjectValues<O>> = {
|
||||
[K in keyof O]: O[K] extends V ? K : never;
|
||||
}[keyof O];
|
||||
export type InferKeys<O extends object, V> = V extends ObjectValues<O>
|
||||
? {
|
||||
[K in keyof O]: O[K] extends V ? K : never;
|
||||
}[keyof O]
|
||||
: never;
|
||||
|
||||
/**
|
||||
* Utility type to obtain the values of a given object. \
|
||||
* Utility type to obtain a union of the values of a given object. \
|
||||
* Functions similar to `keyof E`, except producing the values instead of the keys.
|
||||
* @remarks
|
||||
* This can be used to convert an `enum` interface produced by `typeof Enum` into the union type representing its members.
|
||||
|
@ -23,7 +23,7 @@ import type { Arena } from "#field/arena";
|
||||
import type { Pokemon } from "#field/pokemon";
|
||||
import type {
|
||||
ArenaScreenTagType,
|
||||
ArenaTagTypeData,
|
||||
ArenaTagData,
|
||||
EntryHazardTagType,
|
||||
RoomArenaTagType,
|
||||
SerializableArenaTagType,
|
||||
@ -1663,7 +1663,7 @@ export function getArenaTag(
|
||||
* @param source - An arena tag
|
||||
* @returns The valid arena tag
|
||||
*/
|
||||
export function loadArenaTag(source: ArenaTag | ArenaTagTypeData | { tagType: ArenaTagType.NONE }): ArenaTag {
|
||||
export function loadArenaTag(source: ArenaTag | ArenaTagData | { tagType: ArenaTagType.NONE }): ArenaTag {
|
||||
if (source.tagType === ArenaTagType.NONE) {
|
||||
return new NoneTag();
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ import type { StatStageChangeCallback } from "#phases/stat-stage-change-phase";
|
||||
import i18next from "#plugins/i18n";
|
||||
import type {
|
||||
AbilityBattlerTagType,
|
||||
BattlerTagTypeData,
|
||||
BattlerTagData,
|
||||
ContactSetStatusProtectedTagType,
|
||||
ContactStatStageChangeProtectedTagType,
|
||||
CritStageBoostTagType,
|
||||
@ -3843,7 +3843,7 @@ export function getBattlerTag(
|
||||
* @param source - An object containing the data necessary to reconstruct the BattlerTag.
|
||||
* @returns The valid battler tag
|
||||
*/
|
||||
export function loadBattlerTag(source: BattlerTag | BattlerTagTypeData): BattlerTag {
|
||||
export function loadBattlerTag(source: BattlerTag | BattlerTagData): BattlerTag {
|
||||
// TODO: Remove this bang by fixing the signature of `getBattlerTag`
|
||||
// to allow undefined sourceIds and sourceMoves (with appropriate fallback for tags that require it)
|
||||
const tag = getBattlerTag(source.tagType, source.turnCount, source.sourceMove!, source.sourceId!);
|
||||
|
@ -8903,7 +8903,9 @@ export function initMoves() {
|
||||
.attr(AddArenaTagAttr, ArenaTagType.REFLECT, 5, true)
|
||||
.target(MoveTarget.USER_SIDE),
|
||||
new SelfStatusMove(MoveId.FOCUS_ENERGY, PokemonType.NORMAL, -1, 30, -1, 0, 1)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.CRIT_BOOST, true, true),
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.CRIT_BOOST, true, true)
|
||||
// TODO: Remove once dragon cheer & focus energy are merged into 1 tag
|
||||
.condition((_user, target) => !target.getTag(BattlerTagType.DRAGON_CHEER)),
|
||||
new AttackMove(MoveId.BIDE, PokemonType.NORMAL, MoveCategory.PHYSICAL, -1, -1, 10, -1, 1, 1)
|
||||
.target(MoveTarget.USER)
|
||||
.unimplemented(),
|
||||
@ -11601,6 +11603,8 @@ export function initMoves() {
|
||||
.attr(OpponentHighHpPowerAttr, 100),
|
||||
new StatusMove(MoveId.DRAGON_CHEER, PokemonType.DRAGON, -1, 15, -1, 0, 9)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.DRAGON_CHEER, false, true)
|
||||
// TODO: Remove once dragon cheer & focus energy are merged into 1 tag
|
||||
.condition((_user, target) => !target.getTag(BattlerTagType.CRIT_BOOST))
|
||||
.target(MoveTarget.NEAR_ALLY),
|
||||
new AttackMove(MoveId.ALLURING_VOICE, PokemonType.FAIRY, MoveCategory.SPECIAL, 80, 100, 10, 100, 0, 9)
|
||||
.attr(AddBattlerTagIfBoostedAttr, BattlerTagType.CONFUSED)
|
||||
|
@ -5,14 +5,14 @@ import { Terrain } from "#data/terrain";
|
||||
import { Weather } from "#data/weather";
|
||||
import type { BiomeId } from "#enums/biome-id";
|
||||
import { Arena } from "#field/arena";
|
||||
import type { ArenaTagTypeData } from "#types/arena-tags";
|
||||
import type { ArenaTagData } from "#types/arena-tags";
|
||||
import type { NonFunctionProperties } from "#types/type-helpers";
|
||||
|
||||
export interface SerializedArenaData {
|
||||
biome: BiomeId;
|
||||
weather: NonFunctionProperties<Weather> | null;
|
||||
terrain: NonFunctionProperties<Terrain> | null;
|
||||
tags?: ArenaTagTypeData[];
|
||||
tags?: ArenaTagData[];
|
||||
positionalTags: SerializedPositionalTag[];
|
||||
playerTerasUsed?: number;
|
||||
}
|
||||
@ -31,7 +31,7 @@ export class ArenaData {
|
||||
// is not yet an instance of `ArenaTag`
|
||||
this.tags =
|
||||
source.tags
|
||||
?.map((t: ArenaTag | ArenaTagTypeData) => loadArenaTag(t))
|
||||
?.map((t: ArenaTag | ArenaTagData) => loadArenaTag(t))
|
||||
?.filter((tag): tag is SerializableArenaTag => tag instanceof SerializableArenaTag) ?? [];
|
||||
|
||||
this.playerTerasUsed = source.playerTerasUsed ?? 0;
|
||||
|
13
test/@types/vitest.d.ts
vendored
13
test/@types/vitest.d.ts
vendored
@ -4,6 +4,7 @@ import type { Phase } from "#app/phase";
|
||||
import type Overrides from "#app/overrides";
|
||||
import type { ArenaTag } from "#data/arena-tag";
|
||||
import type { TerrainType } from "#data/terrain";
|
||||
import type { BattlerTag } from "#data/battler-tags";
|
||||
import type { PositionalTag } from "#data/positional-tags/positional-tag";
|
||||
import type { AbilityId } from "#enums/ability-id";
|
||||
import type { ArenaTagSide } from "#enums/arena-tag-side";
|
||||
@ -28,6 +29,7 @@ import type { TurnMove } from "#types/turn-move";
|
||||
import type { AtLeastOne } from "#types/type-helpers";
|
||||
import type { toDmgValue } from "utils/common";
|
||||
import type { expect } from "vitest";
|
||||
import { toHaveBattlerTagOptions } from "#test/test-utils/matchers/to-have-battler-tag";
|
||||
|
||||
declare module "vitest" {
|
||||
interface Assertion<T> {
|
||||
@ -133,10 +135,15 @@ declare module "vitest" {
|
||||
toHaveStatStage(stat: BattleStat, expectedStage: number): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} has a specific {@linkcode BattlerTagType}.
|
||||
* @param expectedBattlerTagType - The expected {@linkcode BattlerTagType}
|
||||
* Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}.
|
||||
* @param expectedTag - A partially-filled {@linkcode BattlerTag} containing the desired properties
|
||||
*/
|
||||
toHaveBattlerTag(expectedBattlerTagType: BattlerTagType): void;
|
||||
toHaveBattlerTag<B extends BattlerTagType>(expectedTag: toHaveBattlerTagOptions<B>): void;
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}.
|
||||
* @param expectedType - The expected {@linkcode BattlerTagType}
|
||||
*/
|
||||
toHaveBattlerTag(expectedType: BattlerTagType): void;
|
||||
|
||||
/**
|
||||
* Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}.
|
||||
|
@ -3,7 +3,6 @@ import { BattlerIndex } from "#enums/battler-index";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { TurnEndPhase } from "#phases/turn-end-phase";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
@ -24,48 +23,35 @@ describe("Abilities - Pastel Veil", () => {
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.moveset([MoveId.TOXIC_THREAD, MoveId.SPLASH])
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemySpecies(SpeciesId.SUNKERN)
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
game.override.battleStyle("double").enemyAbility(AbilityId.BALL_FETCH).enemySpecies(SpeciesId.TOXAPEX);
|
||||
});
|
||||
|
||||
it("prevents the user and its allies from being afflicted by poison", async () => {
|
||||
it("should prevent the user and its allies from being poisoned", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.GALAR_PONYTA]);
|
||||
const ponyta = game.scene.getPlayerField()[1];
|
||||
const magikarp = game.scene.getPlayerField()[0];
|
||||
ponyta.abilityIndex = 1;
|
||||
const [magikarp, ponyta] = game.scene.getPlayerField();
|
||||
game.field.mockAbility(ponyta, AbilityId.PASTEL_VEIL);
|
||||
|
||||
expect(ponyta.hasAbility(AbilityId.PASTEL_VEIL)).toBe(true);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.move.forceEnemyMove(MoveId.TOXIC, BattlerIndex.PLAYER);
|
||||
await game.move.forceEnemyMove(MoveId.TOXIC, BattlerIndex.PLAYER_2);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.TOXIC_THREAD, 1, BattlerIndex.PLAYER);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(magikarp.status?.effect).toBeUndefined();
|
||||
expect(magikarp).toHaveStatusEffect(StatusEffect.NONE);
|
||||
expect(ponyta).toHaveStatusEffect(StatusEffect.NONE);
|
||||
});
|
||||
|
||||
it("it heals the poisoned status condition of allies if user is sent out into battle", async () => {
|
||||
it("should cure allies' poison if user is sent out into battle", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS, SpeciesId.GALAR_PONYTA]);
|
||||
const ponyta = game.scene.getPlayerParty()[2];
|
||||
const magikarp = game.scene.getPlayerField()[0];
|
||||
ponyta.abilityIndex = 1;
|
||||
const [magikarp, , ponyta] = game.scene.getPlayerParty();
|
||||
game.field.mockAbility(ponyta, AbilityId.PASTEL_VEIL);
|
||||
|
||||
expect(ponyta.hasAbility(AbilityId.PASTEL_VEIL)).toBe(true);
|
||||
magikarp.doSetStatus(StatusEffect.POISON);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.TOXIC_THREAD, 1, BattlerIndex.PLAYER);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
expect(magikarp.status?.effect).toBe(StatusEffect.POISON);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.doSwitchPokemon(2);
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(magikarp.status?.effect).toBeUndefined();
|
||||
expect(magikarp).toHaveStatusEffect(StatusEffect.NONE);
|
||||
});
|
||||
});
|
||||
|
@ -2,8 +2,9 @@ import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { TurnEndPhase } from "#phases/turn-end-phase";
|
||||
import { StatusEffect } from "#enums/status-effect";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
@ -26,62 +27,108 @@ describe("Abilities - Sweet Veil", () => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.moveset([MoveId.SPLASH, MoveId.REST, MoveId.YAWN])
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.POWDER);
|
||||
.enemyMoveset(MoveId.SPLASH);
|
||||
});
|
||||
|
||||
it("prevents the user and its allies from falling asleep", async () => {
|
||||
function expectNoStatus() {
|
||||
game.scene.getPlayerField().forEach(p => {
|
||||
expect.soft(p).toHaveStatusEffect(StatusEffect.NONE);
|
||||
});
|
||||
}
|
||||
|
||||
it("should prevent the user and its allies from falling asleep", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SWIRLIX, SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
game.field.mockAbility(game.field.getPlayerPokemon(), AbilityId.SWEET_VEIL);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.move.forceEnemyMove(MoveId.SPORE, BattlerIndex.PLAYER);
|
||||
await game.move.forceEnemyMove(MoveId.SPORE, BattlerIndex.PLAYER_2);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
expect(game.scene.getPlayerField().every(p => p.status?.effect)).toBe(false);
|
||||
expectNoStatus();
|
||||
});
|
||||
|
||||
it("causes Rest to fail when used by the user or its allies", async () => {
|
||||
game.override.enemyMoveset(MoveId.SPLASH);
|
||||
it("should cause Rest to fail when used by the user or its allies", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SWIRLIX, SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.REST, 1);
|
||||
const [swirlix, magikarp] = game.scene.getPlayerField();
|
||||
game.field.mockAbility(swirlix, AbilityId.SWEET_VEIL);
|
||||
swirlix.hp = 1;
|
||||
magikarp.hp = 1;
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
game.move.use(MoveId.REST, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.REST, BattlerIndex.PLAYER_2);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.getPlayerField().every(p => p.status?.effect)).toBe(false);
|
||||
expectNoStatus();
|
||||
expect(swirlix).toHaveUsedMove({ move: MoveId.REST, result: MoveResult.FAIL });
|
||||
expect(magikarp).toHaveUsedMove({ move: MoveId.REST, result: MoveResult.FAIL });
|
||||
});
|
||||
|
||||
it("causes Yawn to fail if used on the user or its allies", async () => {
|
||||
game.override.enemyMoveset(MoveId.YAWN);
|
||||
it("should cause Yawn to fail if used on the user or its allies", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SWIRLIX, SpeciesId.MAGIKARP]);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.SPLASH, 1);
|
||||
const [shuckle, swirlix] = game.scene.getPlayerField();
|
||||
game.field.mockAbility(swirlix, AbilityId.SWEET_VEIL);
|
||||
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.move.forceEnemyMove(MoveId.YAWN, BattlerIndex.PLAYER);
|
||||
await game.move.forceEnemyMove(MoveId.YAWN, BattlerIndex.PLAYER_2);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.getPlayerField().every(p => !!p.getTag(BattlerTagType.DROWSY))).toBe(false);
|
||||
expect(shuckle).not.toHaveBattlerTag(BattlerTagType.DROWSY);
|
||||
expect(swirlix).not.toHaveBattlerTag(BattlerTagType.DROWSY);
|
||||
// TODO: This dooesn't work ATM
|
||||
/*
|
||||
const [karp1, karp2] = game.scene.getEnemyField();
|
||||
expect(karp1).toHaveUsedMove({move: MoveId.YAWN, result: MoveResult.FAIL});
|
||||
expect(karp2).toHaveUsedMove({move: MoveId.YAWN, result: MoveResult.FAIL});
|
||||
*/
|
||||
});
|
||||
|
||||
it("prevents the user and its allies already drowsy due to Yawn from falling asleep.", async () => {
|
||||
game.override.enemySpecies(SpeciesId.PIKACHU).enemyLevel(5).startingLevel(5).enemyMoveset(MoveId.SPLASH);
|
||||
it("should NOT cure allies' sleep status if user is sent out into battle", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.FEEBAS, SpeciesId.SWIRLIX]);
|
||||
|
||||
await game.classicMode.startBattle([SpeciesId.SHUCKLE, SpeciesId.SHUCKLE, SpeciesId.SWIRLIX]);
|
||||
const [magikarp, , swirlix] = game.scene.getPlayerParty();
|
||||
game.field.mockAbility(swirlix, AbilityId.PASTEL_VEIL);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.select(MoveId.YAWN, 1, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH);
|
||||
game.move.use(MoveId.SPORE, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER);
|
||||
await game.toNextTurn();
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase");
|
||||
expect(magikarp).toHaveStatusEffect(StatusEffect.SLEEP);
|
||||
|
||||
expect(game.scene.getPlayerField().some(p => !!p.getTag(BattlerTagType.DROWSY))).toBe(true);
|
||||
|
||||
game.move.select(MoveId.SPLASH);
|
||||
game.move.use(MoveId.SPLASH);
|
||||
game.doSwitchPokemon(2);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(game.scene.getPlayerField().every(p => p.status?.effect)).toBe(false);
|
||||
expect(magikarp).toHaveStatusEffect(StatusEffect.SLEEP);
|
||||
});
|
||||
|
||||
it("should prevent an already-drowsy user or ally from falling asleep", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.SHUCKLE, SpeciesId.SWIRLIX]);
|
||||
|
||||
// Add yawn before granting ability
|
||||
const [shuckle, swirlix] = game.scene.getPlayerField();
|
||||
shuckle.addTag(BattlerTagType.DROWSY, 1);
|
||||
swirlix.addTag(BattlerTagType.DROWSY, 1);
|
||||
|
||||
game.field.mockAbility(shuckle, AbilityId.SWEET_VEIL);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(shuckle).not.toHaveBattlerTag(BattlerTagType.DROWSY);
|
||||
expect(swirlix).not.toHaveBattlerTag(BattlerTagType.DROWSY);
|
||||
expectNoStatus();
|
||||
});
|
||||
});
|
||||
|
@ -1,15 +1,18 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerIndex } from "#enums/battler-index";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { PokemonType } from "#enums/pokemon-type";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Moves - Dragon Cheer", () => {
|
||||
describe("Move - Dragon Cheer", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
@ -24,75 +27,81 @@ describe("Moves - Dragon Cheer", () => {
|
||||
game = new GameManager(phaserGame);
|
||||
game.override
|
||||
.battleStyle("double")
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.enemyLevel(20)
|
||||
.moveset([MoveId.DRAGON_CHEER, MoveId.TACKLE, MoveId.SPLASH]);
|
||||
.enemyLevel(20);
|
||||
});
|
||||
|
||||
it("increases the user's allies' critical hit ratio by one stage", async () => {
|
||||
it("should increase non-Dragon type allies' crit ratios by 1 stage", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.DRAGONAIR, SpeciesId.MAGIKARP]);
|
||||
|
||||
const enemy = game.scene.getEnemyField()[0];
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getCritStage");
|
||||
|
||||
game.move.select(MoveId.DRAGON_CHEER, 0);
|
||||
game.move.select(MoveId.TACKLE, 1, BattlerIndex.ENEMY);
|
||||
|
||||
game.move.use(MoveId.DRAGON_CHEER, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.TACKLE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// After Tackle
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
const [dragonair, magikarp] = game.scene.getPlayerField();
|
||||
expect(dragonair).not.toHaveBattlerTag(BattlerTagType.DRAGON_CHEER);
|
||||
expect(magikarp).toHaveBattlerTag({ tagType: BattlerTagType.DRAGON_CHEER, critStages: 1 });
|
||||
expect(enemy.getCritStage).toHaveReturnedWith(1); // getCritStage is called on defender
|
||||
});
|
||||
|
||||
it("increases the user's Dragon-type allies' critical hit ratio by two stages", async () => {
|
||||
it("should increase Dragon-type allies' crit ratios by 2 stages", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.DRAGONAIR]);
|
||||
|
||||
const enemy = game.scene.getEnemyField()[0];
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getCritStage");
|
||||
|
||||
game.move.select(MoveId.DRAGON_CHEER, 0);
|
||||
game.move.select(MoveId.TACKLE, 1, BattlerIndex.ENEMY);
|
||||
|
||||
game.move.use(MoveId.DRAGON_CHEER, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.TACKLE, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// After Tackle
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
const [magikarp, dragonair] = game.scene.getPlayerField();
|
||||
expect(magikarp).not.toHaveBattlerTag(BattlerTagType.DRAGON_CHEER);
|
||||
expect(dragonair).toHaveBattlerTag({ tagType: BattlerTagType.DRAGON_CHEER, critStages: 2 });
|
||||
expect(enemy.getCritStage).toHaveReturnedWith(2); // getCritStage is called on defender
|
||||
});
|
||||
|
||||
it("applies the effect based on the allies' type upon use of the move, and do not change if the allies' type changes later in battle", async () => {
|
||||
it("should maintain crit boost amount even if user's type is changed", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.DRAGONAIR, SpeciesId.MAGIKARP]);
|
||||
|
||||
const magikarp = game.scene.getPlayerField()[1];
|
||||
const enemy = game.scene.getEnemyField()[0];
|
||||
|
||||
vi.spyOn(enemy, "getCritStage");
|
||||
|
||||
game.move.select(MoveId.DRAGON_CHEER, 0);
|
||||
game.move.select(MoveId.TACKLE, 1, BattlerIndex.ENEMY);
|
||||
|
||||
// Use Reflect Type to become Dragon-type mid-turn
|
||||
game.move.use(MoveId.DRAGON_CHEER, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.REFLECT_TYPE, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER);
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
|
||||
|
||||
// After Tackle
|
||||
await game.phaseInterceptor.to("TurnEndPhase");
|
||||
expect(enemy.getCritStage).toHaveReturnedWith(1); // getCritStage is called on defender
|
||||
|
||||
await game.toNextTurn();
|
||||
|
||||
// Change Magikarp's type to Dragon
|
||||
vi.spyOn(magikarp, "getTypes").mockReturnValue([PokemonType.DRAGON]);
|
||||
expect(magikarp.getTypes()).toEqual([PokemonType.DRAGON]);
|
||||
|
||||
game.move.select(MoveId.SPLASH, 0);
|
||||
game.move.select(MoveId.TACKLE, 1, BattlerIndex.ENEMY);
|
||||
|
||||
await game.setTurnOrder([BattlerIndex.PLAYER_2, BattlerIndex.PLAYER, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(enemy.getCritStage).toHaveReturnedWith(1); // getCritStage is called on defender
|
||||
|
||||
// Dragon cheer added +1 stages
|
||||
const magikarp = game.scene.getPlayerField()[1];
|
||||
expect(magikarp).toHaveBattlerTag({ tagType: BattlerTagType.DRAGON_CHEER, critStages: 1 });
|
||||
expect(magikarp).toHaveTypes([PokemonType.WATER]);
|
||||
|
||||
await game.toEndOfTurn();
|
||||
|
||||
// Should be dragon type, but still with a +1 stage boost
|
||||
expect(magikarp).toHaveTypes([PokemonType.DRAGON]);
|
||||
expect(magikarp).toHaveBattlerTag({ tagType: BattlerTagType.DRAGON_CHEER, critStages: 1 });
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "Focus Energy", tagType: BattlerTagType.CRIT_BOOST },
|
||||
{ name: "Dragon Cheer", tagType: BattlerTagType.DRAGON_CHEER },
|
||||
])("should fail if $name is already present", async ({ tagType }) => {
|
||||
await game.classicMode.startBattle([SpeciesId.DRAGONAIR, SpeciesId.MAGIKARP]);
|
||||
|
||||
const [dragonair, magikarp] = game.scene.getPlayerField();
|
||||
magikarp.addTag(tagType);
|
||||
|
||||
game.move.use(MoveId.DRAGON_CHEER, BattlerIndex.PLAYER);
|
||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER_2);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(dragonair).toHaveUsedMove({ move: MoveId.DRAGON_CHEER, result: MoveResult.FAIL });
|
||||
expect(magikarp).toHaveBattlerTag(tagType);
|
||||
});
|
||||
});
|
||||
|
69
test/moves/focus-energy.test.ts
Normal file
69
test/moves/focus-energy.test.ts
Normal file
@ -0,0 +1,69 @@
|
||||
import { AbilityId } from "#enums/ability-id";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { MoveId } from "#enums/move-id";
|
||||
import { MoveResult } from "#enums/move-result";
|
||||
import { SpeciesId } from "#enums/species-id";
|
||||
import { GameManager } from "#test/test-utils/game-manager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
describe("Move - Focus Energy", () => {
|
||||
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
|
||||
.ability(AbilityId.BALL_FETCH)
|
||||
.battleStyle("single")
|
||||
.criticalHits(false)
|
||||
.enemySpecies(SpeciesId.MAGIKARP)
|
||||
.enemyAbility(AbilityId.BALL_FETCH)
|
||||
.enemyMoveset(MoveId.SPLASH)
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
it("should increase the user's crit ratio by 2 stages", async () => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
game.move.use(MoveId.FOCUS_ENERGY);
|
||||
await game.toNextTurn();
|
||||
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
expect(feebas).toHaveBattlerTag({ tagType: BattlerTagType.CRIT_BOOST, critStages: 2 });
|
||||
|
||||
const enemy = game.field.getEnemyPokemon();
|
||||
vi.spyOn(enemy, "getCritStage");
|
||||
|
||||
game.move.use(MoveId.TACKLE);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(enemy.getCritStage).toHaveReturnedWith(2);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ name: "Focus Energy", tagType: BattlerTagType.CRIT_BOOST },
|
||||
{ name: "Dragon Cheer", tagType: BattlerTagType.DRAGON_CHEER },
|
||||
])("should fail if $name is already present", async ({ tagType }) => {
|
||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||
|
||||
const feebas = game.field.getPlayerPokemon();
|
||||
feebas.addTag(tagType);
|
||||
|
||||
game.move.use(MoveId.FOCUS_ENERGY);
|
||||
await game.toEndOfTurn();
|
||||
|
||||
expect(feebas).toHaveUsedMove({ move: MoveId.FOCUS_ENERGY, result: MoveResult.FAIL });
|
||||
});
|
||||
});
|
@ -6,11 +6,21 @@ import type { OneOther } from "#test/@types/test-helpers";
|
||||
import type { GameManager } from "#test/test-utils/game-manager";
|
||||
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||
import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||
import type { ArenaTagDataMap, SerializableArenaTagType } from "#types/arena-tags";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
// intersection required to preserve T for inferences
|
||||
export type toHaveArenaTagOptions<T extends ArenaTagType> = OneOther<ArenaTagTypeMap[T], "tagType" | "side"> & {
|
||||
tagType: T;
|
||||
/**
|
||||
* Options type for {@linkcode toHaveArenaTag}.
|
||||
* @typeParam A - The {@linkcode ArenaTagType} being checked
|
||||
* @remarks
|
||||
* If A corresponds to a serializable `ArenaTag`, only properties allowed to be serialized
|
||||
* (i.e. can change across instances) will be present and able to be checked.
|
||||
*/
|
||||
export type toHaveArenaTagOptions<A extends ArenaTagType> = OneOther<
|
||||
A extends SerializableArenaTagType ? ArenaTagDataMap[A] : ArenaTagTypeMap[A],
|
||||
"tagType" | "side"
|
||||
> & {
|
||||
tagType: A;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -22,10 +32,10 @@ export type toHaveArenaTagOptions<T extends ArenaTagType> = OneOther<ArenaTagTyp
|
||||
* {@linkcode ArenaTagSide.BOTH} to check both sides
|
||||
* @returns The result of the matching
|
||||
*/
|
||||
export function toHaveArenaTag<T extends ArenaTagType>(
|
||||
export function toHaveArenaTag<A extends ArenaTagType>(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expectedTag: T | toHaveArenaTagOptions<T>,
|
||||
expectedTag: A | toHaveArenaTagOptions<A>,
|
||||
side: ArenaTagSide = ArenaTagSide.BOTH,
|
||||
): SyncExpectationResult {
|
||||
if (!isGameManagerInstance(received)) {
|
||||
|
@ -3,21 +3,39 @@ import type { Pokemon } from "#field/pokemon";
|
||||
/* biome-ignore-end lint/correctness/noUnusedImports: tsdoc imports */
|
||||
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import type { BattlerTagTypeMap } from "#data/battler-tags";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { getEnumStr } from "#test/test-utils/string-utils";
|
||||
import type { OneOther } from "#test/@types/test-helpers";
|
||||
import { getEnumStr, getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||
import type { BattlerTagDataMap, SerializableBattlerTagType } from "#types/battler-tags";
|
||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||
|
||||
// intersection required to preserve T for inferences
|
||||
/**
|
||||
* Matcher that checks if a {@linkcode Pokemon} has a specific {@linkcode BattlerTagType}.
|
||||
* Options type for {@linkcode toHaveBattlerTag}.
|
||||
* @typeParam B - The {@linkcode BattlerTagType} being checked
|
||||
* @remarks
|
||||
* If B corresponds to a serializable `BattlerTag`, only properties allowed to be serialized
|
||||
* (i.e. can change across instances) will be present and able to be checked.
|
||||
*/
|
||||
export type toHaveBattlerTagOptions<B extends BattlerTagType> = (B extends SerializableBattlerTagType
|
||||
? OneOther<BattlerTagDataMap[B], "tagType">
|
||||
: OneOther<BattlerTagTypeMap[B], "tagType">) & {
|
||||
tagType: B;
|
||||
};
|
||||
|
||||
/**
|
||||
* Matcher that checks if a {@linkcode Pokemon} has a specific {@linkcode BattlerTag}.
|
||||
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
||||
* @param expectedBattlerTagType - The {@linkcode BattlerTagType} to check for
|
||||
* @param expectedTag - The `BattlerTagType` of the desired tag, or a partially-filled object
|
||||
* containing the desired properties
|
||||
* @returns Whether the matcher passed
|
||||
*/
|
||||
export function toHaveBattlerTag(
|
||||
export function toHaveBattlerTag<B extends BattlerTagType>(
|
||||
this: MatcherState,
|
||||
received: unknown,
|
||||
expectedBattlerTagType: BattlerTagType,
|
||||
expectedTag: B | toHaveBattlerTagOptions<B>,
|
||||
): SyncExpectationResult {
|
||||
if (!isPokemonInstance(received)) {
|
||||
return {
|
||||
@ -26,18 +44,44 @@ export function toHaveBattlerTag(
|
||||
};
|
||||
}
|
||||
|
||||
const pass = !!received.getTag(expectedBattlerTagType);
|
||||
const pkmName = getPokemonNameWithAffix(received);
|
||||
// "BattlerTagType.SEEDED (=1)"
|
||||
const expectedTagStr = getEnumStr(BattlerTagType, expectedBattlerTagType, { prefix: "BattlerTagType." });
|
||||
|
||||
// Coerce lone `tagType`s into objects
|
||||
const etag = typeof expectedTag === "object" ? expectedTag : { tagType: expectedTag };
|
||||
const gotTag = received.getTag(etag.tagType);
|
||||
|
||||
// If checking exclusively tag type OR no tags were found, break out early.
|
||||
if (typeof expectedTag !== "object" || !gotTag) {
|
||||
const pass = !!gotTag;
|
||||
// "BattlerTagType.SEEDED (=1)"
|
||||
const expectedTagStr = getEnumStr(BattlerTagType, etag.tagType, { prefix: "BattlerTagType." });
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected ${pkmName} to NOT have a tag of type ${expectedTagStr}, but it did!`
|
||||
: `Expected ${pkmName} to have a tag of type ${expectedTagStr}, but it didn't!`,
|
||||
expected: expectedTag,
|
||||
actual: received.summonData.tags.map(t => t.tagType),
|
||||
};
|
||||
}
|
||||
|
||||
// Check for equality with the provided tag
|
||||
const pass = this.equals(gotTag, etag, [
|
||||
...this.customTesters,
|
||||
this.utils.subsetEquality,
|
||||
this.utils.iterableEquality,
|
||||
]);
|
||||
|
||||
const expectedStr = getOnelineDiffStr.call(this, expectedTag);
|
||||
return {
|
||||
pass,
|
||||
message: () =>
|
||||
pass
|
||||
? `Expected ${pkmName} to NOT have ${expectedTagStr}, but it did!`
|
||||
: `Expected ${pkmName} to have ${expectedTagStr}, but it didn't!`,
|
||||
expected: expectedBattlerTagType,
|
||||
actual: received.summonData.tags.map(t => t.tagType),
|
||||
? `Expected ${pkmName} to NOT have a tag matching ${expectedStr}, but it did!`
|
||||
: `Expected ${pkmName} to have a tag matching ${expectedStr}, but it didn't!`,
|
||||
expected: expectedTag,
|
||||
actual: gotTag,
|
||||
};
|
||||
}
|
||||
|
@ -183,5 +183,5 @@ export function getOnelineDiffStr(this: MatcherState, obj: unknown): string {
|
||||
return this.utils
|
||||
.stringify(obj, undefined, { maxLength: 35, indent: 0, printBasicPrototype: false })
|
||||
.replace(/\n/g, " ") // Replace newlines with spaces
|
||||
.replace(/,(\s*)}$/g, "$1}"); // Trim trailing commas
|
||||
.replace(/,(\s*)\}$/g, "$1}"); // Trim trailing commas
|
||||
}
|
||||
|
34
typedoc-plugins/typedoc-plugin-rename-svg.js
Normal file
34
typedoc-plugins/typedoc-plugin-rename-svg.js
Normal file
@ -0,0 +1,34 @@
|
||||
// @ts-check
|
||||
|
||||
import { PageKind, Renderer } from "typedoc";
|
||||
|
||||
/**
|
||||
* @module
|
||||
* Typedoc plugin to run post-processing on the `index.html` file and replace the coverage SVG
|
||||
* for Beta with the newly generated file for the current branch.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {import('typedoc').Application} app
|
||||
*/
|
||||
export function load(app) {
|
||||
// Don't do anything if no REF_NAME was specified (likely indicating a local docs run)
|
||||
if (!process.env.REF_NAME) {
|
||||
return;
|
||||
}
|
||||
app.renderer.on(Renderer.EVENT_END_PAGE, page => {
|
||||
if (page.pageKind === PageKind.Index && page.contents) {
|
||||
page.contents = page.contents
|
||||
// Replace links to the beta documentation site with the current ref name
|
||||
.replace(
|
||||
/href="(.*pagefaultgames.github.io\/pokerogue\/).*?"/, // formatting
|
||||
`href="$1/${process.env.REF_NAME}"`,
|
||||
)
|
||||
// Replace the link to Beta's coverage SVG with the SVG file for the branch in question.
|
||||
.replace(
|
||||
/img src=".*?coverage.svg/, // formatting
|
||||
`img src="coverage.svg"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
56
typedoc.config.js
Normal file
56
typedoc.config.js
Normal file
@ -0,0 +1,56 @@
|
||||
import { globSync } from "node:fs";
|
||||
|
||||
/**
|
||||
* @type {Partial<import("typedoc").TypeDocOptions>}
|
||||
*/
|
||||
const config = {
|
||||
entryPoints: ["./src", "./test/test-utils"],
|
||||
entryPointStrategy: "expand",
|
||||
exclude: ["**/*+.test.ts", "src/polyfills.ts", "src/vite.env.d.ts"],
|
||||
excludeReferences: true, // prevent documenting re-exports
|
||||
requiredToBeDocumented: [
|
||||
"Enum",
|
||||
"EnumMember",
|
||||
"Variable",
|
||||
"Function",
|
||||
"Class",
|
||||
"Interface",
|
||||
"Property",
|
||||
"Method",
|
||||
"Accessor",
|
||||
"TypeAlias",
|
||||
],
|
||||
highlightLanguages: ["javascript", "json", "jsonc", "json5", "tsx", "typescript", "markdown"],
|
||||
plugin: [
|
||||
"typedoc-github-theme",
|
||||
"typedoc-plugin-coverage",
|
||||
"typedoc-plugin-mdn-links",
|
||||
...globSync("./typedoc-plugins/**/*.js").map(plugin => "./" + plugin),
|
||||
],
|
||||
// Avoid emitting docs for branches other than main/beta
|
||||
emit: process.env.DRY_RUN ? "none" : "docs",
|
||||
out: process.env.CI ? "/tmp/docs" : "./typedoc",
|
||||
name: "PokéRogue",
|
||||
readme: "./README.md",
|
||||
coverageLabel: "Documented",
|
||||
coverageSvgWidth: 120, // Increased from 104 baseline due to adding 2 extra letters
|
||||
favicon: "./public/images/logo.png",
|
||||
theme: "typedoc-github-theme",
|
||||
customFooterHtml: "<p>Copyright <strong>Pagefault Games</strong> 2025</p>",
|
||||
customFooterHtmlDisableWrapper: true,
|
||||
navigationLinks: {
|
||||
GitHub: "https://github.com/pagefaultgames/pokerogue",
|
||||
},
|
||||
};
|
||||
|
||||
// If generating docs for main/beta, check the ref name and add an appropriate navigation header
|
||||
if (!process.env.DRY_RUN && process.env.REF_NAME) {
|
||||
const otherRefName = process.env.REF_NAME === "main" ? "beta" : "main";
|
||||
config.navigationLinks = {
|
||||
...config.navigationLinks,
|
||||
// This will be "Switch to Beta" when on main, and vice versa
|
||||
[`Switch to ${otherRefName.charAt(0).toUpperCase() + otherRefName.slice(1).toLowerCase()}`]: `https://pagefaultgames.github.io/pokerogue/${otherRefName}`,
|
||||
};
|
||||
}
|
||||
|
||||
export default config;
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"entryPoints": ["./src"],
|
||||
"entryPointStrategy": "expand",
|
||||
"exclude": ["**/*+.test.ts"],
|
||||
"out": "typedoc",
|
||||
"highlightLanguages": ["javascript", "json", "jsonc", "json5", "tsx", "typescript", "markdown"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user