Merge branch 'beta' into ChampionAdjustments

This commit is contained in:
Blitzy 2025-08-01 09:42:06 -05:00 committed by GitHub
commit 2e63914a34
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1467 additions and 52 deletions

View File

@ -17,7 +17,7 @@
"typecheck": "tsc --noEmit",
"eslint": "eslint --fix .",
"eslint-ci": "eslint .",
"biome": "biome check --write --changed --no-errors-on-unmatched",
"biome": "biome check --write --changed --no-errors-on-unmatched --diagnostic-level=error",
"biome-ci": "biome ci --diagnostic-level=error --reporter=github --no-errors-on-unmatched",
"docs": "typedoc",
"depcruise": "depcruise src test",

View File

@ -0,0 +1,188 @@
{
"textures": [
{
"image": "statuses_tl.png",
"format": "RGBA8888",
"size": {
"w": 22,
"h": 64
},
"scale": 1,
"frames": [
{
"filename": "pokerus",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 22,
"h": 8
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 22,
"h": 8
},
"frame": {
"x": 0,
"y": 0,
"w": 22,
"h": 8
}
},
{
"filename": "burn",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 20,
"h": 8
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 20,
"h": 8
},
"frame": {
"x": 0,
"y": 8,
"w": 20,
"h": 8
}
},
{
"filename": "faint",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 20,
"h": 8
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 20,
"h": 8
},
"frame": {
"x": 0,
"y": 16,
"w": 20,
"h": 8
}
},
{
"filename": "freeze",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 20,
"h": 8
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 20,
"h": 8
},
"frame": {
"x": 0,
"y": 24,
"w": 20,
"h": 8
}
},
{
"filename": "paralysis",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 20,
"h": 8
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 20,
"h": 8
},
"frame": {
"x": 0,
"y": 32,
"w": 20,
"h": 8
}
},
{
"filename": "poison",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 20,
"h": 8
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 20,
"h": 8
},
"frame": {
"x": 0,
"y": 40,
"w": 20,
"h": 8
}
},
{
"filename": "sleep",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 20,
"h": 8
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 20,
"h": 8
},
"frame": {
"x": 0,
"y": 48,
"w": 20,
"h": 8
}
},
{
"filename": "toxic",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 20,
"h": 8
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 20,
"h": 8
},
"frame": {
"x": 0,
"y": 56,
"w": 20,
"h": 8
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:37686e85605d17b806f22d43081c1139:70535ffee63ba61b3397d8470c2c8982:e6649238c018d3630e55681417c698ca$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 B

440
public/images/types_tl.json Normal file
View File

@ -0,0 +1,440 @@
{
"textures": [
{
"image": "types_tl.png",
"format": "RGBA8888",
"size": {
"w": 32,
"h": 280
},
"scale": 1,
"frames": [
{
"filename": "unknown",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
}
},
{
"filename": "bug",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 14,
"w": 32,
"h": 14
}
},
{
"filename": "dark",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 28,
"w": 32,
"h": 14
}
},
{
"filename": "dragon",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 42,
"w": 32,
"h": 14
}
},
{
"filename": "electric",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 56,
"w": 32,
"h": 14
}
},
{
"filename": "fairy",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 70,
"w": 32,
"h": 14
}
},
{
"filename": "fighting",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 84,
"w": 32,
"h": 14
}
},
{
"filename": "fire",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 98,
"w": 32,
"h": 14
}
},
{
"filename": "flying",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 112,
"w": 32,
"h": 14
}
},
{
"filename": "ghost",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 126,
"w": 32,
"h": 14
}
},
{
"filename": "grass",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 140,
"w": 32,
"h": 14
}
},
{
"filename": "ground",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 154,
"w": 32,
"h": 14
}
},
{
"filename": "ice",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 168,
"w": 32,
"h": 14
}
},
{
"filename": "normal",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 182,
"w": 32,
"h": 14
}
},
{
"filename": "poison",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 196,
"w": 32,
"h": 14
}
},
{
"filename": "psychic",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 210,
"w": 32,
"h": 14
}
},
{
"filename": "rock",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 224,
"w": 32,
"h": 14
}
},
{
"filename": "steel",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 238,
"w": 32,
"h": 14
}
},
{
"filename": "water",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 252,
"w": 32,
"h": 14
}
},
{
"filename": "stellar",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 32,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 32,
"h": 14
},
"frame": {
"x": 0,
"y": 266,
"w": 32,
"h": 14
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:f14cf47d9a8f1d40c8e03aa6ba00fff3:6fc4227b57a95d429a1faad4280f7ec8:5961efbfbf4c56b8745347e7a663a32f$"
}
}

BIN
public/images/types_tl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

View File

@ -0,0 +1,62 @@
{
"textures": [
{
"image": "party_discard.png",
"format": "RGBA8888",
"size": {
"w": 75,
"h": 50
},
"scale": 1,
"frames": [
{
"filename": "normal",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 75,
"h": 25
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 75,
"h": 25
},
"frame": {
"x": 0,
"y": 0,
"w": 75,
"h": 25
}
},
{
"filename": "selected",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 75,
"h": 25
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 75,
"h": 25
},
"frame": {
"x": 0,
"y": 25,
"w": 75,
"h": 25
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:17219773dfffd6b1204d988fea3f9462:1127ad21d64bc7ebb9df4fc28f3d2d39:7ad46e8fb4648c3d3d84a746ecb371ea$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

View File

@ -0,0 +1,62 @@
{
"textures": [
{
"image": "party_transfer.png",
"format": "RGBA8888",
"size": {
"w": 75,
"h": 50
},
"scale": 1,
"frames": [
{
"filename": "normal",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 75,
"h": 25
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 75,
"h": 25
},
"frame": {
"x": 0,
"y": 0,
"w": 75,
"h": 25
}
},
{
"filename": "selected",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 75,
"h": 25
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 75,
"h": 25
},
"frame": {
"x": 0,
"y": 25,
"w": 75,
"h": 25
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:17219773dfffd6b1204d988fea3f9462:1127ad21d64bc7ebb9df4fc28f3d2d39:7ad46e8fb4648c3d3d84a746ecb371ea$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 837 B

View File

@ -0,0 +1,62 @@
{
"textures": [
{
"image": "party_discard.png",
"format": "RGBA8888",
"size": {
"w": 75,
"h": 50
},
"scale": 1,
"frames": [
{
"filename": "normal",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 75,
"h": 25
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 75,
"h": 25
},
"frame": {
"x": 0,
"y": 0,
"w": 75,
"h": 25
}
},
{
"filename": "selected",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 75,
"h": 25
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 75,
"h": 25
},
"frame": {
"x": 0,
"y": 25,
"w": 75,
"h": 25
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:17219773dfffd6b1204d988fea3f9462:1127ad21d64bc7ebb9df4fc28f3d2d39:7ad46e8fb4648c3d3d84a746ecb371ea$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

View File

@ -0,0 +1,62 @@
{
"textures": [
{
"image": "party_transfer.png",
"format": "RGBA8888",
"size": {
"w": 75,
"h": 50
},
"scale": 1,
"frames": [
{
"filename": "normal",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 75,
"h": 25
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 75,
"h": 25
},
"frame": {
"x": 0,
"y": 0,
"w": 75,
"h": 25
}
},
{
"filename": "selected",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 75,
"h": 25
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 75,
"h": 25
},
"frame": {
"x": 0,
"y": 25,
"w": 75,
"h": 25
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:17219773dfffd6b1204d988fea3f9462:1127ad21d64bc7ebb9df4fc28f3d2d39:7ad46e8fb4648c3d3d84a746ecb371ea$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 B

@ -1 +1 @@
Subproject commit e2fbba17ea7a96068970ea98a8a84ed3e25b6f07
Subproject commit 7898c0018a70601a6ead76c9dd497ff966cc2e2a

View File

@ -2845,6 +2845,23 @@ export class BattleScene extends SceneBase {
}
return false;
}
/**
* Attempt to discard one or more copies of a held item.
* @param itemModifier - The {@linkcode PokemonHeldItemModifier} being discarded
* @param discardQuantity - The number of copies to remove (up to the amount currently held); default `1`
* @returns Whether the item was successfully discarded.
* Removing fewer items than requested is still considered a success.
*/
tryDiscardHeldItemModifier(itemModifier: PokemonHeldItemModifier, discardQuantity = 1): boolean {
const countTaken = Math.min(discardQuantity, itemModifier.stackCount);
itemModifier.stackCount -= countTaken;
if (itemModifier.stackCount > 0) {
return true;
}
return this.removeModifier(itemModifier);
}
canTransferHeldItemModifier(itemModifier: PokemonHeldItemModifier, target: Pokemon, transferQuantity = 1): boolean {
const mod = itemModifier.clone() as PokemonHeldItemModifier;

View File

@ -71,13 +71,10 @@ import i18next from "i18next";
* // Then we must also define a loadTag method with one of the following signatures
* public override loadTag(source: BaseArenaTag & Pick<ExampleTag, "tagType" | "a" | "b"): void;
* public override loadTag<const T extends this>(source: BaseArenaTag & Pick<T, "tagType" | "a" | "b">): void;
* public override loadTag(source: NonFunctionProperties<ExampleTag>): void;
* }
* ```
* Notes
* - If the class has any subclasses, then the second form of `loadTag` *must* be used.
* - The third form *must not* be used if the class has any getters, as typescript would expect such fields to be
* present in `source`.
*/
/** Interface containing the serializable fields of ArenaTagData. */
@ -1659,7 +1656,10 @@ export function getArenaTag(
* @param source - An arena tag
* @returns The valid arena tag
*/
export function loadArenaTag(source: ArenaTag | ArenaTagTypeData): ArenaTag {
export function loadArenaTag(source: ArenaTag | ArenaTagTypeData | { tagType: ArenaTagType.NONE }): ArenaTag {
if (source.tagType === ArenaTagType.NONE) {
return new NoneTag();
}
const tag =
getArenaTag(source.tagType, source.turnCount, source.sourceMove, source.sourceId, source.side) ?? new NoneTag();
tag.loadTag(source);

View File

@ -44,7 +44,7 @@ import type {
SemiInvulnerableTagType,
TrappingBattlerTagType,
} from "#types/battler-tags";
import type { Mutable, NonFunctionProperties } from "#types/type-helpers";
import type { Mutable } from "#types/type-helpers";
import { BooleanHolder, coerceArray, getFrameMs, isNullOrUndefined, NumberHolder, toDmgValue } from "#utils/common";
/**
@ -80,13 +80,10 @@ import { BooleanHolder, coerceArray, getFrameMs, isNullOrUndefined, NumberHolder
* // Then we must also define a loadTag method with one of the following signatures
* public override loadTag(source: BaseBattlerTag & Pick<ExampleTag, "tagType" | "a" | "b"): void;
* public override loadTag<const T extends this>(source: BaseBattlerTag & Pick<T, "tagType" | "a" | "b">): void;
* public override loadTag(source: NonFunctionProperties<ExampleTag>): void;
* }
* ```
* Notes
* - If the class has any subclasses, then the second form of `loadTag` *must* be used.
* - The third form *must not* be used if the class has any getters, as typescript would expect such fields to be
* present in `source`.
*/
/** Interface containing the serializable fields of BattlerTag */
@ -201,7 +198,7 @@ export class BattlerTag implements BaseBattlerTag {
}
}
export abstract class SerializableBattlerTag extends BattlerTag {
export class SerializableBattlerTag extends BattlerTag {
/** Nonexistent, dummy field to allow typescript to distinguish this class from `BattlerTag` */
private declare __SerializableBattlerTag: never;
}
@ -419,7 +416,6 @@ export class GorillaTacticsTag extends MoveRestrictionBattlerTag {
public override readonly tagType = BattlerTagType.GORILLA_TACTICS;
/** ID of the move that the user is locked into using*/
public readonly moveId: MoveId = MoveId.NONE;
constructor() {
super(BattlerTagType.GORILLA_TACTICS, BattlerTagLapseType.CUSTOM, 0);
}
@ -1235,7 +1231,7 @@ export class EncoreTag extends MoveRestrictionBattlerTag {
);
}
public override loadTag(source: NonFunctionProperties<EncoreTag>): void {
public override loadTag(source: BaseBattlerTag & Pick<EncoreTag, "tagType" | "moveId">): void {
super.loadTag(source);
this.moveId = source.moveId;
}
@ -2038,7 +2034,7 @@ export class TruantTag extends AbilityBattlerTag {
const lastMove = pokemon.getLastXMoves()[0];
if (!lastMove) {
if (!lastMove || lastMove.move === MoveId.NONE) {
// Don't interrupt move if last move was `Moves.NONE` OR no prior move was found
return true;
}
@ -2618,7 +2614,7 @@ export class CommandedTag extends SerializableBattlerTag {
}
}
override loadTag(source: NonFunctionProperties<CommandedTag>): void {
override loadTag(source: BaseBattlerTag & Pick<CommandedTag, "tagType" | "tatsugiriFormKey">): void {
super.loadTag(source);
(this as Mutable<this>).tatsugiriFormKey = source.tatsugiriFormKey;
}
@ -2659,7 +2655,9 @@ export class StockpilingTag extends SerializableBattlerTag {
}
};
public override loadTag(source: NonFunctionProperties<StockpilingTag>): void {
public override loadTag(
source: BaseBattlerTag & Pick<StockpilingTag, "tagType" | "stockpiledCount" | "statChangeCounts">,
): void {
super.loadTag(source);
this.stockpiledCount = source.stockpiledCount || 0;
this.statChangeCounts = {
@ -3006,7 +3004,7 @@ export class AutotomizedTag extends SerializableBattlerTag {
this.onAdd(pokemon);
}
public override loadTag(source: NonFunctionProperties<AutotomizedTag>): void {
public override loadTag(source: BaseBattlerTag & Pick<AutotomizedTag, "tagType" | "autotomizeCount">): void {
super.loadTag(source);
this.autotomizeCount = source.autotomizeCount;
}
@ -3641,7 +3639,7 @@ export function getBattlerTag(
case BattlerTagType.FRENZY:
return new FrenzyTag(turnCount, sourceMove, sourceId);
case BattlerTagType.CHARGING:
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, 1, sourceMove, sourceId);
return new SerializableBattlerTag(tagType, BattlerTagLapseType.CUSTOM, 1, sourceMove, sourceId);
case BattlerTagType.ENCORE:
return new EncoreTag(sourceId);
case BattlerTagType.HELPING_HAND:
@ -3726,10 +3724,10 @@ export function getBattlerTag(
return new DragonCheerTag();
case BattlerTagType.ALWAYS_CRIT:
case BattlerTagType.IGNORE_ACCURACY:
return new BattlerTag(tagType, BattlerTagLapseType.TURN_END, 2, sourceMove);
return new SerializableBattlerTag(tagType, BattlerTagLapseType.TURN_END, 2, sourceMove);
case BattlerTagType.ALWAYS_GET_HIT:
case BattlerTagType.RECEIVE_DOUBLE_DAMAGE:
return new BattlerTag(tagType, BattlerTagLapseType.PRE_MOVE, 1, sourceMove);
return new SerializableBattlerTag(tagType, BattlerTagLapseType.PRE_MOVE, 1, sourceMove);
case BattlerTagType.BYPASS_SLEEP:
return new BattlerTag(tagType, BattlerTagLapseType.TURN_END, turnCount, sourceMove);
case BattlerTagType.IGNORE_FLYING:
@ -3856,7 +3854,7 @@ export type BattlerTagTypeMap = {
[BattlerTagType.POWDER]: PowderTag;
[BattlerTagType.NIGHTMARE]: NightmareTag;
[BattlerTagType.FRENZY]: FrenzyTag;
[BattlerTagType.CHARGING]: BattlerTag;
[BattlerTagType.CHARGING]: SerializableBattlerTag;
[BattlerTagType.ENCORE]: EncoreTag;
[BattlerTagType.HELPING_HAND]: HelpingHandTag;
[BattlerTagType.INGRAIN]: IngrainTag;
@ -3897,10 +3895,10 @@ export type BattlerTagTypeMap = {
[BattlerTagType.FIRE_BOOST]: TypeBoostTag;
[BattlerTagType.CRIT_BOOST]: CritBoostTag;
[BattlerTagType.DRAGON_CHEER]: DragonCheerTag;
[BattlerTagType.ALWAYS_CRIT]: BattlerTag;
[BattlerTagType.IGNORE_ACCURACY]: BattlerTag;
[BattlerTagType.ALWAYS_GET_HIT]: BattlerTag;
[BattlerTagType.RECEIVE_DOUBLE_DAMAGE]: BattlerTag;
[BattlerTagType.ALWAYS_CRIT]: SerializableBattlerTag;
[BattlerTagType.IGNORE_ACCURACY]: SerializableBattlerTag;
[BattlerTagType.ALWAYS_GET_HIT]: SerializableBattlerTag;
[BattlerTagType.RECEIVE_DOUBLE_DAMAGE]: SerializableBattlerTag;
[BattlerTagType.BYPASS_SLEEP]: BattlerTag;
[BattlerTagType.IGNORE_FLYING]: GroundedTag;
[BattlerTagType.ROOSTED]: RoostedTag;

View File

@ -119,6 +119,7 @@ export class LoadingScene extends SceneBase {
this.loadImage("party_bg", "ui");
this.loadImage("party_bg_double", "ui");
this.loadImage("party_bg_double_manage", "ui");
this.loadAtlas("party_slot_main", "ui");
this.loadAtlas("party_slot", "ui");
this.loadImage("party_slot_overlay_lv", "ui");
@ -126,6 +127,8 @@ export class LoadingScene extends SceneBase {
this.loadAtlas("party_slot_hp_overlay", "ui");
this.loadAtlas("party_pb", "ui");
this.loadAtlas("party_cancel", "ui");
this.loadAtlas("party_discard", "ui");
this.loadAtlas("party_transfer", "ui");
this.loadImage("summary_bg", "ui");
this.loadImage("summary_overlay_shiny", "ui");

View File

@ -79,13 +79,13 @@ const fonts: Array<LoadingFontFaceProperty> = [
face: new FontFace("emerald", "url(./fonts/pokemon-bw.ttf)", {
unicodeRange: rangesByLanguage.japanese,
}),
only: ["en", "es", "fr", "it", "de", "pt", "ko", "ja", "ca", "da", "tr", "ro", "ru"],
only: ["en", "es", "fr", "it", "de", "pt", "ko", "ja", "ca", "da", "tr", "ro", "ru", "tl"],
},
{
face: new FontFace("pkmnems", "url(./fonts/pokemon-bw.ttf)", {
unicodeRange: rangesByLanguage.japanese,
}),
only: ["en", "es", "fr", "it", "de", "pt", "ko", "ja", "ca", "da", "tr", "ro", "ru"],
only: ["en", "es", "fr", "it", "de", "pt", "ko", "ja", "ca", "da", "tr", "ro", "ru", "tl"],
},
];
@ -191,6 +191,7 @@ export async function initI18n(): Promise<void> {
"tr",
"ro",
"ru",
"tl",
],
backend: {
loadPath(lng: string, [ns]: string[]) {

View File

@ -981,6 +981,10 @@ export function setSetting(setting: string, value: number): boolean {
label: "Română (Needs Help)",
handler: () => changeLocaleHandler("ro"),
},
{
label: "Tagalog (Needs Help)",
handler: () => changeLocaleHandler("tl"),
},
{
label: i18next.t("settings:back"),
handler: () => cancelHandler(),

View File

@ -69,7 +69,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler {
if (context) {
context.font = styleOptions.fontSize + "px " + styleOptions.fontFamily;
this.transferButtonWidth = context.measureText(i18next.t("modifierSelectUiHandler:transfer")).width;
this.transferButtonWidth = context.measureText(i18next.t("modifierSelectUiHandler:manageItems")).width;
this.checkButtonWidth = context.measureText(i18next.t("modifierSelectUiHandler:checkTeam")).width;
}
@ -81,7 +81,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler {
this.transferButtonContainer.setVisible(false);
ui.add(this.transferButtonContainer);
const transferButtonText = addTextObject(-4, -2, i18next.t("modifierSelectUiHandler:transfer"), TextStyle.PARTY);
const transferButtonText = addTextObject(-4, -2, i18next.t("modifierSelectUiHandler:manageItems"), TextStyle.PARTY);
transferButtonText.setName("text-transfer-btn");
transferButtonText.setOrigin(1, 0);
this.transferButtonContainer.add(transferButtonText);
@ -601,7 +601,7 @@ export class ModifierSelectUiHandler extends AwaitableUiHandler {
(globalScene.game.canvas.width - this.transferButtonWidth - this.checkButtonWidth) / 6 - 30,
OPTION_BUTTON_YPOSITION + 4,
);
ui.showText(i18next.t("modifierSelectUiHandler:transferDesc"));
ui.showText(i18next.t("modifierSelectUiHandler:manageItemsDesc"));
} else if (cursor === 2) {
this.cursorObj.setPosition(
(globalScene.game.canvas.width - this.checkButtonWidth) / 6 - 10,

View File

@ -103,6 +103,11 @@ export enum PartyUiMode {
* This is generally used in for Mystery Encounter or special effects that require the player to select a Pokemon
*/
SELECT,
/**
* Indicates that the party UI is open to select a party member from which items will be discarded.
* This type of selection can be cancelled.
*/
DISCARD,
}
export enum PartyOption {
@ -121,6 +126,7 @@ export enum PartyOption {
RELEASE,
RENAME,
SELECT,
DISCARD,
SCROLL_UP = 1000,
SCROLL_DOWN = 1001,
FORM_CHANGE_ITEM = 2000,
@ -155,6 +161,7 @@ export class PartyUiHandler extends MessageUiHandler {
private partySlotsContainer: Phaser.GameObjects.Container;
private partySlots: PartySlot[];
private partyCancelButton: PartyCancelButton;
private partyDiscardModeButton: PartyDiscardModeButton;
private partyMessageBox: Phaser.GameObjects.NineSlice;
private moveInfoOverlay: MoveInfoOverlay;
@ -180,6 +187,8 @@ export class PartyUiHandler extends MessageUiHandler {
private transferAll: boolean;
private lastCursor = 0;
private lastLeftPokemonCursor = 0;
private lastRightPokemonCursor = 0;
private selectCallback: PartySelectCallback | PartyModifierTransferSelectCallback | null;
private selectFilter: PokemonSelectFilter | PokemonModifierTransferSelectFilter;
private moveSelectFilter: PokemonMoveSelectFilter;
@ -308,6 +317,12 @@ export class PartyUiHandler extends MessageUiHandler {
this.iconAnimHandler = new PokemonIconAnimHandler();
this.iconAnimHandler.setup();
const partyDiscardModeButton = new PartyDiscardModeButton(60, -globalScene.game.canvas.height / 15 - 1, this);
partyContainer.add(partyDiscardModeButton);
this.partyDiscardModeButton = partyDiscardModeButton;
// prepare move overlay. in case it appears to be too big, set the overlayScale to .5
const overlayScale = 1;
this.moveInfoOverlay = new MoveInfoOverlay({
@ -349,8 +364,18 @@ export class PartyUiHandler extends MessageUiHandler {
this.showMovePp = args.length > 6 && args[6];
this.partyContainer.setVisible(true);
this.partyBg.setTexture(`party_bg${globalScene.currentBattle.double ? "_double" : ""}`);
if (this.isItemManageMode()) {
this.partyBg.setTexture(`party_bg${globalScene.currentBattle.double ? "_double_manage" : ""}`);
} else {
this.partyBg.setTexture(`party_bg${globalScene.currentBattle.double ? "_double" : ""}`);
}
this.populatePartySlots();
// If we are currently transferring items, set the icon to its proper state and reveal the button.
if (this.isItemManageMode()) {
this.partyDiscardModeButton.toggleIcon(this.partyUiMode as PartyUiMode.MODIFIER_TRANSFER | PartyUiMode.DISCARD);
}
this.showPartyText();
this.setCursor(0);
return true;
@ -595,7 +620,7 @@ export class PartyUiHandler extends MessageUiHandler {
const option = this.options[this.optionsCursor];
if (button === Button.LEFT) {
/** Decrease quantity for the current item and update UI */
if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER) {
if (this.isItemManageMode()) {
this.transferQuantities[option] =
this.transferQuantities[option] === 1
? this.transferQuantitiesMax[option]
@ -609,7 +634,7 @@ export class PartyUiHandler extends MessageUiHandler {
if (button === Button.RIGHT) {
/** Increase quantity for the current item and update UI */
if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER) {
if (this.isItemManageMode()) {
this.transferQuantities[option] =
this.transferQuantities[option] === this.transferQuantitiesMax[option]
? 1
@ -639,6 +664,45 @@ export class PartyUiHandler extends MessageUiHandler {
return success;
}
private processDiscardMenuInput(pokemon: PlayerPokemon) {
const ui = this.getUi();
const option = this.options[this.optionsCursor];
this.clearOptions();
this.blockInput = true;
this.showText(i18next.t("partyUiHandler:discardConfirmation"), null, () => {
this.blockInput = false;
ui.setModeWithoutClear(
UiMode.CONFIRM,
() => {
ui.setMode(UiMode.PARTY);
this.doDiscard(option, pokemon);
},
() => {
ui.setMode(UiMode.PARTY);
this.showPartyText();
},
);
});
return true;
}
private doDiscard(option: PartyOption, pokemon: PlayerPokemon) {
const itemModifiers = this.getTransferrableItemsFromPokemon(pokemon);
this.clearOptions();
if (option === PartyOption.ALL) {
// Discard all currently held items
for (let i = 0; i < itemModifiers.length; i++) {
globalScene.tryDiscardHeldItemModifier(itemModifiers[i], this.transferQuantities[i]);
}
} else {
// Discard the currently selected item
globalScene.tryDiscardHeldItemModifier(itemModifiers[option], this.transferQuantities[option]);
}
}
private moveOptionCursor(button: Button.UP | Button.DOWN): boolean {
if (button === Button.UP) {
return this.setCursor(this.optionsCursor ? this.optionsCursor - 1 : this.options.length - 1);
@ -725,6 +789,10 @@ export class PartyUiHandler extends MessageUiHandler {
return this.processModifierTransferModeInput(pokemon);
}
if (this.partyUiMode === PartyUiMode.DISCARD) {
return this.processDiscardMenuInput(pokemon);
}
// options specific to the mode (moves)
if (this.partyUiMode === PartyUiMode.REMEMBER_MOVE_MODIFIER) {
return this.processRememberMoveModeInput(pokemon);
@ -864,7 +932,7 @@ export class PartyUiHandler extends MessageUiHandler {
}
if (button === Button.LEFT || button === Button.RIGHT) {
if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER) {
if (this.isItemManageMode()) {
return this.processModifierTransferModeLeftRightInput(button);
}
}
@ -919,10 +987,22 @@ export class PartyUiHandler extends MessageUiHandler {
return !(this.partyUiMode === PartyUiMode.FAINT_SWITCH || this.partyUiMode === PartyUiMode.REVIVAL_BLESSING);
}
/**
* Return whether this UI handler is responsible for managing items.
* Used to ensure proper placement of mode toggle buttons in the UI, etc.
* @returns Whether the current handler is responsible for managing items.
*/
private isItemManageMode(): boolean {
return this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER || this.partyUiMode === PartyUiMode.DISCARD;
}
private processPartyActionInput(): boolean {
const ui = this.getUi();
if (this.cursor < 6) {
if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER && !this.transferMode) {
if (
(this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER && !this.transferMode) ||
this.partyUiMode === PartyUiMode.DISCARD
) {
/** Initialize item quantities for the selected Pokemon */
const itemModifiers = globalScene.findModifiers(
m =>
@ -936,6 +1016,25 @@ export class PartyUiHandler extends MessageUiHandler {
this.showOptions();
ui.playSelect();
}
// Toggle item transfer mode to discard items or vice versa
if (this.cursor === 7) {
switch (this.partyUiMode) {
case PartyUiMode.DISCARD:
this.partyUiMode = PartyUiMode.MODIFIER_TRANSFER;
break;
case PartyUiMode.MODIFIER_TRANSFER:
this.partyUiMode = PartyUiMode.DISCARD;
break;
default:
ui.playError();
return false;
}
this.partyDiscardModeButton.toggleIcon(this.partyUiMode);
ui.playSelect();
return true;
}
// Pressing return button
if (this.cursor === 6) {
if (!this.allowCancel()) {
@ -956,6 +1055,7 @@ export class PartyUiHandler extends MessageUiHandler {
this.clearTransfer();
ui.playSelect();
} else if (this.allowCancel()) {
this.partyDiscardModeButton.clear();
if (this.selectCallback) {
const selectCallback = this.selectCallback;
this.selectCallback = null;
@ -974,30 +1074,74 @@ export class PartyUiHandler extends MessageUiHandler {
const slotCount = this.partySlots.length;
const battlerCount = globalScene.currentBattle.getBattlerCount();
if (this.lastCursor < battlerCount) {
this.lastLeftPokemonCursor = this.lastCursor;
}
if (this.lastCursor >= battlerCount && this.lastCursor < 6) {
this.lastRightPokemonCursor = this.lastCursor;
}
let success = false;
switch (button) {
// Item manage mode adds an extra 8th "toggle mode" button to the UI, located *below* both active party members.
// The following logic serves to ensure its menu behaviour matches its in-game position,
// being selected when scrolling up from the first inactive party member or down from the last active one.
case Button.UP:
if (this.isItemManageMode()) {
if (this.cursor === 1) {
success = this.setCursor(globalScene.currentBattle.double ? 0 : 7);
break;
}
if (this.cursor === 2) {
success = this.setCursor(globalScene.currentBattle.double ? 7 : 1);
break;
}
if (this.cursor === 6) {
success = this.setCursor(slotCount <= globalScene.currentBattle.getBattlerCount() ? 7 : slotCount - 1);
break;
}
if (this.cursor === 7) {
success = this.setCursor(globalScene.currentBattle.double && slotCount > 1 ? 1 : 0);
break;
}
}
success = this.setCursor(this.cursor ? (this.cursor < 6 ? this.cursor - 1 : slotCount - 1) : 6);
break;
case Button.DOWN:
if (this.isItemManageMode()) {
if (this.cursor === 0) {
success = this.setCursor(globalScene.currentBattle.double && slotCount > 1 ? 1 : 7);
break;
}
if (this.cursor === 1) {
success = this.setCursor(globalScene.currentBattle.double ? 7 : slotCount > 2 ? 2 : 6);
break;
}
if (this.cursor === 7) {
success = this.setCursor(
slotCount > globalScene.currentBattle.getBattlerCount() ? globalScene.currentBattle.getBattlerCount() : 6,
);
break;
}
}
success = this.setCursor(this.cursor < 6 ? (this.cursor < slotCount - 1 ? this.cursor + 1 : 6) : 0);
break;
case Button.LEFT:
if (this.cursor >= battlerCount && this.cursor <= 6) {
success = this.setCursor(0);
if (this.cursor === 6) {
success = this.setCursor(this.isItemManageMode() ? 7 : this.lastLeftPokemonCursor);
}
if (this.cursor >= battlerCount && this.cursor < 6) {
success = this.setCursor(this.lastLeftPokemonCursor);
}
break;
case Button.RIGHT:
if (slotCount === battlerCount) {
// Scrolling right from item transfer button or with no backup party members goes to cancel
if (this.cursor === 7 || slotCount <= battlerCount) {
success = this.setCursor(6);
break;
}
if (battlerCount >= 2 && slotCount > battlerCount && this.getCursor() === 0 && this.lastCursor === 1) {
success = this.setCursor(2);
break;
}
if (slotCount > battlerCount && this.cursor < battlerCount) {
success = this.setCursor(this.lastCursor < 6 ? this.lastCursor || battlerCount : battlerCount);
if (this.cursor < battlerCount) {
success = this.setCursor(this.lastRightPokemonCursor || battlerCount);
break;
}
}
@ -1044,11 +1188,15 @@ export class PartyUiHandler extends MessageUiHandler {
this.partySlots[this.lastCursor].deselect();
} else if (this.lastCursor === 6) {
this.partyCancelButton.deselect();
} else if (this.lastCursor === 7) {
this.partyDiscardModeButton.deselect();
}
if (cursor < 6) {
this.partySlots[cursor].select();
} else if (cursor === 6) {
this.partyCancelButton.select();
} else if (cursor === 7) {
this.partyDiscardModeButton.select();
}
}
return changed;
@ -1143,14 +1291,16 @@ export class PartyUiHandler extends MessageUiHandler {
optionsMessage = i18next.t("partyUiHandler:selectAnotherPokemonToSplice");
}
break;
case PartyUiMode.DISCARD:
optionsMessage = i18next.t("partyUiHandler:changeQuantityDiscard");
}
this.showText(optionsMessage, 0);
this.updateOptions();
/** When an item is being selected for transfer, the message box is taller as the message occupies two lines */
if (this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER) {
/** When an item is being selected for transfer or discard, the message box is taller as the message occupies two lines */
if (this.isItemManageMode()) {
this.partyMessageBox.setSize(262 - Math.max(this.optionsBg.displayWidth - 56, 0), 42);
} else {
this.partyMessageBox.setSize(262 - Math.max(this.optionsBg.displayWidth - 56, 0), 30);
@ -1159,6 +1309,20 @@ export class PartyUiHandler extends MessageUiHandler {
this.setCursor(0);
}
showPartyText() {
switch (this.partyUiMode) {
case PartyUiMode.MODIFIER_TRANSFER:
this.showText(i18next.t("partyUiHandler:partyTransfer"));
break;
case PartyUiMode.DISCARD:
this.showText(i18next.t("partyUiHandler:partyDiscard"));
break;
default:
this.showText("", 0);
break;
}
}
private allowBatonModifierSwitch(): boolean {
return !!(
this.partyUiMode !== PartyUiMode.FAINT_SWITCH &&
@ -1276,6 +1440,9 @@ export class PartyUiHandler extends MessageUiHandler {
this.addCommonOptions(pokemon);
}
break;
case PartyUiMode.DISCARD:
this.updateOptionsWithModifierTransferMode(pokemon);
break;
// TODO: This still needs to be broken up.
// It could use a rework differentiating different kind of switches
// to treat baton passing separately from switching on faint.
@ -1381,7 +1548,8 @@ export class PartyUiHandler extends MessageUiHandler {
optionName = "↓";
} else if (
(this.partyUiMode !== PartyUiMode.REMEMBER_MOVE_MODIFIER &&
(this.partyUiMode !== PartyUiMode.MODIFIER_TRANSFER || this.transferMode)) ||
(this.partyUiMode !== PartyUiMode.MODIFIER_TRANSFER || this.transferMode) &&
this.partyUiMode !== PartyUiMode.DISCARD) ||
option === PartyOption.CANCEL
) {
switch (option) {
@ -1444,7 +1612,7 @@ export class PartyUiHandler extends MessageUiHandler {
const itemModifiers = this.getItemModifiers(pokemon);
const itemModifier = itemModifiers[option];
if (
this.partyUiMode === PartyUiMode.MODIFIER_TRANSFER &&
this.isItemManageMode() &&
this.transferQuantitiesMax[option] > 1 &&
!this.transferMode &&
itemModifier !== undefined &&
@ -1474,7 +1642,6 @@ export class PartyUiHandler extends MessageUiHandler {
optionText.x = 15 - this.optionsBg.width;
}
}
startTransfer(): void {
this.transferMode = true;
this.transferCursor = this.cursor;
@ -1608,7 +1775,7 @@ export class PartyUiHandler extends MessageUiHandler {
this.eraseOptionsCursor();
this.partyMessageBox.setSize(262, 30);
this.showText("", 0);
this.showPartyText();
}
eraseOptionsCursor() {
@ -1663,7 +1830,9 @@ class PartySlot extends Phaser.GameObjects.Container {
? -184 +
(globalScene.currentBattle.double ? -40 : 0) +
(28 + (globalScene.currentBattle.double ? 8 : 0)) * slotIndex
: -124 + (globalScene.currentBattle.double ? -8 : 0) + slotIndex * 64,
: partyUiMode === PartyUiMode.MODIFIER_TRANSFER
? -124 + (globalScene.currentBattle.double ? -20 : 0) + slotIndex * 55
: -124 + (globalScene.currentBattle.double ? -8 : 0) + slotIndex * 64,
);
this.slotIndex = slotIndex;
@ -1918,7 +2087,6 @@ class PartySlot extends Phaser.GameObjects.Container {
class PartyCancelButton extends Phaser.GameObjects.Container {
private selected: boolean;
private partyCancelBg: Phaser.GameObjects.Sprite;
private partyCancelPb: Phaser.GameObjects.Sprite;
@ -1965,3 +2133,96 @@ class PartyCancelButton extends Phaser.GameObjects.Container {
this.partyCancelPb.setFrame("party_pb");
}
}
class PartyDiscardModeButton extends Phaser.GameObjects.Container {
private selected: boolean;
private transferIcon: Phaser.GameObjects.Sprite;
private discardIcon: Phaser.GameObjects.Sprite;
private textBox: Phaser.GameObjects.Text;
private party: PartyUiHandler;
constructor(x: number, y: number, party: PartyUiHandler) {
super(globalScene, x, y);
this.setup(party);
}
setup(party: PartyUiHandler) {
this.transferIcon = globalScene.add.sprite(0, 0, "party_transfer");
this.discardIcon = globalScene.add.sprite(0, 0, "party_discard");
this.textBox = addTextObject(-8, -7, i18next.t("partyUiHandler:TRANSFER"), TextStyle.PARTY);
this.party = party;
this.add(this.transferIcon);
this.add(this.discardIcon);
this.add(this.textBox);
this.clear();
}
select() {
if (this.selected) {
return;
}
this.selected = true;
this.party.showText(i18next.t("partyUiHandler:changeMode"));
this.transferIcon.setFrame("selected");
this.discardIcon.setFrame("selected");
}
deselect() {
if (!this.selected) {
return;
}
this.selected = false;
this.party.showPartyText();
this.transferIcon.setFrame("normal");
this.discardIcon.setFrame("normal");
}
/**
* If the current mode deals with transferring items, toggle the discard items button's name and assets.
* @param partyMode - The current {@linkcode PartyUiMode}
* @remarks
* This will also reveal the button if it is currently hidden.
*/
public toggleIcon(partyMode: PartyUiMode.MODIFIER_TRANSFER | PartyUiMode.DISCARD): void {
this.setActive(true).setVisible(true);
switch (partyMode) {
case PartyUiMode.MODIFIER_TRANSFER:
this.transferIcon.setVisible(true);
this.discardIcon.setVisible(false);
this.textBox.setVisible(true);
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;
break;
case PartyUiMode.DISCARD:
this.transferIcon.setVisible(false);
this.discardIcon.setVisible(true);
this.textBox.setVisible(true);
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;
break;
}
}
clear() {
this.setActive(false).setVisible(false);
this.transferIcon.setVisible(false);
this.discardIcon.setVisible(false);
this.textBox.setVisible(false);
}
}

View File

@ -117,6 +117,12 @@ export class SettingsDisplayUiHandler extends AbstractSettingsUiHandler {
label: "Română (Needs Help)",
};
break;
case "tl":
this.settings[languageIndex].options[0] = {
value: "Tagalog",
label: "Tagalog (Needs Help)",
};
break;
default:
this.settings[languageIndex].options[0] = {
value: "English",

View File

@ -176,6 +176,10 @@ const languageSettings: { [key: string]: LanguageSetting } = {
starterInfoYOffset: 0.5,
starterInfoXPos: 26,
},
tl: {
starterInfoTextSize: "56px",
instructionTextSize: "38px",
},
};
const valueReductionMax = 2;

View File

@ -436,6 +436,7 @@ export function hasAllLocalizedSprites(lang?: string): boolean {
case "ja":
case "ca":
case "ru":
case "tl":
return true;
default:
return false;

View File

@ -0,0 +1,72 @@
import { getPokemonNameWithAffix } from "#app/messages";
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 { GameManager } from "#test/test-utils/game-manager";
import i18next from "i18next";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("Ability - Truant", () => {
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)
.moveset([MoveId.SPLASH, MoveId.TACKLE])
.ability(AbilityId.TRUANT)
.enemySpecies(SpeciesId.MAGIKARP)
.enemyAbility(AbilityId.BALL_FETCH)
.enemyMoveset(MoveId.SPLASH);
});
it("should loaf around and prevent using moves every other turn", async () => {
await game.classicMode.startBattle([SpeciesId.FEEBAS]);
const player = game.field.getPlayerPokemon();
const enemy = game.field.getEnemyPokemon();
// Turn 1: Splash succeeds
game.move.select(MoveId.SPLASH);
await game.toNextTurn();
expect(player.getLastXMoves(1)[0]).toEqual(
expect.objectContaining({ move: MoveId.SPLASH, result: MoveResult.SUCCESS }),
);
// Turn 2: Truant activates, cancelling tackle and displaying message
game.move.select(MoveId.TACKLE);
await game.toNextTurn();
expect(player.getLastXMoves(1)[0]).toEqual(expect.objectContaining({ move: MoveId.NONE, result: MoveResult.FAIL }));
expect(enemy.hp).toBe(enemy.getMaxHp());
expect(game.textInterceptor.logs).toContain(
i18next.t("battlerTags:truantLapse", {
pokemonNameWithAffix: getPokemonNameWithAffix(player),
}),
);
// Turn 3: Truant didn't activate, tackle worked
game.move.select(MoveId.TACKLE);
await game.toNextTurn();
expect(player.getLastXMoves(1)[0]).toEqual(
expect.objectContaining({ move: MoveId.TACKLE, result: MoveResult.SUCCESS }),
);
expect(enemy.hp).toBeLessThan(enemy.getMaxHp());
});
});

View File

@ -0,0 +1,172 @@
import { BerryType } from "#enums/berry-type";
import { Button } from "#enums/buttons";
import { MoveId } from "#enums/move-id";
import { SpeciesId } from "#enums/species-id";
import { UiMode } from "#enums/ui-mode";
import type { Pokemon } from "#field/pokemon";
import { GameManager } from "#test/test-utils/game-manager";
import type { ModifierSelectUiHandler } from "#ui/modifier-select-ui-handler";
import type { PartyUiHandler } from "#ui/party-ui-handler";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
describe("UI - Transfer Items", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
beforeAll(() => {
phaserGame = new Phaser.Game({
type: Phaser.HEADLESS,
});
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
});
beforeEach(async () => {
game = new GameManager(phaserGame);
game.override
.battleStyle("single")
.startingLevel(100)
.startingHeldItems([
{ name: "BERRY", count: 1, type: BerryType.SITRUS },
{ name: "BERRY", count: 2, type: BerryType.APICOT },
{ name: "BERRY", count: 2, type: BerryType.LUM },
])
.enemySpecies(SpeciesId.MAGIKARP)
.enemyMoveset(MoveId.SPLASH);
await game.classicMode.startBattle([SpeciesId.RAYQUAZA, SpeciesId.RAYQUAZA, SpeciesId.RAYQUAZA]);
game.move.use(MoveId.DRAGON_CLAW);
await game.phaseInterceptor.to("SelectModifierPhase");
});
it("manage button exists in the proper screen", async () => {
let handlerLength: Phaser.GameObjects.GameObject[] | undefined;
await new Promise<void>(resolve => {
//select manage items menu
game.onNextPrompt("SelectModifierPhase", UiMode.MODIFIER_SELECT, async () => {
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler;
handler.processInput(Button.DOWN);
handler.setCursor(1);
handler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => {
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as PartyUiHandler;
handler.processInput(Button.DOWN);
handler.processInput(Button.ACTION);
handlerLength = handler.optionsContainer.list;
handler.processInput(Button.CANCEL);
resolve();
});
});
expect(handlerLength).toHaveLength(0); // should select manage button, which has no menu
});
it("manage button doesn't exist in the other screens", async () => {
let handlerLength: Phaser.GameObjects.GameObject[] | undefined;
await new Promise<void>(resolve => {
game.onNextPrompt("SelectModifierPhase", UiMode.MODIFIER_SELECT, async () => {
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler;
handler.processInput(Button.DOWN);
handler.setCursor(2);
handler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => {
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as PartyUiHandler;
handler.processInput(Button.DOWN);
handler.processInput(Button.ACTION);
handlerLength = handler.optionsContainer.list;
handler.processInput(Button.CANCEL);
handler.processInput(Button.CANCEL);
resolve();
});
});
expect(handlerLength).toHaveLength(6); // should select 2nd pokemon (length is 5 options + image)
});
// Test that the manage button actually discards items, needs proofreading
it("should discard items when button is selected", async () => {
let pokemon: Pokemon | undefined;
await new Promise<void>(resolve => {
game.onNextPrompt("SelectModifierPhase", UiMode.MODIFIER_SELECT, async () => {
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as ModifierSelectUiHandler;
handler.processInput(Button.DOWN);
handler.setCursor(1);
handler.processInput(Button.ACTION);
});
game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => {
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as PartyUiHandler;
// Enter discard mode and select first party member
handler.setCursor(7);
handler.processInput(Button.ACTION);
handler.setCursor(0);
handler.processInput(Button.ACTION);
pokemon = game.field.getPlayerPokemon();
resolve();
});
});
expect(pokemon).toBeDefined();
if (pokemon) {
expect(pokemon.getHeldItems()).toHaveLength(3);
expect(pokemon.getHeldItems().map(h => h.stackCount)).toEqual([1, 2, 2]);
}
await new Promise<void>(resolve => {
game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => {
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as PartyUiHandler;
handler.processInput(Button.ACTION);
resolve();
});
});
await new Promise<void>(resolve => {
game.onNextPrompt("SelectModifierPhase", UiMode.PARTY, async () => {
await new Promise(r => setTimeout(r, 100));
const handler = game.scene.ui.getHandler() as PartyUiHandler;
handler.processInput(Button.ACTION);
pokemon = game.field.getPlayerPokemon();
handler.processInput(Button.CANCEL);
resolve();
});
});
expect(pokemon).toBeDefined();
if (pokemon) {
// Sitrus berry was discarded, leaving 2 stacks of 2 berries behind
expect(pokemon.getHeldItems()).toHaveLength(2);
expect(pokemon.getHeldItems().map(h => h.stackCount)).toEqual([2, 2]);
}
});
});