mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-08-20 06:19:29 +02:00
Compare commits
74 Commits
49db855b61
...
ecd404f07a
Author | SHA1 | Date | |
---|---|---|---|
|
ecd404f07a | ||
|
b2990aaa15 | ||
|
ee4950633e | ||
|
30058ed70e | ||
|
140e4ab142 | ||
|
76d8357d0b | ||
|
c48ad4d5d4 | ||
|
d776df4b21 | ||
|
3ffa13e6b7 | ||
|
d85bcf2324 | ||
|
7264f56fa1 | ||
|
c34134159d | ||
|
99e1163752 | ||
|
a82768f040 | ||
|
8b1b1cd38a | ||
|
d6c77e0afa | ||
|
9ee0bfb84b | ||
|
c36394b781 | ||
|
a1c718322f | ||
|
2df1eaff10 | ||
|
b3eea02779 | ||
|
b55872ef32 | ||
|
ff6ec7f945 | ||
|
af77e2ef7f | ||
|
6edf75ca42 | ||
|
bda0f39df0 | ||
|
1bfdd8f24b | ||
|
19c0fba6f4 | ||
|
259a9b328d | ||
|
1f9eaf49c6 | ||
|
e1c999566e | ||
|
1cb1797f3b | ||
|
e385c73188 | ||
|
4fd44e8029 | ||
|
d614588783 | ||
|
adeedb84d5 | ||
|
eb1696e93e | ||
|
7d0ee7a9ed | ||
|
d82c105d55 | ||
|
e7b5f5fe99 | ||
|
fefa8e408f | ||
|
903d1a33dd | ||
|
7f2766f832 | ||
|
c6c3cd9f3c | ||
|
0c3ae62d1e | ||
|
779c95ba93 | ||
|
0402b07122 | ||
|
ba39af1bbe | ||
|
d20a47d082 | ||
|
9301e6db87 | ||
|
c24f630caf | ||
|
14f5849502 | ||
|
374474720b | ||
|
2eae68785d | ||
|
c919ea78cb | ||
|
e40b1bd452 | ||
|
a179fdeac6 | ||
|
455f3b6be1 | ||
|
059b9b2a95 | ||
|
b9a4e631db | ||
|
3e2d050d70 | ||
|
8a10cc2037 | ||
|
c76739f629 | ||
|
6f676e4438 | ||
|
3bd10f09e9 | ||
|
8a915d2dc8 | ||
|
d456b3849a | ||
|
82920fb059 | ||
|
4d93958627 | ||
|
47c45bc63e | ||
|
db927e8adb | ||
|
9bfb1bba88 | ||
|
96e4bb5e0e | ||
|
45bbaf2b25 |
@ -81,7 +81,7 @@ For example, here is how you could test a scenario where the player Pokemon has
|
|||||||
```typescript
|
```typescript
|
||||||
const overrides = {
|
const overrides = {
|
||||||
ABILITY_OVERRIDE: AbilityId.DROUGHT,
|
ABILITY_OVERRIDE: AbilityId.DROUGHT,
|
||||||
OPP_MOVESET_OVERRIDE: MoveId.WATER_GUN,
|
ENEMY_MOVESET_OVERRIDE: MoveId.WATER_GUN,
|
||||||
} satisfies Partial<InstanceType<typeof DefaultOverrides>>;
|
} satisfies Partial<InstanceType<typeof DefaultOverrides>>;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@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/crypto-js": "^4.2.0",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/node": "^22.16.5",
|
"@types/node": "^22.16.5",
|
||||||
"@vitest/coverage-istanbul": "^3.2.4",
|
"@vitest/coverage-istanbul": "^3.2.4",
|
||||||
|
@ -48,6 +48,9 @@ importers:
|
|||||||
'@ls-lint/ls-lint':
|
'@ls-lint/ls-lint':
|
||||||
specifier: 2.3.1
|
specifier: 2.3.1
|
||||||
version: 2.3.1
|
version: 2.3.1
|
||||||
|
'@types/crypto-js':
|
||||||
|
specifier: ^4.2.0
|
||||||
|
version: 4.2.2
|
||||||
'@types/jsdom':
|
'@types/jsdom':
|
||||||
specifier: ^21.1.7
|
specifier: ^21.1.7
|
||||||
version: 21.1.7
|
version: 21.1.7
|
||||||
@ -718,6 +721,9 @@ packages:
|
|||||||
'@types/cookie@0.6.0':
|
'@types/cookie@0.6.0':
|
||||||
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||||
|
|
||||||
|
'@types/crypto-js@4.2.2':
|
||||||
|
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
|
||||||
|
|
||||||
'@types/deep-eql@4.0.2':
|
'@types/deep-eql@4.0.2':
|
||||||
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==}
|
||||||
|
|
||||||
@ -2525,6 +2531,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/cookie@0.6.0': {}
|
'@types/cookie@0.6.0': {}
|
||||||
|
|
||||||
|
'@types/crypto-js@4.2.2': {}
|
||||||
|
|
||||||
'@types/deep-eql@4.0.2': {}
|
'@types/deep-eql@4.0.2': {}
|
||||||
|
|
||||||
'@types/estree@1.0.8': {}
|
'@types/estree@1.0.8': {}
|
||||||
|
BIN
public/images/ui/champion_ribbon_emerald.png
Normal file
BIN
public/images/ui/champion_ribbon_emerald.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 225 B |
BIN
public/images/ui/legacy/champion_ribbon_emerald.png
Normal file
BIN
public/images/ui/legacy/champion_ribbon_emerald.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 225 B |
Binary file not shown.
Before Width: | Height: | Size: 837 B After Width: | Height: | Size: 799 B |
146
public/images/ui/party_slot_main_short.json
Normal file
146
public/images/ui/party_slot_main_short.json
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
{
|
||||||
|
"textures": [
|
||||||
|
{
|
||||||
|
"image": "party_slot_main_short.png",
|
||||||
|
"format": "RGBA8888",
|
||||||
|
"size": {
|
||||||
|
"w": 110,
|
||||||
|
"h": 294
|
||||||
|
},
|
||||||
|
"scale": 1,
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"filename": "party_slot_main_short",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "party_slot_main_short_sel",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 41,
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "party_slot_main_short_fnt",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 82,
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "party_slot_main_short_fnt_sel",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 123,
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "party_slot_main_short_swap",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 164,
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename": "party_slot_main_short_swap_sel",
|
||||||
|
"rotated": false,
|
||||||
|
"trimmed": false,
|
||||||
|
"sourceSize": {
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
},
|
||||||
|
"spriteSourceSize": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0,
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
},
|
||||||
|
"frame": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 205,
|
||||||
|
"w": 110,
|
||||||
|
"h": 41
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"app": "https://www.codeandweb.com/texturepacker",
|
||||||
|
"version": "3.0",
|
||||||
|
"smartupdate": "$TexturePacker:SmartUpdate:29685f2f538901cf5bf7f0ed2ea867c3:a080ea6c8cccd1e03244214053e79796:565f7afc5ca419b6ba8dbce51ea30818$"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/images/ui/party_slot_main_short.png
Normal file
BIN
public/images/ui/party_slot_main_short.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 KiB |
@ -1,3 +1,5 @@
|
|||||||
|
import type { RibbonData } from "#system/ribbons/ribbon-data";
|
||||||
|
|
||||||
export interface DexData {
|
export interface DexData {
|
||||||
[key: number]: DexEntry;
|
[key: number]: DexEntry;
|
||||||
}
|
}
|
||||||
@ -10,4 +12,5 @@ export interface DexEntry {
|
|||||||
caughtCount: number;
|
caughtCount: number;
|
||||||
hatchedCount: number;
|
hatchedCount: number;
|
||||||
ivs: number[];
|
ivs: number[];
|
||||||
|
ribbons: RibbonData;
|
||||||
}
|
}
|
||||||
|
@ -103,3 +103,12 @@ export type CoerceNullPropertiesToUndefined<T extends object> = {
|
|||||||
* @typeParam T - The type to render partial
|
* @typeParam T - The type to render partial
|
||||||
*/
|
*/
|
||||||
export type AtLeastOne<T extends object> = Partial<T> & ObjectValues<{ [K in keyof T]: Pick<Required<T>, K> }>;
|
export type AtLeastOne<T extends object> = Partial<T> & ObjectValues<{ [K in keyof T]: Pick<Required<T>, K> }>;
|
||||||
|
|
||||||
|
/** Type helper that adds a brand to a type, used for nominal typing.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* Brands should be either a string or unique symbol. This prevents overlap with other types.
|
||||||
|
*/
|
||||||
|
export declare class Brander<B> {
|
||||||
|
private __brand: B;
|
||||||
|
}
|
||||||
|
@ -17,8 +17,7 @@ export function initLoggedInUser(): void {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateUserInfo(): Promise<[boolean, number]> {
|
export async function updateUserInfo(): Promise<[boolean, number]> {
|
||||||
return new Promise<[boolean, number]>(resolve => {
|
|
||||||
if (bypassLogin) {
|
if (bypassLogin) {
|
||||||
loggedInUser = {
|
loggedInUser = {
|
||||||
username: "Guest",
|
username: "Guest",
|
||||||
@ -36,7 +35,7 @@ export function updateUserInfo(): Promise<[boolean, number]> {
|
|||||||
}
|
}
|
||||||
loggedInUser.lastSessionSlot = lastSessionSlot;
|
loggedInUser.lastSessionSlot = lastSessionSlot;
|
||||||
// Migrate old data from before the username was appended
|
// Migrate old data from before the username was appended
|
||||||
["data", "sessionData", "sessionData1", "sessionData2", "sessionData3", "sessionData4"].map(d => {
|
["data", "sessionData", "sessionData1", "sessionData2", "sessionData3", "sessionData4"].forEach(d => {
|
||||||
const lsItem = localStorage.getItem(d);
|
const lsItem = localStorage.getItem(d);
|
||||||
if (lsItem && !!loggedInUser?.username) {
|
if (lsItem && !!loggedInUser?.username) {
|
||||||
const lsUserItem = localStorage.getItem(`${d}_${loggedInUser.username}`);
|
const lsUserItem = localStorage.getItem(`${d}_${loggedInUser.username}`);
|
||||||
@ -47,15 +46,13 @@ export function updateUserInfo(): Promise<[boolean, number]> {
|
|||||||
localStorage.removeItem(d);
|
localStorage.removeItem(d);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return resolve([true, 200]);
|
return [true, 200];
|
||||||
}
|
}
|
||||||
pokerogueApi.account.getInfo().then(([accountInfo, status]) => {
|
|
||||||
|
const [accountInfo, status] = await pokerogueApi.account.getInfo();
|
||||||
if (!accountInfo) {
|
if (!accountInfo) {
|
||||||
resolve([false, status]);
|
return [false, status];
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
loggedInUser = accountInfo;
|
loggedInUser = accountInfo;
|
||||||
resolve([true, 200]);
|
return [true, 200];
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
@ -944,17 +944,17 @@ export class BattleScene extends SceneBase {
|
|||||||
dataSource?: PokemonData,
|
dataSource?: PokemonData,
|
||||||
postProcess?: (enemyPokemon: EnemyPokemon) => void,
|
postProcess?: (enemyPokemon: EnemyPokemon) => void,
|
||||||
): EnemyPokemon {
|
): EnemyPokemon {
|
||||||
if (Overrides.OPP_LEVEL_OVERRIDE > 0) {
|
if (Overrides.ENEMY_LEVEL_OVERRIDE > 0) {
|
||||||
level = Overrides.OPP_LEVEL_OVERRIDE;
|
level = Overrides.ENEMY_LEVEL_OVERRIDE;
|
||||||
}
|
}
|
||||||
if (Overrides.OPP_SPECIES_OVERRIDE) {
|
if (Overrides.ENEMY_SPECIES_OVERRIDE) {
|
||||||
species = getPokemonSpecies(Overrides.OPP_SPECIES_OVERRIDE);
|
species = getPokemonSpecies(Overrides.ENEMY_SPECIES_OVERRIDE);
|
||||||
// The fact that a Pokemon is a boss or not can change based on its Species and level
|
// The fact that a Pokemon is a boss or not can change based on its Species and level
|
||||||
boss = this.getEncounterBossSegments(this.currentBattle.waveIndex, level, species) > 1;
|
boss = this.getEncounterBossSegments(this.currentBattle.waveIndex, level, species) > 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pokemon = new EnemyPokemon(species, level, trainerSlot, boss, shinyLock, dataSource);
|
const pokemon = new EnemyPokemon(species, level, trainerSlot, boss, shinyLock, dataSource);
|
||||||
if (Overrides.OPP_FUSION_OVERRIDE) {
|
if (Overrides.ENEMY_FUSION_OVERRIDE) {
|
||||||
pokemon.generateFusionSpecies();
|
pokemon.generateFusionSpecies();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1766,10 +1766,10 @@ export class BattleScene extends SceneBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getEncounterBossSegments(waveIndex: number, level: number, species?: PokemonSpecies, forceBoss = false): number {
|
getEncounterBossSegments(waveIndex: number, level: number, species?: PokemonSpecies, forceBoss = false): number {
|
||||||
if (Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE > 1) {
|
if (Overrides.ENEMY_HEALTH_SEGMENTS_OVERRIDE > 1) {
|
||||||
return Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE;
|
return Overrides.ENEMY_HEALTH_SEGMENTS_OVERRIDE;
|
||||||
}
|
}
|
||||||
if (Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE === 1) {
|
if (Overrides.ENEMY_HEALTH_SEGMENTS_OVERRIDE === 1) {
|
||||||
// The rest of the code expects to be returned 0 and not 1 if the enemy is not a boss
|
// The rest of the code expects to be returned 0 and not 1 if the enemy is not a boss
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
@ -101,3 +101,9 @@ export const ANTI_VARIANCE_WEIGHT_MODIFIER = 15;
|
|||||||
* Default: `10000` (0.01%)
|
* Default: `10000` (0.01%)
|
||||||
*/
|
*/
|
||||||
export const FAKE_TITLE_LOGO_CHANCE = 10000;
|
export const FAKE_TITLE_LOGO_CHANCE = 10000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ceiling on friendship amount that can be reached through the use of rare candies.
|
||||||
|
* Using rare candies will never increase friendship beyond this value.
|
||||||
|
*/
|
||||||
|
export const RARE_CANDY_FRIENDSHIP_CAP = 200;
|
||||||
|
@ -1235,7 +1235,7 @@ export class PostDefendContactApplyStatusEffectAbAttr extends PostDefendAbAttr {
|
|||||||
// TODO: Probably want to check against simulated here
|
// TODO: Probably want to check against simulated here
|
||||||
const effect =
|
const effect =
|
||||||
this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randBattleSeedInt(this.effects.length)];
|
this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randBattleSeedInt(this.effects.length)];
|
||||||
attacker.trySetStatus(effect, true, pokemon);
|
attacker.trySetStatus(effect, pokemon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2228,7 +2228,7 @@ export class PostAttackApplyStatusEffectAbAttr extends PostAttackAbAttr {
|
|||||||
apply({ pokemon, opponent }: PostMoveInteractionAbAttrParams): void {
|
apply({ pokemon, opponent }: PostMoveInteractionAbAttrParams): void {
|
||||||
const effect =
|
const effect =
|
||||||
this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randBattleSeedInt(this.effects.length)];
|
this.effects.length === 1 ? this.effects[0] : this.effects[pokemon.randBattleSeedInt(this.effects.length)];
|
||||||
opponent.trySetStatus(effect, true, pokemon);
|
opponent.trySetStatus(effect, pokemon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2383,7 +2383,7 @@ export class SynchronizeStatusAbAttr extends PostSetStatusAbAttr {
|
|||||||
*/
|
*/
|
||||||
override apply({ simulated, effect, sourcePokemon, pokemon }: PostSetStatusAbAttrParams): void {
|
override apply({ simulated, effect, sourcePokemon, pokemon }: PostSetStatusAbAttrParams): void {
|
||||||
if (!simulated && sourcePokemon) {
|
if (!simulated && sourcePokemon) {
|
||||||
sourcePokemon.trySetStatus(effect, true, pokemon);
|
sourcePokemon.trySetStatus(effect, pokemon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3659,7 +3659,8 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
|
|||||||
protected immuneEffects: StatusEffect[];
|
protected immuneEffects: StatusEffect[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param immuneEffects - The status effects to which the Pokémon is immune.
|
* @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application.
|
||||||
|
* If none are provided, will block **all** status effects regardless of type.
|
||||||
*/
|
*/
|
||||||
constructor(...immuneEffects: StatusEffect[]) {
|
constructor(...immuneEffects: StatusEffect[]) {
|
||||||
super();
|
super();
|
||||||
@ -3668,7 +3669,7 @@ export class PreSetStatusEffectImmunityAbAttr extends PreSetStatusAbAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override canApply({ effect }: PreSetStatusAbAttrParams): boolean {
|
override canApply({ effect }: PreSetStatusAbAttrParams): boolean {
|
||||||
return (effect !== StatusEffect.FAINT && this.immuneEffects.length < 1) || this.immuneEffects.includes(effect);
|
return (this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) || this.immuneEffects.includes(effect);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -3720,6 +3721,11 @@ export interface UserFieldStatusEffectImmunityAbAttrParams extends AbAttrBasePar
|
|||||||
*/
|
*/
|
||||||
export class UserFieldStatusEffectImmunityAbAttr extends AbAttr {
|
export class UserFieldStatusEffectImmunityAbAttr extends AbAttr {
|
||||||
protected immuneEffects: StatusEffect[];
|
protected immuneEffects: StatusEffect[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application.
|
||||||
|
* If none are provided, will block **all** status effects regardless of type.
|
||||||
|
*/
|
||||||
constructor(...immuneEffects: StatusEffect[]) {
|
constructor(...immuneEffects: StatusEffect[]) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@ -3728,7 +3734,7 @@ export class UserFieldStatusEffectImmunityAbAttr extends AbAttr {
|
|||||||
|
|
||||||
override canApply({ effect, cancelled }: UserFieldStatusEffectImmunityAbAttrParams): boolean {
|
override canApply({ effect, cancelled }: UserFieldStatusEffectImmunityAbAttrParams): boolean {
|
||||||
return (
|
return (
|
||||||
(!cancelled.value && effect !== StatusEffect.FAINT && this.immuneEffects.length < 1) ||
|
(!cancelled.value && this.immuneEffects.length === 0 && effect !== StatusEffect.FAINT) ||
|
||||||
this.immuneEffects.includes(effect)
|
this.immuneEffects.includes(effect)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -3754,6 +3760,10 @@ export class ConditionalUserFieldStatusEffectImmunityAbAttr extends UserFieldSta
|
|||||||
*/
|
*/
|
||||||
private condition: (target: Pokemon, source: Pokemon | null) => boolean;
|
private condition: (target: Pokemon, source: Pokemon | null) => boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param immuneEffects - An array of {@linkcode StatusEffect}s to prevent application.
|
||||||
|
* If none are provided, will block **all** status effects regardless of type.
|
||||||
|
*/
|
||||||
constructor(condition: (target: Pokemon, source: Pokemon | null) => boolean, ...immuneEffects: StatusEffect[]) {
|
constructor(condition: (target: Pokemon, source: Pokemon | null) => boolean, ...immuneEffects: StatusEffect[]) {
|
||||||
super(...immuneEffects);
|
super(...immuneEffects);
|
||||||
|
|
||||||
@ -7479,8 +7489,7 @@ export function initAbilities() {
|
|||||||
.unsuppressable()
|
.unsuppressable()
|
||||||
.bypassFaint(),
|
.bypassFaint(),
|
||||||
new Ability(AbilityId.CORROSION, 7)
|
new Ability(AbilityId.CORROSION, 7)
|
||||||
.attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ PokemonType.STEEL, PokemonType.POISON ])
|
.attr(IgnoreTypeStatusEffectImmunityAbAttr, [ StatusEffect.POISON, StatusEffect.TOXIC ], [ PokemonType.STEEL, PokemonType.POISON ]),
|
||||||
.edgeCase(), // Should poison itself with toxic orb.
|
|
||||||
new Ability(AbilityId.COMATOSE, 7)
|
new Ability(AbilityId.COMATOSE, 7)
|
||||||
.attr(StatusEffectImmunityAbAttr, ...getNonVolatileStatusEffects())
|
.attr(StatusEffectImmunityAbAttr, ...getNonVolatileStatusEffects())
|
||||||
.attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
|
.attr(BattlerTagImmunityAbAttr, BattlerTagType.DROWSY)
|
||||||
|
@ -890,13 +890,19 @@ class ToxicSpikesTag extends ArenaTrapTag {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
override activateTrap(pokemon: Pokemon, simulated: boolean): boolean {
|
||||||
if (pokemon.isGrounded()) {
|
if (!pokemon.isGrounded()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (simulated) {
|
if (simulated) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pokemon.isOfType(PokemonType.POISON)) {
|
if (pokemon.isOfType(PokemonType.POISON)) {
|
||||||
|
// Neutralize the tag and remove it from the field.
|
||||||
|
// Message cannot be moved to `onRemove` as that requires a reference to the neutralizing pokemon
|
||||||
this.#neutralized = true;
|
this.#neutralized = true;
|
||||||
if (globalScene.arena.removeTag(this.tagType)) {
|
globalScene.arena.removeTagOnSide(this.tagType, this.side);
|
||||||
globalScene.phaseManager.queueMessage(
|
globalScene.phaseManager.queueMessage(
|
||||||
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
i18next.t("arenaTag:toxicSpikesActivateTrapPoison", {
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon),
|
||||||
@ -905,17 +911,10 @@ class ToxicSpikesTag extends ArenaTrapTag {
|
|||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else if (!pokemon.status) {
|
|
||||||
const toxic = this.layers > 1;
|
|
||||||
if (
|
|
||||||
pokemon.trySetStatus(!toxic ? StatusEffect.POISON : StatusEffect.TOXIC, true, null, 0, this.getMoveName())
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
// Attempt to poison the target, suppressing any immunity messages that arise.
|
||||||
|
const effect = this.layers === 1 ? StatusEffect.POISON : StatusEffect.TOXIC;
|
||||||
|
return pokemon.trySetStatus(effect, null, undefined, this.getMoveName(), false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
getMatchupScoreMultiplier(pokemon: Pokemon): number {
|
||||||
|
@ -563,7 +563,7 @@ export class BeakBlastChargingTag extends BattlerTag {
|
|||||||
target: pokemon,
|
target: pokemon,
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
phaseData.attacker.trySetStatus(StatusEffect.BURN, true, pokemon);
|
phaseData.attacker.trySetStatus(StatusEffect.BURN, pokemon);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -1509,7 +1509,7 @@ export class DrowsyTag extends SerializableBattlerTag {
|
|||||||
|
|
||||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||||
if (!super.lapse(pokemon, lapseType)) {
|
if (!super.lapse(pokemon, lapseType)) {
|
||||||
pokemon.trySetStatus(StatusEffect.SLEEP, true);
|
pokemon.trySetStatus(StatusEffect.SLEEP);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1859,7 +1859,7 @@ export class ContactSetStatusProtectedTag extends DamageProtectedTag {
|
|||||||
* @param user - The pokemon that is being attacked and has the tag
|
* @param user - The pokemon that is being attacked and has the tag
|
||||||
*/
|
*/
|
||||||
override onContact(attacker: Pokemon, user: Pokemon): void {
|
override onContact(attacker: Pokemon, user: Pokemon): void {
|
||||||
attacker.trySetStatus(this.#statusEffect, true, user);
|
attacker.trySetStatus(this.#statusEffect, user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2803,7 +2803,7 @@ export class GulpMissileTag extends SerializableBattlerTag {
|
|||||||
if (this.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) {
|
if (this.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) {
|
||||||
globalScene.phaseManager.unshiftNew("StatStageChangePhase", attacker.getBattlerIndex(), false, [Stat.DEF], -1);
|
globalScene.phaseManager.unshiftNew("StatStageChangePhase", attacker.getBattlerIndex(), false, [Stat.DEF], -1);
|
||||||
} else {
|
} else {
|
||||||
attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon);
|
attacker.trySetStatus(StatusEffect.PARALYSIS, pokemon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
@ -20,6 +20,7 @@ import { Trainer } from "#field/trainer";
|
|||||||
import type { ModifierTypeOption } from "#modifiers/modifier-type";
|
import type { ModifierTypeOption } from "#modifiers/modifier-type";
|
||||||
import { PokemonMove } from "#moves/pokemon-move";
|
import { PokemonMove } from "#moves/pokemon-move";
|
||||||
import type { DexAttrProps, GameData } from "#system/game-data";
|
import type { DexAttrProps, GameData } from "#system/game-data";
|
||||||
|
import { RibbonData, type RibbonFlag } from "#system/ribbons/ribbon-data";
|
||||||
import { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common";
|
import { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common";
|
||||||
import { deepCopy } from "#utils/data";
|
import { deepCopy } from "#utils/data";
|
||||||
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";
|
||||||
@ -42,6 +43,15 @@ export abstract class Challenge {
|
|||||||
|
|
||||||
public conditions: ChallengeCondition[];
|
public conditions: ChallengeCondition[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Ribbon awarded on challenge completion, or 0 if the challenge has no ribbon or is not enabled
|
||||||
|
*
|
||||||
|
* @defaultValue 0
|
||||||
|
*/
|
||||||
|
public get ribbonAwarded(): RibbonFlag {
|
||||||
|
return 0 as RibbonFlag;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param id {@link Challenges} The enum value for the challenge
|
* @param id {@link Challenges} The enum value for the challenge
|
||||||
*/
|
*/
|
||||||
@ -423,6 +433,12 @@ type ChallengeCondition = (data: GameData) => boolean;
|
|||||||
* Implements a mono generation challenge.
|
* Implements a mono generation challenge.
|
||||||
*/
|
*/
|
||||||
export class SingleGenerationChallenge extends Challenge {
|
export class SingleGenerationChallenge extends Challenge {
|
||||||
|
public override get ribbonAwarded(): RibbonFlag {
|
||||||
|
// NOTE: This logic will not work for the eventual mono gen 10 ribbon, as
|
||||||
|
// as its flag will not be in sequence with the other mono gen ribbons.
|
||||||
|
return this.value ? ((RibbonData.MONO_GEN_1 << (this.value - 1)) as RibbonFlag) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Challenges.SINGLE_GENERATION, 9);
|
super(Challenges.SINGLE_GENERATION, 9);
|
||||||
}
|
}
|
||||||
@ -686,6 +702,12 @@ interface monotypeOverride {
|
|||||||
* Implements a mono type challenge.
|
* Implements a mono type challenge.
|
||||||
*/
|
*/
|
||||||
export class SingleTypeChallenge extends Challenge {
|
export class SingleTypeChallenge extends Challenge {
|
||||||
|
public override get ribbonAwarded(): RibbonFlag {
|
||||||
|
// `this.value` represents the 1-based index of pokemon type
|
||||||
|
// `RibbonData.MONO_NORMAL` starts the flag position for the types,
|
||||||
|
// and we shift it by 1 for the specific type.
|
||||||
|
return this.value ? ((RibbonData.MONO_NORMAL << (this.value - 1)) as RibbonFlag) : 0;
|
||||||
|
}
|
||||||
private static TYPE_OVERRIDES: monotypeOverride[] = [
|
private static TYPE_OVERRIDES: monotypeOverride[] = [
|
||||||
{ species: SpeciesId.CASTFORM, type: PokemonType.NORMAL, fusion: false },
|
{ species: SpeciesId.CASTFORM, type: PokemonType.NORMAL, fusion: false },
|
||||||
];
|
];
|
||||||
@ -755,6 +777,9 @@ export class SingleTypeChallenge extends Challenge {
|
|||||||
* Implements a fresh start challenge.
|
* Implements a fresh start challenge.
|
||||||
*/
|
*/
|
||||||
export class FreshStartChallenge extends Challenge {
|
export class FreshStartChallenge extends Challenge {
|
||||||
|
public override get ribbonAwarded(): RibbonFlag {
|
||||||
|
return this.value ? RibbonData.FRESH_START : 0;
|
||||||
|
}
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Challenges.FRESH_START, 2);
|
super(Challenges.FRESH_START, 2);
|
||||||
}
|
}
|
||||||
@ -828,6 +853,9 @@ export class FreshStartChallenge extends Challenge {
|
|||||||
* Implements an inverse battle challenge.
|
* Implements an inverse battle challenge.
|
||||||
*/
|
*/
|
||||||
export class InverseBattleChallenge extends Challenge {
|
export class InverseBattleChallenge extends Challenge {
|
||||||
|
public override get ribbonAwarded(): RibbonFlag {
|
||||||
|
return this.value ? RibbonData.INVERSE : 0;
|
||||||
|
}
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Challenges.INVERSE_BATTLE, 1);
|
super(Challenges.INVERSE_BATTLE, 1);
|
||||||
}
|
}
|
||||||
@ -861,6 +889,9 @@ export class InverseBattleChallenge extends Challenge {
|
|||||||
* Implements a flip stat challenge.
|
* Implements a flip stat challenge.
|
||||||
*/
|
*/
|
||||||
export class FlipStatChallenge extends Challenge {
|
export class FlipStatChallenge extends Challenge {
|
||||||
|
public override get ribbonAwarded(): RibbonFlag {
|
||||||
|
return this.value ? RibbonData.FLIP_STATS : 0;
|
||||||
|
}
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Challenges.FLIP_STAT, 1);
|
super(Challenges.FLIP_STAT, 1);
|
||||||
}
|
}
|
||||||
@ -941,6 +972,9 @@ export class LowerStarterPointsChallenge extends Challenge {
|
|||||||
* Implements a No Support challenge
|
* Implements a No Support challenge
|
||||||
*/
|
*/
|
||||||
export class LimitedSupportChallenge extends Challenge {
|
export class LimitedSupportChallenge extends Challenge {
|
||||||
|
public override get ribbonAwarded(): RibbonFlag {
|
||||||
|
return this.value ? ((RibbonData.NO_HEAL << (this.value - 1)) as RibbonFlag) : 0;
|
||||||
|
}
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Challenges.LIMITED_SUPPORT, 3);
|
super(Challenges.LIMITED_SUPPORT, 3);
|
||||||
}
|
}
|
||||||
@ -973,6 +1007,9 @@ export class LimitedSupportChallenge extends Challenge {
|
|||||||
* Implements a Limited Catch challenge
|
* Implements a Limited Catch challenge
|
||||||
*/
|
*/
|
||||||
export class LimitedCatchChallenge extends Challenge {
|
export class LimitedCatchChallenge extends Challenge {
|
||||||
|
public override get ribbonAwarded(): RibbonFlag {
|
||||||
|
return this.value ? RibbonData.LIMITED_CATCH : 0;
|
||||||
|
}
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Challenges.LIMITED_CATCH, 1);
|
super(Challenges.LIMITED_CATCH, 1);
|
||||||
}
|
}
|
||||||
@ -997,6 +1034,9 @@ export class LimitedCatchChallenge extends Challenge {
|
|||||||
* Implements a Permanent Faint challenge
|
* Implements a Permanent Faint challenge
|
||||||
*/
|
*/
|
||||||
export class HardcoreChallenge extends Challenge {
|
export class HardcoreChallenge extends Challenge {
|
||||||
|
public override get ribbonAwarded(): RibbonFlag {
|
||||||
|
return this.value ? RibbonData.HARDCORE : 0;
|
||||||
|
}
|
||||||
constructor() {
|
constructor() {
|
||||||
super(Challenges.HARDCORE, 1);
|
super(Challenges.HARDCORE, 1);
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,7 @@ export class EggHatchData {
|
|||||||
caughtCount: currDexEntry.caughtCount,
|
caughtCount: currDexEntry.caughtCount,
|
||||||
hatchedCount: currDexEntry.hatchedCount,
|
hatchedCount: currDexEntry.hatchedCount,
|
||||||
ivs: [...currDexEntry.ivs],
|
ivs: [...currDexEntry.ivs],
|
||||||
|
ribbons: currDexEntry.ribbons,
|
||||||
};
|
};
|
||||||
this.starterDataEntryBeforeUpdate = {
|
this.starterDataEntryBeforeUpdate = {
|
||||||
moveset: currStarterDataEntry.moveset,
|
moveset: currStarterDataEntry.moveset,
|
||||||
|
@ -88,7 +88,7 @@ import type { AttackMoveResult } from "#types/attack-move-result";
|
|||||||
import type { Localizable } from "#types/locales";
|
import type { Localizable } from "#types/locales";
|
||||||
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
|
import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types";
|
||||||
import type { TurnMove } from "#types/turn-move";
|
import type { TurnMove } from "#types/turn-move";
|
||||||
import { BooleanHolder, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
import { BooleanHolder, coerceArray, type Constructor, isNullOrUndefined, NumberHolder, randSeedFloat, randSeedInt, randSeedItem, toDmgValue } from "#utils/common";
|
||||||
import { getEnumValues } from "#utils/enums";
|
import { getEnumValues } from "#utils/enums";
|
||||||
import { toCamelCase, toTitleCase } from "#utils/strings";
|
import { toCamelCase, toTitleCase } from "#utils/strings";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
@ -1190,8 +1190,9 @@ export abstract class MoveAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @virtual
|
* Return this `MoveAttr`'s associated {@linkcode MoveCondition} or {@linkcode MoveConditionFunc}.
|
||||||
* @returns the {@linkcode MoveCondition} or {@linkcode MoveConditionFunc} for this {@linkcode Move}
|
* The specified condition will be added to all {@linkcode Move}s with this attribute,
|
||||||
|
* and moves **will fail upon use** if _at least 1_ of their attached conditions returns `false`.
|
||||||
*/
|
*/
|
||||||
getCondition(): MoveCondition | MoveConditionFunc | null {
|
getCondition(): MoveCondition | MoveConditionFunc | null {
|
||||||
return null;
|
return null;
|
||||||
@ -1304,15 +1305,21 @@ export class MoveEffectAttr extends MoveAttr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether the {@linkcode Move}'s effects are valid to {@linkcode apply}
|
* Determine whether this {@linkcode MoveAttr}'s effects are able to {@linkcode apply | be applied} to the target.
|
||||||
* @virtual
|
*
|
||||||
* @param user {@linkcode Pokemon} using the move
|
* Will **NOT** cause the move to fail upon returning `false` (unlike {@linkcode getCondition};
|
||||||
* @param target {@linkcode Pokemon} target of the move
|
* merely that the effect for this attribute will be nullified.
|
||||||
* @param move {@linkcode Move} with this attribute
|
* @param user - The {@linkcode Pokemon} using the move
|
||||||
* @param args Set of unique arguments needed by this attribute
|
* @param target - The {@linkcode Pokemon} being targeted by the move, or {@linkcode user} if the move is
|
||||||
* @returns true if basic application of the ability attribute should be possible
|
* {@linkcode selfTarget | self-targeting}
|
||||||
|
* @param move - The {@linkcode Move} being used
|
||||||
|
* @param _args - Set of unique arguments needed by this attribute
|
||||||
|
* @returns `true` if basic application of this `MoveAttr`s effects should be possible
|
||||||
*/
|
*/
|
||||||
canApply(user: Pokemon, target: Pokemon, move: Move, args?: any[]) {
|
// TODO: Decouple this check from the `apply` step
|
||||||
|
// TODO: Make non-damaging moves fail by default if none of their attributes can apply
|
||||||
|
canApply(user: Pokemon, target: Pokemon, move: Move, _args?: any[]) {
|
||||||
|
// TODO: These checks seem redundant
|
||||||
return !! (this.selfTarget ? user.hp && !user.getTag(BattlerTagType.FRENZY) : target.hp)
|
return !! (this.selfTarget ? user.hp && !user.getTag(BattlerTagType.FRENZY) : target.hp)
|
||||||
&& (this.selfTarget || !target.getTag(BattlerTagType.PROTECTED) ||
|
&& (this.selfTarget || !target.getTag(BattlerTagType.PROTECTED) ||
|
||||||
move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target }));
|
move.doesFlagEffectApply({ flag: MoveFlags.IGNORE_PROTECT, user, target }));
|
||||||
@ -1961,19 +1968,17 @@ export class AddSubstituteAttr extends MoveEffectAttr {
|
|||||||
* @see {@linkcode apply}
|
* @see {@linkcode apply}
|
||||||
*/
|
*/
|
||||||
export class HealAttr extends MoveEffectAttr {
|
export class HealAttr extends MoveEffectAttr {
|
||||||
/** The percentage of {@linkcode Stat.HP} to heal */
|
constructor(
|
||||||
private healRatio: number;
|
/** The percentage of {@linkcode Stat.HP} to heal. */
|
||||||
/** Should an animation be shown? */
|
private healRatio: number,
|
||||||
private showAnim: boolean;
|
/** Whether to display a healing animation when healing the target; default `false` */
|
||||||
|
private showAnim = false,
|
||||||
constructor(healRatio?: number, showAnim?: boolean, selfTarget?: boolean) {
|
selfTarget = true
|
||||||
super(selfTarget === undefined || selfTarget);
|
) {
|
||||||
|
super(selfTarget);
|
||||||
this.healRatio = healRatio || 1;
|
|
||||||
this.showAnim = !!showAnim;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||||
this.addHealPhase(this.selfTarget ? user : target, this.healRatio);
|
this.addHealPhase(this.selfTarget ? user : target, this.healRatio);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -1982,15 +1987,65 @@ export class HealAttr extends MoveEffectAttr {
|
|||||||
* Creates a new {@linkcode PokemonHealPhase}.
|
* Creates a new {@linkcode PokemonHealPhase}.
|
||||||
* This heals the target and shows the appropriate message.
|
* This heals the target and shows the appropriate message.
|
||||||
*/
|
*/
|
||||||
addHealPhase(target: Pokemon, healRatio: number) {
|
protected addHealPhase(target: Pokemon, healRatio: number) {
|
||||||
globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(),
|
globalScene.phaseManager.unshiftNew("PokemonHealPhase", target.getBattlerIndex(),
|
||||||
toDmgValue(target.getMaxHp() * healRatio), i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(target) }), true, !this.showAnim);
|
toDmgValue(target.getMaxHp() * healRatio), i18next.t("moveTriggers:healHp", { pokemonName: getPokemonNameWithAffix(target) }), true, !this.showAnim);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
override getTargetBenefitScore(user: Pokemon, target: Pokemon, _move: Move): number {
|
||||||
const score = ((1 - (this.selfTarget ? user : target).getHpRatio()) * 20) - this.healRatio * 10;
|
const score = ((1 - (this.selfTarget ? user : target).getHpRatio()) * 20) - this.healRatio * 10;
|
||||||
return Math.round(score / (1 - this.healRatio / 2));
|
return Math.round(score / (1 - this.healRatio / 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Change to fail move
|
||||||
|
override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean {
|
||||||
|
if (!super.canApply(user, target, _move, _args)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const healedPokemon = this.selfTarget ? user : target;
|
||||||
|
if (healedPokemon.isFullHp()) {
|
||||||
|
globalScene.phaseManager.queueMessage(i18next.t("battle:hpIsFull", {
|
||||||
|
pokemonName: getPokemonNameWithAffix(healedPokemon),
|
||||||
|
}))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute to put the user to sleep for a fixed duration, fully heal them and cure their status.
|
||||||
|
* Used for {@linkcode MoveId.REST}.
|
||||||
|
*/
|
||||||
|
export class RestAttr extends HealAttr {
|
||||||
|
private duration: number;
|
||||||
|
|
||||||
|
constructor(duration: number) {
|
||||||
|
super(1, true);
|
||||||
|
this.duration = duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
|
const wasSet = user.trySetStatus(StatusEffect.SLEEP, user, this.duration, null, true, true,
|
||||||
|
i18next.t("moveTriggers:restBecameHealthy", {
|
||||||
|
pokemonName: getPokemonNameWithAffix(user),
|
||||||
|
}));
|
||||||
|
return wasSet && super.apply(user, target, move, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
override addHealPhase(user: Pokemon): void {
|
||||||
|
globalScene.phaseManager.unshiftNew("PokemonHealPhase", user.getBattlerIndex(), user.getMaxHp(), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: change after HealAttr is changed to fail move
|
||||||
|
override getCondition(): MoveConditionFunc {
|
||||||
|
return (user, target, move) =>
|
||||||
|
super.canApply(user, target, move, [])
|
||||||
|
// Intentionally suppress messages here as we display generic fail msg
|
||||||
|
// TODO: This might have order-of-operation jank
|
||||||
|
&& user.canSetStatus(StatusEffect.SLEEP, true, true, user)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -2262,20 +2317,9 @@ export class BoostHealAttr extends HealAttr {
|
|||||||
* @see {@linkcode apply}
|
* @see {@linkcode apply}
|
||||||
*/
|
*/
|
||||||
export class HealOnAllyAttr extends HealAttr {
|
export class HealOnAllyAttr extends HealAttr {
|
||||||
/**
|
override canApply(user: Pokemon, target: Pokemon, _move: Move, _args?: any[]): boolean {
|
||||||
* @param user {@linkcode Pokemon} using the move
|
// Don't trigger if not targeting an ally
|
||||||
* @param target {@linkcode Pokemon} target of the move
|
return target === user.getAlly() && super.canApply(user, target, _move, _args);
|
||||||
* @param move {@linkcode Move} with this attribute
|
|
||||||
* @param args N/A
|
|
||||||
* @returns true if the function succeeds
|
|
||||||
*/
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
|
||||||
if (user.getAlly() === target) {
|
|
||||||
super.apply(user, target, move, args);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2286,6 +2330,7 @@ export class HealOnAllyAttr extends HealAttr {
|
|||||||
* @see {@linkcode apply}
|
* @see {@linkcode apply}
|
||||||
* @see {@linkcode getUserBenefitScore}
|
* @see {@linkcode getUserBenefitScore}
|
||||||
*/
|
*/
|
||||||
|
// TODO: Make Strength Sap its own attribute that extends off of this one
|
||||||
export class HitHealAttr extends MoveEffectAttr {
|
export class HitHealAttr extends MoveEffectAttr {
|
||||||
private healRatio: number;
|
private healRatio: number;
|
||||||
private healStat: EffectiveStat | null;
|
private healStat: EffectiveStat | null;
|
||||||
@ -2536,49 +2581,50 @@ export class WaterShurikenMultiHitTypeAttr extends ChangeMultiHitTypeAttr {
|
|||||||
|
|
||||||
export class StatusEffectAttr extends MoveEffectAttr {
|
export class StatusEffectAttr extends MoveEffectAttr {
|
||||||
public effect: StatusEffect;
|
public effect: StatusEffect;
|
||||||
public turnsRemaining?: number;
|
|
||||||
public overrideStatus: boolean = false;
|
|
||||||
|
|
||||||
constructor(effect: StatusEffect, selfTarget?: boolean, turnsRemaining?: number, overrideStatus: boolean = false) {
|
constructor(effect: StatusEffect, selfTarget = false) {
|
||||||
super(selfTarget);
|
super(selfTarget);
|
||||||
|
|
||||||
this.effect = effect;
|
this.effect = effect;
|
||||||
this.turnsRemaining = turnsRemaining;
|
|
||||||
this.overrideStatus = overrideStatus;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, true);
|
||||||
const statusCheck = moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance;
|
const statusCheck = moveChance < 0 || moveChance === 100 || user.randBattleSeedInt(100) < moveChance;
|
||||||
const quiet = move.category !== MoveCategory.STATUS;
|
if (!statusCheck) {
|
||||||
if (statusCheck) {
|
|
||||||
const pokemon = this.selfTarget ? user : target;
|
|
||||||
if (user !== target && move.category === MoveCategory.STATUS && !target.canSetStatus(this.effect, quiet, false, user, true)) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (((!pokemon.status || this.overrideStatus) || (pokemon.status.effect === this.effect && moveChance < 0))
|
|
||||||
&& pokemon.trySetStatus(this.effect, true, user, this.turnsRemaining, null, this.overrideStatus, quiet)) {
|
// non-status moves don't play sound effects for failures
|
||||||
|
const quiet = move.category !== MoveCategory.STATUS;
|
||||||
|
|
||||||
|
if (
|
||||||
|
target.trySetStatus(this.effect, user, undefined, null, false, quiet)
|
||||||
|
) {
|
||||||
applyAbAttrs("ConfusionOnStatusEffectAbAttr", {pokemon: user, opponent: target, move, effect: this.effect});
|
applyAbAttrs("ConfusionOnStatusEffectAbAttr", {pokemon: user, opponent: target, move, effect: this.effect});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||||
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, false);
|
const moveChance = this.getMoveChance(user, target, move, this.selfTarget, false);
|
||||||
const score = (moveChance < 0) ? -10 : Math.floor(moveChance * -0.1);
|
const score = moveChance < 0 ? -10 : Math.floor(moveChance * -0.1);
|
||||||
const pokemon = this.selfTarget ? user : target;
|
const pokemon = this.selfTarget ? user : target;
|
||||||
|
|
||||||
return !pokemon.status && pokemon.canSetStatus(this.effect, true, false, user) ? score : 0;
|
return pokemon.canSetStatus(this.effect, true, false, user) ? score : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attribute to randomly apply one of several statuses to the target.
|
||||||
|
* Used for {@linkcode Moves.TRI_ATTACK} and {@linkcode Moves.DIRE_CLAW}.
|
||||||
|
*/
|
||||||
export class MultiStatusEffectAttr extends StatusEffectAttr {
|
export class MultiStatusEffectAttr extends StatusEffectAttr {
|
||||||
public effects: StatusEffect[];
|
public effects: StatusEffect[];
|
||||||
|
|
||||||
constructor(effects: StatusEffect[], selfTarget?: boolean, turnsRemaining?: number, overrideStatus?: boolean) {
|
constructor(effects: StatusEffect[], selfTarget?: boolean) {
|
||||||
super(effects[0], selfTarget, turnsRemaining, overrideStatus);
|
super(effects[0], selfTarget);
|
||||||
this.effects = effects;
|
this.effects = effects;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2611,26 +2657,41 @@ export class PsychoShiftEffectAttr extends MoveEffectAttr {
|
|||||||
* @returns - Whether the effect was successfully applied to the target.
|
* @returns - Whether the effect was successfully applied to the target.
|
||||||
*/
|
*/
|
||||||
apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean {
|
||||||
const statusToApply: StatusEffect | undefined = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined);
|
const statusToApply = user.status?.effect ??
|
||||||
|
(user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : StatusEffect.NONE);
|
||||||
|
|
||||||
if (target.status || !statusToApply) {
|
// Bang is justified as condition func returns early if no status is found
|
||||||
|
if (!target.trySetStatus(statusToApply, user)) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
|
||||||
const canSetStatus = target.canSetStatus(statusToApply, true, false, user);
|
|
||||||
const trySetStatus = canSetStatus ? target.trySetStatus(statusToApply, true, user) : false;
|
|
||||||
|
|
||||||
if (trySetStatus && user.status) {
|
|
||||||
// PsychoShiftTag is added to the user if move succeeds so that the user is healed of its status effect after its move
|
|
||||||
user.addTag(BattlerTagType.PSYCHO_SHIFT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return trySetStatus;
|
if (user.status) {
|
||||||
|
// Add tag to user to heal its status effect after the move ends (unless we have comatose);
|
||||||
|
// occurs after move use to ensure correct Synchronize timing
|
||||||
|
user.addTag(BattlerTagType.PSYCHO_SHIFT)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
getCondition(): MoveConditionFunc {
|
||||||
|
return (user, target) => {
|
||||||
|
if (target.status?.effect) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusToApply = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : StatusEffect.NONE);
|
||||||
|
return !!statusToApply && target.canSetStatus(statusToApply, false, false, user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
getTargetBenefitScore(user: Pokemon, target: Pokemon, move: Move): number {
|
||||||
const statusToApply = user.status?.effect ?? (user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined);
|
const statusToApply =
|
||||||
return !target.status && statusToApply && target.canSetStatus(statusToApply, true, false, user) ? -10 : 0;
|
user.status?.effect ??
|
||||||
|
(user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : StatusEffect.NONE);
|
||||||
|
|
||||||
|
// TODO: Give this a positive user benefit score
|
||||||
|
return !target.status?.effect && statusToApply && target.canSetStatus(statusToApply, true, false, user) ? -10 : 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2690,7 +2751,7 @@ export class StealHeldItemChanceAttr extends MoveEffectAttr {
|
|||||||
* Used for Incinerate and Knock Off.
|
* Used for Incinerate and Knock Off.
|
||||||
* Not Implemented Cases: (Same applies for Thief)
|
* Not Implemented Cases: (Same applies for Thief)
|
||||||
* "If the user faints due to the target's Ability (Rough Skin or Iron Barbs) or held Rocky Helmet, it cannot remove the target's held item."
|
* "If the user faints due to the target's Ability (Rough Skin or Iron Barbs) or held Rocky Helmet, it cannot remove the target's held item."
|
||||||
* "If the Pokémon is knocked out by the attack, Sticky Hold does not protect the held item.""
|
* "If the Pokémon is knocked out by the attack, Sticky Hold does not protect the held item."
|
||||||
*/
|
*/
|
||||||
export class RemoveHeldItemAttr extends MoveEffectAttr {
|
export class RemoveHeldItemAttr extends MoveEffectAttr {
|
||||||
|
|
||||||
@ -2900,7 +2961,7 @@ export class HealStatusEffectAttr extends MoveEffectAttr {
|
|||||||
*/
|
*/
|
||||||
constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) {
|
constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) {
|
||||||
super(selfTarget, { lastHitOnly: true });
|
super(selfTarget, { lastHitOnly: true });
|
||||||
this.effects = [ effects ].flat(1);
|
this.effects = coerceArray(effects)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -4427,6 +4488,10 @@ export class SpitUpPowerAttr extends VariablePowerAttr {
|
|||||||
* Does NOT remove stockpiled stacks.
|
* Does NOT remove stockpiled stacks.
|
||||||
*/
|
*/
|
||||||
export class SwallowHealAttr extends HealAttr {
|
export class SwallowHealAttr extends HealAttr {
|
||||||
|
constructor() {
|
||||||
|
super(1)
|
||||||
|
}
|
||||||
|
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
const stockpilingTag = user.getTag(StockpilingTag);
|
const stockpilingTag = user.getTag(StockpilingTag);
|
||||||
|
|
||||||
@ -7909,7 +7974,7 @@ export class StatusIfBoostedAttr extends MoveEffectAttr {
|
|||||||
*/
|
*/
|
||||||
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean {
|
||||||
if (target.turnData.statStagesIncreased) {
|
if (target.turnData.statStagesIncreased) {
|
||||||
target.trySetStatus(this.effect, true, user);
|
target.trySetStatus(this.effect, user);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -8056,11 +8121,11 @@ const failIfDampCondition: MoveConditionFunc = (user, target, move) => {
|
|||||||
return !cancelled.value;
|
return !cancelled.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
const userSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => user.status?.effect === StatusEffect.SLEEP || user.hasAbility(AbilityId.COMATOSE);
|
const userSleptOrComatoseCondition: MoveConditionFunc = (user) => user.status?.effect === StatusEffect.SLEEP || user.hasAbility(AbilityId.COMATOSE);
|
||||||
|
|
||||||
const targetSleptOrComatoseCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE);
|
const targetSleptOrComatoseCondition: MoveConditionFunc = (_user: Pokemon, target: Pokemon, _move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE);
|
||||||
|
|
||||||
const failIfLastCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => globalScene.phaseManager.phaseQueue.find(phase => phase.is("MovePhase")) !== undefined;
|
const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.findPhase(phase => phase.is("MovePhase")) !== undefined;
|
||||||
|
|
||||||
const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => {
|
||||||
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty();
|
||||||
@ -8938,9 +9003,7 @@ export function initMoves() {
|
|||||||
.attr(MultiHitAttr, MultiHitType._2)
|
.attr(MultiHitAttr, MultiHitType._2)
|
||||||
.makesContact(false),
|
.makesContact(false),
|
||||||
new SelfStatusMove(MoveId.REST, PokemonType.PSYCHIC, -1, 5, -1, 0, 1)
|
new SelfStatusMove(MoveId.REST, PokemonType.PSYCHIC, -1, 5, -1, 0, 1)
|
||||||
.attr(StatusEffectAttr, StatusEffect.SLEEP, true, 3, true)
|
.attr(RestAttr, 3)
|
||||||
.attr(HealAttr, 1, true)
|
|
||||||
.condition((user, target, move) => !user.isFullHp() && user.canSetStatus(StatusEffect.SLEEP, true, true, user))
|
|
||||||
.triageMove(),
|
.triageMove(),
|
||||||
new AttackMove(MoveId.ROCK_SLIDE, PokemonType.ROCK, MoveCategory.PHYSICAL, 75, 90, 10, 30, 0, 1)
|
new AttackMove(MoveId.ROCK_SLIDE, PokemonType.ROCK, MoveCategory.PHYSICAL, 75, 90, 10, 30, 0, 1)
|
||||||
.attr(FlinchAttr)
|
.attr(FlinchAttr)
|
||||||
@ -9286,14 +9349,16 @@ export function initMoves() {
|
|||||||
.condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3)
|
.condition(user => (user.getTag(StockpilingTag)?.stockpiledCount ?? 0) < 3)
|
||||||
.attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true),
|
.attr(AddBattlerTagAttr, BattlerTagType.STOCKPILING, true),
|
||||||
new AttackMove(MoveId.SPIT_UP, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 3)
|
new AttackMove(MoveId.SPIT_UP, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, 100, 10, -1, 0, 3)
|
||||||
.condition(hasStockpileStacksCondition)
|
|
||||||
.attr(SpitUpPowerAttr, 100)
|
.attr(SpitUpPowerAttr, 100)
|
||||||
|
.condition(hasStockpileStacksCondition)
|
||||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true),
|
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true),
|
||||||
new SelfStatusMove(MoveId.SWALLOW, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
new SelfStatusMove(MoveId.SWALLOW, PokemonType.NORMAL, -1, 10, -1, 0, 3)
|
||||||
.condition(hasStockpileStacksCondition)
|
|
||||||
.attr(SwallowHealAttr)
|
.attr(SwallowHealAttr)
|
||||||
|
.condition(hasStockpileStacksCondition)
|
||||||
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true)
|
.attr(RemoveBattlerTagAttr, [ BattlerTagType.STOCKPILING ], true)
|
||||||
.triageMove(),
|
.triageMove()
|
||||||
|
// TODO: Verify if using Swallow at full HP still consumes stacks or not
|
||||||
|
.edgeCase(),
|
||||||
new AttackMove(MoveId.HEAT_WAVE, PokemonType.FIRE, MoveCategory.SPECIAL, 95, 90, 10, 10, 0, 3)
|
new AttackMove(MoveId.HEAT_WAVE, PokemonType.FIRE, MoveCategory.SPECIAL, 95, 90, 10, 10, 0, 3)
|
||||||
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
.attr(HealStatusEffectAttr, true, StatusEffect.FREEZE)
|
||||||
.attr(StatusEffectAttr, StatusEffect.BURN)
|
.attr(StatusEffectAttr, StatusEffect.BURN)
|
||||||
@ -9679,14 +9744,8 @@ export function initMoves() {
|
|||||||
.unimplemented(),
|
.unimplemented(),
|
||||||
new StatusMove(MoveId.PSYCHO_SHIFT, PokemonType.PSYCHIC, 100, 10, -1, 0, 4)
|
new StatusMove(MoveId.PSYCHO_SHIFT, PokemonType.PSYCHIC, 100, 10, -1, 0, 4)
|
||||||
.attr(PsychoShiftEffectAttr)
|
.attr(PsychoShiftEffectAttr)
|
||||||
.condition((user, target, move) => {
|
// TODO: Verify status applied if a statused pokemon obtains Comatose (via Transform) and uses Psycho Shift
|
||||||
let statusToApply = user.hasAbility(AbilityId.COMATOSE) ? StatusEffect.SLEEP : undefined;
|
.edgeCase(),
|
||||||
if (user.status?.effect && isNonVolatileStatusEffect(user.status.effect)) {
|
|
||||||
statusToApply = user.status.effect;
|
|
||||||
}
|
|
||||||
return !!statusToApply && target.canSetStatus(statusToApply, false, false, user);
|
|
||||||
}
|
|
||||||
),
|
|
||||||
new AttackMove(MoveId.TRUMP_CARD, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, -1, 5, -1, 0, 4)
|
new AttackMove(MoveId.TRUMP_CARD, PokemonType.NORMAL, MoveCategory.SPECIAL, -1, -1, 5, -1, 0, 4)
|
||||||
.makesContact()
|
.makesContact()
|
||||||
.attr(LessPPMorePowerAttr),
|
.attr(LessPPMorePowerAttr),
|
||||||
|
@ -11,7 +11,7 @@ import { BooleanHolder, toDmgValue } from "#utils/common";
|
|||||||
* These are the moves assigned to a {@linkcode Pokemon} object.
|
* These are the moves assigned to a {@linkcode Pokemon} object.
|
||||||
* It links to {@linkcode Move} class via the move ID.
|
* It links to {@linkcode Move} class via the move ID.
|
||||||
* Compared to {@linkcode Move}, this class also tracks things like
|
* Compared to {@linkcode Move}, this class also tracks things like
|
||||||
* PP Ups recieved, PP used, etc.
|
* PP Ups received, PP used, etc.
|
||||||
* @see {@linkcode isUsable} - checks if move is restricted, out of PP, or not implemented.
|
* @see {@linkcode isUsable} - checks if move is restricted, out of PP, or not implemented.
|
||||||
* @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID.
|
* @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID.
|
||||||
* @see {@linkcode usePp} - removes a point of PP from the move.
|
* @see {@linkcode usePp} - removes a point of PP from the move.
|
||||||
|
@ -243,8 +243,9 @@ export const FieryFalloutEncounter: MysteryEncounter = MysteryEncounterBuilder.w
|
|||||||
if (burnable?.length > 0) {
|
if (burnable?.length > 0) {
|
||||||
const roll = randSeedInt(burnable.length);
|
const roll = randSeedInt(burnable.length);
|
||||||
const chosenPokemon = burnable[roll];
|
const chosenPokemon = burnable[roll];
|
||||||
if (chosenPokemon.trySetStatus(StatusEffect.BURN)) {
|
if (chosenPokemon.canSetStatus(StatusEffect.BURN, true)) {
|
||||||
// Burn applied
|
// Burn applied
|
||||||
|
chosenPokemon.doSetStatus(StatusEffect.BURN);
|
||||||
encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender());
|
encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender());
|
||||||
encounter.setDialogueToken("abilityName", allAbilities[AbilityId.HEATPROOF].name);
|
encounter.setDialogueToken("abilityName", allAbilities[AbilityId.HEATPROOF].name);
|
||||||
queueEncounterMessage(`${namespace}:option.2.target_burned`);
|
queueEncounterMessage(`${namespace}:option.2.target_burned`);
|
||||||
|
@ -309,7 +309,7 @@ export function getRandomSpeciesByStarterCost(
|
|||||||
*/
|
*/
|
||||||
export function koPlayerPokemon(pokemon: PlayerPokemon) {
|
export function koPlayerPokemon(pokemon: PlayerPokemon) {
|
||||||
pokemon.hp = 0;
|
pokemon.hp = 0;
|
||||||
pokemon.trySetStatus(StatusEffect.FAINT);
|
pokemon.doSetStatus(StatusEffect.FAINT);
|
||||||
pokemon.updateInfo();
|
pokemon.updateInfo();
|
||||||
queueEncounterMessage(
|
queueEncounterMessage(
|
||||||
i18next.t("battle:fainted", {
|
i18next.t("battle:fainted", {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
/** Enum representing all non-volatile status effects. */
|
||||||
|
// TODO: Remove StatusEffect.FAINT
|
||||||
export enum StatusEffect {
|
export enum StatusEffect {
|
||||||
NONE,
|
NONE,
|
||||||
POISON,
|
POISON,
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { Ability, PreAttackModifyDamageAbAttrParams } from "#abilities/ability";
|
import type { Ability, PreAttackModifyDamageAbAttrParams } from "#abilities/ability";
|
||||||
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
|
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
|
||||||
import type { AnySound, BattleScene } from "#app/battle-scene";
|
import type { AnySound, BattleScene } from "#app/battle-scene";
|
||||||
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
|
import { PLAYER_PARTY_MAX_SIZE, RARE_CANDY_FRIENDSHIP_CAP } from "#app/constants";
|
||||||
import { timedEventManager } from "#app/global-event-manager";
|
import { timedEventManager } from "#app/global-event-manager";
|
||||||
import { globalScene } from "#app/global-scene";
|
import { globalScene } from "#app/global-scene";
|
||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
@ -139,6 +139,8 @@ import { populateVariantColors, variantColorCache, variantData } from "#sprites/
|
|||||||
import { achvs } from "#system/achv";
|
import { achvs } from "#system/achv";
|
||||||
import type { StarterDataEntry, StarterMoveset } from "#system/game-data";
|
import type { StarterDataEntry, StarterMoveset } from "#system/game-data";
|
||||||
import type { PokemonData } from "#system/pokemon-data";
|
import type { PokemonData } from "#system/pokemon-data";
|
||||||
|
import { RibbonData } from "#system/ribbons/ribbon-data";
|
||||||
|
import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods";
|
||||||
import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#types/ability-types";
|
import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#types/ability-types";
|
||||||
import type { DamageCalculationResult, DamageResult } from "#types/damage-result";
|
import type { DamageCalculationResult, DamageResult } from "#types/damage-result";
|
||||||
import type { IllusionData } from "#types/illusion-data";
|
import type { IllusionData } from "#types/illusion-data";
|
||||||
@ -235,6 +237,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
public ivs: number[];
|
public ivs: number[];
|
||||||
public nature: Nature;
|
public nature: Nature;
|
||||||
public moveset: PokemonMove[];
|
public moveset: PokemonMove[];
|
||||||
|
/**
|
||||||
|
* This Pokemon's current {@link https://m.bulbapedia.bulbagarden.net/wiki/Status_condition#Non-volatile_status | non-volatile status condition},
|
||||||
|
* or `null` if none exist.
|
||||||
|
* @todo Make private
|
||||||
|
*/
|
||||||
public status: Status | null;
|
public status: Status | null;
|
||||||
public friendship: number;
|
public friendship: number;
|
||||||
public metLevel: number;
|
public metLevel: number;
|
||||||
@ -1825,7 +1832,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
// Overrides moveset based on arrays specified in overrides.ts
|
// Overrides moveset based on arrays specified in overrides.ts
|
||||||
let overrideArray: MoveId | Array<MoveId> = this.isPlayer()
|
let overrideArray: MoveId | Array<MoveId> = this.isPlayer()
|
||||||
? Overrides.MOVESET_OVERRIDE
|
? Overrides.MOVESET_OVERRIDE
|
||||||
: Overrides.OPP_MOVESET_OVERRIDE;
|
: Overrides.ENEMY_MOVESET_OVERRIDE;
|
||||||
overrideArray = coerceArray(overrideArray);
|
overrideArray = coerceArray(overrideArray);
|
||||||
if (overrideArray.length > 0) {
|
if (overrideArray.length > 0) {
|
||||||
if (!this.isPlayer()) {
|
if (!this.isPlayer()) {
|
||||||
@ -2030,8 +2037,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
if (Overrides.ABILITY_OVERRIDE && this.isPlayer()) {
|
if (Overrides.ABILITY_OVERRIDE && this.isPlayer()) {
|
||||||
return allAbilities[Overrides.ABILITY_OVERRIDE];
|
return allAbilities[Overrides.ABILITY_OVERRIDE];
|
||||||
}
|
}
|
||||||
if (Overrides.OPP_ABILITY_OVERRIDE && this.isEnemy()) {
|
if (Overrides.ENEMY_ABILITY_OVERRIDE && this.isEnemy()) {
|
||||||
return allAbilities[Overrides.OPP_ABILITY_OVERRIDE];
|
return allAbilities[Overrides.ENEMY_ABILITY_OVERRIDE];
|
||||||
}
|
}
|
||||||
if (this.isFusion()) {
|
if (this.isFusion()) {
|
||||||
if (!isNullOrUndefined(this.fusionCustomPokemonData?.ability) && this.fusionCustomPokemonData.ability !== -1) {
|
if (!isNullOrUndefined(this.fusionCustomPokemonData?.ability) && this.fusionCustomPokemonData.ability !== -1) {
|
||||||
@ -2060,8 +2067,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
if (Overrides.PASSIVE_ABILITY_OVERRIDE && this.isPlayer()) {
|
if (Overrides.PASSIVE_ABILITY_OVERRIDE && this.isPlayer()) {
|
||||||
return allAbilities[Overrides.PASSIVE_ABILITY_OVERRIDE];
|
return allAbilities[Overrides.PASSIVE_ABILITY_OVERRIDE];
|
||||||
}
|
}
|
||||||
if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && this.isEnemy()) {
|
if (Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE && this.isEnemy()) {
|
||||||
return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE];
|
return allAbilities[Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE];
|
||||||
}
|
}
|
||||||
if (!isNullOrUndefined(this.customPokemonData.passive) && this.customPokemonData.passive !== -1) {
|
if (!isNullOrUndefined(this.customPokemonData.passive) && this.customPokemonData.passive !== -1) {
|
||||||
return allAbilities[this.customPokemonData.passive];
|
return allAbilities[this.customPokemonData.passive];
|
||||||
@ -2128,14 +2135,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
// returns override if valid for current case
|
// returns override if valid for current case
|
||||||
if (
|
if (
|
||||||
(Overrides.HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isPlayer()) ||
|
(Overrides.HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isPlayer()) ||
|
||||||
(Overrides.OPP_HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isEnemy())
|
(Overrides.ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE === false && this.isEnemy())
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
((Overrides.PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.HAS_PASSIVE_ABILITY_OVERRIDE) &&
|
((Overrides.PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.HAS_PASSIVE_ABILITY_OVERRIDE) &&
|
||||||
this.isPlayer()) ||
|
this.isPlayer()) ||
|
||||||
((Overrides.OPP_PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.OPP_HAS_PASSIVE_ABILITY_OVERRIDE) &&
|
((Overrides.ENEMY_PASSIVE_ABILITY_OVERRIDE !== AbilityId.NONE || Overrides.ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE) &&
|
||||||
this.isEnemy())
|
this.isEnemy())
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
@ -3001,8 +3008,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
if (forStarter && this.isPlayer() && Overrides.STARTER_FUSION_SPECIES_OVERRIDE) {
|
if (forStarter && this.isPlayer() && Overrides.STARTER_FUSION_SPECIES_OVERRIDE) {
|
||||||
fusionOverride = getPokemonSpecies(Overrides.STARTER_FUSION_SPECIES_OVERRIDE);
|
fusionOverride = getPokemonSpecies(Overrides.STARTER_FUSION_SPECIES_OVERRIDE);
|
||||||
} else if (this.isEnemy() && Overrides.OPP_FUSION_SPECIES_OVERRIDE) {
|
} else if (this.isEnemy() && Overrides.ENEMY_FUSION_SPECIES_OVERRIDE) {
|
||||||
fusionOverride = getPokemonSpecies(Overrides.OPP_FUSION_SPECIES_OVERRIDE);
|
fusionOverride = getPokemonSpecies(Overrides.ENEMY_FUSION_SPECIES_OVERRIDE);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.fusionSpecies =
|
this.fusionSpecies =
|
||||||
@ -4744,7 +4751,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
* @param reason - The reason for the status application failure -
|
* @param reason - The reason for the status application failure -
|
||||||
* can be "overlap" (already has same status), "other" (generic fail message)
|
* can be "overlap" (already has same status), "other" (generic fail message)
|
||||||
* or a {@linkcode TerrainType} for terrain-based blockages.
|
* or a {@linkcode TerrainType} for terrain-based blockages.
|
||||||
* Defaults to "other".
|
* Default `"other"`
|
||||||
*/
|
*/
|
||||||
queueStatusImmuneMessage(
|
queueStatusImmuneMessage(
|
||||||
quiet: boolean,
|
quiet: boolean,
|
||||||
@ -4773,15 +4780,20 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a status effect can be applied to the Pokemon.
|
* Check if a status effect can be applied to this {@linkcode Pokemon}.
|
||||||
*
|
*
|
||||||
* @param effect The {@linkcode StatusEffect} whose applicability is being checked
|
* @param effect - The {@linkcode StatusEffect} whose applicability is being checked
|
||||||
* @param quiet Whether in-battle messages should trigger or not
|
* @param quiet - Whether to suppress in-battle messages for status checks; default `false`
|
||||||
* @param overrideStatus Whether the Pokemon's current status can be overriden
|
* @param overrideStatus - Whether to allow overriding the Pokemon's current status with a different one; default `false`
|
||||||
* @param sourcePokemon The Pokemon that is setting the status effect
|
* @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target,
|
||||||
* @param ignoreField Whether any field effects (weather, terrain, etc.) should be considered
|
* or `null` if the status is applied from a non-Pokemon source (hazards, etc.); default `null`
|
||||||
|
* @param ignoreField - Whether to ignore field effects (weather, terrain, etc.) preventing status application;
|
||||||
|
* default `false`
|
||||||
|
* @returns Whether {@linkcode effect} can be applied to this Pokemon.
|
||||||
*/
|
*/
|
||||||
canSetStatus(
|
// TODO: Review and verify the message order precedence in mainline if multiple status-blocking effects are present at once
|
||||||
|
// TODO: Make argument order consistent with `trySetStatus`
|
||||||
|
public canSetStatus(
|
||||||
effect: StatusEffect,
|
effect: StatusEffect,
|
||||||
quiet = false,
|
quiet = false,
|
||||||
overrideStatus = false,
|
overrideStatus = false,
|
||||||
@ -4789,6 +4801,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
ignoreField = false,
|
ignoreField = false,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (effect !== StatusEffect.FAINT) {
|
if (effect !== StatusEffect.FAINT) {
|
||||||
|
// Status-overriding moves (i.e. Rest) fail if their respective status already exists;
|
||||||
|
// all other moves fail if the target already has _any_ status
|
||||||
if (overrideStatus ? this.status?.effect === effect : this.status) {
|
if (overrideStatus ? this.status?.effect === effect : this.status) {
|
||||||
this.queueStatusImmuneMessage(quiet, overrideStatus ? "overlap" : "other"); // having different status displays generic fail message
|
this.queueStatusImmuneMessage(quiet, overrideStatus ? "overlap" : "other"); // having different status displays generic fail message
|
||||||
return false;
|
return false;
|
||||||
@ -4801,73 +4815,62 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
|
|
||||||
const types = this.getTypes(true, true);
|
const types = this.getTypes(true, true);
|
||||||
|
|
||||||
|
/* Whether the target is immune to the specific status being applied. */
|
||||||
|
let isImmune = false;
|
||||||
|
/** The reason for a potential blockage; default "other" for type-based. */
|
||||||
|
let reason: "other" | Exclude<TerrainType, TerrainType.NONE> = "other";
|
||||||
|
|
||||||
switch (effect) {
|
switch (effect) {
|
||||||
case StatusEffect.POISON:
|
case StatusEffect.POISON:
|
||||||
case StatusEffect.TOXIC: {
|
case StatusEffect.TOXIC:
|
||||||
// Check if the Pokemon is immune to Poison/Toxic or if the source pokemon is canceling the immunity
|
// Check for type based immunities and/or Corrosion from the applier.
|
||||||
const poisonImmunity = types.map(defType => {
|
isImmune = types.some(defType => {
|
||||||
// Check if the Pokemon is not immune to Poison/Toxic
|
// only 1 immunity needed to block
|
||||||
if (defType !== PokemonType.POISON && defType !== PokemonType.STEEL) {
|
if (defType !== PokemonType.POISON && defType !== PokemonType.STEEL) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the source Pokemon has an ability that cancels the Poison/Toxic immunity
|
// No source (such as from Toxic Spikes) = blocked by default
|
||||||
|
if (!sourcePokemon) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const cancelImmunity = new BooleanHolder(false);
|
const cancelImmunity = new BooleanHolder(false);
|
||||||
// TODO: Determine if we need to pass `quiet` as the value for simulated in this call
|
// TODO: Determine if we need to pass `quiet` as the value for simulated in this call
|
||||||
if (sourcePokemon) {
|
|
||||||
applyAbAttrs("IgnoreTypeStatusEffectImmunityAbAttr", {
|
applyAbAttrs("IgnoreTypeStatusEffectImmunityAbAttr", {
|
||||||
pokemon: sourcePokemon,
|
pokemon: sourcePokemon,
|
||||||
cancelled: cancelImmunity,
|
cancelled: cancelImmunity,
|
||||||
statusEffect: effect,
|
statusEffect: effect,
|
||||||
defenderType: defType,
|
defenderType: defType,
|
||||||
});
|
});
|
||||||
if (cancelImmunity.value) {
|
return !cancelImmunity.value;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this.isOfType(PokemonType.POISON) || this.isOfType(PokemonType.STEEL)) {
|
|
||||||
if (poisonImmunity.includes(true)) {
|
|
||||||
this.queueStatusImmuneMessage(quiet);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
case StatusEffect.PARALYSIS:
|
case StatusEffect.PARALYSIS:
|
||||||
if (this.isOfType(PokemonType.ELECTRIC)) {
|
isImmune = this.isOfType(PokemonType.ELECTRIC);
|
||||||
this.queueStatusImmuneMessage(quiet);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case StatusEffect.SLEEP:
|
case StatusEffect.SLEEP:
|
||||||
if (this.isGrounded() && globalScene.arena.terrain?.terrainType === TerrainType.ELECTRIC) {
|
isImmune = this.isGrounded() && globalScene.arena.getTerrainType() === TerrainType.ELECTRIC;
|
||||||
this.queueStatusImmuneMessage(quiet, TerrainType.ELECTRIC);
|
reason = TerrainType.ELECTRIC;
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case StatusEffect.FREEZE:
|
case StatusEffect.FREEZE: {
|
||||||
if (
|
const weatherType = globalScene.arena.getWeatherType();
|
||||||
|
isImmune =
|
||||||
this.isOfType(PokemonType.ICE) ||
|
this.isOfType(PokemonType.ICE) ||
|
||||||
(!ignoreField &&
|
(!ignoreField && (weatherType === WeatherType.SUNNY || weatherType === WeatherType.HARSH_SUN));
|
||||||
globalScene?.arena?.weather?.weatherType &&
|
|
||||||
[WeatherType.SUNNY, WeatherType.HARSH_SUN].includes(globalScene.arena.weather.weatherType))
|
|
||||||
) {
|
|
||||||
this.queueStatusImmuneMessage(quiet);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case StatusEffect.BURN:
|
|
||||||
if (this.isOfType(PokemonType.FIRE)) {
|
|
||||||
this.queueStatusImmuneMessage(quiet);
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
case StatusEffect.BURN:
|
||||||
|
isImmune = this.isOfType(PokemonType.FIRE);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isImmune) {
|
||||||
|
this.queueStatusImmuneMessage(quiet, reason);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for cancellations from self/ally abilities
|
||||||
const cancelled = new BooleanHolder(false);
|
const cancelled = new BooleanHolder(false);
|
||||||
applyAbAttrs("StatusEffectImmunityAbAttr", { pokemon: this, effect, cancelled, simulated: quiet });
|
applyAbAttrs("StatusEffectImmunityAbAttr", { pokemon: this, effect, cancelled, simulated: quiet });
|
||||||
if (cancelled.value) {
|
if (cancelled.value) {
|
||||||
@ -4883,15 +4886,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
target: this,
|
target: this,
|
||||||
source: sourcePokemon,
|
source: sourcePokemon,
|
||||||
});
|
});
|
||||||
if (cancelled.value) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cancelled.value) {
|
if (cancelled.value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform safeguard checks
|
||||||
if (sourcePokemon && sourcePokemon !== this && this.isSafeguarded(sourcePokemon)) {
|
if (sourcePokemon && sourcePokemon !== this && this.isSafeguarded(sourcePokemon)) {
|
||||||
if (!quiet) {
|
if (!quiet) {
|
||||||
globalScene.phaseManager.queueMessage(
|
globalScene.phaseManager.queueMessage(
|
||||||
@ -4904,18 +4904,36 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
trySetStatus(
|
/**
|
||||||
effect?: StatusEffect,
|
* Attempt to set this Pokemon's status to the specified condition.
|
||||||
asPhase = false,
|
* Enqueues a new `ObtainStatusEffectPhase` to trigger animations, etc.
|
||||||
|
* @param effect - The {@linkcode StatusEffect} to set
|
||||||
|
* @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target,
|
||||||
|
* or `null` if the status is applied from a non-Pokemon source (hazards, etc.); default `null`
|
||||||
|
* @param sleepTurnsRemaining - The number of turns to set {@linkcode StatusEffect.SLEEP} for;
|
||||||
|
* defaults to a random number between 2 and 4 and is unused for non-Sleep statuses
|
||||||
|
* @param sourceText - The text to show for the source of the status effect, if any; default `null`
|
||||||
|
* @param overrideStatus - Whether to allow overriding the Pokemon's current status with a different one; default `false`
|
||||||
|
* @param quiet - Whether to suppress in-battle messages for status checks; default `true`
|
||||||
|
* @param overrideMessage - String containing text to be displayed upon status setting; defaults to normal key for status
|
||||||
|
* and is used exclusively for Rest
|
||||||
|
* @returns Whether the status effect phase was successfully created.
|
||||||
|
* @see {@linkcode doSetStatus} - alternate function that sets status immediately (albeit without condition checks).
|
||||||
|
*/
|
||||||
|
public trySetStatus(
|
||||||
|
effect: StatusEffect,
|
||||||
sourcePokemon: Pokemon | null = null,
|
sourcePokemon: Pokemon | null = null,
|
||||||
turnsRemaining = 0,
|
sleepTurnsRemaining?: number,
|
||||||
sourceText: string | null = null,
|
sourceText: string | null = null,
|
||||||
overrideStatus?: boolean,
|
overrideStatus?: boolean,
|
||||||
quiet = true,
|
quiet = true,
|
||||||
|
overrideMessage?: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
|
// TODO: This needs to propagate failure status for status moves
|
||||||
if (!effect) {
|
if (!effect) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.canSetStatus(effect, quiet, overrideStatus, sourcePokemon)) {
|
if (!this.canSetStatus(effect, quiet, overrideStatus, sourcePokemon)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -4935,48 +4953,79 @@ export abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asPhase) {
|
|
||||||
if (overrideStatus) {
|
if (overrideStatus) {
|
||||||
this.resetStatus(false);
|
this.resetStatus(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
globalScene.phaseManager.unshiftNew(
|
globalScene.phaseManager.unshiftNew(
|
||||||
"ObtainStatusEffectPhase",
|
"ObtainStatusEffectPhase",
|
||||||
this.getBattlerIndex(),
|
this.getBattlerIndex(),
|
||||||
effect,
|
effect,
|
||||||
turnsRemaining,
|
|
||||||
sourceText,
|
|
||||||
sourcePokemon,
|
sourcePokemon,
|
||||||
|
sleepTurnsRemaining,
|
||||||
|
sourceText,
|
||||||
|
overrideMessage,
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
let sleepTurnsRemaining: NumberHolder;
|
/**
|
||||||
|
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||||
|
* @param effect - The {@linkcode StatusEffect} to set
|
||||||
|
* @remarks
|
||||||
|
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||||
|
*/
|
||||||
|
doSetStatus(effect: Exclude<StatusEffect, StatusEffect.SLEEP>): void;
|
||||||
|
/**
|
||||||
|
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||||
|
* @param effect - {@linkcode StatusEffect.SLEEP}
|
||||||
|
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4
|
||||||
|
* @remarks
|
||||||
|
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||||
|
*/
|
||||||
|
doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void;
|
||||||
|
/**
|
||||||
|
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||||
|
* @param effect - The {@linkcode StatusEffect} to set
|
||||||
|
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4
|
||||||
|
* and is unused for all non-sleep Statuses
|
||||||
|
* @remarks
|
||||||
|
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||||
|
*/
|
||||||
|
doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void;
|
||||||
|
/**
|
||||||
|
* Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect.
|
||||||
|
* @param effect - The {@linkcode StatusEffect} to set
|
||||||
|
* @param sleepTurnsRemaining - The number of turns to inflict sleep for; defaults to a random number between 2 and 4
|
||||||
|
* and is unused for all non-sleep Statuses
|
||||||
|
* @remarks
|
||||||
|
* ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller.
|
||||||
|
* @todo Make this and all related fields private and change tests to use a field-based helper or similar
|
||||||
|
*/
|
||||||
|
doSetStatus(
|
||||||
|
effect: StatusEffect,
|
||||||
|
sleepTurnsRemaining = effect !== StatusEffect.SLEEP ? 0 : this.randBattleSeedIntRange(2, 4),
|
||||||
|
): void {
|
||||||
if (effect === StatusEffect.SLEEP) {
|
if (effect === StatusEffect.SLEEP) {
|
||||||
sleepTurnsRemaining = new NumberHolder(this.randBattleSeedIntRange(2, 4));
|
|
||||||
|
|
||||||
this.setFrameRate(4);
|
this.setFrameRate(4);
|
||||||
|
|
||||||
// If the user is invulnerable, lets remove their invulnerability when they fall asleep
|
// If the user is semi-invulnerable when put asleep (such as due to Yawm),
|
||||||
const invulnerableTags = [
|
// remove their invulnerability and cancel the upcoming move from the queue
|
||||||
|
const invulnTagTypes = [
|
||||||
|
BattlerTagType.FLYING,
|
||||||
BattlerTagType.UNDERGROUND,
|
BattlerTagType.UNDERGROUND,
|
||||||
BattlerTagType.UNDERWATER,
|
BattlerTagType.UNDERWATER,
|
||||||
BattlerTagType.HIDDEN,
|
BattlerTagType.HIDDEN,
|
||||||
BattlerTagType.FLYING,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const tag = invulnerableTags.find(t => this.getTag(t));
|
if (this.findTag(t => invulnTagTypes.includes(t.tagType))) {
|
||||||
|
this.findAndRemoveTags(t => invulnTagTypes.includes(t.tagType));
|
||||||
if (tag) {
|
this.getMoveQueue().shift();
|
||||||
this.removeTag(tag);
|
|
||||||
this.getMoveQueue().pop();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sleepTurnsRemaining = sleepTurnsRemaining!; // tell TS compiler it's defined
|
this.status = new Status(effect, 0, sleepTurnsRemaining);
|
||||||
this.status = new Status(effect, 0, sleepTurnsRemaining?.value);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -5822,45 +5871,59 @@ export class PlayerPokemon extends Pokemon {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Add friendship to this Pokemon
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* This adds friendship to the pokemon's friendship stat (used for evolution, return, etc.) and candy progress.
|
||||||
|
* For fusions, candy progress for each species in the fusion is halved.
|
||||||
|
*
|
||||||
|
* @param friendship - The amount of friendship to add. Negative values will reduce friendship, though not below 0.
|
||||||
|
* @param capped - If true, don't allow the friendship gain to exceed 200. Used to cap friendship gains from rare candies.
|
||||||
|
*/
|
||||||
|
addFriendship(friendship: number, capped = false): void {
|
||||||
|
// Short-circuit friendship loss, which doesn't impact candy friendship
|
||||||
|
if (friendship <= 0) {
|
||||||
|
this.friendship = Math.max(this.friendship + friendship, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
addFriendship(friendship: number): void {
|
|
||||||
if (friendship > 0) {
|
|
||||||
const starterSpeciesId = this.species.getRootSpeciesId();
|
const starterSpeciesId = this.species.getRootSpeciesId();
|
||||||
const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0;
|
const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0;
|
||||||
const starterData = [
|
const starterGameData = globalScene.gameData.starterData;
|
||||||
globalScene.gameData.starterData[starterSpeciesId],
|
const starterData: [StarterDataEntry, SpeciesId][] = [[starterGameData[starterSpeciesId], starterSpeciesId]];
|
||||||
fusionStarterSpeciesId ? globalScene.gameData.starterData[fusionStarterSpeciesId] : null,
|
if (fusionStarterSpeciesId) {
|
||||||
].filter(d => !!d);
|
starterData.push([starterGameData[fusionStarterSpeciesId], fusionStarterSpeciesId]);
|
||||||
|
}
|
||||||
const amount = new NumberHolder(friendship);
|
const amount = new NumberHolder(friendship);
|
||||||
globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount);
|
globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount);
|
||||||
const candyFriendshipMultiplier = globalScene.gameMode.isClassic
|
friendship = amount.value;
|
||||||
|
|
||||||
|
const newFriendship = this.friendship + friendship;
|
||||||
|
// If capped is true, only adjust friendship if the new friendship is less than or equal to 200.
|
||||||
|
if (!capped || newFriendship <= RARE_CANDY_FRIENDSHIP_CAP) {
|
||||||
|
this.friendship = Math.min(newFriendship, 255);
|
||||||
|
if (newFriendship >= 255) {
|
||||||
|
globalScene.validateAchv(achvs.MAX_FRIENDSHIP);
|
||||||
|
awardRibbonsToSpeciesLine(this.species.speciesId, RibbonData.FRIENDSHIP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let candyFriendshipMultiplier = globalScene.gameMode.isClassic
|
||||||
? timedEventManager.getClassicFriendshipMultiplier()
|
? timedEventManager.getClassicFriendshipMultiplier()
|
||||||
: 1;
|
: 1;
|
||||||
const fusionReduction = fusionStarterSpeciesId
|
if (fusionStarterSpeciesId) {
|
||||||
? timedEventManager.areFusionsBoosted()
|
candyFriendshipMultiplier /= timedEventManager.areFusionsBoosted() ? 1.5 : 2;
|
||||||
? 1.5 // Divide candy gain for fusions by 1.5 during events
|
|
||||||
: 2 // 2 for fusions outside events
|
|
||||||
: 1; // 1 for non-fused mons
|
|
||||||
const starterAmount = new NumberHolder(Math.floor((amount.value * candyFriendshipMultiplier) / fusionReduction));
|
|
||||||
|
|
||||||
// Add friendship to this PlayerPokemon
|
|
||||||
this.friendship = Math.min(this.friendship + amount.value, 255);
|
|
||||||
if (this.friendship === 255) {
|
|
||||||
globalScene.validateAchv(achvs.MAX_FRIENDSHIP);
|
|
||||||
}
|
}
|
||||||
|
const candyFriendshipAmount = Math.floor(friendship * candyFriendshipMultiplier);
|
||||||
// Add to candy progress for this mon's starter species and its fused species (if it has one)
|
// Add to candy progress for this mon's starter species and its fused species (if it has one)
|
||||||
starterData.forEach((sd: StarterDataEntry, i: number) => {
|
starterData.forEach(([sd, id]: [StarterDataEntry, SpeciesId]) => {
|
||||||
const speciesId = !i ? starterSpeciesId : (fusionStarterSpeciesId as SpeciesId);
|
sd.friendship = (sd.friendship || 0) + candyFriendshipAmount;
|
||||||
sd.friendship = (sd.friendship || 0) + starterAmount.value;
|
if (sd.friendship >= getStarterValueFriendshipCap(speciesStarterCosts[id])) {
|
||||||
if (sd.friendship >= getStarterValueFriendshipCap(speciesStarterCosts[speciesId])) {
|
globalScene.gameData.addStarterCandy(getPokemonSpecies(id), 1);
|
||||||
globalScene.gameData.addStarterCandy(getPokemonSpecies(speciesId), 1);
|
|
||||||
sd.friendship = 0;
|
sd.friendship = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
// Lose friendship upon fainting
|
|
||||||
this.friendship = Math.max(this.friendship + friendship, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPossibleEvolution(evolution: SpeciesFormEvolution | null): Promise<Pokemon> {
|
getPossibleEvolution(evolution: SpeciesFormEvolution | null): Promise<Pokemon> {
|
||||||
@ -6241,22 +6304,22 @@ export class EnemyPokemon extends Pokemon {
|
|||||||
this.setBoss(boss, dataSource?.bossSegments);
|
this.setBoss(boss, dataSource?.bossSegments);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Overrides.OPP_STATUS_OVERRIDE) {
|
if (Overrides.ENEMY_STATUS_OVERRIDE) {
|
||||||
this.status = new Status(Overrides.OPP_STATUS_OVERRIDE, 0, 4);
|
this.status = new Status(Overrides.ENEMY_STATUS_OVERRIDE, 0, 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Overrides.OPP_GENDER_OVERRIDE !== null) {
|
if (Overrides.ENEMY_GENDER_OVERRIDE !== null) {
|
||||||
this.gender = Overrides.OPP_GENDER_OVERRIDE;
|
this.gender = Overrides.ENEMY_GENDER_OVERRIDE;
|
||||||
}
|
}
|
||||||
|
|
||||||
const speciesId = this.species.speciesId;
|
const speciesId = this.species.speciesId;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
speciesId in Overrides.OPP_FORM_OVERRIDES &&
|
speciesId in Overrides.ENEMY_FORM_OVERRIDES &&
|
||||||
!isNullOrUndefined(Overrides.OPP_FORM_OVERRIDES[speciesId]) &&
|
!isNullOrUndefined(Overrides.ENEMY_FORM_OVERRIDES[speciesId]) &&
|
||||||
this.species.forms[Overrides.OPP_FORM_OVERRIDES[speciesId]]
|
this.species.forms[Overrides.ENEMY_FORM_OVERRIDES[speciesId]]
|
||||||
) {
|
) {
|
||||||
this.formIndex = Overrides.OPP_FORM_OVERRIDES[speciesId];
|
this.formIndex = Overrides.ENEMY_FORM_OVERRIDES[speciesId];
|
||||||
} else if (globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) {
|
} else if (globalScene.gameMode.isDaily && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) {
|
||||||
const eventBoss = getDailyEventSeedBoss(globalScene.seed);
|
const eventBoss = getDailyEventSeedBoss(globalScene.seed);
|
||||||
if (!isNullOrUndefined(eventBoss)) {
|
if (!isNullOrUndefined(eventBoss)) {
|
||||||
@ -6266,21 +6329,21 @@ export class EnemyPokemon extends Pokemon {
|
|||||||
|
|
||||||
if (!dataSource) {
|
if (!dataSource) {
|
||||||
this.generateAndPopulateMoveset();
|
this.generateAndPopulateMoveset();
|
||||||
if (shinyLock || Overrides.OPP_SHINY_OVERRIDE === false) {
|
if (shinyLock || Overrides.ENEMY_SHINY_OVERRIDE === false) {
|
||||||
this.shiny = false;
|
this.shiny = false;
|
||||||
} else {
|
} else {
|
||||||
this.trySetShiny();
|
this.trySetShiny();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.shiny && Overrides.OPP_SHINY_OVERRIDE) {
|
if (!this.shiny && Overrides.ENEMY_SHINY_OVERRIDE) {
|
||||||
this.shiny = true;
|
this.shiny = true;
|
||||||
this.initShinySparkle();
|
this.initShinySparkle();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.shiny) {
|
if (this.shiny) {
|
||||||
this.variant = this.generateShinyVariant();
|
this.variant = this.generateShinyVariant();
|
||||||
if (Overrides.OPP_VARIANT_OVERRIDE !== null) {
|
if (Overrides.ENEMY_VARIANT_OVERRIDE !== null) {
|
||||||
this.variant = Overrides.OPP_VARIANT_OVERRIDE;
|
this.variant = Overrides.ENEMY_VARIANT_OVERRIDE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,6 +90,7 @@ export class LoadingScene extends SceneBase {
|
|||||||
this.loadAtlas("shiny_icons", "ui");
|
this.loadAtlas("shiny_icons", "ui");
|
||||||
this.loadImage("ha_capsule", "ui", "ha_capsule.png");
|
this.loadImage("ha_capsule", "ui", "ha_capsule.png");
|
||||||
this.loadImage("champion_ribbon", "ui", "champion_ribbon.png");
|
this.loadImage("champion_ribbon", "ui", "champion_ribbon.png");
|
||||||
|
this.loadImage("champion_ribbon_emerald", "ui", "champion_ribbon_emerald.png");
|
||||||
this.loadImage("icon_spliced", "ui");
|
this.loadImage("icon_spliced", "ui");
|
||||||
this.loadImage("icon_lock", "ui", "icon_lock.png");
|
this.loadImage("icon_lock", "ui", "icon_lock.png");
|
||||||
this.loadImage("icon_stop", "ui", "icon_stop.png");
|
this.loadImage("icon_stop", "ui", "icon_stop.png");
|
||||||
@ -122,6 +123,7 @@ export class LoadingScene extends SceneBase {
|
|||||||
this.loadImage("party_bg_double", "ui");
|
this.loadImage("party_bg_double", "ui");
|
||||||
this.loadImage("party_bg_double_manage", "ui");
|
this.loadImage("party_bg_double_manage", "ui");
|
||||||
this.loadAtlas("party_slot_main", "ui");
|
this.loadAtlas("party_slot_main", "ui");
|
||||||
|
this.loadAtlas("party_slot_main_short", "ui");
|
||||||
this.loadAtlas("party_slot", "ui");
|
this.loadAtlas("party_slot", "ui");
|
||||||
this.loadImage("party_slot_overlay_lv", "ui");
|
this.loadImage("party_slot_overlay_lv", "ui");
|
||||||
this.loadImage("party_slot_hp_bar", "ui");
|
this.loadImage("party_slot_hp_bar", "ui");
|
||||||
|
@ -1733,12 +1733,12 @@ export class TurnStatusEffectModifier extends PokemonHeldItemModifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tries to inflicts the holder with the associated {@linkcode StatusEffect}.
|
* Attempt to inflict the holder with the associated {@linkcode StatusEffect}.
|
||||||
* @param pokemon {@linkcode Pokemon} that holds the held item
|
* @param pokemon - The {@linkcode Pokemon} holding the item
|
||||||
* @returns `true` if the status effect was applied successfully
|
* @returns `true` if the status effect was applied successfully
|
||||||
*/
|
*/
|
||||||
override apply(pokemon: Pokemon): boolean {
|
override apply(pokemon: Pokemon): boolean {
|
||||||
return pokemon.trySetStatus(this.effect, true, undefined, undefined, this.type.name);
|
return pokemon.trySetStatus(this.effect, pokemon, undefined, this.type.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMaxHeldItemCount(_pokemon: Pokemon): number {
|
getMaxHeldItemCount(_pokemon: Pokemon): number {
|
||||||
@ -2304,7 +2304,7 @@ export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier {
|
|||||||
playerPokemon.levelExp = 0;
|
playerPokemon.levelExp = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY);
|
playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY, true);
|
||||||
|
|
||||||
globalScene.phaseManager.unshiftNew(
|
globalScene.phaseManager.unshiftNew(
|
||||||
"LevelUpPhase",
|
"LevelUpPhase",
|
||||||
@ -3605,7 +3605,7 @@ export class EnemyAttackStatusEffectChanceModifier extends EnemyPersistentModifi
|
|||||||
*/
|
*/
|
||||||
override apply(enemyPokemon: Pokemon): boolean {
|
override apply(enemyPokemon: Pokemon): boolean {
|
||||||
if (randSeedFloat() <= this.chance * this.getStackCount()) {
|
if (randSeedFloat() <= this.chance * this.getStackCount()) {
|
||||||
return enemyPokemon.trySetStatus(this.effect, true);
|
return enemyPokemon.trySetStatus(this.effect);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@ -3755,7 +3755,7 @@ export class EnemyFusionChanceModifier extends EnemyPersistentModifier {
|
|||||||
export function overrideModifiers(isPlayer = true): void {
|
export function overrideModifiers(isPlayer = true): void {
|
||||||
const modifiersOverride: ModifierOverride[] = isPlayer
|
const modifiersOverride: ModifierOverride[] = isPlayer
|
||||||
? Overrides.STARTING_MODIFIER_OVERRIDE
|
? Overrides.STARTING_MODIFIER_OVERRIDE
|
||||||
: Overrides.OPP_MODIFIER_OVERRIDE;
|
: Overrides.ENEMY_MODIFIER_OVERRIDE;
|
||||||
if (!modifiersOverride || modifiersOverride.length === 0 || !globalScene) {
|
if (!modifiersOverride || modifiersOverride.length === 0 || !globalScene) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -3797,7 +3797,7 @@ export function overrideModifiers(isPlayer = true): void {
|
|||||||
export function overrideHeldItems(pokemon: Pokemon, isPlayer = true): void {
|
export function overrideHeldItems(pokemon: Pokemon, isPlayer = true): void {
|
||||||
const heldItemsOverride: ModifierOverride[] = isPlayer
|
const heldItemsOverride: ModifierOverride[] = isPlayer
|
||||||
? Overrides.STARTING_HELD_ITEMS_OVERRIDE
|
? Overrides.STARTING_HELD_ITEMS_OVERRIDE
|
||||||
: Overrides.OPP_HELD_ITEMS_OVERRIDE;
|
: Overrides.ENEMY_HELD_ITEMS_OVERRIDE;
|
||||||
if (!heldItemsOverride || heldItemsOverride.length === 0 || !globalScene) {
|
if (!heldItemsOverride || heldItemsOverride.length === 0 || !globalScene) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -179,25 +179,24 @@ class DefaultOverrides {
|
|||||||
// --------------------------
|
// --------------------------
|
||||||
// OPPONENT / ENEMY OVERRIDES
|
// OPPONENT / ENEMY OVERRIDES
|
||||||
// --------------------------
|
// --------------------------
|
||||||
// TODO: rename `OPP_` to `ENEMY_`
|
readonly ENEMY_SPECIES_OVERRIDE: SpeciesId | number = 0;
|
||||||
readonly OPP_SPECIES_OVERRIDE: SpeciesId | number = 0;
|
|
||||||
/**
|
/**
|
||||||
* This will make all opponents fused Pokemon
|
* This will make all opponents fused Pokemon
|
||||||
*/
|
*/
|
||||||
readonly OPP_FUSION_OVERRIDE: boolean = false;
|
readonly ENEMY_FUSION_OVERRIDE: boolean = false;
|
||||||
/**
|
/**
|
||||||
* This will override the species of the fusion only when the opponent is already a fusion
|
* This will override the species of the fusion only when the opponent is already a fusion
|
||||||
*/
|
*/
|
||||||
readonly OPP_FUSION_SPECIES_OVERRIDE: SpeciesId | number = 0;
|
readonly ENEMY_FUSION_SPECIES_OVERRIDE: SpeciesId | number = 0;
|
||||||
readonly OPP_LEVEL_OVERRIDE: number = 0;
|
readonly ENEMY_LEVEL_OVERRIDE: number = 0;
|
||||||
readonly OPP_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE;
|
readonly ENEMY_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE;
|
||||||
readonly OPP_PASSIVE_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE;
|
readonly ENEMY_PASSIVE_ABILITY_OVERRIDE: AbilityId = AbilityId.NONE;
|
||||||
readonly OPP_HAS_PASSIVE_ABILITY_OVERRIDE: boolean | null = null;
|
readonly ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE: boolean | null = null;
|
||||||
readonly OPP_STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE;
|
readonly ENEMY_STATUS_OVERRIDE: StatusEffect = StatusEffect.NONE;
|
||||||
readonly OPP_GENDER_OVERRIDE: Gender | null = null;
|
readonly ENEMY_GENDER_OVERRIDE: Gender | null = null;
|
||||||
readonly OPP_MOVESET_OVERRIDE: MoveId | Array<MoveId> = [];
|
readonly ENEMY_MOVESET_OVERRIDE: MoveId | Array<MoveId> = [];
|
||||||
readonly OPP_SHINY_OVERRIDE: boolean | null = null;
|
readonly ENEMY_SHINY_OVERRIDE: boolean | null = null;
|
||||||
readonly OPP_VARIANT_OVERRIDE: Variant | null = null;
|
readonly ENEMY_VARIANT_OVERRIDE: Variant | null = null;
|
||||||
/**
|
/**
|
||||||
* Overrides the IVs of enemy pokemon. Values must never be outside the range `0` to `31`!
|
* Overrides the IVs of enemy pokemon. Values must never be outside the range `0` to `31`!
|
||||||
* - If set to a number between `0` and `31`, set all IVs of all enemy pokemon to that number.
|
* - If set to a number between `0` and `31`, set all IVs of all enemy pokemon to that number.
|
||||||
@ -207,7 +206,7 @@ class DefaultOverrides {
|
|||||||
readonly ENEMY_IVS_OVERRIDE: number | number[] | null = null;
|
readonly ENEMY_IVS_OVERRIDE: number | number[] | null = null;
|
||||||
/** Override the nature of all enemy pokemon to the specified nature. Disabled if `null`. */
|
/** Override the nature of all enemy pokemon to the specified nature. Disabled if `null`. */
|
||||||
readonly ENEMY_NATURE_OVERRIDE: Nature | null = null;
|
readonly ENEMY_NATURE_OVERRIDE: Nature | null = null;
|
||||||
readonly OPP_FORM_OVERRIDES: Partial<Record<SpeciesId, number>> = {};
|
readonly ENEMY_FORM_OVERRIDES: Partial<Record<SpeciesId, number>> = {};
|
||||||
/**
|
/**
|
||||||
* Override to give the enemy Pokemon a given amount of health segments
|
* Override to give the enemy Pokemon a given amount of health segments
|
||||||
*
|
*
|
||||||
@ -215,7 +214,7 @@ class DefaultOverrides {
|
|||||||
* 1: the Pokemon will have a single health segment and therefore will not be a boss
|
* 1: the Pokemon will have a single health segment and therefore will not be a boss
|
||||||
* 2+: the Pokemon will be a boss with the given number of health segments
|
* 2+: the Pokemon will be a boss with the given number of health segments
|
||||||
*/
|
*/
|
||||||
readonly OPP_HEALTH_SEGMENTS_OVERRIDE: number = 0;
|
readonly ENEMY_HEALTH_SEGMENTS_OVERRIDE: number = 0;
|
||||||
|
|
||||||
// -------------
|
// -------------
|
||||||
// EGG OVERRIDES
|
// EGG OVERRIDES
|
||||||
@ -277,12 +276,12 @@ class DefaultOverrides {
|
|||||||
*
|
*
|
||||||
* Note that any previous modifiers are cleared.
|
* Note that any previous modifiers are cleared.
|
||||||
*/
|
*/
|
||||||
readonly OPP_MODIFIER_OVERRIDE: ModifierOverride[] = [];
|
readonly ENEMY_MODIFIER_OVERRIDE: ModifierOverride[] = [];
|
||||||
|
|
||||||
/** Override array of {@linkcode ModifierOverride}s used to provide held items to first party member when starting a new game. */
|
/** Override array of {@linkcode ModifierOverride}s used to provide held items to first party member when starting a new game. */
|
||||||
readonly STARTING_HELD_ITEMS_OVERRIDE: ModifierOverride[] = [];
|
readonly STARTING_HELD_ITEMS_OVERRIDE: ModifierOverride[] = [];
|
||||||
/** Override array of {@linkcode ModifierOverride}s used to provide held items to enemies on spawn. */
|
/** Override array of {@linkcode ModifierOverride}s used to provide held items to enemies on spawn. */
|
||||||
readonly OPP_HELD_ITEMS_OVERRIDE: ModifierOverride[] = [];
|
readonly ENEMY_HELD_ITEMS_OVERRIDE: ModifierOverride[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override array of {@linkcode ModifierOverride}s used to replace the generated item rolls after a wave.
|
* Override array of {@linkcode ModifierOverride}s used to replace the generated item rolls after a wave.
|
||||||
|
@ -268,7 +268,7 @@ export class AttemptCapturePhase extends PokemonPhase {
|
|||||||
const removePokemon = () => {
|
const removePokemon = () => {
|
||||||
globalScene.addFaintedEnemyScore(pokemon);
|
globalScene.addFaintedEnemyScore(pokemon);
|
||||||
pokemon.hp = 0;
|
pokemon.hp = 0;
|
||||||
pokemon.trySetStatus(StatusEffect.FAINT);
|
pokemon.doSetStatus(StatusEffect.FAINT);
|
||||||
globalScene.clearEnemyHeldItemModifiers();
|
globalScene.clearEnemyHeldItemModifiers();
|
||||||
pokemon.leaveField(true, true, true);
|
pokemon.leaveField(true, true, true);
|
||||||
};
|
};
|
||||||
|
@ -45,7 +45,7 @@ export class AttemptRunPhase extends FieldPhase {
|
|||||||
enemyField.forEach(enemyPokemon => {
|
enemyField.forEach(enemyPokemon => {
|
||||||
enemyPokemon.hideInfo().then(() => enemyPokemon.destroy());
|
enemyPokemon.hideInfo().then(() => enemyPokemon.destroy());
|
||||||
enemyPokemon.hp = 0;
|
enemyPokemon.hp = 0;
|
||||||
enemyPokemon.trySetStatus(StatusEffect.FAINT);
|
enemyPokemon.doSetStatus(StatusEffect.FAINT);
|
||||||
});
|
});
|
||||||
|
|
||||||
globalScene.phaseManager.pushNew("BattleEndPhase", false);
|
globalScene.phaseManager.pushNew("BattleEndPhase", false);
|
||||||
|
@ -229,7 +229,7 @@ export class EncounterPhase extends BattlePhase {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const overridedBossSegments = Overrides.OPP_HEALTH_SEGMENTS_OVERRIDE > 1;
|
const overridedBossSegments = Overrides.ENEMY_HEALTH_SEGMENTS_OVERRIDE > 1;
|
||||||
// for double battles, reduce the health segments for boss Pokemon unless there is an override
|
// for double battles, reduce the health segments for boss Pokemon unless there is an override
|
||||||
if (!overridedBossSegments && battle.enemyParty.filter(p => p.isBoss()).length > 1) {
|
if (!overridedBossSegments && battle.enemyParty.filter(p => p.isBoss()).length > 1) {
|
||||||
for (const enemyPokemon of battle.enemyParty) {
|
for (const enemyPokemon of battle.enemyParty) {
|
||||||
|
@ -205,7 +205,7 @@ export class FaintPhase extends PokemonPhase {
|
|||||||
pokemon.lapseTags(BattlerTagLapseType.FAINT);
|
pokemon.lapseTags(BattlerTagLapseType.FAINT);
|
||||||
|
|
||||||
pokemon.y -= 150;
|
pokemon.y -= 150;
|
||||||
pokemon.trySetStatus(StatusEffect.FAINT);
|
pokemon.doSetStatus(StatusEffect.FAINT);
|
||||||
if (pokemon.isPlayer()) {
|
if (pokemon.isPlayer()) {
|
||||||
globalScene.currentBattle.removeFaintedParticipant(pokemon as PlayerPokemon);
|
globalScene.currentBattle.removeFaintedParticipant(pokemon as PlayerPokemon);
|
||||||
} else {
|
} else {
|
||||||
|
@ -19,8 +19,11 @@ import { ChallengeData } from "#system/challenge-data";
|
|||||||
import type { SessionSaveData } from "#system/game-data";
|
import type { SessionSaveData } from "#system/game-data";
|
||||||
import { ModifierData as PersistentModifierData } from "#system/modifier-data";
|
import { ModifierData as PersistentModifierData } from "#system/modifier-data";
|
||||||
import { PokemonData } from "#system/pokemon-data";
|
import { PokemonData } from "#system/pokemon-data";
|
||||||
|
import { RibbonData, type RibbonFlag } from "#system/ribbons/ribbon-data";
|
||||||
|
import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods";
|
||||||
import { TrainerData } from "#system/trainer-data";
|
import { TrainerData } from "#system/trainer-data";
|
||||||
import { trainerConfigs } from "#trainers/trainer-config";
|
import { trainerConfigs } from "#trainers/trainer-config";
|
||||||
|
import { checkSpeciesValidForChallenge, isNuzlockeChallenge } from "#utils/challenge-utils";
|
||||||
import { isLocal, isLocalServerConnected } from "#utils/common";
|
import { isLocal, isLocalServerConnected } from "#utils/common";
|
||||||
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
import { getPokemonSpecies } from "#utils/pokemon-utils";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
@ -111,6 +114,40 @@ export class GameOverPhase extends BattlePhase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Submethod of {@linkcode handleGameOver} that awards ribbons to Pokémon in the player's party based on the current
|
||||||
|
* game mode and challenges.
|
||||||
|
*/
|
||||||
|
private awardRibbons(): void {
|
||||||
|
let ribbonFlags = 0;
|
||||||
|
if (globalScene.gameMode.isClassic) {
|
||||||
|
ribbonFlags |= RibbonData.CLASSIC;
|
||||||
|
}
|
||||||
|
if (isNuzlockeChallenge()) {
|
||||||
|
ribbonFlags |= RibbonData.NUZLOCKE;
|
||||||
|
}
|
||||||
|
for (const challenge of globalScene.gameMode.challenges) {
|
||||||
|
const ribbon = challenge.ribbonAwarded;
|
||||||
|
if (challenge.value && ribbon) {
|
||||||
|
ribbonFlags |= ribbon;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Award ribbons to all Pokémon in the player's party that are considered valid
|
||||||
|
// for the current game mode and challenges.
|
||||||
|
for (const pokemon of globalScene.getPlayerParty()) {
|
||||||
|
const species = pokemon.species;
|
||||||
|
if (
|
||||||
|
checkSpeciesValidForChallenge(
|
||||||
|
species,
|
||||||
|
globalScene.gameData.getSpeciesDexAttrProps(species, pokemon.getDexAttr()),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
awardRibbonsToSpeciesLine(species.speciesId, ribbonFlags as RibbonFlag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleGameOver(): void {
|
handleGameOver(): void {
|
||||||
const doGameOver = (newClear: boolean) => {
|
const doGameOver = (newClear: boolean) => {
|
||||||
globalScene.disableMenu = true;
|
globalScene.disableMenu = true;
|
||||||
@ -122,12 +159,12 @@ export class GameOverPhase extends BattlePhase {
|
|||||||
globalScene.validateAchv(achvs.UNEVOLVED_CLASSIC_VICTORY);
|
globalScene.validateAchv(achvs.UNEVOLVED_CLASSIC_VICTORY);
|
||||||
globalScene.gameData.gameStats.sessionsWon++;
|
globalScene.gameData.gameStats.sessionsWon++;
|
||||||
for (const pokemon of globalScene.getPlayerParty()) {
|
for (const pokemon of globalScene.getPlayerParty()) {
|
||||||
this.awardRibbon(pokemon);
|
this.awardFirstClassicCompletion(pokemon);
|
||||||
|
|
||||||
if (pokemon.species.getRootSpeciesId() !== pokemon.species.getRootSpeciesId(true)) {
|
if (pokemon.species.getRootSpeciesId() !== pokemon.species.getRootSpeciesId(true)) {
|
||||||
this.awardRibbon(pokemon, true);
|
this.awardFirstClassicCompletion(pokemon, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
this.awardRibbons();
|
||||||
} else if (globalScene.gameMode.isDaily && newClear) {
|
} else if (globalScene.gameMode.isDaily && newClear) {
|
||||||
globalScene.gameData.gameStats.dailyRunSessionsWon++;
|
globalScene.gameData.gameStats.dailyRunSessionsWon++;
|
||||||
}
|
}
|
||||||
@ -263,7 +300,7 @@ export class GameOverPhase extends BattlePhase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
awardRibbon(pokemon: Pokemon, forStarter = false): void {
|
awardFirstClassicCompletion(pokemon: Pokemon, forStarter = false): void {
|
||||||
const speciesId = getPokemonSpecies(pokemon.species.speciesId);
|
const speciesId = getPokemonSpecies(pokemon.species.speciesId);
|
||||||
const speciesRibbonCount = globalScene.gameData.incrementRibbonCount(speciesId, forStarter);
|
const speciesRibbonCount = globalScene.gameData.incrementRibbonCount(speciesId, forStarter);
|
||||||
// first time classic win, award voucher
|
// first time classic win, award voucher
|
||||||
|
@ -269,8 +269,8 @@ export class MovePhase extends BattlePhase {
|
|||||||
globalScene.phaseManager.queueMessage(
|
globalScene.phaseManager.queueMessage(
|
||||||
getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
|
getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)),
|
||||||
);
|
);
|
||||||
this.pokemon.resetStatus();
|
// cannot use `asPhase=true` as it will cause status to be reset _after_ move condition checks fire
|
||||||
this.pokemon.updateInfo();
|
this.pokemon.resetStatus(false, false, false, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,55 +3,56 @@ import { globalScene } from "#app/global-scene";
|
|||||||
import { getPokemonNameWithAffix } from "#app/messages";
|
import { getPokemonNameWithAffix } from "#app/messages";
|
||||||
import { CommonBattleAnim } from "#data/battle-anims";
|
import { CommonBattleAnim } from "#data/battle-anims";
|
||||||
import { SpeciesFormChangeStatusEffectTrigger } from "#data/form-change-triggers";
|
import { SpeciesFormChangeStatusEffectTrigger } from "#data/form-change-triggers";
|
||||||
import { getStatusEffectObtainText, getStatusEffectOverlapText } from "#data/status-effect";
|
import { getStatusEffectObtainText } from "#data/status-effect";
|
||||||
import type { BattlerIndex } from "#enums/battler-index";
|
import type { BattlerIndex } from "#enums/battler-index";
|
||||||
import { CommonAnim } from "#enums/move-anims-common";
|
import { CommonAnim } from "#enums/move-anims-common";
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
import type { Pokemon } from "#field/pokemon";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import { PokemonPhase } from "#phases/pokemon-phase";
|
import { PokemonPhase } from "#phases/pokemon-phase";
|
||||||
import { isNullOrUndefined } from "#utils/common";
|
|
||||||
|
|
||||||
export class ObtainStatusEffectPhase extends PokemonPhase {
|
export class ObtainStatusEffectPhase extends PokemonPhase {
|
||||||
public readonly phaseName = "ObtainStatusEffectPhase";
|
public readonly phaseName = "ObtainStatusEffectPhase";
|
||||||
private statusEffect?: StatusEffect;
|
|
||||||
private turnsRemaining?: number;
|
|
||||||
private sourceText?: string | null;
|
|
||||||
private sourcePokemon?: Pokemon | null;
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param battlerIndex - The {@linkcode BattlerIndex} of the Pokemon obtaining the status effect.
|
||||||
|
* @param statusEffect - The {@linkcode StatusEffect} being applied.
|
||||||
|
* @param sourcePokemon - The {@linkcode Pokemon} applying the status effect to the target,
|
||||||
|
* or `null` if the status is applied from a non-Pokemon source (hazards, etc.); default `null`.
|
||||||
|
* @param sleepTurnsRemaining - The number of turns to set {@linkcode StatusEffect.SLEEP} for;
|
||||||
|
* defaults to a random number between 2 and 4 and is unused for non-Sleep statuses.
|
||||||
|
* @param sourceText - The text to show for the source of the status effect, if any; default `null`.
|
||||||
|
* @param statusMessage - A string containing text to be displayed upon status setting;
|
||||||
|
* defaults to normal key for status if empty or omitted.
|
||||||
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
battlerIndex: BattlerIndex,
|
battlerIndex: BattlerIndex,
|
||||||
statusEffect?: StatusEffect,
|
private statusEffect: StatusEffect,
|
||||||
turnsRemaining?: number,
|
private sourcePokemon: Pokemon | null = null,
|
||||||
sourceText?: string | null,
|
private sleepTurnsRemaining?: number,
|
||||||
sourcePokemon?: Pokemon | null,
|
sourceText: string | null = null, // TODO: This should take `undefined` instead of `null`
|
||||||
|
private statusMessage = "",
|
||||||
) {
|
) {
|
||||||
super(battlerIndex);
|
super(battlerIndex);
|
||||||
|
|
||||||
this.statusEffect = statusEffect;
|
this.statusMessage ||= getStatusEffectObtainText(
|
||||||
this.turnsRemaining = turnsRemaining;
|
statusEffect,
|
||||||
this.sourceText = sourceText;
|
getPokemonNameWithAffix(this.getPokemon()),
|
||||||
this.sourcePokemon = sourcePokemon;
|
sourceText ?? undefined,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
const pokemon = this.getPokemon();
|
const pokemon = this.getPokemon();
|
||||||
if (pokemon && !pokemon.status) {
|
|
||||||
if (pokemon.trySetStatus(this.statusEffect, false, this.sourcePokemon)) {
|
pokemon.doSetStatus(this.statusEffect, this.sleepTurnsRemaining);
|
||||||
if (this.turnsRemaining) {
|
|
||||||
pokemon.status!.sleepTurnsRemaining = this.turnsRemaining;
|
|
||||||
}
|
|
||||||
pokemon.updateInfo(true);
|
pokemon.updateInfo(true);
|
||||||
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(false, () => {
|
|
||||||
globalScene.phaseManager.queueMessage(
|
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect - 1), pokemon).play(false, () => {
|
||||||
getStatusEffectObtainText(
|
globalScene.phaseManager.queueMessage(this.statusMessage);
|
||||||
this.statusEffect,
|
if (this.statusEffect && this.statusEffect !== StatusEffect.FAINT) {
|
||||||
getPokemonNameWithAffix(pokemon),
|
|
||||||
this.sourceText ?? undefined,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (!isNullOrUndefined(this.statusEffect) && this.statusEffect !== StatusEffect.FAINT) {
|
|
||||||
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeStatusEffectTrigger, true);
|
globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeStatusEffectTrigger, true);
|
||||||
// If mold breaker etc was used to set this status, it shouldn't apply to abilities activated afterwards
|
// If the status was applied from a move, ensure abilities are not ignored for follow-up triggers.
|
||||||
|
// TODO: Ensure this isn't breaking any other phases unshifted afterwards
|
||||||
globalScene.arena.setIgnoreAbilities(false);
|
globalScene.arena.setIgnoreAbilities(false);
|
||||||
applyAbAttrs("PostSetStatusAbAttr", {
|
applyAbAttrs("PostSetStatusAbAttr", {
|
||||||
pokemon,
|
pokemon,
|
||||||
@ -61,13 +62,5 @@ export class ObtainStatusEffectPhase extends PokemonPhase {
|
|||||||
}
|
}
|
||||||
this.end();
|
this.end();
|
||||||
});
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
} else if (pokemon.status?.effect === this.statusEffect) {
|
|
||||||
globalScene.phaseManager.queueMessage(
|
|
||||||
getStatusEffectOverlapText(this.statusEffect ?? StatusEffect.NONE, getPokemonNameWithAffix(pokemon)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.end();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import { HealAchv } from "#system/achv";
|
|||||||
import { NumberHolder } from "#utils/common";
|
import { NumberHolder } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
|
|
||||||
|
// TODO: Refactor this - it has far too many arguments
|
||||||
export class PokemonHealPhase extends CommonAnimPhase {
|
export class PokemonHealPhase extends CommonAnimPhase {
|
||||||
public readonly phaseName = "PokemonHealPhase";
|
public readonly phaseName = "PokemonHealPhase";
|
||||||
private hpHealed: number;
|
private hpHealed: number;
|
||||||
@ -28,7 +29,7 @@ export class PokemonHealPhase extends CommonAnimPhase {
|
|||||||
battlerIndex: BattlerIndex,
|
battlerIndex: BattlerIndex,
|
||||||
hpHealed: number,
|
hpHealed: number,
|
||||||
message: string | null,
|
message: string | null,
|
||||||
showFullHpMessage: boolean,
|
showFullHpMessage = true,
|
||||||
skipAnim = false,
|
skipAnim = false,
|
||||||
revive = false,
|
revive = false,
|
||||||
healStatus = false,
|
healStatus = false,
|
||||||
@ -72,6 +73,7 @@ export class PokemonHealPhase extends CommonAnimPhase {
|
|||||||
this.message = null;
|
this.message = null;
|
||||||
return super.end();
|
return super.end();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (healOrDamage) {
|
if (healOrDamage) {
|
||||||
const hpRestoreMultiplier = new NumberHolder(1);
|
const hpRestoreMultiplier = new NumberHolder(1);
|
||||||
if (!this.revive) {
|
if (!this.revive) {
|
||||||
|
@ -56,15 +56,15 @@ export class PokerogueSessionSavedataApi extends ApiBase {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a session savedata.
|
* Update a session savedata.
|
||||||
* @param params The {@linkcode UpdateSessionSavedataRequest} to send
|
* @param params - The request to send
|
||||||
* @param rawSavedata The raw savedata (as `string`)
|
* @param rawSavedata - The raw, unencrypted savedata
|
||||||
* @returns An error message if something went wrong
|
* @returns An error message if something went wrong
|
||||||
*/
|
*/
|
||||||
public async update(params: UpdateSessionSavedataRequest, rawSavedata: string) {
|
public async update(params: UpdateSessionSavedataRequest, rawSavedata: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const urlSearchParams = this.toUrlSearchParams(params);
|
const urlSearchParams = this.toUrlSearchParams(params);
|
||||||
const response = await this.doPost(`/savedata/session/update?${urlSearchParams}`, rawSavedata);
|
|
||||||
|
|
||||||
|
const response = await this.doPost(`/savedata/session/update?${urlSearchParams}`, rawSavedata);
|
||||||
return await response.text();
|
return await response.text();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("Could not update session savedata!", err);
|
console.warn("Could not update session savedata!", err);
|
||||||
|
@ -5,7 +5,6 @@ import {
|
|||||||
FlipStatChallenge,
|
FlipStatChallenge,
|
||||||
FreshStartChallenge,
|
FreshStartChallenge,
|
||||||
InverseBattleChallenge,
|
InverseBattleChallenge,
|
||||||
LimitedCatchChallenge,
|
|
||||||
SingleGenerationChallenge,
|
SingleGenerationChallenge,
|
||||||
SingleTypeChallenge,
|
SingleTypeChallenge,
|
||||||
} from "#data/challenge";
|
} from "#data/challenge";
|
||||||
@ -14,6 +13,7 @@ import { PlayerGender } from "#enums/player-gender";
|
|||||||
import { getShortenedStatKey, Stat } from "#enums/stat";
|
import { getShortenedStatKey, Stat } from "#enums/stat";
|
||||||
import { TurnHeldItemTransferModifier } from "#modifiers/modifier";
|
import { TurnHeldItemTransferModifier } from "#modifiers/modifier";
|
||||||
import type { ConditionFn } from "#types/common";
|
import type { ConditionFn } from "#types/common";
|
||||||
|
import { isNuzlockeChallenge } from "#utils/challenge-utils";
|
||||||
import { NumberHolder } from "#utils/common";
|
import { NumberHolder } from "#utils/common";
|
||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import type { Modifier } from "typescript";
|
import type { Modifier } from "typescript";
|
||||||
@ -924,18 +924,7 @@ export const achvs = {
|
|||||||
globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0),
|
globalScene.gameMode.challenges.some(c => c.id === Challenges.INVERSE_BATTLE && c.value > 0),
|
||||||
).setSecret(),
|
).setSecret(),
|
||||||
// TODO: Decide on icon
|
// TODO: Decide on icon
|
||||||
NUZLOCKE: new ChallengeAchv(
|
NUZLOCKE: new ChallengeAchv("NUZLOCKE", "", "NUZLOCKE.description", "leaf_stone", 100, isNuzlockeChallenge),
|
||||||
"NUZLOCKE",
|
|
||||||
"",
|
|
||||||
"NUZLOCKE.description",
|
|
||||||
"leaf_stone",
|
|
||||||
100,
|
|
||||||
c =>
|
|
||||||
c instanceof LimitedCatchChallenge &&
|
|
||||||
c.value > 0 &&
|
|
||||||
globalScene.gameMode.challenges.some(c => c.id === Challenges.HARDCORE && c.value > 0) &&
|
|
||||||
globalScene.gameMode.challenges.some(c => c.id === Challenges.FRESH_START && c.value > 0),
|
|
||||||
),
|
|
||||||
BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 50).setSecret(),
|
BREEDERS_IN_SPACE: new Achv("BREEDERS_IN_SPACE", "", "BREEDERS_IN_SPACE.description", "moon_stone", 50).setSecret(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -48,6 +48,7 @@ import { EggData } from "#system/egg-data";
|
|||||||
import { GameStats } from "#system/game-stats";
|
import { GameStats } from "#system/game-stats";
|
||||||
import { ModifierData as PersistentModifierData } from "#system/modifier-data";
|
import { ModifierData as PersistentModifierData } from "#system/modifier-data";
|
||||||
import { PokemonData } from "#system/pokemon-data";
|
import { PokemonData } from "#system/pokemon-data";
|
||||||
|
import { RibbonData } from "#system/ribbons/ribbon-data";
|
||||||
import { resetSettings, SettingKeys, setSetting } from "#system/settings";
|
import { resetSettings, SettingKeys, setSetting } from "#system/settings";
|
||||||
import { SettingGamepad, setSettingGamepad, settingGamepadDefaults } from "#system/settings-gamepad";
|
import { SettingGamepad, setSettingGamepad, settingGamepadDefaults } from "#system/settings-gamepad";
|
||||||
import type { SettingKeyboard } from "#system/settings-keyboard";
|
import type { SettingKeyboard } from "#system/settings-keyboard";
|
||||||
@ -127,7 +128,8 @@ export interface SessionSaveData {
|
|||||||
battleType: BattleType;
|
battleType: BattleType;
|
||||||
trainer: TrainerData;
|
trainer: TrainerData;
|
||||||
gameVersion: string;
|
gameVersion: string;
|
||||||
runNameText: string;
|
/** The player-chosen name of the run */
|
||||||
|
name: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
challenges: ChallengeData[];
|
challenges: ChallengeData[];
|
||||||
mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME,
|
mysteryEncounterType: MysteryEncounterType | -1; // Only defined when current wave is ME,
|
||||||
@ -402,7 +404,7 @@ export class GameData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public initSystem(systemDataStr: string, cachedSystemDataStr?: string): Promise<boolean> {
|
public initSystem(systemDataStr: string, cachedSystemDataStr?: string): Promise<boolean> {
|
||||||
return new Promise<boolean>(resolve => {
|
const { promise, resolve } = Promise.withResolvers<boolean>();
|
||||||
try {
|
try {
|
||||||
let systemData = this.parseSystemData(systemDataStr);
|
let systemData = this.parseSystemData(systemDataStr);
|
||||||
|
|
||||||
@ -516,7 +518,7 @@ export class GameData {
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
}
|
}
|
||||||
});
|
return promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -627,6 +629,9 @@ export class GameData {
|
|||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
if (k === "ribbons") {
|
||||||
|
return RibbonData.fromJSON(v);
|
||||||
|
}
|
||||||
|
|
||||||
return k.endsWith("Attr") && !["natureAttr", "abilityAttr", "passiveAttr"].includes(k) ? BigInt(v ?? 0) : v;
|
return k.endsWith("Attr") && !["natureAttr", "abilityAttr", "passiveAttr"].includes(k) ? BigInt(v ?? 0) : v;
|
||||||
}) as SystemSaveData;
|
}) as SystemSaveData;
|
||||||
@ -982,21 +987,21 @@ export class GameData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async renameSession(slotId: number, newName: string): Promise<boolean> {
|
async renameSession(slotId: number, newName: string): Promise<boolean> {
|
||||||
return new Promise(async resolve => {
|
|
||||||
if (slotId < 0) {
|
if (slotId < 0) {
|
||||||
return resolve(false);
|
return false;
|
||||||
|
}
|
||||||
|
if (newName === "") {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
const sessionData: SessionSaveData | null = await this.getSession(slotId);
|
const sessionData: SessionSaveData | null = await this.getSession(slotId);
|
||||||
|
|
||||||
if (!sessionData) {
|
if (!sessionData) {
|
||||||
return resolve(false);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newName === "") {
|
sessionData.name = newName;
|
||||||
return resolve(true);
|
// update timestamp by 1 to ensure the session is saved
|
||||||
}
|
sessionData.timestamp += 1;
|
||||||
|
|
||||||
sessionData.runNameText = newName;
|
|
||||||
const updatedDataStr = JSON.stringify(sessionData);
|
const updatedDataStr = JSON.stringify(sessionData);
|
||||||
const encrypted = encrypt(updatedDataStr, bypassLogin);
|
const encrypted = encrypt(updatedDataStr, bypassLogin);
|
||||||
const secretId = this.secretId;
|
const secretId = this.secretId;
|
||||||
@ -1007,26 +1012,20 @@ export class GameData {
|
|||||||
`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`,
|
`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`,
|
||||||
encrypt(updatedDataStr, bypassLogin),
|
encrypt(updatedDataStr, bypassLogin),
|
||||||
);
|
);
|
||||||
resolve(true);
|
return true;
|
||||||
return;
|
}
|
||||||
|
|
||||||
|
const response = await pokerogueApi.savedata.session.update(
|
||||||
|
{ slot: slotId, trainerId, secretId, clientSessionId },
|
||||||
|
updatedDataStr,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
pokerogueApi.savedata.session
|
|
||||||
.update({ slot: slotId, trainerId, secretId, clientSessionId }, encrypted)
|
|
||||||
.then(error => {
|
|
||||||
if (error) {
|
|
||||||
console.error("Failed to update session name:", error);
|
|
||||||
resolve(false);
|
|
||||||
} else {
|
|
||||||
localStorage.setItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, encrypted);
|
localStorage.setItem(`sessionData${slotId ? slotId : ""}_${loggedInUser?.username}`, encrypted);
|
||||||
updateUserInfo().then(success => {
|
const success = await updateUserInfo();
|
||||||
if (success !== null && !success) {
|
return !(success !== null && !success);
|
||||||
return resolve(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
resolve(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
|
loadSession(slotId: number, sessionData?: SessionSaveData): Promise<boolean> {
|
||||||
@ -1634,6 +1633,7 @@ export class GameData {
|
|||||||
caughtCount: 0,
|
caughtCount: 0,
|
||||||
hatchedCount: 0,
|
hatchedCount: 0,
|
||||||
ivs: [0, 0, 0, 0, 0, 0],
|
ivs: [0, 0, 0, 0, 0, 0],
|
||||||
|
ribbons: new RibbonData(0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1878,6 +1878,12 @@ export class GameData {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Increase the number of classic ribbons won with this species.
|
||||||
|
* @param species - The species to increment the ribbon count for
|
||||||
|
* @param forStarter - If true, will increment the ribbon count for the root species of the given species
|
||||||
|
* @returns The number of classic wins after incrementing.
|
||||||
|
*/
|
||||||
incrementRibbonCount(species: PokemonSpecies, forStarter = false): number {
|
incrementRibbonCount(species: PokemonSpecies, forStarter = false): number {
|
||||||
const speciesIdToIncrement: SpeciesId = species.getRootSpeciesId(forStarter);
|
const speciesIdToIncrement: SpeciesId = species.getRootSpeciesId(forStarter);
|
||||||
|
|
||||||
@ -2177,6 +2183,9 @@ export class GameData {
|
|||||||
if (!entry.hasOwnProperty("natureAttr") || (entry.caughtAttr && !entry.natureAttr)) {
|
if (!entry.hasOwnProperty("natureAttr") || (entry.caughtAttr && !entry.natureAttr)) {
|
||||||
entry.natureAttr = this.defaultDexData?.[k].natureAttr || 1 << randInt(25, 1);
|
entry.natureAttr = this.defaultDexData?.[k].natureAttr || 1 << randInt(25, 1);
|
||||||
}
|
}
|
||||||
|
if (!entry.hasOwnProperty("ribbons")) {
|
||||||
|
entry.ribbons = new RibbonData(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
148
src/system/ribbons/ribbon-data.ts
Normal file
148
src/system/ribbons/ribbon-data.ts
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import type { Brander } from "#types/type-helpers";
|
||||||
|
|
||||||
|
export type RibbonFlag = (number & Brander<"RibbonFlag">) | 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for ribbon data management. Usually constructed via the {@linkcode fromJSON} method.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* Stores information about the ribbons earned by a species using a bitfield.
|
||||||
|
*/
|
||||||
|
export class RibbonData {
|
||||||
|
/** Internal bitfield storing the unlock state for each ribbon */
|
||||||
|
private payload: number;
|
||||||
|
|
||||||
|
//#region Ribbons
|
||||||
|
//#region Monotype challenge ribbons
|
||||||
|
/** Ribbon for winning the normal monotype challenge */
|
||||||
|
public static readonly MONO_NORMAL = 0x1 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the fighting monotype challenge */
|
||||||
|
public static readonly MONO_FIGHTING = 0x2 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the flying monotype challenge */
|
||||||
|
public static readonly MONO_FLYING = 0x4 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the poision monotype challenge */
|
||||||
|
public static readonly MONO_POISON = 0x8 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the ground monotype challenge */
|
||||||
|
public static readonly MONO_GROUND = 0x10 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the rock monotype challenge */
|
||||||
|
public static readonly MONO_ROCK = 0x20 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the bug monotype challenge */
|
||||||
|
public static readonly MONO_BUG = 0x40 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the ghost monotype challenge */
|
||||||
|
public static readonly MONO_GHOST = 0x80 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the steel monotype challenge */
|
||||||
|
public static readonly MONO_STEEL = 0x100 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the fire monotype challenge */
|
||||||
|
public static readonly MONO_FIRE = 0x200 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the water monotype challenge */
|
||||||
|
public static readonly MONO_WATER = 0x400 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the grass monotype challenge */
|
||||||
|
public static readonly MONO_GRASS = 0x800 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the electric monotype challenge */
|
||||||
|
public static readonly MONO_ELECTRIC = 0x1000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the psychic monotype challenge */
|
||||||
|
public static readonly MONO_PSYCHIC = 0x2000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the ice monotype challenge */
|
||||||
|
public static readonly MONO_ICE = 0x4000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the dragon monotype challenge */
|
||||||
|
public static readonly MONO_DRAGON = 0x8000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the dark monotype challenge */
|
||||||
|
public static readonly MONO_DARK = 0x10000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the fairy monotype challenge */
|
||||||
|
public static readonly MONO_FAIRY = 0x20000 as RibbonFlag;
|
||||||
|
//#endregion Monotype ribbons
|
||||||
|
|
||||||
|
//#region Monogen ribbons
|
||||||
|
/** Ribbon for winning the the mono gen 1 challenge */
|
||||||
|
public static readonly MONO_GEN_1 = 0x40000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the the mono gen 2 challenge */
|
||||||
|
public static readonly MONO_GEN_2 = 0x80000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the mono gen 3 challenge */
|
||||||
|
public static readonly MONO_GEN_3 = 0x100000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the mono gen 4 challenge */
|
||||||
|
public static readonly MONO_GEN_4 = 0x200000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the mono gen 5 challenge */
|
||||||
|
public static readonly MONO_GEN_5 = 0x400000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the mono gen 6 challenge */
|
||||||
|
public static readonly MONO_GEN_6 = 0x800000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the mono gen 7 challenge */
|
||||||
|
public static readonly MONO_GEN_7 = 0x1000000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the mono gen 8 challenge */
|
||||||
|
public static readonly MONO_GEN_8 = 0x2000000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the mono gen 9 challenge */
|
||||||
|
public static readonly MONO_GEN_9 = 0x4000000 as RibbonFlag;
|
||||||
|
//#endregion Monogen ribbons
|
||||||
|
|
||||||
|
/** Ribbon for winning classic */
|
||||||
|
public static readonly CLASSIC = 0x8000000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the nuzzlocke challenge */
|
||||||
|
public static readonly NUZLOCKE = 0x10000000 as RibbonFlag;
|
||||||
|
/** Ribbon for reaching max friendship */
|
||||||
|
public static readonly FRIENDSHIP = 0x20000000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the flip stats challenge */
|
||||||
|
public static readonly FLIP_STATS = 0x40000000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the inverse challenge */
|
||||||
|
public static readonly INVERSE = 0x80000000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the fresh start challenge */
|
||||||
|
public static readonly FRESH_START = 0x100000000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the hardcore challenge */
|
||||||
|
public static readonly HARDCORE = 0x200000000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the limited catch challenge */
|
||||||
|
public static readonly LIMITED_CATCH = 0x400000000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the limited support challenge set to no heal */
|
||||||
|
public static readonly NO_HEAL = 0x800000000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the limited uspport challenge set to no shop */
|
||||||
|
public static readonly NO_SHOP = 0x1000000000 as RibbonFlag;
|
||||||
|
/** Ribbon for winning the limited support challenge set to both*/
|
||||||
|
public static readonly NO_SUPPORT = 0x2000000000 as RibbonFlag;
|
||||||
|
|
||||||
|
// NOTE: max possible ribbon flag is 0x20000000000000 (53 total ribbons)
|
||||||
|
// Once this is exceeded, bitfield needs to be changed to a bigint or even a uint array
|
||||||
|
// Note that this has no impact on serialization as it is stored in hex.
|
||||||
|
|
||||||
|
//#endregion Ribbons
|
||||||
|
|
||||||
|
/** Create a new instance of RibbonData. Generally, {@linkcode fromJSON} is used instead. */
|
||||||
|
constructor(value: number) {
|
||||||
|
this.payload = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Serialize the bitfield payload as a hex encoded string */
|
||||||
|
public toJSON(): string {
|
||||||
|
return this.payload.toString(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a hexadecimal string representation of the bitfield into a `RibbonData` instance
|
||||||
|
*
|
||||||
|
* @param value - Hexadecimal string representation of the bitfield (without the leading 0x)
|
||||||
|
* @returns A new instance of `RibbonData` initialized with the provided bitfield.
|
||||||
|
*/
|
||||||
|
public static fromJSON(value: string): RibbonData {
|
||||||
|
try {
|
||||||
|
return new RibbonData(Number.parseInt(value, 16));
|
||||||
|
} catch {
|
||||||
|
return new RibbonData(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Award one or more ribbons to the ribbon data by setting the corresponding flags in the bitfield.
|
||||||
|
*
|
||||||
|
* @param flags - The flags to set. Can be a single flag or multiple flags.
|
||||||
|
*/
|
||||||
|
public award(...flags: [RibbonFlag, ...RibbonFlag[]]): void {
|
||||||
|
for (const f of flags) {
|
||||||
|
this.payload |= f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a specific ribbon has been awarded
|
||||||
|
* @param flag - The ribbon to check
|
||||||
|
* @returns Whether the specified flag has been awarded
|
||||||
|
*/
|
||||||
|
public has(flag: RibbonFlag): boolean {
|
||||||
|
return !!(this.payload & flag);
|
||||||
|
}
|
||||||
|
}
|
20
src/system/ribbons/ribbon-methods.ts
Normal file
20
src/system/ribbons/ribbon-methods.ts
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { globalScene } from "#app/global-scene";
|
||||||
|
import { pokemonPrevolutions } from "#balance/pokemon-evolutions";
|
||||||
|
import type { SpeciesId } from "#enums/species-id";
|
||||||
|
import type { RibbonFlag } from "#system/ribbons/ribbon-data";
|
||||||
|
import { isNullOrUndefined } from "#utils/common";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Award one or more ribbons to a species and its pre-evolutions
|
||||||
|
*
|
||||||
|
* @param id - The ID of the species to award ribbons to
|
||||||
|
* @param ribbons - The ribbon(s) to award (use bitwise OR to combine multiple)
|
||||||
|
*/
|
||||||
|
export function awardRibbonsToSpeciesLine(id: SpeciesId, ribbons: RibbonFlag): void {
|
||||||
|
const dexData = globalScene.gameData.dexData;
|
||||||
|
dexData[id].ribbons.award(ribbons);
|
||||||
|
// Mark all pre-evolutions of the Pokémon with the same ribbon flags.
|
||||||
|
for (let prevoId = pokemonPrevolutions[id]; !isNullOrUndefined(prevoId); prevoId = pokemonPrevolutions[prevoId]) {
|
||||||
|
dexData[id].ribbons.award(ribbons);
|
||||||
|
}
|
||||||
|
}
|
@ -31,6 +31,11 @@ import { toTitleCase } from "#utils/strings";
|
|||||||
import i18next from "i18next";
|
import i18next from "i18next";
|
||||||
import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
|
import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
|
||||||
|
|
||||||
|
const DISCARD_BUTTON_X = 60;
|
||||||
|
const DISCARD_BUTTON_X_DOUBLES = 64;
|
||||||
|
const DISCARD_BUTTON_Y = -73;
|
||||||
|
const DISCARD_BUTTON_Y_DOUBLES = -58;
|
||||||
|
|
||||||
const defaultMessage = i18next.t("partyUiHandler:choosePokemon");
|
const defaultMessage = i18next.t("partyUiHandler:choosePokemon");
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -301,7 +306,7 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
const partyMessageText = addTextObject(10, 8, defaultMessage, TextStyle.WINDOW, { maxLines: 2 });
|
const partyMessageText = addTextObject(10, 8, defaultMessage, TextStyle.WINDOW, { maxLines: 2 });
|
||||||
partyMessageText.setName("text-party-msg");
|
partyMessageText.setName("text-party-msg");
|
||||||
|
|
||||||
partyMessageText.setOrigin(0, 0);
|
partyMessageText.setOrigin(0);
|
||||||
partyMessageBoxContainer.add(partyMessageText);
|
partyMessageBoxContainer.add(partyMessageText);
|
||||||
|
|
||||||
this.message = partyMessageText;
|
this.message = partyMessageText;
|
||||||
@ -317,10 +322,8 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
this.iconAnimHandler = new PokemonIconAnimHandler();
|
this.iconAnimHandler = new PokemonIconAnimHandler();
|
||||||
this.iconAnimHandler.setup();
|
this.iconAnimHandler.setup();
|
||||||
|
|
||||||
const partyDiscardModeButton = new PartyDiscardModeButton(60, -globalScene.game.canvas.height / 15 - 1, this);
|
const partyDiscardModeButton = new PartyDiscardModeButton(DISCARD_BUTTON_X, DISCARD_BUTTON_Y, this);
|
||||||
|
|
||||||
partyContainer.add(partyDiscardModeButton);
|
partyContainer.add(partyDiscardModeButton);
|
||||||
|
|
||||||
this.partyDiscardModeButton = partyDiscardModeButton;
|
this.partyDiscardModeButton = partyDiscardModeButton;
|
||||||
|
|
||||||
// prepare move overlay
|
// prepare move overlay
|
||||||
@ -1233,7 +1236,7 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
}
|
}
|
||||||
if (!this.optionsCursorObj) {
|
if (!this.optionsCursorObj) {
|
||||||
this.optionsCursorObj = globalScene.add.image(0, 0, "cursor");
|
this.optionsCursorObj = globalScene.add.image(0, 0, "cursor");
|
||||||
this.optionsCursorObj.setOrigin(0, 0);
|
this.optionsCursorObj.setOrigin(0);
|
||||||
this.optionsContainer.add(this.optionsCursorObj);
|
this.optionsContainer.add(this.optionsCursorObj);
|
||||||
}
|
}
|
||||||
this.optionsCursorObj.setPosition(
|
this.optionsCursorObj.setPosition(
|
||||||
@ -1605,7 +1608,7 @@ export class PartyUiHandler extends MessageUiHandler {
|
|||||||
optionText.setColor("#40c8f8");
|
optionText.setColor("#40c8f8");
|
||||||
optionText.setShadowColor("#006090");
|
optionText.setShadowColor("#006090");
|
||||||
}
|
}
|
||||||
optionText.setOrigin(0, 0);
|
optionText.setOrigin(0);
|
||||||
|
|
||||||
/** For every item that has stack bigger than 1, display the current quantity selection */
|
/** For every item that has stack bigger than 1, display the current quantity selection */
|
||||||
const itemModifiers = this.getItemModifiers(pokemon);
|
const itemModifiers = this.getItemModifiers(pokemon);
|
||||||
@ -1802,6 +1805,7 @@ class PartySlot extends Phaser.GameObjects.Container {
|
|||||||
private selected: boolean;
|
private selected: boolean;
|
||||||
private transfer: boolean;
|
private transfer: boolean;
|
||||||
private slotIndex: number;
|
private slotIndex: number;
|
||||||
|
private isBenched: boolean;
|
||||||
private pokemon: PlayerPokemon;
|
private pokemon: PlayerPokemon;
|
||||||
|
|
||||||
private slotBg: Phaser.GameObjects.Image;
|
private slotBg: Phaser.GameObjects.Image;
|
||||||
@ -1812,6 +1816,7 @@ class PartySlot extends Phaser.GameObjects.Container {
|
|||||||
public slotHpText: Phaser.GameObjects.Text;
|
public slotHpText: Phaser.GameObjects.Text;
|
||||||
public slotDescriptionLabel: Phaser.GameObjects.Text; // this is used to show text instead of the HP bar i.e. for showing "Able"/"Not Able" for TMs when you try to learn them
|
public slotDescriptionLabel: Phaser.GameObjects.Text; // this is used to show text instead of the HP bar i.e. for showing "Able"/"Not Able" for TMs when you try to learn them
|
||||||
|
|
||||||
|
private slotBgKey: string;
|
||||||
private pokemonIcon: Phaser.GameObjects.Container;
|
private pokemonIcon: Phaser.GameObjects.Container;
|
||||||
private iconAnimHandler: PokemonIconAnimHandler;
|
private iconAnimHandler: PokemonIconAnimHandler;
|
||||||
|
|
||||||
@ -1822,19 +1827,34 @@ class PartySlot extends Phaser.GameObjects.Container {
|
|||||||
partyUiMode: PartyUiMode,
|
partyUiMode: PartyUiMode,
|
||||||
tmMoveId: MoveId,
|
tmMoveId: MoveId,
|
||||||
) {
|
) {
|
||||||
super(
|
const isBenched = slotIndex >= globalScene.currentBattle.getBattlerCount();
|
||||||
globalScene,
|
const isDoubleBattle = globalScene.currentBattle.double;
|
||||||
slotIndex >= globalScene.currentBattle.getBattlerCount() ? 230.5 : 64,
|
const isItemManageMode = partyUiMode === PartyUiMode.MODIFIER_TRANSFER || partyUiMode === PartyUiMode.DISCARD;
|
||||||
slotIndex >= globalScene.currentBattle.getBattlerCount()
|
|
||||||
? -184 +
|
/*
|
||||||
(globalScene.currentBattle.double ? -40 : 0) +
|
* Here we determine the position of the slot.
|
||||||
(28 + (globalScene.currentBattle.double ? 8 : 0)) * slotIndex
|
* The x coordinate depends on whether the pokemon is on the field or in the bench.
|
||||||
: partyUiMode === PartyUiMode.MODIFIER_TRANSFER
|
* The y coordinate depends on various factors, such as the number of pokémon on the field,
|
||||||
? -124 + (globalScene.currentBattle.double ? -20 : 0) + slotIndex * 55
|
* and whether the transfer/discard button is also on the screen.
|
||||||
: -124 + (globalScene.currentBattle.double ? -8 : 0) + slotIndex * 64,
|
*/
|
||||||
);
|
const slotPositionX = isBenched ? 143 : 9;
|
||||||
|
|
||||||
|
let slotPositionY: number;
|
||||||
|
if (isBenched) {
|
||||||
|
slotPositionY = -196 + (isDoubleBattle ? -40 : 0);
|
||||||
|
slotPositionY += (28 + (isDoubleBattle ? 8 : 0)) * slotIndex;
|
||||||
|
} else {
|
||||||
|
slotPositionY = -148.5;
|
||||||
|
if (isDoubleBattle) {
|
||||||
|
slotPositionY += isItemManageMode ? -20 : -8;
|
||||||
|
}
|
||||||
|
slotPositionY += (isItemManageMode ? (isDoubleBattle ? 47 : 55) : 64) * slotIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
super(globalScene, slotPositionX, slotPositionY);
|
||||||
|
|
||||||
this.slotIndex = slotIndex;
|
this.slotIndex = slotIndex;
|
||||||
|
this.isBenched = isBenched;
|
||||||
this.pokemon = pokemon;
|
this.pokemon = pokemon;
|
||||||
this.iconAnimHandler = iconAnimHandler;
|
this.iconAnimHandler = iconAnimHandler;
|
||||||
|
|
||||||
@ -1848,27 +1868,75 @@ class PartySlot extends Phaser.GameObjects.Container {
|
|||||||
setup(partyUiMode: PartyUiMode, tmMoveId: MoveId) {
|
setup(partyUiMode: PartyUiMode, tmMoveId: MoveId) {
|
||||||
const currentLanguage = i18next.resolvedLanguage ?? "en";
|
const currentLanguage = i18next.resolvedLanguage ?? "en";
|
||||||
const offsetJa = currentLanguage === "ja";
|
const offsetJa = currentLanguage === "ja";
|
||||||
|
const isItemManageMode = partyUiMode === PartyUiMode.MODIFIER_TRANSFER || partyUiMode === PartyUiMode.DISCARD;
|
||||||
|
|
||||||
const battlerCount = globalScene.currentBattle.getBattlerCount();
|
this.slotBgKey = this.isBenched
|
||||||
|
? "party_slot"
|
||||||
|
: isItemManageMode && globalScene.currentBattle.double
|
||||||
|
? "party_slot_main_short"
|
||||||
|
: "party_slot_main";
|
||||||
|
const fullSlotBgKey = this.pokemon.hp ? this.slotBgKey : `${this.slotBgKey}${"_fnt"}`;
|
||||||
|
this.slotBg = globalScene.add.sprite(0, 0, this.slotBgKey, fullSlotBgKey);
|
||||||
|
this.slotBg.setOrigin(0);
|
||||||
|
this.add(this.slotBg);
|
||||||
|
|
||||||
const slotKey = `party_slot${this.slotIndex >= battlerCount ? "" : "_main"}`;
|
const genderSymbol = getGenderSymbol(this.pokemon.getGender(true));
|
||||||
|
const isFusion = this.pokemon.isFusion();
|
||||||
|
|
||||||
const slotBg = globalScene.add.sprite(0, 0, slotKey, `${slotKey}${this.pokemon.hp ? "" : "_fnt"}`);
|
// Here we define positions and offsets
|
||||||
this.slotBg = slotBg;
|
// Base values are for the active pokemon; they are changed for benched pokemon,
|
||||||
|
// or for active pokemon if in a double battle in item management mode.
|
||||||
|
|
||||||
this.add(slotBg);
|
// icon position relative to slot background
|
||||||
|
let slotPb = { x: 4, y: 4 };
|
||||||
|
// name position relative to slot background
|
||||||
|
let namePosition = { x: 24, y: 10 + (offsetJa ? 2 : 0) };
|
||||||
|
// maximum allowed length of name; must accomodate fusion symbol
|
||||||
|
let maxNameTextWidth = 76 - (isFusion ? 8 : 0);
|
||||||
|
// "Lv." label position relative to slot background
|
||||||
|
let levelLabelPosition = { x: 24 + 8, y: 10 + 12 };
|
||||||
|
// offset from "Lv." to the level number; should not be changed.
|
||||||
|
const levelTextToLevelLabelOffset = { x: 9, y: offsetJa ? 1.5 : 0 };
|
||||||
|
// offests from "Lv." to gender, spliced and status icons, these depend on the type of slot.
|
||||||
|
let genderTextToLevelLabelOffset = { x: 68 - (isFusion ? 8 : 0), y: -9 };
|
||||||
|
let splicedIconToLevelLabelOffset = { x: 68, y: 3.5 - 12 };
|
||||||
|
let statusIconToLevelLabelOffset = { x: 55, y: 0 };
|
||||||
|
// offset from the name to the shiny icon (on the left); should not be changed.
|
||||||
|
const shinyIconToNameOffset = { x: -9, y: 3 };
|
||||||
|
// hp bar position relative to slot background
|
||||||
|
let hpBarPosition = { x: 8, y: 31 };
|
||||||
|
// offsets of hp bar overlay (showing the remaining hp) and number; should not be changed.
|
||||||
|
const hpOverlayToBarOffset = { x: 16, y: 2 };
|
||||||
|
const hpTextToBarOffset = { x: -3, y: -2 + (offsetJa ? 2 : 0) };
|
||||||
|
// description position relative to slot background
|
||||||
|
let descriptionLabelPosition = { x: 32, y: 46 };
|
||||||
|
|
||||||
const slotPb = globalScene.add.sprite(
|
// If in item management mode, the active slots are shorter
|
||||||
this.slotIndex >= battlerCount ? -85.5 : -51,
|
if (isItemManageMode && globalScene.currentBattle.double && !this.isBenched) {
|
||||||
this.slotIndex >= battlerCount ? 0 : -20.5,
|
namePosition.y -= 8;
|
||||||
"party_pb",
|
levelLabelPosition.y -= 8;
|
||||||
);
|
hpBarPosition.y -= 8;
|
||||||
this.slotPb = slotPb;
|
descriptionLabelPosition.y -= 8;
|
||||||
|
}
|
||||||
|
|
||||||
this.add(slotPb);
|
// Benched slots have significantly different parameters
|
||||||
|
if (this.isBenched) {
|
||||||
|
slotPb = { x: 2, y: 12 };
|
||||||
|
namePosition = { x: 21, y: 2 + (offsetJa ? 2 : 0) };
|
||||||
|
maxNameTextWidth = 52;
|
||||||
|
levelLabelPosition = { x: 21 + 8, y: 2 + 12 };
|
||||||
|
genderTextToLevelLabelOffset = { x: 36, y: 0 };
|
||||||
|
splicedIconToLevelLabelOffset = { x: 36 + (genderSymbol ? 8 : 0), y: 0.5 };
|
||||||
|
statusIconToLevelLabelOffset = { x: 43, y: 0 };
|
||||||
|
hpBarPosition = { x: 72, y: 6 };
|
||||||
|
descriptionLabelPosition = { x: 94, y: 16 };
|
||||||
|
}
|
||||||
|
|
||||||
this.pokemonIcon = globalScene.addPokemonIcon(this.pokemon, slotPb.x, slotPb.y, 0.5, 0.5, true);
|
this.slotPb = globalScene.add.sprite(0, 0, "party_pb");
|
||||||
|
this.slotPb.setPosition(slotPb.x, slotPb.y);
|
||||||
|
this.add(this.slotPb);
|
||||||
|
|
||||||
|
this.pokemonIcon = globalScene.addPokemonIcon(this.pokemon, this.slotPb.x, this.slotPb.y, 0.5, 0.5, true);
|
||||||
this.add(this.pokemonIcon);
|
this.add(this.pokemonIcon);
|
||||||
|
|
||||||
this.iconAnimHandler.addOrUpdate(this.pokemonIcon, PokemonIconAnimMode.PASSIVE);
|
this.iconAnimHandler.addOrUpdate(this.pokemonIcon, PokemonIconAnimMode.PASSIVE);
|
||||||
@ -1882,7 +1950,7 @@ class PartySlot extends Phaser.GameObjects.Container {
|
|||||||
const nameSizeTest = addTextObject(0, 0, displayName, TextStyle.PARTY);
|
const nameSizeTest = addTextObject(0, 0, displayName, TextStyle.PARTY);
|
||||||
nameTextWidth = nameSizeTest.displayWidth;
|
nameTextWidth = nameSizeTest.displayWidth;
|
||||||
|
|
||||||
while (nameTextWidth > (this.slotIndex >= battlerCount ? 52 : 76 - (this.pokemon.fusionSpecies ? 8 : 0))) {
|
while (nameTextWidth > maxNameTextWidth) {
|
||||||
displayName = `${displayName.slice(0, displayName.endsWith(".") ? -2 : -1).trimEnd()}.`;
|
displayName = `${displayName.slice(0, displayName.endsWith(".") ? -2 : -1).trimEnd()}.`;
|
||||||
nameSizeTest.setText(displayName);
|
nameSizeTest.setText(displayName);
|
||||||
nameTextWidth = nameSizeTest.displayWidth;
|
nameTextWidth = nameSizeTest.displayWidth;
|
||||||
@ -1891,78 +1959,59 @@ class PartySlot extends Phaser.GameObjects.Container {
|
|||||||
nameSizeTest.destroy();
|
nameSizeTest.destroy();
|
||||||
|
|
||||||
this.slotName = addTextObject(0, 0, displayName, TextStyle.PARTY);
|
this.slotName = addTextObject(0, 0, displayName, TextStyle.PARTY);
|
||||||
this.slotName.setPositionRelative(
|
this.slotName.setPositionRelative(this.slotBg, namePosition.x, namePosition.y);
|
||||||
slotBg,
|
this.slotName.setOrigin(0);
|
||||||
this.slotIndex >= battlerCount ? 21 : 24,
|
|
||||||
(this.slotIndex >= battlerCount ? 2 : 10) + (offsetJa ? 2 : 0),
|
|
||||||
);
|
|
||||||
this.slotName.setOrigin(0, 0);
|
|
||||||
|
|
||||||
const slotLevelLabel = globalScene.add.image(0, 0, "party_slot_overlay_lv");
|
const slotLevelLabel = globalScene.add
|
||||||
slotLevelLabel.setPositionRelative(
|
.image(0, 0, "party_slot_overlay_lv")
|
||||||
slotBg,
|
.setPositionRelative(this.slotBg, levelLabelPosition.x, levelLabelPosition.y)
|
||||||
(this.slotIndex >= battlerCount ? 21 : 24) + 8,
|
.setOrigin(0);
|
||||||
(this.slotIndex >= battlerCount ? 2 : 10) + 12,
|
|
||||||
);
|
|
||||||
slotLevelLabel.setOrigin(0, 0);
|
|
||||||
|
|
||||||
const slotLevelText = addTextObject(
|
const slotLevelText = addTextObject(
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
this.pokemon.level.toString(),
|
this.pokemon.level.toString(),
|
||||||
this.pokemon.level < globalScene.getMaxExpLevel() ? TextStyle.PARTY : TextStyle.PARTY_RED,
|
this.pokemon.level < globalScene.getMaxExpLevel() ? TextStyle.PARTY : TextStyle.PARTY_RED,
|
||||||
);
|
)
|
||||||
slotLevelText.setPositionRelative(slotLevelLabel, 9, offsetJa ? 1.5 : 0);
|
.setPositionRelative(slotLevelLabel, levelTextToLevelLabelOffset.x, levelTextToLevelLabelOffset.y)
|
||||||
slotLevelText.setOrigin(0, 0.25);
|
.setOrigin(0, 0.25);
|
||||||
|
|
||||||
slotInfoContainer.add([this.slotName, slotLevelLabel, slotLevelText]);
|
slotInfoContainer.add([this.slotName, slotLevelLabel, slotLevelText]);
|
||||||
|
|
||||||
const genderSymbol = getGenderSymbol(this.pokemon.getGender(true));
|
|
||||||
|
|
||||||
if (genderSymbol) {
|
if (genderSymbol) {
|
||||||
const slotGenderText = addTextObject(0, 0, genderSymbol, TextStyle.PARTY);
|
const slotGenderText = addTextObject(0, 0, genderSymbol, TextStyle.PARTY)
|
||||||
slotGenderText.setColor(getGenderColor(this.pokemon.getGender(true)));
|
.setColor(getGenderColor(this.pokemon.getGender(true)))
|
||||||
slotGenderText.setShadowColor(getGenderColor(this.pokemon.getGender(true), true));
|
.setShadowColor(getGenderColor(this.pokemon.getGender(true), true))
|
||||||
if (this.slotIndex >= battlerCount) {
|
.setPositionRelative(slotLevelLabel, genderTextToLevelLabelOffset.x, genderTextToLevelLabelOffset.y)
|
||||||
slotGenderText.setPositionRelative(slotLevelLabel, 36, 0);
|
.setOrigin(0, 0.25);
|
||||||
} else {
|
|
||||||
slotGenderText.setPositionRelative(this.slotName, 76 - (this.pokemon.fusionSpecies ? 8 : 0), 3);
|
|
||||||
}
|
|
||||||
slotGenderText.setOrigin(0, 0.25);
|
|
||||||
|
|
||||||
slotInfoContainer.add(slotGenderText);
|
slotInfoContainer.add(slotGenderText);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.pokemon.fusionSpecies) {
|
if (isFusion) {
|
||||||
const splicedIcon = globalScene.add.image(0, 0, "icon_spliced");
|
const splicedIcon = globalScene.add
|
||||||
splicedIcon.setScale(0.5);
|
.image(0, 0, "icon_spliced")
|
||||||
splicedIcon.setOrigin(0, 0);
|
.setScale(0.5)
|
||||||
if (this.slotIndex >= battlerCount) {
|
.setOrigin(0)
|
||||||
splicedIcon.setPositionRelative(slotLevelLabel, 36 + (genderSymbol ? 8 : 0), 0.5);
|
.setPositionRelative(slotLevelLabel, splicedIconToLevelLabelOffset.x, splicedIconToLevelLabelOffset.y);
|
||||||
} else {
|
|
||||||
splicedIcon.setPositionRelative(this.slotName, 76, 3.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
slotInfoContainer.add(splicedIcon);
|
slotInfoContainer.add(splicedIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.pokemon.status) {
|
if (this.pokemon.status) {
|
||||||
const statusIndicator = globalScene.add.sprite(0, 0, getLocalizedSpriteKey("statuses"));
|
const statusIndicator = globalScene.add
|
||||||
statusIndicator.setFrame(StatusEffect[this.pokemon.status?.effect].toLowerCase());
|
.sprite(0, 0, getLocalizedSpriteKey("statuses"))
|
||||||
statusIndicator.setOrigin(0, 0);
|
.setFrame(StatusEffect[this.pokemon.status?.effect].toLowerCase())
|
||||||
statusIndicator.setPositionRelative(slotLevelLabel, this.slotIndex >= battlerCount ? 43 : 55, 0);
|
.setOrigin(0)
|
||||||
|
.setPositionRelative(slotLevelLabel, statusIconToLevelLabelOffset.x, statusIconToLevelLabelOffset.y);
|
||||||
slotInfoContainer.add(statusIndicator);
|
slotInfoContainer.add(statusIndicator);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.pokemon.isShiny()) {
|
if (this.pokemon.isShiny()) {
|
||||||
const doubleShiny = this.pokemon.isDoubleShiny(false);
|
const doubleShiny = this.pokemon.isDoubleShiny(false);
|
||||||
|
|
||||||
const shinyStar = globalScene.add.image(0, 0, `shiny_star_small${doubleShiny ? "_1" : ""}`);
|
const shinyStar = globalScene.add
|
||||||
shinyStar.setOrigin(0, 0);
|
.image(0, 0, `shiny_star_small${doubleShiny ? "_1" : ""}`)
|
||||||
shinyStar.setPositionRelative(this.slotName, -9, 3);
|
.setOrigin(0)
|
||||||
shinyStar.setTint(getVariantTint(this.pokemon.getBaseVariant()));
|
.setPositionRelative(this.slotName, shinyIconToNameOffset.x, shinyIconToNameOffset.y)
|
||||||
|
.setTint(getVariantTint(this.pokemon.getBaseVariant()));
|
||||||
slotInfoContainer.add(shinyStar);
|
slotInfoContainer.add(shinyStar);
|
||||||
|
|
||||||
if (doubleShiny) {
|
if (doubleShiny) {
|
||||||
@ -1971,50 +2020,38 @@ class PartySlot extends Phaser.GameObjects.Container {
|
|||||||
.setOrigin(0)
|
.setOrigin(0)
|
||||||
.setPosition(shinyStar.x, shinyStar.y)
|
.setPosition(shinyStar.x, shinyStar.y)
|
||||||
.setTint(getVariantTint(this.pokemon.fusionVariant));
|
.setTint(getVariantTint(this.pokemon.fusionVariant));
|
||||||
|
|
||||||
slotInfoContainer.add(fusionShinyStar);
|
slotInfoContainer.add(fusionShinyStar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.slotHpBar = globalScene.add.image(0, 0, "party_slot_hp_bar");
|
this.slotHpBar = globalScene.add
|
||||||
this.slotHpBar.setPositionRelative(
|
.image(0, 0, "party_slot_hp_bar")
|
||||||
slotBg,
|
.setOrigin(0)
|
||||||
this.slotIndex >= battlerCount ? 72 : 8,
|
.setVisible(false)
|
||||||
this.slotIndex >= battlerCount ? 6 : 31,
|
.setPositionRelative(this.slotBg, hpBarPosition.x, hpBarPosition.y);
|
||||||
);
|
|
||||||
this.slotHpBar.setOrigin(0, 0);
|
|
||||||
this.slotHpBar.setVisible(false);
|
|
||||||
|
|
||||||
const hpRatio = this.pokemon.getHpRatio();
|
const hpRatio = this.pokemon.getHpRatio();
|
||||||
|
|
||||||
this.slotHpOverlay = globalScene.add.sprite(
|
this.slotHpOverlay = globalScene.add
|
||||||
0,
|
.sprite(0, 0, "party_slot_hp_overlay", hpRatio > 0.5 ? "high" : hpRatio > 0.25 ? "medium" : "low")
|
||||||
0,
|
.setOrigin(0)
|
||||||
"party_slot_hp_overlay",
|
.setPositionRelative(this.slotHpBar, hpOverlayToBarOffset.x, hpOverlayToBarOffset.y)
|
||||||
hpRatio > 0.5 ? "high" : hpRatio > 0.25 ? "medium" : "low",
|
.setScale(hpRatio, 1)
|
||||||
);
|
.setVisible(false);
|
||||||
this.slotHpOverlay.setPositionRelative(this.slotHpBar, 16, 2);
|
|
||||||
this.slotHpOverlay.setOrigin(0, 0);
|
|
||||||
this.slotHpOverlay.setScale(hpRatio, 1);
|
|
||||||
this.slotHpOverlay.setVisible(false);
|
|
||||||
|
|
||||||
this.slotHpText = addTextObject(0, 0, `${this.pokemon.hp}/${this.pokemon.getMaxHp()}`, TextStyle.PARTY);
|
this.slotHpText = addTextObject(0, 0, `${this.pokemon.hp}/${this.pokemon.getMaxHp()}`, TextStyle.PARTY)
|
||||||
this.slotHpText.setPositionRelative(
|
.setOrigin(1, 0)
|
||||||
|
.setPositionRelative(
|
||||||
this.slotHpBar,
|
this.slotHpBar,
|
||||||
this.slotHpBar.width - 3,
|
this.slotHpBar.width + hpTextToBarOffset.x,
|
||||||
this.slotHpBar.height - 2 + (offsetJa ? 2 : 0),
|
this.slotHpBar.height + hpTextToBarOffset.y,
|
||||||
);
|
) // TODO: annoying because it contains the width
|
||||||
this.slotHpText.setOrigin(1, 0);
|
.setVisible(false);
|
||||||
this.slotHpText.setVisible(false);
|
|
||||||
|
|
||||||
this.slotDescriptionLabel = addTextObject(0, 0, "", TextStyle.MESSAGE);
|
this.slotDescriptionLabel = addTextObject(0, 0, "", TextStyle.MESSAGE)
|
||||||
this.slotDescriptionLabel.setPositionRelative(
|
.setOrigin(0, 1)
|
||||||
slotBg,
|
.setVisible(false)
|
||||||
this.slotIndex >= battlerCount ? 94 : 32,
|
.setPositionRelative(this.slotBg, descriptionLabelPosition.x, descriptionLabelPosition.y);
|
||||||
this.slotIndex >= battlerCount ? 16 : 46,
|
|
||||||
);
|
|
||||||
this.slotDescriptionLabel.setOrigin(0, 1);
|
|
||||||
this.slotDescriptionLabel.setVisible(false);
|
|
||||||
|
|
||||||
slotInfoContainer.add([this.slotHpBar, this.slotHpOverlay, this.slotHpText, this.slotDescriptionLabel]);
|
slotInfoContainer.add([this.slotHpBar, this.slotHpOverlay, this.slotHpText, this.slotDescriptionLabel]);
|
||||||
|
|
||||||
@ -2076,10 +2113,9 @@ class PartySlot extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateSlotTexture(): void {
|
private updateSlotTexture(): void {
|
||||||
const battlerCount = globalScene.currentBattle.getBattlerCount();
|
|
||||||
this.slotBg.setTexture(
|
this.slotBg.setTexture(
|
||||||
`party_slot${this.slotIndex >= battlerCount ? "" : "_main"}`,
|
this.slotBgKey,
|
||||||
`party_slot${this.slotIndex >= battlerCount ? "" : "_main"}${this.transfer ? "_swap" : this.pokemon.hp ? "" : "_fnt"}${this.selected ? "_sel" : ""}`,
|
`${this.slotBgKey}${this.transfer ? "_swap" : this.pokemon.hp ? "" : "_fnt"}${this.selected ? "_sel" : ""}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2198,10 +2234,6 @@ class PartyDiscardModeButton extends Phaser.GameObjects.Container {
|
|||||||
this.discardIcon.setVisible(false);
|
this.discardIcon.setVisible(false);
|
||||||
this.textBox.setVisible(true);
|
this.textBox.setVisible(true);
|
||||||
this.textBox.setText(i18next.t("partyUiHandler:TRANSFER"));
|
this.textBox.setText(i18next.t("partyUiHandler:TRANSFER"));
|
||||||
this.setPosition(
|
|
||||||
globalScene.currentBattle.double ? 64 : 60,
|
|
||||||
globalScene.currentBattle.double ? -48 : -globalScene.game.canvas.height / 15 - 1,
|
|
||||||
);
|
|
||||||
this.transferIcon.displayWidth = this.textBox.text.length * 9 + 3;
|
this.transferIcon.displayWidth = this.textBox.text.length * 9 + 3;
|
||||||
break;
|
break;
|
||||||
case PartyUiMode.DISCARD:
|
case PartyUiMode.DISCARD:
|
||||||
@ -2209,13 +2241,13 @@ class PartyDiscardModeButton extends Phaser.GameObjects.Container {
|
|||||||
this.discardIcon.setVisible(true);
|
this.discardIcon.setVisible(true);
|
||||||
this.textBox.setVisible(true);
|
this.textBox.setVisible(true);
|
||||||
this.textBox.setText(i18next.t("partyUiHandler:DISCARD"));
|
this.textBox.setText(i18next.t("partyUiHandler:DISCARD"));
|
||||||
this.setPosition(
|
|
||||||
globalScene.currentBattle.double ? 64 : 60,
|
|
||||||
globalScene.currentBattle.double ? -48 : -globalScene.game.canvas.height / 15 - 1,
|
|
||||||
);
|
|
||||||
this.discardIcon.displayWidth = this.textBox.text.length * 9 + 3;
|
this.discardIcon.displayWidth = this.textBox.text.length * 9 + 3;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
this.setPosition(
|
||||||
|
globalScene.currentBattle.double ? DISCARD_BUTTON_X_DOUBLES : DISCARD_BUTTON_X,
|
||||||
|
globalScene.currentBattle.double ? DISCARD_BUTTON_Y_DOUBLES : DISCARD_BUTTON_Y,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
|
@ -208,7 +208,7 @@ export class RunInfoUiHandler extends UiHandler {
|
|||||||
headerText.setOrigin(0, 0);
|
headerText.setOrigin(0, 0);
|
||||||
headerText.setPositionRelative(headerBg, 8, 4);
|
headerText.setPositionRelative(headerBg, 8, 4);
|
||||||
this.runContainer.add(headerText);
|
this.runContainer.add(headerText);
|
||||||
const runName = addTextObject(0, 0, this.runInfo.runNameText, TextStyle.WINDOW);
|
const runName = addTextObject(0, 0, this.runInfo.name, TextStyle.WINDOW);
|
||||||
runName.setOrigin(0, 0);
|
runName.setOrigin(0, 0);
|
||||||
runName.setPositionRelative(headerBg, 60, 4);
|
runName.setPositionRelative(headerBg, 60, 4);
|
||||||
this.runContainer.add(runName);
|
this.runContainer.add(runName);
|
||||||
|
@ -377,7 +377,7 @@ export class SaveSlotSelectUiHandler extends MessageUiHandler {
|
|||||||
"select_cursor_highlight_thick",
|
"select_cursor_highlight_thick",
|
||||||
undefined,
|
undefined,
|
||||||
294,
|
294,
|
||||||
this.sessionSlots[prevSlotIndex ?? 0]?.saveData?.runNameText ? 50 : 60,
|
this.sessionSlots[prevSlotIndex ?? 0]?.saveData?.name ? 50 : 60,
|
||||||
6,
|
6,
|
||||||
6,
|
6,
|
||||||
6,
|
6,
|
||||||
@ -553,10 +553,10 @@ class SessionSlot extends Phaser.GameObjects.Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setupWithData(data: SessionSaveData) {
|
async setupWithData(data: SessionSaveData) {
|
||||||
const hasName = data?.runNameText;
|
const hasName = data?.name;
|
||||||
this.remove(this.loadingLabel, true);
|
this.remove(this.loadingLabel, true);
|
||||||
if (hasName) {
|
if (hasName) {
|
||||||
const nameLabel = addTextObject(8, 5, data.runNameText, TextStyle.WINDOW);
|
const nameLabel = addTextObject(8, 5, data.name, TextStyle.WINDOW);
|
||||||
this.add(nameLabel);
|
this.add(nameLabel);
|
||||||
} else {
|
} else {
|
||||||
const fallbackName = this.decideFallback(data);
|
const fallbackName = this.decideFallback(data);
|
||||||
|
@ -45,6 +45,7 @@ import type { Variant } from "#sprites/variant";
|
|||||||
import { getVariantIcon, getVariantTint } from "#sprites/variant";
|
import { getVariantIcon, getVariantTint } from "#sprites/variant";
|
||||||
import { achvs } from "#system/achv";
|
import { achvs } from "#system/achv";
|
||||||
import type { DexAttrProps, StarterAttributes, StarterMoveset } from "#system/game-data";
|
import type { DexAttrProps, StarterAttributes, StarterMoveset } from "#system/game-data";
|
||||||
|
import { RibbonData } from "#system/ribbons/ribbon-data";
|
||||||
import { SettingKeyboard } from "#system/settings-keyboard";
|
import { SettingKeyboard } from "#system/settings-keyboard";
|
||||||
import type { DexEntry } from "#types/dex-data";
|
import type { DexEntry } from "#types/dex-data";
|
||||||
import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler";
|
||||||
@ -3226,6 +3227,8 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
onScreenFirstIndex + maxRows * maxColumns - 1,
|
onScreenFirstIndex + maxRows * maxColumns - 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const gameData = globalScene.gameData;
|
||||||
|
|
||||||
this.starterSelectScrollBar.setScrollCursor(this.scrollCursor);
|
this.starterSelectScrollBar.setScrollCursor(this.scrollCursor);
|
||||||
|
|
||||||
let pokerusCursorIndex = 0;
|
let pokerusCursorIndex = 0;
|
||||||
@ -3265,9 +3268,9 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
|
|
||||||
container.label.setVisible(true);
|
container.label.setVisible(true);
|
||||||
const speciesVariants =
|
const speciesVariants =
|
||||||
speciesId && globalScene.gameData.dexData[speciesId].caughtAttr & DexAttr.SHINY
|
speciesId && gameData.dexData[speciesId].caughtAttr & DexAttr.SHINY
|
||||||
? [DexAttr.DEFAULT_VARIANT, DexAttr.VARIANT_2, DexAttr.VARIANT_3].filter(
|
? [DexAttr.DEFAULT_VARIANT, DexAttr.VARIANT_2, DexAttr.VARIANT_3].filter(
|
||||||
v => !!(globalScene.gameData.dexData[speciesId].caughtAttr & v),
|
v => !!(gameData.dexData[speciesId].caughtAttr & v),
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
for (let v = 0; v < 3; v++) {
|
for (let v = 0; v < 3; v++) {
|
||||||
@ -3282,12 +3285,15 @@ export class StarterSelectUiHandler extends MessageUiHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
container.starterPassiveBgs.setVisible(!!globalScene.gameData.starterData[speciesId].passiveAttr);
|
container.starterPassiveBgs.setVisible(!!gameData.starterData[speciesId].passiveAttr);
|
||||||
container.hiddenAbilityIcon.setVisible(
|
container.hiddenAbilityIcon.setVisible(
|
||||||
!!globalScene.gameData.dexData[speciesId].caughtAttr &&
|
!!gameData.dexData[speciesId].caughtAttr && !!(gameData.starterData[speciesId].abilityAttr & 4),
|
||||||
!!(globalScene.gameData.starterData[speciesId].abilityAttr & 4),
|
);
|
||||||
|
container.classicWinIcon
|
||||||
|
.setVisible(gameData.starterData[speciesId].classicWinCount > 0)
|
||||||
|
.setTexture(
|
||||||
|
gameData.dexData[speciesId].ribbons.has(RibbonData.NUZLOCKE) ? "champion_ribbon_emerald" : "champion_ribbon",
|
||||||
);
|
);
|
||||||
container.classicWinIcon.setVisible(globalScene.gameData.starterData[speciesId].classicWinCount > 0);
|
|
||||||
container.favoriteIcon.setVisible(this.starterPreferences[speciesId]?.favorite ?? false);
|
container.favoriteIcon.setVisible(this.starterPreferences[speciesId]?.favorite ?? false);
|
||||||
|
|
||||||
// 'Candy Icon' mode
|
// 'Candy Icon' mode
|
||||||
|
@ -4,6 +4,7 @@ import { pokemonEvolutions } from "#balance/pokemon-evolutions";
|
|||||||
import { pokemonFormChanges } from "#data/pokemon-forms";
|
import { pokemonFormChanges } from "#data/pokemon-forms";
|
||||||
import type { PokemonSpecies } from "#data/pokemon-species";
|
import type { PokemonSpecies } from "#data/pokemon-species";
|
||||||
import { ChallengeType } from "#enums/challenge-type";
|
import { ChallengeType } from "#enums/challenge-type";
|
||||||
|
import { Challenges } from "#enums/challenges";
|
||||||
import type { MoveId } from "#enums/move-id";
|
import type { MoveId } from "#enums/move-id";
|
||||||
import type { MoveSourceType } from "#enums/move-source-type";
|
import type { MoveSourceType } from "#enums/move-source-type";
|
||||||
import type { SpeciesId } from "#enums/species-id";
|
import type { SpeciesId } from "#enums/species-id";
|
||||||
@ -378,7 +379,7 @@ export function checkStarterValidForChallenge(species: PokemonSpecies, props: De
|
|||||||
* @param soft - If `true`, allow it if it could become valid through a form change.
|
* @param soft - If `true`, allow it if it could become valid through a form change.
|
||||||
* @returns `true` if the species is considered valid.
|
* @returns `true` if the species is considered valid.
|
||||||
*/
|
*/
|
||||||
function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrProps, soft: boolean) {
|
export function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrProps, soft: boolean) {
|
||||||
const isValidForChallenge = new BooleanHolder(true);
|
const isValidForChallenge = new BooleanHolder(true);
|
||||||
applyChallenges(ChallengeType.STARTER_CHOICE, species, isValidForChallenge, props);
|
applyChallenges(ChallengeType.STARTER_CHOICE, species, isValidForChallenge, props);
|
||||||
if (!soft || !pokemonFormChanges.hasOwnProperty(species.speciesId)) {
|
if (!soft || !pokemonFormChanges.hasOwnProperty(species.speciesId)) {
|
||||||
@ -407,3 +408,28 @@ function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrPr
|
|||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @returns Whether the current game mode meets the criteria to be considered a Nuzlocke challenge */
|
||||||
|
export function isNuzlockeChallenge(): boolean {
|
||||||
|
let isFreshStart = false;
|
||||||
|
let isLimitedCatch = false;
|
||||||
|
let isHardcore = false;
|
||||||
|
for (const challenge of globalScene.gameMode.challenges) {
|
||||||
|
// value is 0 if challenge is not active
|
||||||
|
if (!challenge.value) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (challenge.id) {
|
||||||
|
case Challenges.FRESH_START:
|
||||||
|
isFreshStart = true;
|
||||||
|
break;
|
||||||
|
case Challenges.LIMITED_CATCH:
|
||||||
|
isLimitedCatch = true;
|
||||||
|
break;
|
||||||
|
case Challenges.HARDCORE:
|
||||||
|
isHardcore = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isFreshStart && isLimitedCatch && isHardcore;
|
||||||
|
}
|
||||||
|
@ -45,17 +45,17 @@ export function deepMergeSpriteData(dest: object, source: object) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function encrypt(data: string, bypassLogin: boolean): string {
|
export function encrypt(data: string, bypassLogin: boolean): string {
|
||||||
return (bypassLogin
|
if (bypassLogin) {
|
||||||
? (data: string) => btoa(encodeURIComponent(data))
|
return btoa(encodeURIComponent(data));
|
||||||
: (data: string) => AES.encrypt(data, saveKey))(data) as unknown as string; // TODO: is this correct?
|
}
|
||||||
|
return AES.encrypt(data, saveKey).toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function decrypt(data: string, bypassLogin: boolean): string {
|
export function decrypt(data: string, bypassLogin: boolean): string {
|
||||||
return (
|
if (bypassLogin) {
|
||||||
bypassLogin
|
return decodeURIComponent(atob(data));
|
||||||
? (data: string) => decodeURIComponent(atob(data))
|
}
|
||||||
: (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8)
|
return AES.decrypt(data, saveKey).toString(enc.Utf8);
|
||||||
)(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// the latest data saved/loaded for the Starter Preferences. Required to reduce read/writes. Initialize as "{}", since this is the default value and no data needs to be stored if present.
|
// the latest data saved/loaded for the Starter Preferences. Required to reduce read/writes. Initialize as "{}", since this is the default value and no data needs to be stored if present.
|
||||||
|
27
test/@types/test-helpers.ts
Normal file
27
test/@types/test-helpers.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import type { AtLeastOne, NonFunctionPropertiesRecursive as nonFunc } from "#types/type-helpers";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper type to admit an object containing the given properties
|
||||||
|
* _and_ at least 1 other non-function property.
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* type foo = {
|
||||||
|
* qux: 1 | 2 | 3,
|
||||||
|
* bar: number,
|
||||||
|
* baz: string
|
||||||
|
* quux: () => void; // ignored!
|
||||||
|
* }
|
||||||
|
*
|
||||||
|
* type quxAndSomethingElse = OneOther<foo, "qux">
|
||||||
|
*
|
||||||
|
* const good1: quxAndSomethingElse = {qux: 1, bar: 3} // OK!
|
||||||
|
* const good2: quxAndSomethingElse = {qux: 2, baz: "4", bar: 12} // OK!
|
||||||
|
* const bad1: quxAndSomethingElse = {baz: "4", bar: 12} // Errors because `qux` is required
|
||||||
|
* const bad2: quxAndSomethingElse = {qux: 1} // Errors because at least 1 thing _other_ than `qux` is required
|
||||||
|
* ```
|
||||||
|
* @typeParam O - The object to source keys from
|
||||||
|
* @typeParam K - One or more of O's keys to render mandatory
|
||||||
|
*/
|
||||||
|
export type OneOther<O extends object, K extends keyof O> = AtLeastOne<Omit<nonFunc<O>, K>> & {
|
||||||
|
[key in K]: O[K];
|
||||||
|
};
|
140
test/@types/vitest.d.ts
vendored
140
test/@types/vitest.d.ts
vendored
@ -1,23 +1,32 @@
|
|||||||
import type { TerrainType } from "#app/data/terrain";
|
import type { TerrainType } from "#app/data/terrain";
|
||||||
|
import type Overrides from "#app/overrides";
|
||||||
|
import type { ArenaTag } from "#data/arena-tag";
|
||||||
|
import type { PositionalTag } from "#data/positional-tags/positional-tag";
|
||||||
import type { AbilityId } from "#enums/ability-id";
|
import type { AbilityId } from "#enums/ability-id";
|
||||||
|
import type { ArenaTagSide } from "#enums/arena-tag-side";
|
||||||
|
import type { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
import type { BattlerTagType } from "#enums/battler-tag-type";
|
import type { BattlerTagType } from "#enums/battler-tag-type";
|
||||||
import type { MoveId } from "#enums/move-id";
|
import type { MoveId } from "#enums/move-id";
|
||||||
import type { PokemonType } from "#enums/pokemon-type";
|
import type { PokemonType } from "#enums/pokemon-type";
|
||||||
|
import type { PositionalTagType } from "#enums/positional-tag-type";
|
||||||
import type { BattleStat, EffectiveStat, Stat } from "#enums/stat";
|
import type { BattleStat, EffectiveStat, Stat } from "#enums/stat";
|
||||||
import type { StatusEffect } from "#enums/status-effect";
|
import type { StatusEffect } from "#enums/status-effect";
|
||||||
import type { WeatherType } from "#enums/weather-type";
|
import type { WeatherType } from "#enums/weather-type";
|
||||||
|
import type { Arena } from "#field/arena";
|
||||||
import type { Pokemon } from "#field/pokemon";
|
import type { Pokemon } from "#field/pokemon";
|
||||||
import type { ToHaveEffectiveStatMatcherOptions } from "#test/test-utils/matchers/to-have-effective-stat";
|
import type { PokemonMove } from "#moves/pokemon-move";
|
||||||
|
import type { toHaveArenaTagOptions } from "#test/test-utils/matchers/to-have-arena-tag";
|
||||||
|
import type { toHaveEffectiveStatOptions } from "#test/test-utils/matchers/to-have-effective-stat";
|
||||||
|
import type { toHavePositionalTagOptions } from "#test/test-utils/matchers/to-have-positional-tag";
|
||||||
import type { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect";
|
import type { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect";
|
||||||
import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types";
|
import type { toHaveTypesOptions } from "#test/test-utils/matchers/to-have-types";
|
||||||
import type { TurnMove } from "#types/turn-move";
|
import type { TurnMove } from "#types/turn-move";
|
||||||
import type { AtLeastOne } from "#types/type-helpers";
|
import type { AtLeastOne } from "#types/type-helpers";
|
||||||
|
import type { toDmgValue } from "utils/common";
|
||||||
import type { expect } from "vitest";
|
import type { expect } from "vitest";
|
||||||
import type Overrides from "#app/overrides";
|
|
||||||
import type { PokemonMove } from "#moves/pokemon-move";
|
|
||||||
|
|
||||||
declare module "vitest" {
|
declare module "vitest" {
|
||||||
interface Assertion {
|
interface Assertion<T> {
|
||||||
/**
|
/**
|
||||||
* Check whether an array contains EXACTLY the given items (in any order).
|
* Check whether an array contains EXACTLY the given items (in any order).
|
||||||
*
|
*
|
||||||
@ -27,45 +36,9 @@ declare module "vitest" {
|
|||||||
* @param expected - The expected contents of the array, in any order
|
* @param expected - The expected contents of the array, in any order
|
||||||
* @see {@linkcode expect.arrayContaining}
|
* @see {@linkcode expect.arrayContaining}
|
||||||
*/
|
*/
|
||||||
toEqualArrayUnsorted<E>(expected: E[]): void;
|
toEqualArrayUnsorted(expected: T[]): void;
|
||||||
|
|
||||||
/**
|
// #region Arena Matchers
|
||||||
* Check whether a {@linkcode Pokemon}'s current typing includes the given types.
|
|
||||||
*
|
|
||||||
* @param expected - The expected types (in any order)
|
|
||||||
* @param options - The options passed to the matcher
|
|
||||||
*/
|
|
||||||
toHaveTypes(expected: [PokemonType, ...PokemonType[]], options?: toHaveTypesOptions): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Matcher to check the contents of a {@linkcode Pokemon}'s move history.
|
|
||||||
*
|
|
||||||
* @param expectedValue - The expected value; can be a {@linkcode MoveId} or a partially filled {@linkcode TurnMove}
|
|
||||||
* containing the desired properties to check
|
|
||||||
* @param index - The index of the move history entry to check, in order from most recent to least recent.
|
|
||||||
* Default `0` (last used move)
|
|
||||||
* @see {@linkcode Pokemon.getLastXMoves}
|
|
||||||
*/
|
|
||||||
toHaveUsedMove(expected: MoveId | AtLeastOne<TurnMove>, index?: number): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether a {@linkcode Pokemon}'s effective stat is as expected
|
|
||||||
* (checked after all stat value modifications).
|
|
||||||
*
|
|
||||||
* @param stat - The {@linkcode EffectiveStat} to check
|
|
||||||
* @param expectedValue - The expected value of {@linkcode stat}
|
|
||||||
* @param options - (Optional) The {@linkcode ToHaveEffectiveStatMatcherOptions}
|
|
||||||
* @remarks
|
|
||||||
* If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead.
|
|
||||||
*/
|
|
||||||
toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: ToHaveEffectiveStatMatcherOptions): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether a {@linkcode Pokemon} has taken a specific amount of damage.
|
|
||||||
* @param expectedDamageTaken - The expected amount of damage taken
|
|
||||||
* @param roundDown - Whether to round down {@linkcode expectedDamageTaken} with {@linkcode toDmgValue}; default `true`
|
|
||||||
*/
|
|
||||||
toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether the current {@linkcode WeatherType} is as expected.
|
* Check whether the current {@linkcode WeatherType} is as expected.
|
||||||
@ -80,9 +53,60 @@ declare module "vitest" {
|
|||||||
toHaveTerrain(expectedTerrainType: TerrainType): void;
|
toHaveTerrain(expectedTerrainType: TerrainType): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether a {@linkcode Pokemon} is at full HP.
|
* Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}.
|
||||||
|
* @param expectedTag - A partially-filled {@linkcode ArenaTag} containing the desired properties
|
||||||
*/
|
*/
|
||||||
toHaveFullHp(): void;
|
toHaveArenaTag<A extends ArenaTagType>(expectedTag: toHaveArenaTagOptions<A>): void;
|
||||||
|
/**
|
||||||
|
* Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}.
|
||||||
|
* @param expectedType - The {@linkcode ArenaTagType} of the desired tag
|
||||||
|
* @param side - The {@linkcode ArenaTagSide | side(s) of the field} the tag should affect; default {@linkcode ArenaTagSide.BOTH}
|
||||||
|
*/
|
||||||
|
toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the current {@linkcode Arena} contains the given {@linkcode PositionalTag}.
|
||||||
|
* @param expectedTag - A partially-filled `PositionalTag` containing the desired properties
|
||||||
|
*/
|
||||||
|
toHavePositionalTag<P extends PositionalTagType>(expectedTag: toHavePositionalTagOptions<P>): void;
|
||||||
|
/**
|
||||||
|
* Check whether the current {@linkcode Arena} contains the given number of {@linkcode PositionalTag}s.
|
||||||
|
* @param expectedType - The {@linkcode PositionalTagType} of the desired tag
|
||||||
|
* @param count - The number of instances of {@linkcode expectedType} that should be active;
|
||||||
|
* defaults to `1` and must be within the range `[0, 4]`
|
||||||
|
*/
|
||||||
|
toHavePositionalTag(expectedType: PositionalTagType, count?: number): void;
|
||||||
|
|
||||||
|
// #endregion Arena Matchers
|
||||||
|
|
||||||
|
// #region Pokemon Matchers
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon}'s current typing includes the given types.
|
||||||
|
* @param expectedTypes - The expected {@linkcode PokemonType}s to check against; must have length `>0`
|
||||||
|
* @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher
|
||||||
|
*/
|
||||||
|
toHaveTypes(expectedTypes: PokemonType[], options?: toHaveTypesOptions): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon} has used a move matching the given criteria.
|
||||||
|
* @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used,
|
||||||
|
* or a partially filled {@linkcode TurnMove} containing the desired properties to check
|
||||||
|
* @param index - The index of the move history entry to check, in order from most recent to least recent; default `0`
|
||||||
|
* @see {@linkcode Pokemon.getLastXMoves}
|
||||||
|
*/
|
||||||
|
toHaveUsedMove(expectedMove: MoveId | AtLeastOne<TurnMove>, index?: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon}'s effective stat is as expected
|
||||||
|
* (checked after all stat value modifications).
|
||||||
|
* @param stat - The {@linkcode EffectiveStat} to check
|
||||||
|
* @param expectedValue - The expected value of {@linkcode stat}
|
||||||
|
* @param options - The {@linkcode toHaveEffectiveStatOptions | options} passed to the matcher
|
||||||
|
* @remarks
|
||||||
|
* If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead.
|
||||||
|
*/
|
||||||
|
toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: toHaveEffectiveStatOptions): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}.
|
* Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}.
|
||||||
@ -106,7 +130,7 @@ declare module "vitest" {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}.
|
* Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}.
|
||||||
* @param expectedAbilityId - The expected {@linkcode AbilityId}
|
* @param expectedAbilityId - The `AbilityId` to check for
|
||||||
*/
|
*/
|
||||||
toHaveAbilityApplied(expectedAbilityId: AbilityId): void;
|
toHaveAbilityApplied(expectedAbilityId: AbilityId): void;
|
||||||
|
|
||||||
@ -116,24 +140,36 @@ declare module "vitest" {
|
|||||||
*/
|
*/
|
||||||
toHaveHp(expectedHp: number): void;
|
toHaveHp(expectedHp: number): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon} has taken a specific amount of damage.
|
||||||
|
* @param expectedDamageTaken - The expected amount of damage taken
|
||||||
|
* @param roundDown - Whether to round down `expectedDamageTaken` with {@linkcode toDmgValue}; default `true`
|
||||||
|
*/
|
||||||
|
toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}).
|
* Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}).
|
||||||
* @remarks
|
* @remarks
|
||||||
* When checking whether an enemy wild Pokemon is fainted, one must reference it in a variable _before_ the fainting effect occurs
|
* When checking whether an enemy wild Pokemon is fainted, one must store a reference to it in a variable _before_ the fainting effect occurs.
|
||||||
* as otherwise the Pokemon will be GC'ed and rendered `undefined`.
|
* Otherwise, the Pokemon will be removed from the field and garbage collected.
|
||||||
*/
|
*/
|
||||||
toHaveFainted(): void;
|
toHaveFainted(): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether a {@linkcode Pokemon} is at full HP.
|
||||||
|
*/
|
||||||
|
toHaveFullHp(): void;
|
||||||
/**
|
/**
|
||||||
* Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves.
|
* Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves.
|
||||||
* @param expectedValue - The {@linkcode MoveId} of the {@linkcode PokemonMove} that should have consumed PP
|
* @param moveId - The {@linkcode MoveId} corresponding to the {@linkcode PokemonMove} that should have consumed PP
|
||||||
* @param ppUsed - The numerical amount of PP that should have been consumed,
|
* @param ppUsed - The numerical amount of PP that should have been consumed,
|
||||||
* or `all` to indicate the move should be _out_ of PP
|
* or `all` to indicate the move should be _out_ of PP
|
||||||
* @remarks
|
* @remarks
|
||||||
* If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.OPP_MOVESET_OVERRIDE},
|
* If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.ENEMY_MOVESET_OVERRIDE}
|
||||||
* does not contain {@linkcode expectedMove}
|
* or does not contain exactly one copy of `moveId`, this will fail the test.
|
||||||
* or contains the desired move more than once, this will fail the test.
|
|
||||||
*/
|
*/
|
||||||
toHaveUsedPP(expectedMove: MoveId, ppUsed: number | "all"): void;
|
toHaveUsedPP(moveId: MoveId, ppUsed: number | "all"): void;
|
||||||
|
|
||||||
|
// #endregion Pokemon Matchers
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
import { GameManager } from "#test/test-utils/game-manager";
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
@ -22,25 +23,66 @@ describe("Abilities - Corrosion", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
game.override
|
game.override
|
||||||
.moveset([MoveId.SPLASH])
|
|
||||||
.battleStyle("single")
|
.battleStyle("single")
|
||||||
.criticalHits(false)
|
.criticalHits(false)
|
||||||
.enemySpecies(SpeciesId.GRIMER)
|
.enemySpecies(SpeciesId.GRIMER)
|
||||||
.enemyAbility(AbilityId.CORROSION)
|
.ability(AbilityId.CORROSION)
|
||||||
.enemyMoveset(MoveId.TOXIC);
|
.enemyAbility(AbilityId.NO_GUARD)
|
||||||
|
.enemyMoveset(MoveId.SPLASH);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("If a Poison- or Steel-type Pokémon with this Ability poisons a target with Synchronize, Synchronize does not gain the ability to poison Poison- or Steel-type Pokémon.", async () => {
|
it.each<{ name: string; species: SpeciesId }>([
|
||||||
game.override.ability(AbilityId.SYNCHRONIZE);
|
{ name: "Poison", species: SpeciesId.GRIMER },
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
{ name: "Steel", species: SpeciesId.KLINK },
|
||||||
|
])("should grant the user the ability to poison $name-type opponents", async ({ species }) => {
|
||||||
|
game.override.enemySpecies(species);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.SALANDIT]);
|
||||||
|
|
||||||
const playerPokemon = game.scene.getPlayerPokemon();
|
const enemy = game.field.getEnemyPokemon();
|
||||||
const enemyPokemon = game.scene.getEnemyPokemon();
|
expect(enemy.status?.effect).toBeUndefined();
|
||||||
expect(playerPokemon!.status).toBeUndefined();
|
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
game.move.use(MoveId.POISON_GAS);
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
await game.toEndOfTurn();
|
||||||
expect(playerPokemon!.status).toBeDefined();
|
|
||||||
expect(enemyPokemon!.status).toBeUndefined();
|
expect(enemy.status?.effect).toBe(StatusEffect.POISON);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not affect Toxic Spikes", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.SALANDIT]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.TOXIC_SPIKES);
|
||||||
|
await game.doKillOpponents();
|
||||||
|
await game.toNextWave();
|
||||||
|
|
||||||
|
const enemyPokemon = game.field.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon.status).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not affect an opponent's Synchronize ability", async () => {
|
||||||
|
game.override.enemyAbility(AbilityId.SYNCHRONIZE);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.ARBOK]);
|
||||||
|
|
||||||
|
const playerPokemon = game.field.getPlayerPokemon();
|
||||||
|
const enemyPokemon = game.field.getEnemyPokemon();
|
||||||
|
expect(enemyPokemon.status?.effect).toBeUndefined();
|
||||||
|
|
||||||
|
game.move.use(MoveId.TOXIC);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(enemyPokemon.status?.effect).toBe(StatusEffect.TOXIC);
|
||||||
|
expect(playerPokemon.status?.effect).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should affect the user's held Toxic Orb", async () => {
|
||||||
|
game.override.startingHeldItems([{ name: "TOXIC_ORB", count: 1 }]);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.SALAZZLE]);
|
||||||
|
|
||||||
|
const salazzle = game.field.getPlayerPokemon();
|
||||||
|
expect(salazzle.status?.effect).toBeUndefined();
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(salazzle.status?.effect).toBe(StatusEffect.TOXIC);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -49,6 +49,7 @@ describe("Abilities - Healer", () => {
|
|||||||
const user = game.field.getPlayerPokemon();
|
const user = game.field.getPlayerPokemon();
|
||||||
// Only want one magikarp to have the ability
|
// Only want one magikarp to have the ability
|
||||||
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
||||||
|
|
||||||
game.move.select(MoveId.SPLASH);
|
game.move.select(MoveId.SPLASH);
|
||||||
// faint the ally
|
// faint the ally
|
||||||
game.move.select(MoveId.LUNAR_DANCE, 1);
|
game.move.select(MoveId.LUNAR_DANCE, 1);
|
||||||
@ -62,9 +63,10 @@ describe("Abilities - Healer", () => {
|
|||||||
it("should heal the status of an ally if the ally has a status", async () => {
|
it("should heal the status of an ally if the ally has a status", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
await game.classicMode.startBattle([SpeciesId.MAGIKARP, SpeciesId.MAGIKARP]);
|
||||||
const [user, ally] = game.scene.getPlayerField();
|
const [user, ally] = game.scene.getPlayerField();
|
||||||
|
|
||||||
// Only want one magikarp to have the ability.
|
// Only want one magikarp to have the ability.
|
||||||
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
||||||
expect(ally.trySetStatus(StatusEffect.BURN)).toBe(true);
|
ally.doSetStatus(StatusEffect.BURN);
|
||||||
game.move.select(MoveId.SPLASH);
|
game.move.select(MoveId.SPLASH);
|
||||||
game.move.select(MoveId.SPLASH, 1);
|
game.move.select(MoveId.SPLASH, 1);
|
||||||
|
|
||||||
@ -80,7 +82,7 @@ describe("Abilities - Healer", () => {
|
|||||||
const [user, ally] = game.scene.getPlayerField();
|
const [user, ally] = game.scene.getPlayerField();
|
||||||
// Only want one magikarp to have the ability.
|
// Only want one magikarp to have the ability.
|
||||||
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
vi.spyOn(user, "getAbility").mockReturnValue(allAbilities[AbilityId.HEALER]);
|
||||||
expect(ally.trySetStatus(StatusEffect.BURN)).toBe(true);
|
ally.doSetStatus(StatusEffect.BURN);
|
||||||
game.move.select(MoveId.SPLASH);
|
game.move.select(MoveId.SPLASH);
|
||||||
game.move.select(MoveId.SPLASH, 1);
|
game.move.select(MoveId.SPLASH, 1);
|
||||||
await game.phaseInterceptor.to("TurnEndPhase");
|
await game.phaseInterceptor.to("TurnEndPhase");
|
||||||
|
@ -79,9 +79,9 @@ describe("Abilities - Infiltrator", () => {
|
|||||||
|
|
||||||
game.scene.arena.addTag(ArenaTagType.SAFEGUARD, 1, MoveId.NONE, enemy.id, ArenaTagSide.ENEMY, true);
|
game.scene.arena.addTag(ArenaTagType.SAFEGUARD, 1, MoveId.NONE, enemy.id, ArenaTagSide.ENEMY, true);
|
||||||
|
|
||||||
game.move.select(MoveId.SPORE);
|
game.move.use(MoveId.SPORE);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
await game.phaseInterceptor.to("BerryPhase", false);
|
|
||||||
expect(enemy.status?.effect).toBe(StatusEffect.SLEEP);
|
expect(enemy.status?.effect).toBe(StatusEffect.SLEEP);
|
||||||
expect(player.waveData.abilitiesApplied).toContain(AbilityId.INFILTRATOR);
|
expect(player.waveData.abilitiesApplied).toContain(AbilityId.INFILTRATOR);
|
||||||
});
|
});
|
||||||
|
@ -1,51 +0,0 @@
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
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";
|
|
||||||
|
|
||||||
describe("Abilities - Insomnia", () => {
|
|
||||||
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
|
|
||||||
.moveset([MoveId.SPLASH])
|
|
||||||
.ability(AbilityId.BALL_FETCH)
|
|
||||||
.battleStyle("single")
|
|
||||||
.criticalHits(false)
|
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove sleep when gained", async () => {
|
|
||||||
game.override
|
|
||||||
.ability(AbilityId.INSOMNIA)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.moveset(MoveId.SKILL_SWAP)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
|
||||||
const enemy = game.scene.getEnemyPokemon();
|
|
||||||
enemy?.trySetStatus(StatusEffect.SLEEP);
|
|
||||||
expect(enemy?.status?.effect).toBe(StatusEffect.SLEEP);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SKILL_SWAP);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(enemy?.status).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,51 +0,0 @@
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
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";
|
|
||||||
|
|
||||||
describe("Abilities - Limber", () => {
|
|
||||||
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
|
|
||||||
.moveset([MoveId.SPLASH])
|
|
||||||
.ability(AbilityId.BALL_FETCH)
|
|
||||||
.battleStyle("single")
|
|
||||||
.criticalHits(false)
|
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove paralysis when gained", async () => {
|
|
||||||
game.override
|
|
||||||
.ability(AbilityId.LIMBER)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.moveset(MoveId.SKILL_SWAP)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
|
||||||
const enemy = game.scene.getEnemyPokemon();
|
|
||||||
enemy?.trySetStatus(StatusEffect.PARALYSIS);
|
|
||||||
expect(enemy?.status?.effect).toBe(StatusEffect.PARALYSIS);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SKILL_SWAP);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(enemy?.status).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,51 +0,0 @@
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
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";
|
|
||||||
|
|
||||||
describe("Abilities - Magma Armor", () => {
|
|
||||||
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
|
|
||||||
.moveset([MoveId.SPLASH])
|
|
||||||
.ability(AbilityId.BALL_FETCH)
|
|
||||||
.battleStyle("single")
|
|
||||||
.criticalHits(false)
|
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove freeze when gained", async () => {
|
|
||||||
game.override
|
|
||||||
.ability(AbilityId.MAGMA_ARMOR)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.moveset(MoveId.SKILL_SWAP)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
|
||||||
const enemy = game.scene.getEnemyPokemon();
|
|
||||||
enemy?.trySetStatus(StatusEffect.FREEZE);
|
|
||||||
expect(enemy?.status?.effect).toBe(StatusEffect.FREEZE);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SKILL_SWAP);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(enemy?.status).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
95
test/abilities/status-immunity-ab-attrs.test.ts
Normal file
95
test/abilities/status-immunity-ab-attrs.test.ts
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { allMoves } from "#data/data-lists";
|
||||||
|
import { AbilityId } from "#enums/ability-id";
|
||||||
|
import { MoveId } from "#enums/move-id";
|
||||||
|
import { MoveResult } from "#enums/move-result";
|
||||||
|
import { SpeciesId } from "#enums/species-id";
|
||||||
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
|
import { StatusEffectAttr } from "#moves/move";
|
||||||
|
import { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
import { toTitleCase } from "#utils/strings";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
describe.each<{ name: string; ability: AbilityId; status: StatusEffect }>([
|
||||||
|
{ name: "Vital Spirit", ability: AbilityId.VITAL_SPIRIT, status: StatusEffect.SLEEP },
|
||||||
|
{ name: "Insomnia", ability: AbilityId.INSOMNIA, status: StatusEffect.SLEEP },
|
||||||
|
{ name: "Immunity", ability: AbilityId.IMMUNITY, status: StatusEffect.POISON },
|
||||||
|
{ name: "Magma Armor", ability: AbilityId.MAGMA_ARMOR, status: StatusEffect.FREEZE },
|
||||||
|
{ name: "Limber", ability: AbilityId.LIMBER, status: StatusEffect.PARALYSIS },
|
||||||
|
{ name: "Thermal Exchange", ability: AbilityId.THERMAL_EXCHANGE, status: StatusEffect.BURN },
|
||||||
|
{ name: "Water Veil", ability: AbilityId.WATER_VEIL, status: StatusEffect.BURN },
|
||||||
|
{ name: "Water Bubble", ability: AbilityId.WATER_BUBBLE, status: StatusEffect.BURN },
|
||||||
|
])("Abilities - $name", ({ ability, status }) => {
|
||||||
|
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
|
||||||
|
.battleStyle("single")
|
||||||
|
.criticalHits(false)
|
||||||
|
.enemyLevel(100)
|
||||||
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
|
.enemyAbility(ability)
|
||||||
|
.enemyMoveset(MoveId.SPLASH);
|
||||||
|
|
||||||
|
// Mock Lumina Crash and Spore to be our status-inflicting moves of choice
|
||||||
|
vi.spyOn(allMoves[MoveId.LUMINA_CRASH], "attrs", "get").mockReturnValue([new StatusEffectAttr(status, false)]);
|
||||||
|
vi.spyOn(allMoves[MoveId.SPORE], "attrs", "get").mockReturnValue([new StatusEffectAttr(status, false)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
const statusStr = toTitleCase(StatusEffect[status]);
|
||||||
|
|
||||||
|
it(`should prevent application of ${statusStr} without failing damaging moves`, async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
|
const karp = game.field.getEnemyPokemon();
|
||||||
|
expect(karp.status?.effect).toBeUndefined();
|
||||||
|
expect(karp.canSetStatus(status)).toBe(false);
|
||||||
|
|
||||||
|
game.move.use(MoveId.LUMINA_CRASH);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(karp.status?.effect).toBeUndefined();
|
||||||
|
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should cure ${statusStr} upon being gained`, async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
|
const feebas = game.field.getPlayerPokemon();
|
||||||
|
feebas.doSetStatus(status);
|
||||||
|
expect(feebas.status?.effect).toBe(status);
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPLASH);
|
||||||
|
await game.move.forceEnemyMove(MoveId.SKILL_SWAP);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(feebas.status?.effect).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: This does not propagate failures currently
|
||||||
|
it.todo(
|
||||||
|
`should cause status moves inflicting ${statusStr} to count as failed if no other effects can be applied`,
|
||||||
|
async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
|
game.move.use(MoveId.SPORE);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
const karp = game.field.getEnemyPokemon();
|
||||||
|
expect(karp.status?.effect).toBeUndefined();
|
||||||
|
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
@ -1,51 +0,0 @@
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
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";
|
|
||||||
|
|
||||||
describe("Abilities - Thermal Exchange", () => {
|
|
||||||
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
|
|
||||||
.moveset([MoveId.SPLASH])
|
|
||||||
.ability(AbilityId.BALL_FETCH)
|
|
||||||
.battleStyle("single")
|
|
||||||
.criticalHits(false)
|
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove burn when gained", async () => {
|
|
||||||
game.override
|
|
||||||
.ability(AbilityId.THERMAL_EXCHANGE)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.moveset(MoveId.SKILL_SWAP)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
|
||||||
const enemy = game.scene.getEnemyPokemon();
|
|
||||||
enemy?.trySetStatus(StatusEffect.BURN);
|
|
||||||
expect(enemy?.status?.effect).toBe(StatusEffect.BURN);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SKILL_SWAP);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(enemy?.status).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,51 +0,0 @@
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
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";
|
|
||||||
|
|
||||||
describe("Abilities - Vital Spirit", () => {
|
|
||||||
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
|
|
||||||
.moveset([MoveId.SPLASH])
|
|
||||||
.ability(AbilityId.BALL_FETCH)
|
|
||||||
.battleStyle("single")
|
|
||||||
.criticalHits(false)
|
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove sleep when gained", async () => {
|
|
||||||
game.override
|
|
||||||
.ability(AbilityId.INSOMNIA)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.moveset(MoveId.SKILL_SWAP)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
|
||||||
const enemy = game.scene.getEnemyPokemon();
|
|
||||||
enemy?.trySetStatus(StatusEffect.SLEEP);
|
|
||||||
expect(enemy?.status?.effect).toBe(StatusEffect.SLEEP);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SKILL_SWAP);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(enemy?.status).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,51 +0,0 @@
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
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";
|
|
||||||
|
|
||||||
describe("Abilities - Water Bubble", () => {
|
|
||||||
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
|
|
||||||
.moveset([MoveId.SPLASH])
|
|
||||||
.ability(AbilityId.BALL_FETCH)
|
|
||||||
.battleStyle("single")
|
|
||||||
.criticalHits(false)
|
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove burn when gained", async () => {
|
|
||||||
game.override
|
|
||||||
.ability(AbilityId.THERMAL_EXCHANGE)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.moveset(MoveId.SKILL_SWAP)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
|
||||||
const enemy = game.scene.getEnemyPokemon();
|
|
||||||
enemy?.trySetStatus(StatusEffect.BURN);
|
|
||||||
expect(enemy?.status?.effect).toBe(StatusEffect.BURN);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SKILL_SWAP);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(enemy?.status).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,51 +0,0 @@
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
|
||||||
import { MoveId } from "#enums/move-id";
|
|
||||||
import { SpeciesId } from "#enums/species-id";
|
|
||||||
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";
|
|
||||||
|
|
||||||
describe("Abilities - Water Veil", () => {
|
|
||||||
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
|
|
||||||
.moveset([MoveId.SPLASH])
|
|
||||||
.ability(AbilityId.BALL_FETCH)
|
|
||||||
.battleStyle("single")
|
|
||||||
.criticalHits(false)
|
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should remove burn when gained", async () => {
|
|
||||||
game.override
|
|
||||||
.ability(AbilityId.THERMAL_EXCHANGE)
|
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.moveset(MoveId.SKILL_SWAP)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
|
||||||
const enemy = game.scene.getEnemyPokemon();
|
|
||||||
enemy?.trySetStatus(StatusEffect.BURN);
|
|
||||||
expect(enemy?.status?.effect).toBe(StatusEffect.BURN);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SKILL_SWAP);
|
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(enemy?.status).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
@ -6,7 +6,7 @@ import { GameManager } from "#test/test-utils/game-manager";
|
|||||||
import Phaser from "phaser";
|
import Phaser from "phaser";
|
||||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
describe("Abilities - Immunity", () => {
|
describe("Spec - Pokemon Functions", () => {
|
||||||
let phaserGame: Phaser.Game;
|
let phaserGame: Phaser.Game;
|
||||||
let game: GameManager;
|
let game: GameManager;
|
||||||
|
|
||||||
@ -23,29 +23,29 @@ describe("Abilities - Immunity", () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
game.override
|
game.override
|
||||||
.moveset([MoveId.SPLASH])
|
|
||||||
.ability(AbilityId.BALL_FETCH)
|
|
||||||
.battleStyle("single")
|
.battleStyle("single")
|
||||||
|
.startingLevel(100)
|
||||||
.criticalHits(false)
|
.criticalHits(false)
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
|
.ability(AbilityId.BALL_FETCH)
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
.enemyMoveset(MoveId.SPLASH);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should remove poison when gained", async () => {
|
describe("doSetStatus", () => {
|
||||||
game.override
|
it("should change the Pokemon's status, ignoring feasibility checks", async () => {
|
||||||
.ability(AbilityId.IMMUNITY)
|
await game.classicMode.startBattle([SpeciesId.ACCELGOR]);
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
|
||||||
.moveset(MoveId.SKILL_SWAP)
|
|
||||||
.enemyMoveset(MoveId.SPLASH);
|
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
|
||||||
const enemy = game.scene.getEnemyPokemon();
|
|
||||||
enemy?.trySetStatus(StatusEffect.POISON);
|
|
||||||
expect(enemy?.status?.effect).toBe(StatusEffect.POISON);
|
|
||||||
|
|
||||||
game.move.select(MoveId.SKILL_SWAP);
|
const player = game.field.getPlayerPokemon();
|
||||||
await game.phaseInterceptor.to("BerryPhase");
|
|
||||||
|
|
||||||
expect(enemy?.status).toBeNull();
|
expect(player.status?.effect).toBeUndefined();
|
||||||
|
player.doSetStatus(StatusEffect.BURN);
|
||||||
|
expect(player.status?.effect).toBe(StatusEffect.BURN);
|
||||||
|
|
||||||
|
expect(player.canSetStatus(StatusEffect.SLEEP)).toBe(false);
|
||||||
|
player.doSetStatus(StatusEffect.SLEEP, 5);
|
||||||
|
expect(player.status?.effect).toBe(StatusEffect.SLEEP);
|
||||||
|
expect(player.status?.sleepTurnsRemaining).toBe(5);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
@ -25,15 +25,6 @@ describe("Spec - Pokemon", () => {
|
|||||||
game = new GameManager(phaserGame);
|
game = new GameManager(phaserGame);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not crash when trying to set status of undefined", async () => {
|
|
||||||
await game.classicMode.runToSummon([SpeciesId.ABRA]);
|
|
||||||
|
|
||||||
const pkm = game.field.getPlayerPokemon();
|
|
||||||
expect(pkm).toBeDefined();
|
|
||||||
|
|
||||||
expect(pkm.trySetStatus(undefined)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("Add To Party", () => {
|
describe("Add To Party", () => {
|
||||||
let scene: BattleScene;
|
let scene: BattleScene;
|
||||||
|
|
||||||
|
@ -1,10 +1,12 @@
|
|||||||
import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted";
|
import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted";
|
||||||
import { toHaveAbilityApplied } from "#test/test-utils/matchers/to-have-ability-applied";
|
import { toHaveAbilityApplied } from "#test/test-utils/matchers/to-have-ability-applied";
|
||||||
|
import { toHaveArenaTag } from "#test/test-utils/matchers/to-have-arena-tag";
|
||||||
import { toHaveBattlerTag } from "#test/test-utils/matchers/to-have-battler-tag";
|
import { toHaveBattlerTag } from "#test/test-utils/matchers/to-have-battler-tag";
|
||||||
import { toHaveEffectiveStat } from "#test/test-utils/matchers/to-have-effective-stat";
|
import { toHaveEffectiveStat } from "#test/test-utils/matchers/to-have-effective-stat";
|
||||||
import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted";
|
import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted";
|
||||||
import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp";
|
import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp";
|
||||||
import { toHaveHp } from "#test/test-utils/matchers/to-have-hp";
|
import { toHaveHp } from "#test/test-utils/matchers/to-have-hp";
|
||||||
|
import { toHavePositionalTag } from "#test/test-utils/matchers/to-have-positional-tag";
|
||||||
import { toHaveStatStage } from "#test/test-utils/matchers/to-have-stat-stage";
|
import { toHaveStatStage } from "#test/test-utils/matchers/to-have-stat-stage";
|
||||||
import { toHaveStatusEffect } from "#test/test-utils/matchers/to-have-status-effect";
|
import { toHaveStatusEffect } from "#test/test-utils/matchers/to-have-status-effect";
|
||||||
import { toHaveTakenDamage } from "#test/test-utils/matchers/to-have-taken-damage";
|
import { toHaveTakenDamage } from "#test/test-utils/matchers/to-have-taken-damage";
|
||||||
@ -22,18 +24,20 @@ import { expect } from "vitest";
|
|||||||
|
|
||||||
expect.extend({
|
expect.extend({
|
||||||
toEqualArrayUnsorted,
|
toEqualArrayUnsorted,
|
||||||
|
toHaveWeather,
|
||||||
|
toHaveTerrain,
|
||||||
|
toHaveArenaTag,
|
||||||
|
toHavePositionalTag,
|
||||||
toHaveTypes,
|
toHaveTypes,
|
||||||
toHaveUsedMove,
|
toHaveUsedMove,
|
||||||
toHaveEffectiveStat,
|
toHaveEffectiveStat,
|
||||||
toHaveTakenDamage,
|
|
||||||
toHaveWeather,
|
|
||||||
toHaveTerrain,
|
|
||||||
toHaveFullHp,
|
|
||||||
toHaveStatusEffect,
|
toHaveStatusEffect,
|
||||||
toHaveStatStage,
|
toHaveStatStage,
|
||||||
toHaveBattlerTag,
|
toHaveBattlerTag,
|
||||||
toHaveAbilityApplied,
|
toHaveAbilityApplied,
|
||||||
toHaveHp,
|
toHaveHp,
|
||||||
|
toHaveTakenDamage,
|
||||||
|
toHaveFullHp,
|
||||||
toHaveFainted,
|
toHaveFainted,
|
||||||
toHaveUsedPP,
|
toHaveUsedPP,
|
||||||
});
|
});
|
||||||
|
@ -73,7 +73,7 @@ describe("Moves - Beat Up", () => {
|
|||||||
|
|
||||||
const playerPokemon = game.field.getPlayerPokemon();
|
const playerPokemon = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
game.scene.getPlayerParty()[1].trySetStatus(StatusEffect.BURN);
|
game.scene.getPlayerParty()[1].doSetStatus(StatusEffect.BURN);
|
||||||
|
|
||||||
game.move.select(MoveId.BEAT_UP);
|
game.move.select(MoveId.BEAT_UP);
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ describe("Moves - Fusion Flare", () => {
|
|||||||
await game.phaseInterceptor.to(TurnStartPhase, false);
|
await game.phaseInterceptor.to(TurnStartPhase, false);
|
||||||
|
|
||||||
// Inflict freeze quietly and check if it was properly inflicted
|
// Inflict freeze quietly and check if it was properly inflicted
|
||||||
partyMember.trySetStatus(StatusEffect.FREEZE, false);
|
partyMember.doSetStatus(StatusEffect.FREEZE);
|
||||||
expect(partyMember.status!.effect).toBe(StatusEffect.FREEZE);
|
expect(partyMember.status!.effect).toBe(StatusEffect.FREEZE);
|
||||||
|
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
146
test/moves/rest.test.ts
Normal file
146
test/moves/rest.test.ts
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
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 { Stat } from "#enums/stat";
|
||||||
|
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";
|
||||||
|
|
||||||
|
describe("Move - Rest", () => {
|
||||||
|
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.EKANS)
|
||||||
|
.enemyAbility(AbilityId.BALL_FETCH)
|
||||||
|
.enemyMoveset(MoveId.SPLASH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fully heal the user, cure its prior status and put it to sleep", async () => {
|
||||||
|
game.override.statusEffect(StatusEffect.POISON);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||||
|
|
||||||
|
const snorlax = game.field.getPlayerPokemon();
|
||||||
|
snorlax.hp = 1;
|
||||||
|
expect(snorlax.status?.effect).toBe(StatusEffect.POISON);
|
||||||
|
|
||||||
|
game.move.use(MoveId.REST);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(snorlax.hp).toBe(snorlax.getMaxHp());
|
||||||
|
expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should always last 3 turns", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||||
|
|
||||||
|
const snorlax = game.field.getPlayerPokemon();
|
||||||
|
snorlax.hp = 1;
|
||||||
|
|
||||||
|
// Cf https://bulbapedia.bulbagarden.net/wiki/Rest_(move):
|
||||||
|
// > The user is unable to use MoveId while asleep for 2 turns after the turn when Rest is used.
|
||||||
|
game.move.use(MoveId.REST);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP);
|
||||||
|
expect(snorlax.status?.sleepTurnsRemaining).toBe(3);
|
||||||
|
|
||||||
|
game.move.use(MoveId.SWORDS_DANCE);
|
||||||
|
await game.toNextTurn();
|
||||||
|
expect(snorlax.status?.sleepTurnsRemaining).toBe(2);
|
||||||
|
|
||||||
|
game.move.use(MoveId.SWORDS_DANCE);
|
||||||
|
await game.toNextTurn();
|
||||||
|
expect(snorlax.status?.sleepTurnsRemaining).toBe(1);
|
||||||
|
|
||||||
|
game.move.use(MoveId.SWORDS_DANCE);
|
||||||
|
await game.toNextTurn();
|
||||||
|
expect(snorlax.status?.effect).toBeUndefined();
|
||||||
|
expect(snorlax.getStatStage(Stat.ATK)).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve non-volatile status conditions", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||||
|
|
||||||
|
const snorlax = game.field.getPlayerPokemon();
|
||||||
|
snorlax.hp = 1;
|
||||||
|
snorlax.addTag(BattlerTagType.CONFUSED, 999);
|
||||||
|
|
||||||
|
game.move.use(MoveId.REST);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(snorlax.getTag(BattlerTagType.CONFUSED)).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each<{ name: string; status?: StatusEffect; ability?: AbilityId; dmg?: number }>([
|
||||||
|
{ name: "is at full HP", dmg: 0 },
|
||||||
|
{ name: "is grounded on Electric Terrain", ability: AbilityId.ELECTRIC_SURGE },
|
||||||
|
{ name: "is grounded on Misty Terrain", ability: AbilityId.MISTY_SURGE },
|
||||||
|
{ name: "has Comatose", ability: AbilityId.COMATOSE },
|
||||||
|
])("should fail if the user $name", async ({ status = StatusEffect.NONE, ability = AbilityId.NONE, dmg = 1 }) => {
|
||||||
|
game.override.ability(ability).statusEffect(status);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||||
|
|
||||||
|
const snorlax = game.field.getPlayerPokemon();
|
||||||
|
|
||||||
|
snorlax.hp = snorlax.getMaxHp() - dmg;
|
||||||
|
|
||||||
|
game.move.use(MoveId.REST);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(snorlax.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail if called while already asleep", async () => {
|
||||||
|
game.override.statusEffect(StatusEffect.SLEEP).moveset([MoveId.REST, MoveId.SLEEP_TALK]);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||||
|
|
||||||
|
const snorlax = game.field.getPlayerPokemon();
|
||||||
|
snorlax.hp = 1;
|
||||||
|
|
||||||
|
// Need to use sleep talk here since you normally can't move while asleep
|
||||||
|
game.move.select(MoveId.SLEEP_TALK);
|
||||||
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
|
expect(snorlax.isFullHp()).toBe(false);
|
||||||
|
expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP);
|
||||||
|
expect(snorlax.getLastXMoves(-1).map(tm => tm.result)).toEqual([MoveResult.FAIL, MoveResult.SUCCESS]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should succeed if called the same turn as the user wakes", async () => {
|
||||||
|
game.override.statusEffect(StatusEffect.SLEEP);
|
||||||
|
await game.classicMode.startBattle([SpeciesId.SNORLAX]);
|
||||||
|
|
||||||
|
const snorlax = game.field.getPlayerPokemon();
|
||||||
|
snorlax.hp = 1;
|
||||||
|
|
||||||
|
expect(snorlax.status?.effect).toBe(StatusEffect.SLEEP);
|
||||||
|
snorlax.status!.sleepTurnsRemaining = 1;
|
||||||
|
|
||||||
|
game.move.use(MoveId.REST);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(snorlax.status!.effect).toBe(StatusEffect.SLEEP);
|
||||||
|
expect(snorlax.isFullHp()).toBe(true);
|
||||||
|
expect(snorlax.getLastXMoves()[0].result).toBe(MoveResult.SUCCESS);
|
||||||
|
expect(snorlax.status!.sleepTurnsRemaining).toBeGreaterThan(1);
|
||||||
|
});
|
||||||
|
});
|
@ -1,6 +1,7 @@
|
|||||||
import { AbilityId } from "#enums/ability-id";
|
import { AbilityId } from "#enums/ability-id";
|
||||||
import { MoveId } from "#enums/move-id";
|
import { MoveId } from "#enums/move-id";
|
||||||
import { MoveResult } from "#enums/move-result";
|
import { MoveResult } from "#enums/move-result";
|
||||||
|
import { MoveUseMode } from "#enums/move-use-mode";
|
||||||
import { SpeciesId } from "#enums/species-id";
|
import { SpeciesId } from "#enums/species-id";
|
||||||
import { Stat } from "#enums/stat";
|
import { Stat } from "#enums/stat";
|
||||||
import { StatusEffect } from "#enums/status-effect";
|
import { StatusEffect } from "#enums/status-effect";
|
||||||
@ -31,13 +32,36 @@ describe("Moves - Sleep Talk", () => {
|
|||||||
.battleStyle("single")
|
.battleStyle("single")
|
||||||
.criticalHits(false)
|
.criticalHits(false)
|
||||||
.enemySpecies(SpeciesId.MAGIKARP)
|
.enemySpecies(SpeciesId.MAGIKARP)
|
||||||
.enemyAbility(AbilityId.BALL_FETCH)
|
.enemyAbility(AbilityId.NO_GUARD)
|
||||||
.enemyMoveset(MoveId.SPLASH)
|
.enemyMoveset(MoveId.SPLASH)
|
||||||
.enemyLevel(100);
|
.enemyLevel(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should fail when the user is not asleep", async () => {
|
it("should call a random valid move if the user is asleep", async () => {
|
||||||
game.override.statusEffect(StatusEffect.NONE);
|
game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.FLY, MoveId.SWORDS_DANCE]); // Dig and Fly are invalid moves, Swords Dance should always be called
|
||||||
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
|
game.move.select(MoveId.SLEEP_TALK);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
const feebas = game.field.getPlayerPokemon();
|
||||||
|
expect(feebas.getStatStage(Stat.ATK)).toBe(2);
|
||||||
|
expect(feebas.getLastXMoves(2)).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
move: MoveId.SWORDS_DANCE,
|
||||||
|
result: MoveResult.SUCCESS,
|
||||||
|
useMode: MoveUseMode.FOLLOW_UP,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
move: MoveId.SLEEP_TALK,
|
||||||
|
result: MoveResult.SUCCESS,
|
||||||
|
useMode: MoveUseMode.NORMAL,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail if the user is not asleep", async () => {
|
||||||
|
game.override.statusEffect(StatusEffect.POISON);
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
game.move.select(MoveId.SLEEP_TALK);
|
game.move.select(MoveId.SLEEP_TALK);
|
||||||
@ -45,6 +69,19 @@ describe("Moves - Sleep Talk", () => {
|
|||||||
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should fail the turn the user wakes up from Sleep", async () => {
|
||||||
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
|
const feebas = game.field.getPlayerPokemon();
|
||||||
|
expect(feebas.status?.effect).toBe(StatusEffect.SLEEP);
|
||||||
|
feebas.status!.sleepTurnsRemaining = 1;
|
||||||
|
|
||||||
|
game.move.select(MoveId.SLEEP_TALK);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
expect(feebas).toHaveUsedMove({ result: MoveResult.FAIL });
|
||||||
|
});
|
||||||
|
|
||||||
it("should fail if the user has no valid moves", async () => {
|
it("should fail if the user has no valid moves", async () => {
|
||||||
game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.METRONOME, MoveId.SOLAR_BEAM]);
|
game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.METRONOME, MoveId.SOLAR_BEAM]);
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
@ -54,22 +91,15 @@ describe("Moves - Sleep Talk", () => {
|
|||||||
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
expect(game.field.getPlayerPokemon().getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should call a random valid move if the user is asleep", async () => {
|
it("should apply secondary effects of the called move", async () => {
|
||||||
game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.FLY, MoveId.SWORDS_DANCE]); // Dig and Fly are invalid moves, Swords Dance should always be called
|
game.override.moveset([MoveId.SLEEP_TALK, MoveId.SCALE_SHOT]);
|
||||||
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
|
||||||
|
|
||||||
game.move.select(MoveId.SLEEP_TALK);
|
game.move.select(MoveId.SLEEP_TALK);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK));
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should apply secondary effects of a move", async () => {
|
const feebas = game.field.getPlayerPokemon();
|
||||||
game.override.moveset([MoveId.SLEEP_TALK, MoveId.DIG, MoveId.FLY, MoveId.WOOD_HAMMER]); // Dig and Fly are invalid moves, Wood Hammer should always be called
|
expect(feebas.getStatStage(Stat.SPD)).toBe(1);
|
||||||
await game.classicMode.startBattle();
|
expect(feebas.getStatStage(Stat.DEF)).toBe(-1);
|
||||||
|
|
||||||
game.move.select(MoveId.SLEEP_TALK);
|
|
||||||
await game.toNextTurn();
|
|
||||||
|
|
||||||
expect(game.field.getPlayerPokemon().isFullHp()).toBeFalsy(); // Wood Hammer recoil effect should be applied
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -39,15 +39,6 @@ describe("Move - Wish", () => {
|
|||||||
.enemyLevel(100);
|
.enemyLevel(100);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Expect that wish is active with the specified number of attacks.
|
|
||||||
* @param numAttacks - The number of wish instances that should be queued; default `1`
|
|
||||||
*/
|
|
||||||
function expectWishActive(numAttacks = 1) {
|
|
||||||
const wishes = game.scene.arena.positionalTagManager["tags"].filter(t => t.tagType === PositionalTagType.WISH);
|
|
||||||
expect(wishes).toHaveLength(numAttacks);
|
|
||||||
}
|
|
||||||
|
|
||||||
it("should heal the Pokemon in the current slot for 50% of the user's maximum HP", async () => {
|
it("should heal the Pokemon in the current slot for 50% of the user's maximum HP", async () => {
|
||||||
await game.classicMode.startBattle([SpeciesId.ALOMOMOLA, SpeciesId.BLISSEY]);
|
await game.classicMode.startBattle([SpeciesId.ALOMOMOLA, SpeciesId.BLISSEY]);
|
||||||
|
|
||||||
@ -58,19 +49,19 @@ describe("Move - Wish", () => {
|
|||||||
game.move.use(MoveId.WISH);
|
game.move.use(MoveId.WISH);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expectWishActive();
|
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||||
|
|
||||||
game.doSwitchPokemon(1);
|
game.doSwitchPokemon(1);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expectWishActive(0);
|
expect(game).toHavePositionalTag(PositionalTagType.WISH, 0);
|
||||||
expect(game.textInterceptor.logs).toContain(
|
expect(game.textInterceptor.logs).toContain(
|
||||||
i18next.t("arenaTag:wishTagOnAdd", {
|
i18next.t("arenaTag:wishTagOnAdd", {
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(alomomola),
|
pokemonNameWithAffix: getPokemonNameWithAffix(alomomola),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
expect(alomomola.hp).toBe(1);
|
expect(alomomola).toHaveHp(1);
|
||||||
expect(blissey.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1);
|
expect(blissey).toHaveHp(toDmgValue(alomomola.getMaxHp() / 2) + 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should work if the user has full HP, but not if it already has an active Wish", async () => {
|
it("should work if the user has full HP, but not if it already has an active Wish", async () => {
|
||||||
@ -82,13 +73,13 @@ describe("Move - Wish", () => {
|
|||||||
game.move.use(MoveId.WISH);
|
game.move.use(MoveId.WISH);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expectWishActive();
|
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||||
|
|
||||||
game.move.use(MoveId.WISH);
|
game.move.use(MoveId.WISH);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
expect(alomomola.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1);
|
expect(alomomola.hp).toBe(toDmgValue(alomomola.getMaxHp() / 2) + 1);
|
||||||
expect(alomomola.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
|
expect(alomomola).toHaveUsedMove({ result: MoveResult.FAIL });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should function independently of Future Sight", async () => {
|
it("should function independently of Future Sight", async () => {
|
||||||
@ -103,7 +94,8 @@ describe("Move - Wish", () => {
|
|||||||
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expectWishActive(1);
|
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||||
|
expect(game).toHavePositionalTag(PositionalTagType.DELAYED_ATTACK);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should work in double battles and trigger in order of creation", async () => {
|
it("should work in double battles and trigger in order of creation", async () => {
|
||||||
@ -127,7 +119,7 @@ describe("Move - Wish", () => {
|
|||||||
await game.setTurnOrder(oldOrder.map(p => p.getBattlerIndex()));
|
await game.setTurnOrder(oldOrder.map(p => p.getBattlerIndex()));
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expectWishActive(4);
|
expect(game).toHavePositionalTag(PositionalTagType.WISH, 4);
|
||||||
|
|
||||||
// Lower speed to change turn order
|
// Lower speed to change turn order
|
||||||
alomomola.setStatStage(Stat.SPD, 6);
|
alomomola.setStatStage(Stat.SPD, 6);
|
||||||
@ -141,7 +133,7 @@ describe("Move - Wish", () => {
|
|||||||
await game.phaseInterceptor.to("PositionalTagPhase");
|
await game.phaseInterceptor.to("PositionalTagPhase");
|
||||||
|
|
||||||
// all wishes have activated and added healing phases
|
// all wishes have activated and added healing phases
|
||||||
expectWishActive(0);
|
expect(game).toHavePositionalTag(PositionalTagType.WISH, 0);
|
||||||
|
|
||||||
const healPhases = game.scene.phaseManager.phaseQueue.filter(p => p.is("PokemonHealPhase"));
|
const healPhases = game.scene.phaseManager.phaseQueue.filter(p => p.is("PokemonHealPhase"));
|
||||||
expect(healPhases).toHaveLength(4);
|
expect(healPhases).toHaveLength(4);
|
||||||
@ -165,14 +157,14 @@ describe("Move - Wish", () => {
|
|||||||
game.move.use(MoveId.WISH, BattlerIndex.PLAYER_2);
|
game.move.use(MoveId.WISH, BattlerIndex.PLAYER_2);
|
||||||
await game.toNextTurn();
|
await game.toNextTurn();
|
||||||
|
|
||||||
expectWishActive();
|
expect(game).toHavePositionalTag(PositionalTagType.WISH);
|
||||||
|
|
||||||
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
game.move.use(MoveId.SPLASH, BattlerIndex.PLAYER);
|
||||||
game.move.use(MoveId.MEMENTO, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
game.move.use(MoveId.MEMENTO, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY_2);
|
||||||
await game.toEndOfTurn();
|
await game.toEndOfTurn();
|
||||||
|
|
||||||
// Wish went away without doing anything
|
// Wish went away without doing anything
|
||||||
expectWishActive(0);
|
expect(game).toHavePositionalTag(PositionalTagType.WISH, 0);
|
||||||
expect(game.textInterceptor.logs).not.toContain(
|
expect(game.textInterceptor.logs).not.toContain(
|
||||||
i18next.t("arenaTag:wishTagOnAdd", {
|
i18next.t("arenaTag:wishTagOnAdd", {
|
||||||
pokemonNameWithAffix: getPokemonNameWithAffix(blissey),
|
pokemonNameWithAffix: getPokemonNameWithAffix(blissey),
|
||||||
|
@ -63,7 +63,7 @@ describe("Mystery Encounter Utils", () => {
|
|||||||
// Both pokemon fainted
|
// Both pokemon fainted
|
||||||
scene.getPlayerParty().forEach(p => {
|
scene.getPlayerParty().forEach(p => {
|
||||||
p.hp = 0;
|
p.hp = 0;
|
||||||
p.trySetStatus(StatusEffect.FAINT);
|
p.doSetStatus(StatusEffect.FAINT);
|
||||||
void p.updateInfo();
|
void p.updateInfo();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -83,7 +83,7 @@ describe("Mystery Encounter Utils", () => {
|
|||||||
// Only faint 1st pokemon
|
// Only faint 1st pokemon
|
||||||
const party = scene.getPlayerParty();
|
const party = scene.getPlayerParty();
|
||||||
party[0].hp = 0;
|
party[0].hp = 0;
|
||||||
party[0].trySetStatus(StatusEffect.FAINT);
|
party[0].doSetStatus(StatusEffect.FAINT);
|
||||||
await party[0].updateInfo();
|
await party[0].updateInfo();
|
||||||
|
|
||||||
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
||||||
@ -102,7 +102,7 @@ describe("Mystery Encounter Utils", () => {
|
|||||||
// Only faint 1st pokemon
|
// Only faint 1st pokemon
|
||||||
const party = scene.getPlayerParty();
|
const party = scene.getPlayerParty();
|
||||||
party[0].hp = 0;
|
party[0].hp = 0;
|
||||||
party[0].trySetStatus(StatusEffect.FAINT);
|
party[0].doSetStatus(StatusEffect.FAINT);
|
||||||
await party[0].updateInfo();
|
await party[0].updateInfo();
|
||||||
|
|
||||||
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
||||||
@ -121,7 +121,7 @@ describe("Mystery Encounter Utils", () => {
|
|||||||
// Only faint 1st pokemon
|
// Only faint 1st pokemon
|
||||||
const party = scene.getPlayerParty();
|
const party = scene.getPlayerParty();
|
||||||
party[0].hp = 0;
|
party[0].hp = 0;
|
||||||
party[0].trySetStatus(StatusEffect.FAINT);
|
party[0].doSetStatus(StatusEffect.FAINT);
|
||||||
await party[0].updateInfo();
|
await party[0].updateInfo();
|
||||||
|
|
||||||
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
|
||||||
@ -167,7 +167,7 @@ describe("Mystery Encounter Utils", () => {
|
|||||||
const party = scene.getPlayerParty();
|
const party = scene.getPlayerParty();
|
||||||
party[0].level = 100;
|
party[0].level = 100;
|
||||||
party[0].hp = 0;
|
party[0].hp = 0;
|
||||||
party[0].trySetStatus(StatusEffect.FAINT);
|
party[0].doSetStatus(StatusEffect.FAINT);
|
||||||
await party[0].updateInfo();
|
await party[0].updateInfo();
|
||||||
party[1].level = 10;
|
party[1].level = 10;
|
||||||
|
|
||||||
@ -206,7 +206,7 @@ describe("Mystery Encounter Utils", () => {
|
|||||||
const party = scene.getPlayerParty();
|
const party = scene.getPlayerParty();
|
||||||
party[0].level = 10;
|
party[0].level = 10;
|
||||||
party[0].hp = 0;
|
party[0].hp = 0;
|
||||||
party[0].trySetStatus(StatusEffect.FAINT);
|
party[0].doSetStatus(StatusEffect.FAINT);
|
||||||
await party[0].updateInfo();
|
await party[0].updateInfo();
|
||||||
party[1].level = 100;
|
party[1].level = 100;
|
||||||
|
|
||||||
|
@ -224,7 +224,7 @@ export class GameManager {
|
|||||||
// This will consider all battle entry dialog as seens and skip them
|
// This will consider all battle entry dialog as seens and skip them
|
||||||
vi.spyOn(this.scene.ui, "shouldSkipDialogue").mockReturnValue(true);
|
vi.spyOn(this.scene.ui, "shouldSkipDialogue").mockReturnValue(true);
|
||||||
|
|
||||||
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0) {
|
if (overrides.ENEMY_HELD_ITEMS_OVERRIDE.length === 0) {
|
||||||
this.removeEnemyHeldItems();
|
this.removeEnemyHeldItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ export class ChallengeModeHelper extends GameManagerHelper {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await this.game.phaseInterceptor.run(EncounterPhase);
|
await this.game.phaseInterceptor.run(EncounterPhase);
|
||||||
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
|
if (overrides.ENEMY_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
|
||||||
this.game.removeEnemyHeldItems();
|
this.game.removeEnemyHeldItems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ export class ClassicModeHelper extends GameManagerHelper {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await this.game.phaseInterceptor.to(EncounterPhase);
|
await this.game.phaseInterceptor.to(EncounterPhase);
|
||||||
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
|
if (overrides.ENEMY_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
|
||||||
this.game.removeEnemyHeldItems();
|
this.game.removeEnemyHeldItems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ export class DailyModeHelper extends GameManagerHelper {
|
|||||||
|
|
||||||
await this.game.phaseInterceptor.to(EncounterPhase);
|
await this.game.phaseInterceptor.to(EncounterPhase);
|
||||||
|
|
||||||
if (overrides.OPP_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
|
if (overrides.ENEMY_HELD_ITEMS_OVERRIDE.length === 0 && this.game.override.removeEnemyStartingItems) {
|
||||||
this.game.removeEnemyHeldItems();
|
this.game.removeEnemyHeldItems();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -228,8 +228,8 @@ export class MoveHelper extends GameManagerHelper {
|
|||||||
console.warn("Player moveset override disabled due to use of `game.move.changeMoveset`!");
|
console.warn("Player moveset override disabled due to use of `game.move.changeMoveset`!");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (coerceArray(Overrides.OPP_MOVESET_OVERRIDE).length > 0) {
|
if (coerceArray(Overrides.ENEMY_MOVESET_OVERRIDE).length > 0) {
|
||||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([]);
|
vi.spyOn(Overrides, "ENEMY_MOVESET_OVERRIDE", "get").mockReturnValue([]);
|
||||||
console.warn("Enemy moveset override disabled due to use of `game.move.changeMoveset`!");
|
console.warn("Enemy moveset override disabled due to use of `game.move.changeMoveset`!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -302,8 +302,8 @@ export class MoveHelper extends GameManagerHelper {
|
|||||||
(this.game.scene.phaseManager.getCurrentPhase() as EnemyCommandPhase).getFieldIndex()
|
(this.game.scene.phaseManager.getCurrentPhase() as EnemyCommandPhase).getFieldIndex()
|
||||||
];
|
];
|
||||||
|
|
||||||
if ([Overrides.OPP_MOVESET_OVERRIDE].flat().length > 0) {
|
if ([Overrides.ENEMY_MOVESET_OVERRIDE].flat().length > 0) {
|
||||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([]);
|
vi.spyOn(Overrides, "ENEMY_MOVESET_OVERRIDE", "get").mockReturnValue([]);
|
||||||
console.warn(
|
console.warn(
|
||||||
"Warning: `forceEnemyMove` overwrites the Pokemon's moveset and disables the enemy moveset override!",
|
"Warning: `forceEnemyMove` overwrites the Pokemon's moveset and disables the enemy moveset override!",
|
||||||
);
|
);
|
||||||
|
@ -406,7 +406,7 @@ export class OverridesHelper extends GameManagerHelper {
|
|||||||
* @returns `this`
|
* @returns `this`
|
||||||
*/
|
*/
|
||||||
public enemySpecies(species: SpeciesId | number): this {
|
public enemySpecies(species: SpeciesId | number): this {
|
||||||
vi.spyOn(Overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(species);
|
vi.spyOn(Overrides, "ENEMY_SPECIES_OVERRIDE", "get").mockReturnValue(species);
|
||||||
this.log(`Enemy Pokemon species set to ${SpeciesId[species]} (=${species})!`);
|
this.log(`Enemy Pokemon species set to ${SpeciesId[species]} (=${species})!`);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -416,7 +416,7 @@ export class OverridesHelper extends GameManagerHelper {
|
|||||||
* @returns `this`
|
* @returns `this`
|
||||||
*/
|
*/
|
||||||
public enableEnemyFusion(): this {
|
public enableEnemyFusion(): this {
|
||||||
vi.spyOn(Overrides, "OPP_FUSION_OVERRIDE", "get").mockReturnValue(true);
|
vi.spyOn(Overrides, "ENEMY_FUSION_OVERRIDE", "get").mockReturnValue(true);
|
||||||
this.log("Enemy Pokemon is a random fusion!");
|
this.log("Enemy Pokemon is a random fusion!");
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -427,7 +427,7 @@ export class OverridesHelper extends GameManagerHelper {
|
|||||||
* @returns `this`
|
* @returns `this`
|
||||||
*/
|
*/
|
||||||
public enemyFusionSpecies(species: SpeciesId | number): this {
|
public enemyFusionSpecies(species: SpeciesId | number): this {
|
||||||
vi.spyOn(Overrides, "OPP_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species);
|
vi.spyOn(Overrides, "ENEMY_FUSION_SPECIES_OVERRIDE", "get").mockReturnValue(species);
|
||||||
this.log(`Enemy Pokemon fusion species set to ${SpeciesId[species]} (=${species})!`);
|
this.log(`Enemy Pokemon fusion species set to ${SpeciesId[species]} (=${species})!`);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -438,7 +438,7 @@ export class OverridesHelper extends GameManagerHelper {
|
|||||||
* @returns `this`
|
* @returns `this`
|
||||||
*/
|
*/
|
||||||
public enemyAbility(ability: AbilityId): this {
|
public enemyAbility(ability: AbilityId): this {
|
||||||
vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(ability);
|
vi.spyOn(Overrides, "ENEMY_ABILITY_OVERRIDE", "get").mockReturnValue(ability);
|
||||||
this.log(`Enemy Pokemon ability set to ${AbilityId[ability]} (=${ability})!`);
|
this.log(`Enemy Pokemon ability set to ${AbilityId[ability]} (=${ability})!`);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -449,7 +449,7 @@ export class OverridesHelper extends GameManagerHelper {
|
|||||||
* @returns `this`
|
* @returns `this`
|
||||||
*/
|
*/
|
||||||
public enemyPassiveAbility(passiveAbility: AbilityId): this {
|
public enemyPassiveAbility(passiveAbility: AbilityId): this {
|
||||||
vi.spyOn(Overrides, "OPP_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility);
|
vi.spyOn(Overrides, "ENEMY_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(passiveAbility);
|
||||||
this.log(`Enemy Pokemon PASSIVE ability set to ${AbilityId[passiveAbility]} (=${passiveAbility})!`);
|
this.log(`Enemy Pokemon PASSIVE ability set to ${AbilityId[passiveAbility]} (=${passiveAbility})!`);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -460,7 +460,7 @@ export class OverridesHelper extends GameManagerHelper {
|
|||||||
* @returns `this`
|
* @returns `this`
|
||||||
*/
|
*/
|
||||||
public enemyHasPassiveAbility(hasPassiveAbility: boolean | null): this {
|
public enemyHasPassiveAbility(hasPassiveAbility: boolean | null): this {
|
||||||
vi.spyOn(Overrides, "OPP_HAS_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(hasPassiveAbility);
|
vi.spyOn(Overrides, "ENEMY_HAS_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(hasPassiveAbility);
|
||||||
if (hasPassiveAbility === null) {
|
if (hasPassiveAbility === null) {
|
||||||
this.log("Enemy Pokemon PASSIVE ability no longer force enabled or disabled!");
|
this.log("Enemy Pokemon PASSIVE ability no longer force enabled or disabled!");
|
||||||
} else {
|
} else {
|
||||||
@ -475,7 +475,7 @@ export class OverridesHelper extends GameManagerHelper {
|
|||||||
* @returns `this`
|
* @returns `this`
|
||||||
*/
|
*/
|
||||||
public enemyMoveset(moveset: MoveId | MoveId[]): this {
|
public enemyMoveset(moveset: MoveId | MoveId[]): this {
|
||||||
vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue(moveset);
|
vi.spyOn(Overrides, "ENEMY_MOVESET_OVERRIDE", "get").mockReturnValue(moveset);
|
||||||
moveset = coerceArray(moveset);
|
moveset = coerceArray(moveset);
|
||||||
const movesetStr = moveset.map(moveId => MoveId[moveId]).join(", ");
|
const movesetStr = moveset.map(moveId => MoveId[moveId]).join(", ");
|
||||||
this.log(`Enemy Pokemon moveset set to ${movesetStr} (=[${moveset.join(", ")}])!`);
|
this.log(`Enemy Pokemon moveset set to ${movesetStr} (=[${moveset.join(", ")}])!`);
|
||||||
@ -488,7 +488,7 @@ export class OverridesHelper extends GameManagerHelper {
|
|||||||
* @returns `this`
|
* @returns `this`
|
||||||
*/
|
*/
|
||||||
public enemyLevel(level: number): this {
|
public enemyLevel(level: number): this {
|
||||||
vi.spyOn(Overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(level);
|
vi.spyOn(Overrides, "ENEMY_LEVEL_OVERRIDE", "get").mockReturnValue(level);
|
||||||
this.log(`Enemy Pokemon level set to ${level}!`);
|
this.log(`Enemy Pokemon level set to ${level}!`);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -499,7 +499,7 @@ export class OverridesHelper extends GameManagerHelper {
|
|||||||
* @returns `this`
|
* @returns `this`
|
||||||
*/
|
*/
|
||||||
public enemyStatusEffect(statusEffect: StatusEffect): this {
|
public enemyStatusEffect(statusEffect: StatusEffect): this {
|
||||||
vi.spyOn(Overrides, "OPP_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);
|
vi.spyOn(Overrides, "ENEMY_STATUS_OVERRIDE", "get").mockReturnValue(statusEffect);
|
||||||
this.log(`Enemy Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`);
|
this.log(`Enemy Pokemon status-effect set to ${StatusEffect[statusEffect]} (=${statusEffect})!`);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -510,7 +510,7 @@ export class OverridesHelper extends GameManagerHelper {
|
|||||||
* @returns `this`
|
* @returns `this`
|
||||||
*/
|
*/
|
||||||
public enemyHeldItems(items: ModifierOverride[]): this {
|
public enemyHeldItems(items: ModifierOverride[]): this {
|
||||||
vi.spyOn(Overrides, "OPP_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items);
|
vi.spyOn(Overrides, "ENEMY_HELD_ITEMS_OVERRIDE", "get").mockReturnValue(items);
|
||||||
this.log("Enemy Pokemon held items set to:", items);
|
this.log("Enemy Pokemon held items set to:", items);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -571,7 +571,7 @@ export class OverridesHelper extends GameManagerHelper {
|
|||||||
* @param variant - (Optional) The enemy's shiny {@linkcode Variant}.
|
* @param variant - (Optional) The enemy's shiny {@linkcode Variant}.
|
||||||
*/
|
*/
|
||||||
enemyShiny(shininess: boolean | null, variant?: Variant): this {
|
enemyShiny(shininess: boolean | null, variant?: Variant): this {
|
||||||
vi.spyOn(Overrides, "OPP_SHINY_OVERRIDE", "get").mockReturnValue(shininess);
|
vi.spyOn(Overrides, "ENEMY_SHINY_OVERRIDE", "get").mockReturnValue(shininess);
|
||||||
if (shininess === null) {
|
if (shininess === null) {
|
||||||
this.log("Disabled enemy Pokemon shiny override!");
|
this.log("Disabled enemy Pokemon shiny override!");
|
||||||
} else {
|
} else {
|
||||||
@ -579,7 +579,7 @@ export class OverridesHelper extends GameManagerHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (variant !== undefined) {
|
if (variant !== undefined) {
|
||||||
vi.spyOn(Overrides, "OPP_VARIANT_OVERRIDE", "get").mockReturnValue(variant);
|
vi.spyOn(Overrides, "ENEMY_VARIANT_OVERRIDE", "get").mockReturnValue(variant);
|
||||||
this.log(`Set enemy shiny variant to be ${variant}!`);
|
this.log(`Set enemy shiny variant to be ${variant}!`);
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
@ -594,7 +594,7 @@ export class OverridesHelper extends GameManagerHelper {
|
|||||||
* @returns `this`
|
* @returns `this`
|
||||||
*/
|
*/
|
||||||
public enemyHealthSegments(healthSegments: number): this {
|
public enemyHealthSegments(healthSegments: number): this {
|
||||||
vi.spyOn(Overrides, "OPP_HEALTH_SEGMENTS_OVERRIDE", "get").mockReturnValue(healthSegments);
|
vi.spyOn(Overrides, "ENEMY_HEALTH_SEGMENTS_OVERRIDE", "get").mockReturnValue(healthSegments);
|
||||||
this.log("Enemy Pokemon health segments set to:", healthSegments);
|
this.log("Enemy Pokemon health segments set to:", healthSegments);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||||
|
import { receivedStr } from "#test/test-utils/test-utils";
|
||||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -14,22 +15,22 @@ export function toEqualArrayUnsorted(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!Array.isArray(received)) {
|
if (!Array.isArray(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected an array, but got ${this.utils.stringify(received)}!`,
|
message: () => `Expected to receive an array, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (received.length !== expected.length) {
|
if (received.length !== expected.length) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: false,
|
||||||
message: () => `Expected to receive array of length ${received.length}, but got ${expected.length} instead!`,
|
message: () => `Expected to receive an array of length ${received.length}, but got ${expected.length} instead!`,
|
||||||
actual: received,
|
|
||||||
expected,
|
expected,
|
||||||
|
actual: received,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const actualSorted = received.slice().sort();
|
const actualSorted = received.toSorted();
|
||||||
const expectedSorted = expected.slice().sort();
|
const expectedSorted = expected.toSorted();
|
||||||
const pass = this.equals(actualSorted, expectedSorted, [...this.customTesters, this.utils.iterableEquality]);
|
const pass = this.equals(actualSorted, expectedSorted, [...this.customTesters, this.utils.iterableEquality]);
|
||||||
|
|
||||||
const actualStr = getOnelineDiffStr.call(this, actualSorted);
|
const actualStr = getOnelineDiffStr.call(this, actualSorted);
|
||||||
|
@ -21,8 +21,8 @@ export function toHaveAbilityApplied(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to recieve a Pokemon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokemon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
77
test/test-utils/matchers/to-have-arena-tag.ts
Normal file
77
test/test-utils/matchers/to-have-arena-tag.ts
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import type { ArenaTag, ArenaTagTypeMap } from "#data/arena-tag";
|
||||||
|
import type { ArenaTagSide } from "#enums/arena-tag-side";
|
||||||
|
import type { ArenaTagType } from "#enums/arena-tag-type";
|
||||||
|
import type { OneOther } from "#test/@types/test-helpers";
|
||||||
|
// biome-ignore lint/correctness/noUnusedImports: TSDoc
|
||||||
|
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 { 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;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher to check if the {@linkcode Arena} has a given {@linkcode ArenaTag} active.
|
||||||
|
* @param received - The object to check. Should be the current {@linkcode GameManager}.
|
||||||
|
* @param expectedTag - The `ArenaTagType` of the desired tag, or a partially-filled object
|
||||||
|
* containing the desired properties
|
||||||
|
* @param side - The {@linkcode ArenaTagSide | side of the field} the tag should affect, or
|
||||||
|
* {@linkcode ArenaTagSide.BOTH} to check both sides
|
||||||
|
* @returns The result of the matching
|
||||||
|
*/
|
||||||
|
export function toHaveArenaTag<T extends ArenaTagType>(
|
||||||
|
this: MatcherState,
|
||||||
|
received: unknown,
|
||||||
|
expectedTag: T | toHaveArenaTagOptions<T>,
|
||||||
|
side?: ArenaTagSide,
|
||||||
|
): SyncExpectationResult {
|
||||||
|
if (!isGameManagerInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: this.isNot,
|
||||||
|
message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!received.scene?.arena) {
|
||||||
|
return {
|
||||||
|
pass: this.isNot,
|
||||||
|
message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coerce lone `tagType`s into objects
|
||||||
|
// Bangs are ok as we enforce safety via overloads
|
||||||
|
// @ts-expect-error - Typescript is being stupid as tag type and side will always exist
|
||||||
|
const etag: Partial<ArenaTag> & { tagType: T; side: ArenaTagSide } =
|
||||||
|
typeof expectedTag === "object" ? expectedTag : { tagType: expectedTag, side: side! };
|
||||||
|
|
||||||
|
// We need to get all tags for the case of checking properties of a tag present on both sides of the arena
|
||||||
|
const tags = received.scene.arena.findTagsOnSide(t => t.tagType === etag.tagType, etag.side);
|
||||||
|
if (tags.length === 0) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected the Arena to have a tag of type ${etag.tagType}, but it didn't!`,
|
||||||
|
expected: etag.tagType,
|
||||||
|
actual: received.scene.arena.tags.map(t => t.tagType),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass if any of the matching tags meet our criteria
|
||||||
|
const pass = tags.some(tag =>
|
||||||
|
this.equals(tag, expectedTag, [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const expectedStr = getOnelineDiffStr.call(this, expectedTag);
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected the Arena to NOT have a tag matching ${expectedStr}, but it did!`
|
||||||
|
: `Expected the Arena to have a tag matching ${expectedStr}, but it didn't!`,
|
||||||
|
expected: expectedTag,
|
||||||
|
actual: tags,
|
||||||
|
};
|
||||||
|
}
|
@ -6,7 +6,7 @@ import { getStatName } from "#test/test-utils/string-utils";
|
|||||||
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
import { isPokemonInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
export interface ToHaveEffectiveStatMatcherOptions {
|
export interface toHaveEffectiveStatOptions {
|
||||||
/**
|
/**
|
||||||
* The target {@linkcode Pokemon}
|
* The target {@linkcode Pokemon}
|
||||||
* @see {@linkcode Pokemon.getEffectiveStat}
|
* @see {@linkcode Pokemon.getEffectiveStat}
|
||||||
@ -30,7 +30,7 @@ export interface ToHaveEffectiveStatMatcherOptions {
|
|||||||
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
||||||
* @param stat - The {@linkcode EffectiveStat} to check
|
* @param stat - The {@linkcode EffectiveStat} to check
|
||||||
* @param expectedValue - The expected value of the {@linkcode stat}
|
* @param expectedValue - The expected value of the {@linkcode stat}
|
||||||
* @param options - The {@linkcode ToHaveEffectiveStatMatcherOptions}
|
* @param options - The {@linkcode toHaveEffectiveStatOptions}
|
||||||
* @returns Whether the matcher passed
|
* @returns Whether the matcher passed
|
||||||
*/
|
*/
|
||||||
export function toHaveEffectiveStat(
|
export function toHaveEffectiveStat(
|
||||||
@ -38,11 +38,11 @@ export function toHaveEffectiveStat(
|
|||||||
received: unknown,
|
received: unknown,
|
||||||
stat: EffectiveStat,
|
stat: EffectiveStat,
|
||||||
expectedValue: number,
|
expectedValue: number,
|
||||||
{ enemy, move, isCritical = false }: ToHaveEffectiveStatMatcherOptions = {},
|
{ enemy, move, isCritical = false }: toHaveEffectiveStatOptions = {},
|
||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
|||||||
export function toHaveFainted(this: MatcherState, received: unknown): SyncExpectationResult {
|
export function toHaveFainted(this: MatcherState, received: unknown): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
|||||||
export function toHaveFullHp(this: MatcherState, received: unknown): SyncExpectationResult {
|
export function toHaveFullHp(this: MatcherState, received: unknown): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
|||||||
export function toHaveHp(this: MatcherState, received: unknown, expectedHp: number): SyncExpectationResult {
|
export function toHaveHp(this: MatcherState, received: unknown, expectedHp: number): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
107
test/test-utils/matchers/to-have-positional-tag.ts
Normal file
107
test/test-utils/matchers/to-have-positional-tag.ts
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
// biome-ignore-start lint/correctness/noUnusedImports: TSDoc
|
||||||
|
import type { GameManager } from "#test/test-utils/game-manager";
|
||||||
|
// biome-ignore-end lint/correctness/noUnusedImports: TSDoc
|
||||||
|
|
||||||
|
import type { serializedPosTagMap } from "#data/positional-tags/load-positional-tag";
|
||||||
|
import type { PositionalTagType } from "#enums/positional-tag-type";
|
||||||
|
import type { OneOther } from "#test/@types/test-helpers";
|
||||||
|
import { getOnelineDiffStr } from "#test/test-utils/string-utils";
|
||||||
|
import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils";
|
||||||
|
import { toTitleCase } from "#utils/strings";
|
||||||
|
import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
||||||
|
|
||||||
|
export type toHavePositionalTagOptions<P extends PositionalTagType> = OneOther<serializedPosTagMap[P], "tagType"> & {
|
||||||
|
tagType: P;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matcher to check if the {@linkcode Arena} has a certain number of {@linkcode PositionalTag}s active.
|
||||||
|
* @param received - The object to check. Should be the current {@linkcode GameManager}
|
||||||
|
* @param expectedTag - The {@linkcode PositionalTagType} of the desired tag, or a partially-filled {@linkcode PositionalTag}
|
||||||
|
* containing the desired properties
|
||||||
|
* @param count - The number of tags that should be active; defaults to `1` and must be within the range `[0, 4]`
|
||||||
|
* @returns The result of the matching
|
||||||
|
*/
|
||||||
|
export function toHavePositionalTag<P extends PositionalTagType>(
|
||||||
|
this: MatcherState,
|
||||||
|
received: unknown,
|
||||||
|
expectedTag: P | toHavePositionalTagOptions<P>,
|
||||||
|
count = 1,
|
||||||
|
): SyncExpectationResult {
|
||||||
|
if (!isGameManagerInstance(received)) {
|
||||||
|
return {
|
||||||
|
pass: this.isNot,
|
||||||
|
message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!received.scene?.arena?.positionalTagManager) {
|
||||||
|
return {
|
||||||
|
pass: this.isNot,
|
||||||
|
message: () =>
|
||||||
|
`Expected GameManager.${received.scene?.arena ? "scene.arena.positionalTagManager" : received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Increase limit if triple battles are added
|
||||||
|
if (count < 0 || count > 4) {
|
||||||
|
return {
|
||||||
|
pass: this.isNot,
|
||||||
|
message: () => `Expected count to be between 0 and 4, but got ${count} instead!`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const allTags = received.scene.arena.positionalTagManager.tags;
|
||||||
|
const tagType = typeof expectedTag === "string" ? expectedTag : expectedTag.tagType;
|
||||||
|
const matchingTags = allTags.filter(t => t.tagType === tagType);
|
||||||
|
|
||||||
|
// If checking exclusively tag type, check solely the number of matching tags on field
|
||||||
|
if (typeof expectedTag === "string") {
|
||||||
|
const pass = matchingTags.length === count;
|
||||||
|
const expectedStr = getPosTagStr(expectedTag);
|
||||||
|
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected the Arena to NOT have ${count} ${expectedStr} active, but it did!`
|
||||||
|
: `Expected the Arena to have ${count} ${expectedStr} active, but got ${matchingTags.length} instead!`,
|
||||||
|
expected: expectedTag,
|
||||||
|
actual: allTags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for equality with the provided object
|
||||||
|
if (matchingTags.length === 0) {
|
||||||
|
return {
|
||||||
|
pass: false,
|
||||||
|
message: () => `Expected the Arena to have a tag of type ${expectedTag.tagType}, but it didn't!`,
|
||||||
|
expected: expectedTag.tagType,
|
||||||
|
actual: received.scene.arena.tags.map(t => t.tagType),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass if any of the matching tags meet the criteria
|
||||||
|
const pass = matchingTags.some(tag =>
|
||||||
|
this.equals(tag, expectedTag, [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const expectedStr = getOnelineDiffStr.call(this, expectedTag);
|
||||||
|
return {
|
||||||
|
pass,
|
||||||
|
message: () =>
|
||||||
|
pass
|
||||||
|
? `Expected the Arena to NOT have a tag matching ${expectedStr}, but it did!`
|
||||||
|
: `Expected the Arena to have a tag matching ${expectedStr}, but it didn't!`,
|
||||||
|
expected: expectedTag,
|
||||||
|
actual: matchingTags,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPosTagStr(pType: PositionalTagType, count = 1): string {
|
||||||
|
let ret = toTitleCase(pType) + "Tag";
|
||||||
|
if (count > 1) {
|
||||||
|
ret += "s";
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
@ -23,14 +23,14 @@ export function toHaveStatStage(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (expectedStage < -6 || expectedStage > 6) {
|
if (expectedStage < -6 || expectedStage > 6) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected ${expectedStage} to be within the range [-6, 6]!`,
|
message: () => `Expected ${expectedStage} to be within the range [-6, 6]!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,7 @@ export function toHaveStatusEffect(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -37,10 +37,8 @@ export function toHaveStatusEffect(
|
|||||||
const actualEffect = received.status?.effect ?? StatusEffect.NONE;
|
const actualEffect = received.status?.effect ?? StatusEffect.NONE;
|
||||||
|
|
||||||
// Check exclusively effect equality first, coercing non-matching status effects to numbers.
|
// Check exclusively effect equality first, coercing non-matching status effects to numbers.
|
||||||
if (actualEffect !== (expectedStatus as Exclude<typeof expectedStatus, StatusEffect>)?.effect) {
|
if (typeof expectedStatus === "object" && actualEffect !== expectedStatus.effect) {
|
||||||
// This is actually 100% safe as `expectedStatus?.effect` will evaluate to `undefined` if a StatusEffect was passed,
|
expectedStatus = expectedStatus.effect;
|
||||||
// which will never match actualEffect by definition
|
|
||||||
expectedStatus = (expectedStatus as Exclude<typeof expectedStatus, StatusEffect>).effect;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof expectedStatus === "number") {
|
if (typeof expectedStatus === "number") {
|
||||||
|
@ -24,7 +24,7 @@ export function toHaveTakenDamage(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -20,15 +20,15 @@ export function toHaveTerrain(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isGameManagerInstance(received)) {
|
if (!isGameManagerInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected GameManager, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!received.scene?.arena) {
|
if (!received.scene?.arena) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`,
|
message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,8 +41,8 @@ export function toHaveTerrain(
|
|||||||
pass,
|
pass,
|
||||||
message: () =>
|
message: () =>
|
||||||
pass
|
pass
|
||||||
? `Expected Arena to NOT have ${expectedStr} active, but it did!`
|
? `Expected the Arena to NOT have ${expectedStr} active, but it did!`
|
||||||
: `Expected Arena to have ${expectedStr} active, but got ${actualStr} instead!`,
|
: `Expected the Arena to have ${expectedStr} active, but got ${actualStr} instead!`,
|
||||||
expected: expectedTerrainType,
|
expected: expectedTerrainType,
|
||||||
actual,
|
actual,
|
||||||
};
|
};
|
||||||
|
@ -7,10 +7,16 @@ import { isPokemonInstance, receivedStr } from "../test-utils";
|
|||||||
|
|
||||||
export interface toHaveTypesOptions {
|
export interface toHaveTypesOptions {
|
||||||
/**
|
/**
|
||||||
* Whether to enforce exact matches (`true`) or superset matches (`false`).
|
* Value dictating the strength of the enforced typing match.
|
||||||
* @defaultValue `true`
|
*
|
||||||
|
* Possible values (in ascending order of strength) are:
|
||||||
|
* - `"ordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **and in the same order**
|
||||||
|
* - `"unordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **without checking order**
|
||||||
|
* - `"superset"`: Enforce that the {@linkcode Pokemon}'s types are **a superset of** the expected types
|
||||||
|
* (all must be present, but extras can be there)
|
||||||
|
* @defaultValue `"unordered"`
|
||||||
*/
|
*/
|
||||||
exact?: boolean;
|
mode?: "ordered" | "unordered" | "superset";
|
||||||
/**
|
/**
|
||||||
* Optional arguments to pass to {@linkcode Pokemon.getTypes}.
|
* Optional arguments to pass to {@linkcode Pokemon.getTypes}.
|
||||||
*/
|
*/
|
||||||
@ -18,35 +24,54 @@ export interface toHaveTypesOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Matcher that checks if an array contains exactly the given items, disregarding order.
|
* Matcher that checks if a Pokemon's typing is as expected.
|
||||||
* @param received - The object to check. Should be an array of one or more {@linkcode PokemonType}s.
|
* @param received - The object to check. Should be a {@linkcode Pokemon}
|
||||||
* @param options - The {@linkcode toHaveTypesOptions | options} for this matcher
|
* @param expectedTypes - An array of one or more {@linkcode PokemonType}s to compare against.
|
||||||
|
* @param mode - The mode to perform the matching in.
|
||||||
|
* Possible values (in ascending order of strength) are:
|
||||||
|
* - `"ordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **and in the same order**
|
||||||
|
* - `"unordered"`: Enforce that the {@linkcode Pokemon}'s types are identical **without checking order**
|
||||||
|
* - `"superset"`: Enforce that the {@linkcode Pokemon}'s types are **a superset of** the expected types
|
||||||
|
* (all must be present, but extras can be there)
|
||||||
|
*
|
||||||
|
* Default `unordered`
|
||||||
|
* @param args - Extra arguments passed to {@linkcode Pokemon.getTypes}
|
||||||
* @returns The result of the matching
|
* @returns The result of the matching
|
||||||
*/
|
*/
|
||||||
export function toHaveTypes(
|
export function toHaveTypes(
|
||||||
this: MatcherState,
|
this: MatcherState,
|
||||||
received: unknown,
|
received: unknown,
|
||||||
expected: [PokemonType, ...PokemonType[]],
|
expectedTypes: [PokemonType, ...PokemonType[]],
|
||||||
options: toHaveTypesOptions = {},
|
{ mode = "unordered", args = [] }: toHaveTypesOptions = {},
|
||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to recieve a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const actualTypes = received.getTypes(...(options.args ?? [])).sort();
|
// Return early if no types were passed in
|
||||||
const expectedTypes = expected.slice().sort();
|
if (expectedTypes.length === 0) {
|
||||||
|
return {
|
||||||
|
pass: this.isNot,
|
||||||
|
message: () => "Expected to receive a non-empty array of PokemonTypes!",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Avoid sorting the types if strict ordering is desired
|
||||||
|
const actualSorted = mode === "ordered" ? received.getTypes(...args) : received.getTypes(...args).toSorted();
|
||||||
|
const expectedSorted = mode === "ordered" ? expectedTypes : expectedTypes.toSorted();
|
||||||
|
|
||||||
// Exact matches do not care about subset equality
|
// Exact matches do not care about subset equality
|
||||||
const matchers = options.exact
|
const matchers =
|
||||||
|
mode === "superset"
|
||||||
? [...this.customTesters, this.utils.iterableEquality]
|
? [...this.customTesters, this.utils.iterableEquality]
|
||||||
: [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality];
|
: [...this.customTesters, this.utils.subsetEquality, this.utils.iterableEquality];
|
||||||
const pass = this.equals(actualTypes, expectedTypes, matchers);
|
const pass = this.equals(actualSorted, expectedSorted, matchers);
|
||||||
|
|
||||||
const actualStr = stringifyEnumArray(PokemonType, actualTypes);
|
const actualStr = stringifyEnumArray(PokemonType, actualSorted);
|
||||||
const expectedStr = stringifyEnumArray(PokemonType, expectedTypes);
|
const expectedStr = stringifyEnumArray(PokemonType, expectedSorted);
|
||||||
const pkmName = getPokemonNameWithAffix(received);
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -55,7 +80,7 @@ export function toHaveTypes(
|
|||||||
pass
|
pass
|
||||||
? `Expected ${pkmName} to NOT have types ${expectedStr}, but it did!`
|
? `Expected ${pkmName} to NOT have types ${expectedStr}, but it did!`
|
||||||
: `Expected ${pkmName} to have types ${expectedStr}, but got ${actualStr} instead!`,
|
: `Expected ${pkmName} to have types ${expectedStr}, but got ${actualStr} instead!`,
|
||||||
expected: expectedTypes,
|
expected: expectedSorted,
|
||||||
actual: actualTypes,
|
actual: actualSorted,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
|||||||
/**
|
/**
|
||||||
* Matcher to check the contents of a {@linkcode Pokemon}'s move history.
|
* Matcher to check the contents of a {@linkcode Pokemon}'s move history.
|
||||||
* @param received - The actual value received. Should be a {@linkcode Pokemon}
|
* @param received - The actual value received. Should be a {@linkcode Pokemon}
|
||||||
* @param expectedValue - The {@linkcode MoveId} the Pokemon is expected to have used,
|
* @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used,
|
||||||
* or a partially filled {@linkcode TurnMove} containing the desired properties to check
|
* or a partially filled {@linkcode TurnMove} containing the desired properties to check
|
||||||
* @param index - The index of the move history entry to check, in order from most recent to least recent.
|
* @param index - The index of the move history entry to check, in order from most recent to least recent.
|
||||||
* Default `0` (last used move)
|
* Default `0` (last used move)
|
||||||
@ -22,12 +22,12 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
|||||||
export function toHaveUsedMove(
|
export function toHaveUsedMove(
|
||||||
this: MatcherState,
|
this: MatcherState,
|
||||||
received: unknown,
|
received: unknown,
|
||||||
expectedResult: MoveId | AtLeastOne<TurnMove>,
|
expectedMove: MoveId | AtLeastOne<TurnMove>,
|
||||||
index = 0,
|
index = 0,
|
||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -37,34 +37,33 @@ export function toHaveUsedMove(
|
|||||||
|
|
||||||
if (move === undefined) {
|
if (move === undefined) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected ${pkmName} to have used ${index + 1} moves, but it didn't!`,
|
message: () => `Expected ${pkmName} to have used ${index + 1} moves, but it didn't!`,
|
||||||
actual: received.getLastXMoves(-1),
|
actual: received.getLastXMoves(-1),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Coerce to a `TurnMove`
|
// Coerce to a `TurnMove`
|
||||||
if (typeof expectedResult === "number") {
|
if (typeof expectedMove === "number") {
|
||||||
expectedResult = { move: expectedResult };
|
expectedMove = { move: expectedMove };
|
||||||
}
|
}
|
||||||
|
|
||||||
const moveIndexStr = index === 0 ? "last move" : `${getOrdinal(index)} most recent move`;
|
const moveIndexStr = index === 0 ? "last move" : `${getOrdinal(index)} most recent move`;
|
||||||
|
|
||||||
const pass = this.equals(move, expectedResult, [
|
const pass = this.equals(move, expectedMove, [
|
||||||
...this.customTesters,
|
...this.customTesters,
|
||||||
this.utils.subsetEquality,
|
this.utils.subsetEquality,
|
||||||
this.utils.iterableEquality,
|
this.utils.iterableEquality,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const expectedStr = getOnelineDiffStr.call(this, expectedResult);
|
const expectedStr = getOnelineDiffStr.call(this, expectedMove);
|
||||||
return {
|
return {
|
||||||
pass,
|
pass,
|
||||||
message: () =>
|
message: () =>
|
||||||
pass
|
pass
|
||||||
? `Expected ${pkmName}'s ${moveIndexStr} to NOT match ${expectedStr}, but it did!`
|
? `Expected ${pkmName}'s ${moveIndexStr} to NOT match ${expectedStr}, but it did!`
|
||||||
: // Replace newlines with spaces to preserve one-line ness
|
: `Expected ${pkmName}'s ${moveIndexStr} to match ${expectedStr}, but it didn't!`,
|
||||||
`Expected ${pkmName}'s ${moveIndexStr} to match ${expectedStr}, but it didn't!`,
|
expected: expectedMove,
|
||||||
expected: expectedResult,
|
|
||||||
actual: move,
|
actual: move,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
|||||||
/**
|
/**
|
||||||
* Matcher to check the amount of PP consumed by a {@linkcode Pokemon}.
|
* Matcher to check the amount of PP consumed by a {@linkcode Pokemon}.
|
||||||
* @param received - The actual value received. Should be a {@linkcode Pokemon}
|
* @param received - The actual value received. Should be a {@linkcode Pokemon}
|
||||||
* @param expectedValue - The {@linkcode MoveId} that should have consumed PP
|
* @param moveId - The {@linkcode MoveId} that should have consumed PP
|
||||||
* @param ppUsed - The numerical amount of PP that should have been consumed,
|
* @param ppUsed - The numerical amount of PP that should have been consumed,
|
||||||
* or `all` to indicate the move should be _out_ of PP
|
* or `all` to indicate the move should be _out_ of PP
|
||||||
* @returns Whether the matcher passed
|
* @returns Whether the matcher passed
|
||||||
@ -23,35 +23,35 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect";
|
|||||||
export function toHaveUsedPP(
|
export function toHaveUsedPP(
|
||||||
this: MatcherState,
|
this: MatcherState,
|
||||||
received: unknown,
|
received: unknown,
|
||||||
expectedMove: MoveId,
|
moveId: MoveId,
|
||||||
ppUsed: number | "all",
|
ppUsed: number | "all",
|
||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isPokemonInstance(received)) {
|
if (!isPokemonInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a Pokémon, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const override = received.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.OPP_MOVESET_OVERRIDE;
|
const override = received.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.ENEMY_MOVESET_OVERRIDE;
|
||||||
if (coerceArray(override).length > 0) {
|
if (coerceArray(override).length > 0) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () =>
|
message: () =>
|
||||||
`Cannot test for PP consumption with ${received.isPlayer() ? "player" : "enemy"} moveset overrides active!`,
|
`Cannot test for PP consumption with ${received.isPlayer() ? "player" : "enemy"} moveset overrides active!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const pkmName = getPokemonNameWithAffix(received);
|
const pkmName = getPokemonNameWithAffix(received);
|
||||||
const moveStr = getEnumStr(MoveId, expectedMove);
|
const moveStr = getEnumStr(MoveId, moveId);
|
||||||
|
|
||||||
const movesetMoves = received.getMoveset().filter(pm => pm.moveId === expectedMove);
|
const movesetMoves = received.getMoveset().filter(pm => pm.moveId === moveId);
|
||||||
if (movesetMoves.length !== 1) {
|
if (movesetMoves.length !== 1) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () =>
|
message: () =>
|
||||||
`Expected MoveId.${moveStr} to appear in ${pkmName}'s moveset exactly once, but got ${movesetMoves.length} times!`,
|
`Expected MoveId.${moveStr} to appear in ${pkmName}'s moveset exactly once, but got ${movesetMoves.length} times!`,
|
||||||
expected: expectedMove,
|
expected: moveId,
|
||||||
actual: received.getMoveset(),
|
actual: received.getMoveset(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -20,15 +20,15 @@ export function toHaveWeather(
|
|||||||
): SyncExpectationResult {
|
): SyncExpectationResult {
|
||||||
if (!isGameManagerInstance(received)) {
|
if (!isGameManagerInstance(received)) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected GameManager, but got ${receivedStr(received)}!`,
|
message: () => `Expected to receive a GameManager, but got ${receivedStr(received)}!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!received.scene?.arena) {
|
if (!received.scene?.arena) {
|
||||||
return {
|
return {
|
||||||
pass: false,
|
pass: this.isNot,
|
||||||
message: () => `Expected GameManager.${received.scene ? "scene" : "scene.arena"} to be defined!`,
|
message: () => `Expected GameManager.${received.scene ? "scene.arena" : "scene"} to be defined!`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,8 +41,8 @@ export function toHaveWeather(
|
|||||||
pass,
|
pass,
|
||||||
message: () =>
|
message: () =>
|
||||||
pass
|
pass
|
||||||
? `Expected Arena to NOT have ${expectedStr} weather active, but it did!`
|
? `Expected the Arena to NOT have ${expectedStr} weather active, but it did!`
|
||||||
: `Expected Arena to have ${expectedStr} weather active, but got ${actualStr} instead!`,
|
: `Expected the Arena to have ${expectedStr} weather active, but got ${actualStr} instead!`,
|
||||||
expected: expectedWeatherType,
|
expected: expectedWeatherType,
|
||||||
actual,
|
actual,
|
||||||
};
|
};
|
||||||
|
@ -34,10 +34,10 @@ interface getEnumStrOptions {
|
|||||||
* @returns The stringified representation of `val` as dictated by the options.
|
* @returns The stringified representation of `val` as dictated by the options.
|
||||||
* @example
|
* @example
|
||||||
* ```ts
|
* ```ts
|
||||||
* enum fakeEnum {
|
* enum testEnum {
|
||||||
* ONE: 1,
|
* ONE = 1,
|
||||||
* TWO: 2,
|
* TWO = 2,
|
||||||
* THREE: 3,
|
* THREE = 3,
|
||||||
* }
|
* }
|
||||||
* getEnumStr(fakeEnum, fakeEnum.ONE); // Output: "ONE (=1)"
|
* getEnumStr(fakeEnum, fakeEnum.ONE); // Output: "ONE (=1)"
|
||||||
* getEnumStr(fakeEnum, fakeEnum.TWO, {casing: "Title", prefix: "fakeEnum.", suffix: "!!!"}); // Output: "fakeEnum.TWO!!! (=2)"
|
* getEnumStr(fakeEnum, fakeEnum.TWO, {casing: "Title", prefix: "fakeEnum.", suffix: "!!!"}); // Output: "fakeEnum.TWO!!! (=2)"
|
||||||
@ -174,10 +174,14 @@ export function getStatName(s: Stat): string {
|
|||||||
* Convert an object into a oneline diff to be shown in an error message.
|
* Convert an object into a oneline diff to be shown in an error message.
|
||||||
* @param obj - The object to return the oneline diff of
|
* @param obj - The object to return the oneline diff of
|
||||||
* @returns The updated diff
|
* @returns The updated diff
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* const diff = getOnelineDiffStr.call(this, obj)
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
export function getOnelineDiffStr(this: MatcherState, obj: unknown): string {
|
export function getOnelineDiffStr(this: MatcherState, obj: unknown): string {
|
||||||
return this.utils
|
return this.utils
|
||||||
.stringify(obj, undefined, { maxLength: 35, indent: 0, printBasicPrototype: false })
|
.stringify(obj, undefined, { maxLength: 35, indent: 0, printBasicPrototype: false })
|
||||||
.replace(/\n/g, " ") // Replace newlines with spaces
|
.replace(/\n/g, " ") // Replace newlines with spaces
|
||||||
.replace(/,(\s*)}$/g, "$1}");
|
.replace(/,(\s*)}$/g, "$1}"); // Trim trailing commas
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user