diff --git a/package.json b/package.json index 71a8b1ae334..d3494da677c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/images/statuses_tl.json b/public/images/statuses_tl.json new file mode 100644 index 00000000000..094b0188d69 --- /dev/null +++ b/public/images/statuses_tl.json @@ -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$" + } +} diff --git a/public/images/statuses_tl.png b/public/images/statuses_tl.png new file mode 100644 index 00000000000..9f24c6a0810 Binary files /dev/null and b/public/images/statuses_tl.png differ diff --git a/public/images/types_tl.json b/public/images/types_tl.json new file mode 100644 index 00000000000..2706c6f49f3 --- /dev/null +++ b/public/images/types_tl.json @@ -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$" + } +} diff --git a/public/images/types_tl.png b/public/images/types_tl.png new file mode 100644 index 00000000000..b9fdceba7da Binary files /dev/null and b/public/images/types_tl.png differ diff --git a/public/images/ui/legacy/party_bg_double_manage.png b/public/images/ui/legacy/party_bg_double_manage.png new file mode 100644 index 00000000000..2bf2d63c315 Binary files /dev/null and b/public/images/ui/legacy/party_bg_double_manage.png differ diff --git a/public/images/ui/legacy/party_discard.json b/public/images/ui/legacy/party_discard.json new file mode 100644 index 00000000000..4aa563fcd77 --- /dev/null +++ b/public/images/ui/legacy/party_discard.json @@ -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$" + } +} diff --git a/public/images/ui/legacy/party_discard.png b/public/images/ui/legacy/party_discard.png new file mode 100644 index 00000000000..d95ba696015 Binary files /dev/null and b/public/images/ui/legacy/party_discard.png differ diff --git a/public/images/ui/legacy/party_transfer.json b/public/images/ui/legacy/party_transfer.json new file mode 100644 index 00000000000..7cfcf5ccc30 --- /dev/null +++ b/public/images/ui/legacy/party_transfer.json @@ -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$" + } +} diff --git a/public/images/ui/legacy/party_transfer.png b/public/images/ui/legacy/party_transfer.png new file mode 100644 index 00000000000..36879547676 Binary files /dev/null and b/public/images/ui/legacy/party_transfer.png differ diff --git a/public/images/ui/party_bg_double_manage.png b/public/images/ui/party_bg_double_manage.png new file mode 100644 index 00000000000..e85413b5fb5 Binary files /dev/null and b/public/images/ui/party_bg_double_manage.png differ diff --git a/public/images/ui/party_discard.json b/public/images/ui/party_discard.json new file mode 100644 index 00000000000..4aa563fcd77 --- /dev/null +++ b/public/images/ui/party_discard.json @@ -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$" + } +} diff --git a/public/images/ui/party_discard.png b/public/images/ui/party_discard.png new file mode 100644 index 00000000000..e56c845eadc Binary files /dev/null and b/public/images/ui/party_discard.png differ diff --git a/public/images/ui/party_transfer.json b/public/images/ui/party_transfer.json new file mode 100644 index 00000000000..7cfcf5ccc30 --- /dev/null +++ b/public/images/ui/party_transfer.json @@ -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$" + } +} diff --git a/public/images/ui/party_transfer.png b/public/images/ui/party_transfer.png new file mode 100644 index 00000000000..45815a156b5 Binary files /dev/null and b/public/images/ui/party_transfer.png differ diff --git a/public/locales b/public/locales index e2fbba17ea7..7898c0018a7 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit e2fbba17ea7a96068970ea98a8a84ed3e25b6f07 +Subproject commit 7898c0018a70601a6ead76c9dd497ff966cc2e2a diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 9af954c3852..1864de2c6f4 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -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; diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 9f2a5e09667..15c2cde1d58 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -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(source: BaseArenaTag & Pick): void; - * public override loadTag(source: NonFunctionProperties): 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); diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index e21065c184f..edeff293aa0 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -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(source: BaseBattlerTag & Pick): void; - * public override loadTag(source: NonFunctionProperties): 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): void { + public override loadTag(source: BaseBattlerTag & Pick): 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): void { + override loadTag(source: BaseBattlerTag & Pick): void { super.loadTag(source); (this as Mutable).tatsugiriFormKey = source.tatsugiriFormKey; } @@ -2659,7 +2655,9 @@ export class StockpilingTag extends SerializableBattlerTag { } }; - public override loadTag(source: NonFunctionProperties): void { + public override loadTag( + source: BaseBattlerTag & Pick, + ): 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): void { + public override loadTag(source: BaseBattlerTag & Pick): 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; diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 706ea01a16a..c5b0263e785 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -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"); diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index 89946b2691b..62fc73a10a3 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -79,13 +79,13 @@ const fonts: Array = [ 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 { "tr", "ro", "ru", + "tl", ], backend: { loadPath(lng: string, [ns]: string[]) { diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts index 33087f2509e..32d9e0ee2be 100644 --- a/src/system/settings/settings.ts +++ b/src/system/settings/settings.ts @@ -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(), diff --git a/src/ui/modifier-select-ui-handler.ts b/src/ui/modifier-select-ui-handler.ts index 50d88738d32..16eecf6993d 100644 --- a/src/ui/modifier-select-ui-handler.ts +++ b/src/ui/modifier-select-ui-handler.ts @@ -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, diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 915cc76fd73..b259316f6fa 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -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); + } +} diff --git a/src/ui/settings/settings-display-ui-handler.ts b/src/ui/settings/settings-display-ui-handler.ts index 3c261d6ddab..1a0481b8e8d 100644 --- a/src/ui/settings/settings-display-ui-handler.ts +++ b/src/ui/settings/settings-display-ui-handler.ts @@ -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", diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 974f24e706f..6929d6f818d 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -176,6 +176,10 @@ const languageSettings: { [key: string]: LanguageSetting } = { starterInfoYOffset: 0.5, starterInfoXPos: 26, }, + tl: { + starterInfoTextSize: "56px", + instructionTextSize: "38px", + }, }; const valueReductionMax = 2; diff --git a/src/utils/common.ts b/src/utils/common.ts index 66a74ed2c33..1c75dac93b4 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -436,6 +436,7 @@ export function hasAllLocalizedSprites(lang?: string): boolean { case "ja": case "ca": case "ru": + case "tl": return true; default: return false; diff --git a/test/abilities/truant.test.ts b/test/abilities/truant.test.ts new file mode 100644 index 00000000000..0d71cd393b0 --- /dev/null +++ b/test/abilities/truant.test.ts @@ -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()); + }); +}); diff --git a/test/ui/item-manage-button.test.ts b/test/ui/item-manage-button.test.ts new file mode 100644 index 00000000000..a7ea76918a5 --- /dev/null +++ b/test/ui/item-manage-button.test.ts @@ -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(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(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(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(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(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]); + } + }); +});