diff --git a/CREDITS.md b/CREDITS.md index 6bd73d72901..ff1205b840e 100644 --- a/CREDITS.md +++ b/CREDITS.md @@ -470,13 +470,13 @@ In addition to the lists below, please check [the PokéRogue wiki](https://wiki. ### 🇧🇷 Portuguese (pt-BR) - Zé Ricardo -### 🇨🇳 Chinese (zh-CN) +### 🇨🇳 Chinese (zh-Hans) - dddsenic - mercurius - VittorioVeneto - Yonmaru -### 🇹🇼 Chinese (zh-TW) +### 🇹🇼 Chinese (zh-Hant) - mercurius - Seagull @@ -485,7 +485,7 @@ In addition to the lists below, please check [the PokéRogue wiki](https://wiki. - Rafa (es-ES) - GINK-SS (ko) - prostagma (pt-BR) -- Ei (zh-TW) +- Ei (zh-Hant) ## Wiki Translators @@ -516,7 +516,7 @@ In addition to the lists below, please check [the PokéRogue wiki](https://wiki. - Sushi - Zé Ricardo -### 🇨🇳 Chinese (zh-CN) +### 🇨🇳 Chinese (zh-Hans) - jw-0- ### Past contributors @@ -528,11 +528,11 @@ In addition to the lists below, please check [the PokéRogue wiki](https://wiki. - Telor (fr) - dorri (ko) - Little Moder_eldenring (ko) -- Andy (zh-CN) -- Black Feather (zh-CN) -- itschili (zh-CN) -- RimKnight (zh-CN) -- Yubari (zh-CN) +- Andy (zh-Hans) +- Black Feather (zh-Hans) +- itschili (zh-Hans) +- RimKnight (zh-Hans) +- Yubari (zh-Hans) ## 🇺🇸 English Proofreaders - Cheyu diff --git a/package.json b/package.json index 1fb25c5d4ba..ac8bca50f76 100644 --- a/package.json +++ b/package.json @@ -74,5 +74,5 @@ "engines": { "node": ">=22.0.0" }, - "packageManager": "pnpm@10.16.1" + "packageManager": "pnpm@10.17.0" } diff --git a/public/images/events/aprf25-zh-CN.png b/public/images/events/aprf25-zh-Hans.png similarity index 100% rename from public/images/events/aprf25-zh-CN.png rename to public/images/events/aprf25-zh-Hans.png diff --git a/public/images/events/egg-update_zh-CN.png b/public/images/events/egg-update_zh-Hans.png similarity index 100% rename from public/images/events/egg-update_zh-CN.png rename to public/images/events/egg-update_zh-Hans.png diff --git a/public/images/events/halloween2024-event-zh-CN.png b/public/images/events/halloween2024-event-zh-Hans.png similarity index 100% rename from public/images/events/halloween2024-event-zh-CN.png rename to public/images/events/halloween2024-event-zh-Hans.png diff --git a/public/images/events/pkmnday2025event-zh-CN.png b/public/images/events/pkmnday2025event-zh-Hans.png similarity index 100% rename from public/images/events/pkmnday2025event-zh-CN.png rename to public/images/events/pkmnday2025event-zh-Hans.png diff --git a/public/images/events/pride2025-es-MX.png b/public/images/events/pride2025-es-419.png similarity index 100% rename from public/images/events/pride2025-es-MX.png rename to public/images/events/pride2025-es-419.png diff --git a/public/images/events/pride2025-zh-CN.png b/public/images/events/pride2025-zh-Hans.png similarity index 100% rename from public/images/events/pride2025-zh-CN.png rename to public/images/events/pride2025-zh-Hans.png diff --git a/public/images/events/pride2025-zh-TW.png b/public/images/events/pride2025-zh-Hant.png similarity index 100% rename from public/images/events/pride2025-zh-TW.png rename to public/images/events/pride2025-zh-Hant.png diff --git a/public/images/events/september-update-zh-CN.png b/public/images/events/september-update-zh-Hans.png similarity index 100% rename from public/images/events/september-update-zh-CN.png rename to public/images/events/september-update-zh-Hans.png diff --git a/public/images/events/spr25event-es-MX.png b/public/images/events/spr25event-es-419.png similarity index 100% rename from public/images/events/spr25event-es-MX.png rename to public/images/events/spr25event-es-419.png diff --git a/public/images/events/spr25event-zh-CN.png b/public/images/events/spr25event-zh-Hans.png similarity index 100% rename from public/images/events/spr25event-zh-CN.png rename to public/images/events/spr25event-zh-Hans.png diff --git a/public/images/events/valentines2025event-zh-CN.png b/public/images/events/valentines2025event-zh-Hans.png similarity index 100% rename from public/images/events/valentines2025event-zh-CN.png rename to public/images/events/valentines2025event-zh-Hans.png diff --git a/public/images/events/winter_holidays2024-event-zh-CN.png b/public/images/events/winter_holidays2024-event-zh-Hans.png similarity index 100% rename from public/images/events/winter_holidays2024-event-zh-CN.png rename to public/images/events/winter_holidays2024-event-zh-Hans.png diff --git a/public/images/events/yearofthesnakeevent-zh-CN.png b/public/images/events/yearofthesnakeevent-zh-Hans.png similarity index 100% rename from public/images/events/yearofthesnakeevent-zh-CN.png rename to public/images/events/yearofthesnakeevent-zh-Hans.png diff --git a/public/images/statuses_es-MX.json b/public/images/statuses_es-419.json similarity index 98% rename from public/images/statuses_es-MX.json rename to public/images/statuses_es-419.json index 8b09ed391c4..87eb8ec83c1 100644 --- a/public/images/statuses_es-MX.json +++ b/public/images/statuses_es-419.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "statuses_es-MX.png", + "image": "statuses_es-419.png", "format": "RGBA8888", "size": { "w": 22, diff --git a/public/images/statuses_es-MX.png b/public/images/statuses_es-419.png similarity index 100% rename from public/images/statuses_es-MX.png rename to public/images/statuses_es-419.png diff --git a/public/images/statuses_zh-CN.json b/public/images/statuses_zh-Hans.json similarity index 98% rename from public/images/statuses_zh-CN.json rename to public/images/statuses_zh-Hans.json index 28760650ecd..f46a1d88465 100644 --- a/public/images/statuses_zh-CN.json +++ b/public/images/statuses_zh-Hans.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "statuses_zh_CN.png", + "image": "statuses_zh_Hans.png", "format": "RGBA8888", "size": { "w": 22, diff --git a/public/images/statuses_zh-CN.png b/public/images/statuses_zh-Hans.png similarity index 100% rename from public/images/statuses_zh-CN.png rename to public/images/statuses_zh-Hans.png diff --git a/public/images/statuses_zh-TW.json b/public/images/statuses_zh-Hant.json similarity index 98% rename from public/images/statuses_zh-TW.json rename to public/images/statuses_zh-Hant.json index bf05b2ab0d5..66ee9502857 100644 --- a/public/images/statuses_zh-TW.json +++ b/public/images/statuses_zh-Hant.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "statuses.png", + "image": "statuses_zh-Hant.png", "format": "RGBA8888", "size": { "w": 22, diff --git a/public/images/statuses_zh-TW.png b/public/images/statuses_zh-Hant.png similarity index 100% rename from public/images/statuses_zh-TW.png rename to public/images/statuses_zh-Hant.png diff --git a/public/images/types_zh-CN.json b/public/images/types_es-419.json similarity index 99% rename from public/images/types_zh-CN.json rename to public/images/types_es-419.json index e82d3c56468..1e7cbd7a6e8 100644 --- a/public/images/types_zh-CN.json +++ b/public/images/types_es-419.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "types_zh-CN.png", + "image": "types_es-419.png", "format": "RGBA8888", "size": { "w": 32, diff --git a/public/images/types_es-MX.png b/public/images/types_es-419.png similarity index 100% rename from public/images/types_es-MX.png rename to public/images/types_es-419.png diff --git a/public/images/types_zh-TW.json b/public/images/types_zh-Hans.json similarity index 99% rename from public/images/types_zh-TW.json rename to public/images/types_zh-Hans.json index 18c51ab61f4..15b3af70fa1 100644 --- a/public/images/types_zh-TW.json +++ b/public/images/types_zh-Hans.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "types_zh-TW.png", + "image": "types_zh-Hans.png", "format": "RGBA8888", "size": { "w": 32, diff --git a/public/images/types_zh-CN.png b/public/images/types_zh-Hans.png similarity index 100% rename from public/images/types_zh-CN.png rename to public/images/types_zh-Hans.png diff --git a/public/images/types_es-MX.json b/public/images/types_zh-Hant.json similarity index 99% rename from public/images/types_es-MX.json rename to public/images/types_zh-Hant.json index b3dbfcd697f..8f912d44041 100644 --- a/public/images/types_es-MX.json +++ b/public/images/types_zh-Hant.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "types_es-MX.png", + "image": "types_zh-Hant.png", "format": "RGBA8888", "size": { "w": 32, diff --git a/public/images/types_zh-TW.png b/public/images/types_zh-Hant.png similarity index 100% rename from public/images/types_zh-TW.png rename to public/images/types_zh-Hant.png diff --git a/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_ca.png b/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_ca.png index a457468d8d0..5477e3385a8 100644 Binary files a/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_ca.png and b/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_ca.png differ diff --git a/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_overlay_shiny_ca.png b/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_overlay_shiny_ca.png index 3277a28a59b..1640e46caa0 100644 Binary files a/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_overlay_shiny_ca.png and b/public/images/ui/legacy/text_images/ca/summary/summary_dexnb_label_overlay_shiny_ca.png differ diff --git a/public/images/ui/legacy/text_images/ca/summary/summary_stats_expbar_title_ca.png b/public/images/ui/legacy/text_images/ca/summary/summary_stats_expbar_title_ca.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/ca/summary/summary_stats_expbar_title_ca.png and b/public/images/ui/legacy/text_images/ca/summary/summary_stats_expbar_title_ca.png differ diff --git a/public/images/ui/legacy/text_images/en/battle_ui/overlay_exp_label.png b/public/images/ui/legacy/text_images/en/battle_ui/overlay_exp_label.png index 40b5e8925a1..acb04a84a31 100644 Binary files a/public/images/ui/legacy/text_images/en/battle_ui/overlay_exp_label.png and b/public/images/ui/legacy/text_images/en/battle_ui/overlay_exp_label.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_dexnb_label.png b/public/images/ui/legacy/text_images/en/summary/summary_dexnb_label.png index eab90a91c7f..bf568c486aa 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_dexnb_label.png and b/public/images/ui/legacy/text_images/en/summary/summary_dexnb_label.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_moves_descriptions_title.png b/public/images/ui/legacy/text_images/en/summary/summary_moves_descriptions_title.png index 3d2b4d08376..e83e8cafbfc 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_moves_descriptions_title.png and b/public/images/ui/legacy/text_images/en/summary/summary_moves_descriptions_title.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_moves_effect_title.png b/public/images/ui/legacy/text_images/en/summary/summary_moves_effect_title.png index 55fb0efd832..55c4b545d98 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_moves_effect_title.png and b/public/images/ui/legacy/text_images/en/summary/summary_moves_effect_title.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_moves_moves_title.png b/public/images/ui/legacy/text_images/en/summary/summary_moves_moves_title.png index d869ab4e311..6bbb29c9c5f 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_moves_moves_title.png and b/public/images/ui/legacy/text_images/en/summary/summary_moves_moves_title.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_profile_ability.png b/public/images/ui/legacy/text_images/en/summary/summary_profile_ability.png index 6600db26802..a05c22b7d47 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_profile_ability.png and b/public/images/ui/legacy/text_images/en/summary/summary_profile_ability.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_profile_memo_title.png b/public/images/ui/legacy/text_images/en/summary/summary_profile_memo_title.png index 14cdf099044..3d69c20e57f 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_profile_memo_title.png and b/public/images/ui/legacy/text_images/en/summary/summary_profile_memo_title.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_profile_passive.png b/public/images/ui/legacy/text_images/en/summary/summary_profile_passive.png index 66f56ff435e..c026e87a215 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_profile_passive.png and b/public/images/ui/legacy/text_images/en/summary/summary_profile_passive.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_profile_profile_title.png b/public/images/ui/legacy/text_images/en/summary/summary_profile_profile_title.png index 8d4f82df3b3..4170dccf682 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_profile_profile_title.png and b/public/images/ui/legacy/text_images/en/summary/summary_profile_profile_title.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_stats_expbar_title.png b/public/images/ui/legacy/text_images/en/summary/summary_stats_expbar_title.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_stats_expbar_title.png and b/public/images/ui/legacy/text_images/en/summary/summary_stats_expbar_title.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_stats_item_title.png b/public/images/ui/legacy/text_images/en/summary/summary_stats_item_title.png index 5752b28288c..42e08b3e52a 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_stats_item_title.png and b/public/images/ui/legacy/text_images/en/summary/summary_stats_item_title.png differ diff --git a/public/images/ui/legacy/text_images/en/summary/summary_stats_stats_title.png b/public/images/ui/legacy/text_images/en/summary/summary_stats_stats_title.png index 5531819ef66..f602a43c39d 100644 Binary files a/public/images/ui/legacy/text_images/en/summary/summary_stats_stats_title.png and b/public/images/ui/legacy/text_images/en/summary/summary_stats_stats_title.png differ diff --git a/public/images/ui/legacy/text_images/es-MX/battle_ui/overlay_exp_label_es-MX.png b/public/images/ui/legacy/text_images/es-419/battle_ui/overlay_exp_label_es-419.png similarity index 100% rename from public/images/ui/legacy/text_images/es-MX/battle_ui/overlay_exp_label_es-MX.png rename to public/images/ui/legacy/text_images/es-419/battle_ui/overlay_exp_label_es-419.png diff --git a/public/images/ui/legacy/text_images/es-MX/battle_ui/overlay_hp_label_boss_es-MX.png b/public/images/ui/legacy/text_images/es-419/battle_ui/overlay_hp_label_boss_es-419.png similarity index 100% rename from public/images/ui/legacy/text_images/es-MX/battle_ui/overlay_hp_label_boss_es-MX.png rename to public/images/ui/legacy/text_images/es-419/battle_ui/overlay_hp_label_boss_es-419.png diff --git a/public/images/ui/legacy/text_images/es-MX/battle_ui/overlay_hp_label_es-MX.png b/public/images/ui/legacy/text_images/es-419/battle_ui/overlay_hp_label_es-419.png similarity index 100% rename from public/images/ui/legacy/text_images/es-MX/battle_ui/overlay_hp_label_es-MX.png rename to public/images/ui/legacy/text_images/es-419/battle_ui/overlay_hp_label_es-419.png diff --git a/public/images/ui/legacy/text_images/es-MX/battle_ui/overlay_lv_es-MX.png b/public/images/ui/legacy/text_images/es-419/battle_ui/overlay_lv_es-419.png similarity index 100% rename from public/images/ui/legacy/text_images/es-MX/battle_ui/overlay_lv_es-MX.png rename to public/images/ui/legacy/text_images/es-419/battle_ui/overlay_lv_es-419.png diff --git a/public/images/ui/legacy/text_images/es-MX/battle_ui/pbinfo_stat_es_MX.json b/public/images/ui/legacy/text_images/es-419/battle_ui/pbinfo_stat_es-419.json similarity index 98% rename from public/images/ui/legacy/text_images/es-MX/battle_ui/pbinfo_stat_es_MX.json rename to public/images/ui/legacy/text_images/es-419/battle_ui/pbinfo_stat_es-419.json index 98bd43e7dd9..d520739c067 100644 --- a/public/images/ui/legacy/text_images/es-MX/battle_ui/pbinfo_stat_es_MX.json +++ b/public/images/ui/legacy/text_images/es-419/battle_ui/pbinfo_stat_es-419.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "pbinfo_stat_es-MX.png", + "image": "pbinfo_stat_es-419.png", "format": "RGBA8888", "size": { "w": 112, diff --git a/public/images/ui/legacy/text_images/es-ES/battle_ui/pbinfo_stat_es_ES.png b/public/images/ui/legacy/text_images/es-419/battle_ui/pbinfo_stat_es-419.png similarity index 100% rename from public/images/ui/legacy/text_images/es-ES/battle_ui/pbinfo_stat_es_ES.png rename to public/images/ui/legacy/text_images/es-419/battle_ui/pbinfo_stat_es-419.png diff --git a/public/images/ui/legacy/text_images/es-MX/party_ui/party_slot_overlay_hp_es-MX.png b/public/images/ui/legacy/text_images/es-419/party_ui/party_slot_overlay_hp_es-419.png similarity index 100% rename from public/images/ui/legacy/text_images/es-MX/party_ui/party_slot_overlay_hp_es-MX.png rename to public/images/ui/legacy/text_images/es-419/party_ui/party_slot_overlay_hp_es-419.png diff --git a/public/images/ui/legacy/text_images/es-MX/party_ui/party_slot_overlay_lv_es-MX.png b/public/images/ui/legacy/text_images/es-419/party_ui/party_slot_overlay_lv_es-419.png similarity index 100% rename from public/images/ui/legacy/text_images/es-MX/party_ui/party_slot_overlay_lv_es-MX.png rename to public/images/ui/legacy/text_images/es-419/party_ui/party_slot_overlay_lv_es-419.png diff --git a/public/images/ui/text_images/es-MX/summary/summary_dexnb_label_es-MX.png b/public/images/ui/legacy/text_images/es-419/summary/summary_dexnb_label_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_dexnb_label_es-MX.png rename to public/images/ui/legacy/text_images/es-419/summary/summary_dexnb_label_es-419.png diff --git a/public/images/ui/legacy/text_images/es-419/summary/summary_dexnb_label_overlay_shiny_es-419.png b/public/images/ui/legacy/text_images/es-419/summary/summary_dexnb_label_overlay_shiny_es-419.png new file mode 100644 index 00000000000..1640e46caa0 Binary files /dev/null and b/public/images/ui/legacy/text_images/es-419/summary/summary_dexnb_label_overlay_shiny_es-419.png differ diff --git a/public/images/ui/text_images/es-MX/summary/summary_moves_descriptions_title_es-MX.png b/public/images/ui/legacy/text_images/es-419/summary/summary_moves_descriptions_title_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_moves_descriptions_title_es-MX.png rename to public/images/ui/legacy/text_images/es-419/summary/summary_moves_descriptions_title_es-419.png diff --git a/public/images/ui/text_images/es-MX/summary/summary_moves_effect_title_es-MX.png b/public/images/ui/legacy/text_images/es-419/summary/summary_moves_effect_title_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_moves_effect_title_es-MX.png rename to public/images/ui/legacy/text_images/es-419/summary/summary_moves_effect_title_es-419.png diff --git a/public/images/ui/text_images/es-MX/summary/summary_moves_moves_title_es-MX.png b/public/images/ui/legacy/text_images/es-419/summary/summary_moves_moves_title_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_moves_moves_title_es-MX.png rename to public/images/ui/legacy/text_images/es-419/summary/summary_moves_moves_title_es-419.png diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_overlay_pp_es-MX.png b/public/images/ui/legacy/text_images/es-419/summary/summary_moves_overlay_pp_es-419.png similarity index 100% rename from public/images/ui/legacy/text_images/es-MX/summary/summary_moves_overlay_pp_es-MX.png rename to public/images/ui/legacy/text_images/es-419/summary/summary_moves_overlay_pp_es-419.png diff --git a/public/images/ui/text_images/es-MX/summary/summary_profile_ability_es-MX.png b/public/images/ui/legacy/text_images/es-419/summary/summary_profile_ability_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_profile_ability_es-MX.png rename to public/images/ui/legacy/text_images/es-419/summary/summary_profile_ability_es-419.png diff --git a/public/images/ui/text_images/es-MX/summary/summary_profile_memo_title_es-MX.png b/public/images/ui/legacy/text_images/es-419/summary/summary_profile_memo_title_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_profile_memo_title_es-MX.png rename to public/images/ui/legacy/text_images/es-419/summary/summary_profile_memo_title_es-419.png diff --git a/public/images/ui/text_images/es-MX/summary/summary_profile_passive_es-MX.png b/public/images/ui/legacy/text_images/es-419/summary/summary_profile_passive_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_profile_passive_es-MX.png rename to public/images/ui/legacy/text_images/es-419/summary/summary_profile_passive_es-419.png diff --git a/public/images/ui/text_images/es-MX/summary/summary_profile_profile_title_es-MX.png b/public/images/ui/legacy/text_images/es-419/summary/summary_profile_profile_title_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_profile_profile_title_es-MX.png rename to public/images/ui/legacy/text_images/es-419/summary/summary_profile_profile_title_es-419.png diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_exp_title_es-MX.png b/public/images/ui/legacy/text_images/es-419/summary/summary_stats_exp_title_es-419.png similarity index 100% rename from public/images/ui/legacy/text_images/es-MX/summary/summary_stats_exp_title_es-MX.png rename to public/images/ui/legacy/text_images/es-419/summary/summary_stats_exp_title_es-419.png diff --git a/public/images/ui/legacy/text_images/es-419/summary/summary_stats_expbar_title_es-419.png b/public/images/ui/legacy/text_images/es-419/summary/summary_stats_expbar_title_es-419.png new file mode 100644 index 00000000000..da999975932 Binary files /dev/null and b/public/images/ui/legacy/text_images/es-419/summary/summary_stats_expbar_title_es-419.png differ diff --git a/public/images/ui/text_images/es-MX/summary/summary_stats_item_title_es-MX.png b/public/images/ui/legacy/text_images/es-419/summary/summary_stats_item_title_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_stats_item_title_es-MX.png rename to public/images/ui/legacy/text_images/es-419/summary/summary_stats_item_title_es-419.png diff --git a/public/images/ui/text_images/es-MX/summary/summary_stats_stats_title_es-MX.png b/public/images/ui/legacy/text_images/es-419/summary/summary_stats_stats_title_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_stats_stats_title_es-MX.png rename to public/images/ui/legacy/text_images/es-419/summary/summary_stats_stats_title_es-419.png diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_tabs_1_es-MX.png b/public/images/ui/legacy/text_images/es-419/summary/summary_tabs_1_es-419.png similarity index 100% rename from public/images/ui/legacy/text_images/es-MX/summary/summary_tabs_1_es-MX.png rename to public/images/ui/legacy/text_images/es-419/summary/summary_tabs_1_es-419.png diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_tabs_2_es-MX.png b/public/images/ui/legacy/text_images/es-419/summary/summary_tabs_2_es-419.png similarity index 100% rename from public/images/ui/legacy/text_images/es-MX/summary/summary_tabs_2_es-MX.png rename to public/images/ui/legacy/text_images/es-419/summary/summary_tabs_2_es-419.png diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_tabs_3_es-MX.png b/public/images/ui/legacy/text_images/es-419/summary/summary_tabs_3_es-419.png similarity index 100% rename from public/images/ui/legacy/text_images/es-MX/summary/summary_tabs_3_es-MX.png rename to public/images/ui/legacy/text_images/es-419/summary/summary_tabs_3_es-419.png diff --git a/public/images/ui/legacy/text_images/es-ES/battle_ui/pbinfo_stat_es_ES.json b/public/images/ui/legacy/text_images/es-ES/battle_ui/pbinfo_stat_es-ES.json similarity index 100% rename from public/images/ui/legacy/text_images/es-ES/battle_ui/pbinfo_stat_es_ES.json rename to public/images/ui/legacy/text_images/es-ES/battle_ui/pbinfo_stat_es-ES.json diff --git a/public/images/ui/legacy/text_images/es-MX/battle_ui/pbinfo_stat_es_MX.png b/public/images/ui/legacy/text_images/es-ES/battle_ui/pbinfo_stat_es-ES.png similarity index 100% rename from public/images/ui/legacy/text_images/es-MX/battle_ui/pbinfo_stat_es_MX.png rename to public/images/ui/legacy/text_images/es-ES/battle_ui/pbinfo_stat_es-ES.png diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_dexnb_label_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_dexnb_label_es-ES.png index a457468d8d0..5477e3385a8 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_dexnb_label_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_dexnb_label_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_dexnb_label_overlay_shiny_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_dexnb_label_overlay_shiny_es-ES.png index 3277a28a59b..1640e46caa0 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_dexnb_label_overlay_shiny_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_dexnb_label_overlay_shiny_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_descriptions_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_descriptions_title_es-ES.png index ffcae31894d..3a4e3c7c375 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_descriptions_title_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_descriptions_title_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_effect_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_effect_title_es-ES.png index 50ce2f51d6f..cf8d1309848 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_effect_title_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_effect_title_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_moves_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_moves_title_es-ES.png index ffca8bdfa10..a601ae79e4f 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_moves_title_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_moves_moves_title_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_ability_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_ability_es-ES.png index b1b1a84ddcf..71bffe95cfe 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_ability_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_ability_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_memo_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_memo_title_es-ES.png index e837a58e4f9..b7ef7c91fc5 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_memo_title_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_memo_title_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_passive_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_passive_es-ES.png index 885453e3e98..a50e3cacf58 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_passive_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_passive_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_profile_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_profile_title_es-ES.png index 51ba9300dab..a5ed0e3e169 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_profile_title_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_profile_profile_title_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_expbar_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_expbar_title_es-ES.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_expbar_title_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_expbar_title_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_item_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_item_title_es-ES.png index 15fdb806125..9236aaa1ff8 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_item_title_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_item_title_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_stats_title_es-ES.png b/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_stats_title_es-ES.png index 2233461522c..5b59c12984f 100644 Binary files a/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_stats_title_es-ES.png and b/public/images/ui/legacy/text_images/es-ES/summary/summary_stats_stats_title_es-ES.png differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_dexnb_label_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_dexnb_label_es-MX.png deleted file mode 100644 index a457468d8d0..00000000000 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_dexnb_label_es-MX.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_dexnb_label_overlay_shiny_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_dexnb_label_overlay_shiny_es-MX.png deleted file mode 100644 index 3277a28a59b..00000000000 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_dexnb_label_overlay_shiny_es-MX.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_descriptions_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_descriptions_title_es-MX.png deleted file mode 100644 index ffcae31894d..00000000000 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_descriptions_title_es-MX.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_effect_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_effect_title_es-MX.png deleted file mode 100644 index 50ce2f51d6f..00000000000 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_effect_title_es-MX.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_moves_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_moves_title_es-MX.png deleted file mode 100644 index ffca8bdfa10..00000000000 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_moves_moves_title_es-MX.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_ability_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_ability_es-MX.png deleted file mode 100644 index b1b1a84ddcf..00000000000 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_ability_es-MX.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_memo_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_memo_title_es-MX.png deleted file mode 100644 index e837a58e4f9..00000000000 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_memo_title_es-MX.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_passive_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_passive_es-MX.png deleted file mode 100644 index 885453e3e98..00000000000 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_passive_es-MX.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_profile_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_profile_title_es-MX.png deleted file mode 100644 index 51ba9300dab..00000000000 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_profile_profile_title_es-MX.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_expbar_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_expbar_title_es-MX.png deleted file mode 100644 index e9dfb10e5d6..00000000000 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_expbar_title_es-MX.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_item_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_item_title_es-MX.png deleted file mode 100644 index 15fdb806125..00000000000 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_item_title_es-MX.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_stats_title_es-MX.png b/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_stats_title_es-MX.png deleted file mode 100644 index 2233461522c..00000000000 Binary files a/public/images/ui/legacy/text_images/es-MX/summary/summary_stats_stats_title_es-MX.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/fr/battle_ui/overlay_exp_label_fr.png b/public/images/ui/legacy/text_images/fr/battle_ui/overlay_exp_label_fr.png index 40b5e8925a1..acb04a84a31 100644 Binary files a/public/images/ui/legacy/text_images/fr/battle_ui/overlay_exp_label_fr.png and b/public/images/ui/legacy/text_images/fr/battle_ui/overlay_exp_label_fr.png differ diff --git a/public/images/ui/legacy/text_images/ja/battle_ui/overlay_exp_label_ja.png b/public/images/ui/legacy/text_images/ja/battle_ui/overlay_exp_label_ja.png index 40b5e8925a1..acb04a84a31 100644 Binary files a/public/images/ui/legacy/text_images/ja/battle_ui/overlay_exp_label_ja.png and b/public/images/ui/legacy/text_images/ja/battle_ui/overlay_exp_label_ja.png differ diff --git a/public/images/ui/legacy/text_images/ja/summary/summary_stats_expbar_title_ja.png b/public/images/ui/legacy/text_images/ja/summary/summary_stats_expbar_title_ja.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/ja/summary/summary_stats_expbar_title_ja.png and b/public/images/ui/legacy/text_images/ja/summary/summary_stats_expbar_title_ja.png differ diff --git a/public/images/ui/legacy/text_images/ko/battle_ui/overlay_exp_label_ko.png b/public/images/ui/legacy/text_images/ko/battle_ui/overlay_exp_label_ko.png index 40b5e8925a1..acb04a84a31 100644 Binary files a/public/images/ui/legacy/text_images/ko/battle_ui/overlay_exp_label_ko.png and b/public/images/ui/legacy/text_images/ko/battle_ui/overlay_exp_label_ko.png differ diff --git a/public/images/ui/legacy/text_images/ko/summary/summary_stats_expbar_title_ko.png b/public/images/ui/legacy/text_images/ko/summary/summary_stats_expbar_title_ko.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/ko/summary/summary_stats_expbar_title_ko.png and b/public/images/ui/legacy/text_images/ko/summary/summary_stats_expbar_title_ko.png differ diff --git a/public/images/ui/legacy/text_images/pt-BR/summary/summary_dexnb_label_overlay_shiny_pt-BR.png b/public/images/ui/legacy/text_images/pt-BR/summary/summary_dexnb_label_overlay_shiny_pt-BR.png index 3277a28a59b..1640e46caa0 100644 Binary files a/public/images/ui/legacy/text_images/pt-BR/summary/summary_dexnb_label_overlay_shiny_pt-BR.png and b/public/images/ui/legacy/text_images/pt-BR/summary/summary_dexnb_label_overlay_shiny_pt-BR.png differ diff --git a/public/images/ui/legacy/text_images/pt-BR/summary/summary_stats_expbar_title_pt-BR.png b/public/images/ui/legacy/text_images/pt-BR/summary/summary_stats_expbar_title_pt-BR.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/pt-BR/summary/summary_stats_expbar_title_pt-BR.png and b/public/images/ui/legacy/text_images/pt-BR/summary/summary_stats_expbar_title_pt-BR.png differ diff --git a/public/images/ui/legacy/text_images/ro/battle_ui/overlay_exp_label_ro.png b/public/images/ui/legacy/text_images/ro/battle_ui/overlay_exp_label_ro.png index 40b5e8925a1..acb04a84a31 100644 Binary files a/public/images/ui/legacy/text_images/ro/battle_ui/overlay_exp_label_ro.png and b/public/images/ui/legacy/text_images/ro/battle_ui/overlay_exp_label_ro.png differ diff --git a/public/images/ui/legacy/text_images/ro/summary/summary_stats_expbar_title_ro.png b/public/images/ui/legacy/text_images/ro/summary/summary_stats_expbar_title_ro.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/ro/summary/summary_stats_expbar_title_ro.png and b/public/images/ui/legacy/text_images/ro/summary/summary_stats_expbar_title_ro.png differ diff --git a/public/images/ui/legacy/text_images/tl/battle_ui/overlay_exp_label_tl.png b/public/images/ui/legacy/text_images/tl/battle_ui/overlay_exp_label_tl.png index 40b5e8925a1..acb04a84a31 100644 Binary files a/public/images/ui/legacy/text_images/tl/battle_ui/overlay_exp_label_tl.png and b/public/images/ui/legacy/text_images/tl/battle_ui/overlay_exp_label_tl.png differ diff --git a/public/images/ui/legacy/text_images/tl/summary/summary_stats_expbar_title.png b/public/images/ui/legacy/text_images/tl/summary/summary_stats_expbar_title.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/tl/summary/summary_stats_expbar_title.png and b/public/images/ui/legacy/text_images/tl/summary/summary_stats_expbar_title.png differ diff --git a/public/images/ui/legacy/text_images/tr/summary/summary_stats_expbar_title_tr.png b/public/images/ui/legacy/text_images/tr/summary/summary_stats_expbar_title_tr.png index e9dfb10e5d6..da999975932 100644 Binary files a/public/images/ui/legacy/text_images/tr/summary/summary_stats_expbar_title_tr.png and b/public/images/ui/legacy/text_images/tr/summary/summary_stats_expbar_title_tr.png differ diff --git a/public/images/ui/legacy/text_images/zh-CN/battle_ui/overlay_exp_label_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/battle_ui/overlay_exp_label_zh-CN.png deleted file mode 100644 index 40b5e8925a1..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-CN/battle_ui/overlay_exp_label_zh-CN.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_dexnb_label_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_dexnb_label_zh-CN.png deleted file mode 100644 index eab90a91c7f..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_dexnb_label_zh-CN.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_descriptions_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_descriptions_title_zh-CN.png deleted file mode 100644 index 3d2b4d08376..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_descriptions_title_zh-CN.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_effect_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_effect_title_zh-CN.png deleted file mode 100644 index 55fb0efd832..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_effect_title_zh-CN.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_moves_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_moves_title_zh-CN.png deleted file mode 100644 index d869ab4e311..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_moves_title_zh-CN.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_ability_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_ability_zh-CN.png deleted file mode 100644 index 6600db26802..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_ability_zh-CN.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_memo_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_memo_title_zh-CN.png deleted file mode 100644 index 14cdf099044..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_memo_title_zh-CN.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_passive_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_passive_zh-CN.png deleted file mode 100644 index 66f56ff435e..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_passive_zh-CN.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_profile_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_profile_title_zh-CN.png deleted file mode 100644 index 8d4f82df3b3..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_profile_profile_title_zh-CN.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_expbar_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_expbar_title_zh-CN.png deleted file mode 100644 index e9dfb10e5d6..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_expbar_title_zh-CN.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_item_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_item_title_zh-CN.png deleted file mode 100644 index 5752b28288c..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_item_title_zh-CN.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_stats_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_stats_title_zh-CN.png deleted file mode 100644 index 5531819ef66..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_stats_title_zh-CN.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-Hans/battle_ui/overlay_exp_label_zh-Hans.png b/public/images/ui/legacy/text_images/zh-Hans/battle_ui/overlay_exp_label_zh-Hans.png new file mode 100644 index 00000000000..acb04a84a31 Binary files /dev/null and b/public/images/ui/legacy/text_images/zh-Hans/battle_ui/overlay_exp_label_zh-Hans.png differ diff --git a/public/images/ui/legacy/text_images/zh-CN/battle_ui/overlay_hp_label_boss_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/battle_ui/overlay_hp_label_boss_zh-Hans.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-CN/battle_ui/overlay_hp_label_boss_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/battle_ui/overlay_hp_label_boss_zh-Hans.png diff --git a/public/images/ui/legacy/text_images/zh-CN/battle_ui/overlay_hp_label_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/battle_ui/overlay_hp_label_zh-Hans.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-CN/battle_ui/overlay_hp_label_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/battle_ui/overlay_hp_label_zh-Hans.png diff --git a/public/images/ui/legacy/text_images/zh-CN/battle_ui/overlay_lv_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/battle_ui/overlay_lv_zh-Hans.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-CN/battle_ui/overlay_lv_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/battle_ui/overlay_lv_zh-Hans.png diff --git a/public/images/ui/legacy/text_images/zh-CN/battle_ui/pbinfo_stat_zh-CN.json b/public/images/ui/legacy/text_images/zh-Hans/battle_ui/pbinfo_stat_zh-Hans.json similarity index 98% rename from public/images/ui/legacy/text_images/zh-CN/battle_ui/pbinfo_stat_zh-CN.json rename to public/images/ui/legacy/text_images/zh-Hans/battle_ui/pbinfo_stat_zh-Hans.json index 49649bbc315..621d1575569 100644 --- a/public/images/ui/legacy/text_images/zh-CN/battle_ui/pbinfo_stat_zh-CN.json +++ b/public/images/ui/legacy/text_images/zh-Hans/battle_ui/pbinfo_stat_zh-Hans.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "pbinfo_stat_zh-CN.png", + "image": "pbinfo_stat_zh-Hans.png", "format": "RGBA8888", "size": { "w": 112, diff --git a/public/images/ui/legacy/text_images/zh-CN/battle_ui/pbinfo_stat_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/battle_ui/pbinfo_stat_zh-Hans.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-CN/battle_ui/pbinfo_stat_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/battle_ui/pbinfo_stat_zh-Hans.png diff --git a/public/images/ui/legacy/text_images/zh-CN/party_ui/party_slot_overlay_hp_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/party_ui/party_slot_overlay_hp_zh-Hans.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-CN/party_ui/party_slot_overlay_hp_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/party_ui/party_slot_overlay_hp_zh-Hans.png diff --git a/public/images/ui/legacy/text_images/zh-CN/party_ui/party_slot_overlay_lv_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/party_ui/party_slot_overlay_lv_zh-Hans.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-CN/party_ui/party_slot_overlay_lv_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/party_ui/party_slot_overlay_lv_zh-Hans.png diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_dexnb_label_overlay_shiny_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_dexnb_label_overlay_shiny_zh-Hans.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-CN/summary/summary_dexnb_label_overlay_shiny_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/summary/summary_dexnb_label_overlay_shiny_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/summary/summary_dexnb_label_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_dexnb_label_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_dexnb_label_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/summary/summary_dexnb_label_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/summary/summary_moves_descriptions_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_moves_descriptions_title_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_moves_descriptions_title_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/summary/summary_moves_descriptions_title_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/summary/summary_moves_effect_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_moves_effect_title_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_moves_effect_title_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/summary/summary_moves_effect_title_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/summary/summary_moves_moves_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_moves_moves_title_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_moves_moves_title_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/summary/summary_moves_moves_title_zh-Hans.png diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_overlay_pp_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_moves_overlay_pp_zh-Hans.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-CN/summary/summary_moves_overlay_pp_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/summary/summary_moves_overlay_pp_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/summary/summary_profile_ability_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_profile_ability_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_profile_ability_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/summary/summary_profile_ability_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/summary/summary_profile_memo_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_profile_memo_title_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_profile_memo_title_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/summary/summary_profile_memo_title_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/summary/summary_profile_passive_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_profile_passive_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_profile_passive_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/summary/summary_profile_passive_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/summary/summary_profile_profile_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_profile_profile_title_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_profile_profile_title_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/summary/summary_profile_profile_title_zh-Hans.png diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_exp_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_stats_exp_title_zh-Hans.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-CN/summary/summary_stats_exp_title_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/summary/summary_stats_exp_title_zh-Hans.png diff --git a/public/images/ui/legacy/text_images/zh-Hans/summary/summary_stats_expbar_title_zh-Hans.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_stats_expbar_title_zh-Hans.png new file mode 100644 index 00000000000..da999975932 Binary files /dev/null and b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_stats_expbar_title_zh-Hans.png differ diff --git a/public/images/ui/text_images/zh-CN/summary/summary_stats_item_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_stats_item_title_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_stats_item_title_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/summary/summary_stats_item_title_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/summary/summary_stats_stats_title_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_stats_stats_title_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_stats_stats_title_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/summary/summary_stats_stats_title_zh-Hans.png diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_tabs_1_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_tabs_1_zh-Hans.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-CN/summary/summary_tabs_1_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/summary/summary_tabs_1_zh-Hans.png diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_tabs_2_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_tabs_2_zh-Hans.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-CN/summary/summary_tabs_2_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/summary/summary_tabs_2_zh-Hans.png diff --git a/public/images/ui/legacy/text_images/zh-CN/summary/summary_tabs_3_zh-CN.png b/public/images/ui/legacy/text_images/zh-Hans/summary/summary_tabs_3_zh-Hans.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-CN/summary/summary_tabs_3_zh-CN.png rename to public/images/ui/legacy/text_images/zh-Hans/summary/summary_tabs_3_zh-Hans.png diff --git a/public/images/ui/legacy/text_images/zh-Hant/battle_ui/overlay_exp_label_zh-Hant.png b/public/images/ui/legacy/text_images/zh-Hant/battle_ui/overlay_exp_label_zh-Hant.png new file mode 100644 index 00000000000..acb04a84a31 Binary files /dev/null and b/public/images/ui/legacy/text_images/zh-Hant/battle_ui/overlay_exp_label_zh-Hant.png differ diff --git a/public/images/ui/legacy/text_images/zh-TW/battle_ui/overlay_hp_label_boss_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/battle_ui/overlay_hp_label_boss_zh-Hant.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-TW/battle_ui/overlay_hp_label_boss_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/battle_ui/overlay_hp_label_boss_zh-Hant.png diff --git a/public/images/ui/legacy/text_images/zh-TW/battle_ui/overlay_hp_label_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/battle_ui/overlay_hp_label_zh-Hant.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-TW/battle_ui/overlay_hp_label_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/battle_ui/overlay_hp_label_zh-Hant.png diff --git a/public/images/ui/legacy/text_images/zh-TW/battle_ui/overlay_lv_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/battle_ui/overlay_lv_zh-Hant.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-TW/battle_ui/overlay_lv_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/battle_ui/overlay_lv_zh-Hant.png diff --git a/public/images/ui/legacy/text_images/zh-TW/battle_ui/pbinfo_stat_zh-TW.json b/public/images/ui/legacy/text_images/zh-Hant/battle_ui/pbinfo_stat_zh-Hant.json similarity index 98% rename from public/images/ui/legacy/text_images/zh-TW/battle_ui/pbinfo_stat_zh-TW.json rename to public/images/ui/legacy/text_images/zh-Hant/battle_ui/pbinfo_stat_zh-Hant.json index 5a2e0fe2c30..86a1409b207 100644 --- a/public/images/ui/legacy/text_images/zh-TW/battle_ui/pbinfo_stat_zh-TW.json +++ b/public/images/ui/legacy/text_images/zh-Hant/battle_ui/pbinfo_stat_zh-Hant.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "pbinfo_stat_zh-TW.png", + "image": "pbinfo_stat_zh-Hant.png", "format": "RGBA8888", "size": { "w": 112, diff --git a/public/images/ui/legacy/text_images/zh-TW/battle_ui/pbinfo_stat_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/battle_ui/pbinfo_stat_zh-Hant.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-TW/battle_ui/pbinfo_stat_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/battle_ui/pbinfo_stat_zh-Hant.png diff --git a/public/images/ui/legacy/text_images/zh-TW/party_ui/party_slot_overlay_hp_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/party_ui/party_slot_overlay_hp_zh-Hant.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-TW/party_ui/party_slot_overlay_hp_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/party_ui/party_slot_overlay_hp_zh-Hant.png diff --git a/public/images/ui/legacy/text_images/zh-TW/party_ui/party_slot_overlay_lv_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/party_ui/party_slot_overlay_lv_zh-Hant.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-TW/party_ui/party_slot_overlay_lv_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/party_ui/party_slot_overlay_lv_zh-Hant.png diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_dexnb_label_overlay_shiny_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_dexnb_label_overlay_shiny_zh-Hant.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-TW/summary/summary_dexnb_label_overlay_shiny_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/summary/summary_dexnb_label_overlay_shiny_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/summary/summary_dexnb_label_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_dexnb_label_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_dexnb_label_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/summary/summary_dexnb_label_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/summary/summary_moves_descriptions_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_moves_descriptions_title_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_moves_descriptions_title_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/summary/summary_moves_descriptions_title_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/summary/summary_moves_effect_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_moves_effect_title_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_moves_effect_title_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/summary/summary_moves_effect_title_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/summary/summary_moves_moves_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_moves_moves_title_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_moves_moves_title_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/summary/summary_moves_moves_title_zh-Hant.png diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_overlay_pp_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_moves_overlay_pp_zh-Hant.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_overlay_pp_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/summary/summary_moves_overlay_pp_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/summary/summary_profile_ability_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_profile_ability_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_profile_ability_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/summary/summary_profile_ability_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/summary/summary_profile_memo_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_profile_memo_title_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_profile_memo_title_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/summary/summary_profile_memo_title_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/summary/summary_profile_passive_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_profile_passive_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_profile_passive_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/summary/summary_profile_passive_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/summary/summary_profile_profile_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_profile_profile_title_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_profile_profile_title_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/summary/summary_profile_profile_title_zh-Hant.png diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_exp_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_stats_exp_title_zh-Hant.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_exp_title_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/summary/summary_stats_exp_title_zh-Hant.png diff --git a/public/images/ui/legacy/text_images/zh-Hant/summary/summary_stats_expbar_title_zh-Hant.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_stats_expbar_title_zh-Hant.png new file mode 100644 index 00000000000..da999975932 Binary files /dev/null and b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_stats_expbar_title_zh-Hant.png differ diff --git a/public/images/ui/text_images/zh-TW/summary/summary_stats_item_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_stats_item_title_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_stats_item_title_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/summary/summary_stats_item_title_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/summary/summary_stats_stats_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_stats_stats_title_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_stats_stats_title_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/summary/summary_stats_stats_title_zh-Hant.png diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_tabs_1_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_tabs_1_zh-Hant.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-TW/summary/summary_tabs_1_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/summary/summary_tabs_1_zh-Hant.png diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_tabs_2_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_tabs_2_zh-Hant.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-TW/summary/summary_tabs_2_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/summary/summary_tabs_2_zh-Hant.png diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_tabs_3_zh-TW.png b/public/images/ui/legacy/text_images/zh-Hant/summary/summary_tabs_3_zh-Hant.png similarity index 100% rename from public/images/ui/legacy/text_images/zh-TW/summary/summary_tabs_3_zh-TW.png rename to public/images/ui/legacy/text_images/zh-Hant/summary/summary_tabs_3_zh-Hant.png diff --git a/public/images/ui/legacy/text_images/zh-TW/battle_ui/overlay_exp_label_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/battle_ui/overlay_exp_label_zh-TW.png deleted file mode 100644 index 40b5e8925a1..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-TW/battle_ui/overlay_exp_label_zh-TW.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_dexnb_label_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_dexnb_label_zh-TW.png deleted file mode 100644 index eab90a91c7f..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_dexnb_label_zh-TW.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_descriptions_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_descriptions_title_zh-TW.png deleted file mode 100644 index 3d2b4d08376..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_descriptions_title_zh-TW.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_effect_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_effect_title_zh-TW.png deleted file mode 100644 index 55fb0efd832..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_effect_title_zh-TW.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_moves_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_moves_title_zh-TW.png deleted file mode 100644 index d869ab4e311..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_moves_moves_title_zh-TW.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_ability_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_ability_zh-TW.png deleted file mode 100644 index 6600db26802..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_ability_zh-TW.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_memo_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_memo_title_zh-TW.png deleted file mode 100644 index 14cdf099044..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_memo_title_zh-TW.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_passive_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_passive_zh-TW.png deleted file mode 100644 index 66f56ff435e..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_passive_zh-TW.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_profile_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_profile_title_zh-TW.png deleted file mode 100644 index 8d4f82df3b3..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_profile_profile_title_zh-TW.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_expbar_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_expbar_title_zh-TW.png deleted file mode 100644 index e9dfb10e5d6..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_expbar_title_zh-TW.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_item_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_item_title_zh-TW.png deleted file mode 100644 index 5752b28288c..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_item_title_zh-TW.png and /dev/null differ diff --git a/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_stats_title_zh-TW.png b/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_stats_title_zh-TW.png deleted file mode 100644 index 5531819ef66..00000000000 Binary files a/public/images/ui/legacy/text_images/zh-TW/summary/summary_stats_stats_title_zh-TW.png and /dev/null differ diff --git a/public/images/ui/text_images/es-MX/battle_ui/overlay_exp_label_es-MX.png b/public/images/ui/text_images/es-419/battle_ui/overlay_exp_label_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/battle_ui/overlay_exp_label_es-MX.png rename to public/images/ui/text_images/es-419/battle_ui/overlay_exp_label_es-419.png diff --git a/public/images/ui/text_images/es-MX/battle_ui/overlay_hp_label_boss_es-MX.png b/public/images/ui/text_images/es-419/battle_ui/overlay_hp_label_boss_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/battle_ui/overlay_hp_label_boss_es-MX.png rename to public/images/ui/text_images/es-419/battle_ui/overlay_hp_label_boss_es-419.png diff --git a/public/images/ui/text_images/es-MX/battle_ui/overlay_hp_label_es-MX.png b/public/images/ui/text_images/es-419/battle_ui/overlay_hp_label_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/battle_ui/overlay_hp_label_es-MX.png rename to public/images/ui/text_images/es-419/battle_ui/overlay_hp_label_es-419.png diff --git a/public/images/ui/text_images/es-MX/battle_ui/overlay_lv_es-MX.png b/public/images/ui/text_images/es-419/battle_ui/overlay_lv_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/battle_ui/overlay_lv_es-MX.png rename to public/images/ui/text_images/es-419/battle_ui/overlay_lv_es-419.png diff --git a/public/images/ui/text_images/es-MX/battle_ui/pbinfo_stat_es-MX.json b/public/images/ui/text_images/es-419/battle_ui/pbinfo_stat_es-419.json similarity index 98% rename from public/images/ui/text_images/es-MX/battle_ui/pbinfo_stat_es-MX.json rename to public/images/ui/text_images/es-419/battle_ui/pbinfo_stat_es-419.json index b372566656b..7de7bc437d8 100644 --- a/public/images/ui/text_images/es-MX/battle_ui/pbinfo_stat_es-MX.json +++ b/public/images/ui/text_images/es-419/battle_ui/pbinfo_stat_es-419.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "pbinfo_stat_es-MX.png", + "image": "pbinfo_stat_es-419.png", "format": "RGBA8888", "size": { "w": 120, diff --git a/public/images/ui/text_images/es-MX/battle_ui/pbinfo_stat_es-MX.png b/public/images/ui/text_images/es-419/battle_ui/pbinfo_stat_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/battle_ui/pbinfo_stat_es-MX.png rename to public/images/ui/text_images/es-419/battle_ui/pbinfo_stat_es-419.png diff --git a/public/images/ui/text_images/es-MX/party_ui/party_slot_overlay_hp_es-MX.png b/public/images/ui/text_images/es-419/party_ui/party_slot_overlay_hp_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/party_ui/party_slot_overlay_hp_es-MX.png rename to public/images/ui/text_images/es-419/party_ui/party_slot_overlay_hp_es-419.png diff --git a/public/images/ui/text_images/es-MX/party_ui/party_slot_overlay_lv_es-MX.png b/public/images/ui/text_images/es-419/party_ui/party_slot_overlay_lv_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/party_ui/party_slot_overlay_lv_es-MX.png rename to public/images/ui/text_images/es-419/party_ui/party_slot_overlay_lv_es-419.png diff --git a/public/images/ui/text_images/es-419/summary/summary_dexnb_label_es-419.png b/public/images/ui/text_images/es-419/summary/summary_dexnb_label_es-419.png new file mode 100644 index 00000000000..5477e3385a8 Binary files /dev/null and b/public/images/ui/text_images/es-419/summary/summary_dexnb_label_es-419.png differ diff --git a/public/images/ui/text_images/es-MX/summary/summary_dexnb_label_overlay_shiny_es-MX.png b/public/images/ui/text_images/es-419/summary/summary_dexnb_label_overlay_shiny_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_dexnb_label_overlay_shiny_es-MX.png rename to public/images/ui/text_images/es-419/summary/summary_dexnb_label_overlay_shiny_es-419.png diff --git a/public/images/ui/text_images/es-419/summary/summary_moves_descriptions_title_es-419.png b/public/images/ui/text_images/es-419/summary/summary_moves_descriptions_title_es-419.png new file mode 100644 index 00000000000..3a4e3c7c375 Binary files /dev/null and b/public/images/ui/text_images/es-419/summary/summary_moves_descriptions_title_es-419.png differ diff --git a/public/images/ui/text_images/es-419/summary/summary_moves_effect_title_es-419.png b/public/images/ui/text_images/es-419/summary/summary_moves_effect_title_es-419.png new file mode 100644 index 00000000000..cf8d1309848 Binary files /dev/null and b/public/images/ui/text_images/es-419/summary/summary_moves_effect_title_es-419.png differ diff --git a/public/images/ui/text_images/es-419/summary/summary_moves_moves_title_es-419.png b/public/images/ui/text_images/es-419/summary/summary_moves_moves_title_es-419.png new file mode 100644 index 00000000000..a601ae79e4f Binary files /dev/null and b/public/images/ui/text_images/es-419/summary/summary_moves_moves_title_es-419.png differ diff --git a/public/images/ui/text_images/es-MX/summary/summary_moves_overlay_pp_es-MX.png b/public/images/ui/text_images/es-419/summary/summary_moves_overlay_pp_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_moves_overlay_pp_es-MX.png rename to public/images/ui/text_images/es-419/summary/summary_moves_overlay_pp_es-419.png diff --git a/public/images/ui/text_images/es-419/summary/summary_profile_ability_es-419.png b/public/images/ui/text_images/es-419/summary/summary_profile_ability_es-419.png new file mode 100644 index 00000000000..71bffe95cfe Binary files /dev/null and b/public/images/ui/text_images/es-419/summary/summary_profile_ability_es-419.png differ diff --git a/public/images/ui/text_images/es-419/summary/summary_profile_memo_title_es-419.png b/public/images/ui/text_images/es-419/summary/summary_profile_memo_title_es-419.png new file mode 100644 index 00000000000..b7ef7c91fc5 Binary files /dev/null and b/public/images/ui/text_images/es-419/summary/summary_profile_memo_title_es-419.png differ diff --git a/public/images/ui/text_images/es-419/summary/summary_profile_passive_es-419.png b/public/images/ui/text_images/es-419/summary/summary_profile_passive_es-419.png new file mode 100644 index 00000000000..a50e3cacf58 Binary files /dev/null and b/public/images/ui/text_images/es-419/summary/summary_profile_passive_es-419.png differ diff --git a/public/images/ui/text_images/es-419/summary/summary_profile_profile_title_es-419.png b/public/images/ui/text_images/es-419/summary/summary_profile_profile_title_es-419.png new file mode 100644 index 00000000000..a5ed0e3e169 Binary files /dev/null and b/public/images/ui/text_images/es-419/summary/summary_profile_profile_title_es-419.png differ diff --git a/public/images/ui/text_images/es-MX/summary/summary_stats_exp_title_es-MX.png b/public/images/ui/text_images/es-419/summary/summary_stats_exp_title_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_stats_exp_title_es-MX.png rename to public/images/ui/text_images/es-419/summary/summary_stats_exp_title_es-419.png diff --git a/public/images/ui/text_images/es-MX/summary/summary_stats_expbar_title_es-MX.png b/public/images/ui/text_images/es-419/summary/summary_stats_expbar_title_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_stats_expbar_title_es-MX.png rename to public/images/ui/text_images/es-419/summary/summary_stats_expbar_title_es-419.png diff --git a/public/images/ui/text_images/es-419/summary/summary_stats_item_title_es-419.png b/public/images/ui/text_images/es-419/summary/summary_stats_item_title_es-419.png new file mode 100644 index 00000000000..9236aaa1ff8 Binary files /dev/null and b/public/images/ui/text_images/es-419/summary/summary_stats_item_title_es-419.png differ diff --git a/public/images/ui/text_images/es-419/summary/summary_stats_stats_title_es-419.png b/public/images/ui/text_images/es-419/summary/summary_stats_stats_title_es-419.png new file mode 100644 index 00000000000..5b59c12984f Binary files /dev/null and b/public/images/ui/text_images/es-419/summary/summary_stats_stats_title_es-419.png differ diff --git a/public/images/ui/text_images/es-MX/summary/summary_tabs_1_es-MX.png b/public/images/ui/text_images/es-419/summary/summary_tabs_1_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_tabs_1_es-MX.png rename to public/images/ui/text_images/es-419/summary/summary_tabs_1_es-419.png diff --git a/public/images/ui/text_images/es-MX/summary/summary_tabs_2_es-MX.png b/public/images/ui/text_images/es-419/summary/summary_tabs_2_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_tabs_2_es-MX.png rename to public/images/ui/text_images/es-419/summary/summary_tabs_2_es-419.png diff --git a/public/images/ui/text_images/es-MX/summary/summary_tabs_3_es-MX.png b/public/images/ui/text_images/es-419/summary/summary_tabs_3_es-419.png similarity index 100% rename from public/images/ui/text_images/es-MX/summary/summary_tabs_3_es-MX.png rename to public/images/ui/text_images/es-419/summary/summary_tabs_3_es-419.png diff --git a/public/images/ui/text_images/zh-CN/battle_ui/overlay_exp_label_zh-CN.png b/public/images/ui/text_images/zh-Hans/battle_ui/overlay_exp_label_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/battle_ui/overlay_exp_label_zh-CN.png rename to public/images/ui/text_images/zh-Hans/battle_ui/overlay_exp_label_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/battle_ui/overlay_hp_label_boss_zh-CN.png b/public/images/ui/text_images/zh-Hans/battle_ui/overlay_hp_label_boss_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/battle_ui/overlay_hp_label_boss_zh-CN.png rename to public/images/ui/text_images/zh-Hans/battle_ui/overlay_hp_label_boss_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/battle_ui/overlay_hp_label_zh-CN.png b/public/images/ui/text_images/zh-Hans/battle_ui/overlay_hp_label_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/battle_ui/overlay_hp_label_zh-CN.png rename to public/images/ui/text_images/zh-Hans/battle_ui/overlay_hp_label_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/battle_ui/overlay_lv_zh-CN.png b/public/images/ui/text_images/zh-Hans/battle_ui/overlay_lv_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/battle_ui/overlay_lv_zh-CN.png rename to public/images/ui/text_images/zh-Hans/battle_ui/overlay_lv_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/battle_ui/pbinfo_stat_zh-CN.json b/public/images/ui/text_images/zh-Hans/battle_ui/pbinfo_stat_zh-Hans.json similarity index 98% rename from public/images/ui/text_images/zh-CN/battle_ui/pbinfo_stat_zh-CN.json rename to public/images/ui/text_images/zh-Hans/battle_ui/pbinfo_stat_zh-Hans.json index 22a1da0b536..c59160eccf0 100644 --- a/public/images/ui/text_images/zh-CN/battle_ui/pbinfo_stat_zh-CN.json +++ b/public/images/ui/text_images/zh-Hans/battle_ui/pbinfo_stat_zh-Hans.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "pbinfo_stat_zh-CN.png", + "image": "pbinfo_stat_zh-Hans.png", "format": "RGBA8888", "size": { "w": 120, diff --git a/public/images/ui/text_images/zh-CN/battle_ui/pbinfo_stat_zh-CN.png b/public/images/ui/text_images/zh-Hans/battle_ui/pbinfo_stat_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/battle_ui/pbinfo_stat_zh-CN.png rename to public/images/ui/text_images/zh-Hans/battle_ui/pbinfo_stat_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/party_ui/party_slot_overlay_hp_zh-CN.png b/public/images/ui/text_images/zh-Hans/party_ui/party_slot_overlay_hp_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/party_ui/party_slot_overlay_hp_zh-CN.png rename to public/images/ui/text_images/zh-Hans/party_ui/party_slot_overlay_hp_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/party_ui/party_slot_overlay_lv_zh-CN.png b/public/images/ui/text_images/zh-Hans/party_ui/party_slot_overlay_lv_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/party_ui/party_slot_overlay_lv_zh-CN.png rename to public/images/ui/text_images/zh-Hans/party_ui/party_slot_overlay_lv_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/summary/summary_dexnb_label_overlay_shiny_zh-CN.png b/public/images/ui/text_images/zh-Hans/summary/summary_dexnb_label_overlay_shiny_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_dexnb_label_overlay_shiny_zh-CN.png rename to public/images/ui/text_images/zh-Hans/summary/summary_dexnb_label_overlay_shiny_zh-Hans.png diff --git a/public/images/ui/text_images/zh-Hans/summary/summary_dexnb_label_zh-Hans.png b/public/images/ui/text_images/zh-Hans/summary/summary_dexnb_label_zh-Hans.png new file mode 100644 index 00000000000..bf568c486aa Binary files /dev/null and b/public/images/ui/text_images/zh-Hans/summary/summary_dexnb_label_zh-Hans.png differ diff --git a/public/images/ui/text_images/zh-Hans/summary/summary_moves_descriptions_title_zh-Hans.png b/public/images/ui/text_images/zh-Hans/summary/summary_moves_descriptions_title_zh-Hans.png new file mode 100644 index 00000000000..e83e8cafbfc Binary files /dev/null and b/public/images/ui/text_images/zh-Hans/summary/summary_moves_descriptions_title_zh-Hans.png differ diff --git a/public/images/ui/text_images/zh-Hans/summary/summary_moves_effect_title_zh-Hans.png b/public/images/ui/text_images/zh-Hans/summary/summary_moves_effect_title_zh-Hans.png new file mode 100644 index 00000000000..fbbaac0b260 Binary files /dev/null and b/public/images/ui/text_images/zh-Hans/summary/summary_moves_effect_title_zh-Hans.png differ diff --git a/public/images/ui/text_images/zh-Hans/summary/summary_moves_moves_title_zh-Hans.png b/public/images/ui/text_images/zh-Hans/summary/summary_moves_moves_title_zh-Hans.png new file mode 100644 index 00000000000..6bbb29c9c5f Binary files /dev/null and b/public/images/ui/text_images/zh-Hans/summary/summary_moves_moves_title_zh-Hans.png differ diff --git a/public/images/ui/text_images/zh-CN/summary/summary_moves_overlay_pp_zh-CN.png b/public/images/ui/text_images/zh-Hans/summary/summary_moves_overlay_pp_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_moves_overlay_pp_zh-CN.png rename to public/images/ui/text_images/zh-Hans/summary/summary_moves_overlay_pp_zh-Hans.png diff --git a/public/images/ui/text_images/zh-Hans/summary/summary_profile_ability_zh-Hans.png b/public/images/ui/text_images/zh-Hans/summary/summary_profile_ability_zh-Hans.png new file mode 100644 index 00000000000..a05c22b7d47 Binary files /dev/null and b/public/images/ui/text_images/zh-Hans/summary/summary_profile_ability_zh-Hans.png differ diff --git a/public/images/ui/text_images/zh-Hans/summary/summary_profile_memo_title_zh-Hans.png b/public/images/ui/text_images/zh-Hans/summary/summary_profile_memo_title_zh-Hans.png new file mode 100644 index 00000000000..3d69c20e57f Binary files /dev/null and b/public/images/ui/text_images/zh-Hans/summary/summary_profile_memo_title_zh-Hans.png differ diff --git a/public/images/ui/text_images/zh-Hans/summary/summary_profile_passive_zh-Hans.png b/public/images/ui/text_images/zh-Hans/summary/summary_profile_passive_zh-Hans.png new file mode 100644 index 00000000000..c026e87a215 Binary files /dev/null and b/public/images/ui/text_images/zh-Hans/summary/summary_profile_passive_zh-Hans.png differ diff --git a/public/images/ui/text_images/zh-Hans/summary/summary_profile_profile_title_zh-Hans.png b/public/images/ui/text_images/zh-Hans/summary/summary_profile_profile_title_zh-Hans.png new file mode 100644 index 00000000000..4170dccf682 Binary files /dev/null and b/public/images/ui/text_images/zh-Hans/summary/summary_profile_profile_title_zh-Hans.png differ diff --git a/public/images/ui/text_images/zh-CN/summary/summary_stats_exp_title_zh-CN.png b/public/images/ui/text_images/zh-Hans/summary/summary_stats_exp_title_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_stats_exp_title_zh-CN.png rename to public/images/ui/text_images/zh-Hans/summary/summary_stats_exp_title_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/summary/summary_stats_expbar_title_zh-CN.png b/public/images/ui/text_images/zh-Hans/summary/summary_stats_expbar_title_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_stats_expbar_title_zh-CN.png rename to public/images/ui/text_images/zh-Hans/summary/summary_stats_expbar_title_zh-Hans.png diff --git a/public/images/ui/text_images/zh-Hans/summary/summary_stats_item_title_zh-Hans.png b/public/images/ui/text_images/zh-Hans/summary/summary_stats_item_title_zh-Hans.png new file mode 100644 index 00000000000..42e08b3e52a Binary files /dev/null and b/public/images/ui/text_images/zh-Hans/summary/summary_stats_item_title_zh-Hans.png differ diff --git a/public/images/ui/text_images/zh-Hans/summary/summary_stats_stats_title_zh-Hans.png b/public/images/ui/text_images/zh-Hans/summary/summary_stats_stats_title_zh-Hans.png new file mode 100644 index 00000000000..f602a43c39d Binary files /dev/null and b/public/images/ui/text_images/zh-Hans/summary/summary_stats_stats_title_zh-Hans.png differ diff --git a/public/images/ui/text_images/zh-CN/summary/summary_tabs_1_zh-CN.png b/public/images/ui/text_images/zh-Hans/summary/summary_tabs_1_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_tabs_1_zh-CN.png rename to public/images/ui/text_images/zh-Hans/summary/summary_tabs_1_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/summary/summary_tabs_2_zh-CN.png b/public/images/ui/text_images/zh-Hans/summary/summary_tabs_2_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_tabs_2_zh-CN.png rename to public/images/ui/text_images/zh-Hans/summary/summary_tabs_2_zh-Hans.png diff --git a/public/images/ui/text_images/zh-CN/summary/summary_tabs_3_zh-CN.png b/public/images/ui/text_images/zh-Hans/summary/summary_tabs_3_zh-Hans.png similarity index 100% rename from public/images/ui/text_images/zh-CN/summary/summary_tabs_3_zh-CN.png rename to public/images/ui/text_images/zh-Hans/summary/summary_tabs_3_zh-Hans.png diff --git a/public/images/ui/text_images/zh-TW/battle_ui/overlay_exp_label_zh-TW.png b/public/images/ui/text_images/zh-Hant/battle_ui/overlay_exp_label_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/battle_ui/overlay_exp_label_zh-TW.png rename to public/images/ui/text_images/zh-Hant/battle_ui/overlay_exp_label_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/battle_ui/overlay_hp_label_boss_zh-TW.png b/public/images/ui/text_images/zh-Hant/battle_ui/overlay_hp_label_boss_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/battle_ui/overlay_hp_label_boss_zh-TW.png rename to public/images/ui/text_images/zh-Hant/battle_ui/overlay_hp_label_boss_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/battle_ui/overlay_hp_label_zh-TW.png b/public/images/ui/text_images/zh-Hant/battle_ui/overlay_hp_label_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/battle_ui/overlay_hp_label_zh-TW.png rename to public/images/ui/text_images/zh-Hant/battle_ui/overlay_hp_label_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/battle_ui/overlay_lv_zh-TW.png b/public/images/ui/text_images/zh-Hant/battle_ui/overlay_lv_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/battle_ui/overlay_lv_zh-TW.png rename to public/images/ui/text_images/zh-Hant/battle_ui/overlay_lv_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/battle_ui/pbinfo_stat_zh-TW.json b/public/images/ui/text_images/zh-Hant/battle_ui/pbinfo_stat_zh-Hant.json similarity index 98% rename from public/images/ui/text_images/zh-TW/battle_ui/pbinfo_stat_zh-TW.json rename to public/images/ui/text_images/zh-Hant/battle_ui/pbinfo_stat_zh-Hant.json index 26c01d8bcc2..c6e137515b2 100644 --- a/public/images/ui/text_images/zh-TW/battle_ui/pbinfo_stat_zh-TW.json +++ b/public/images/ui/text_images/zh-Hant/battle_ui/pbinfo_stat_zh-Hant.json @@ -1,7 +1,7 @@ { "textures": [ { - "image": "pbinfo_stat_zh-TW.png", + "image": "pbinfo_stat_zh-Hant.png", "format": "RGBA8888", "size": { "w": 120, diff --git a/public/images/ui/text_images/zh-TW/battle_ui/pbinfo_stat_zh-TW.png b/public/images/ui/text_images/zh-Hant/battle_ui/pbinfo_stat_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/battle_ui/pbinfo_stat_zh-TW.png rename to public/images/ui/text_images/zh-Hant/battle_ui/pbinfo_stat_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/party_ui/party_slot_overlay_hp_zh-TW.png b/public/images/ui/text_images/zh-Hant/party_ui/party_slot_overlay_hp_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/party_ui/party_slot_overlay_hp_zh-TW.png rename to public/images/ui/text_images/zh-Hant/party_ui/party_slot_overlay_hp_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/party_ui/party_slot_overlay_lv_zh-TW.png b/public/images/ui/text_images/zh-Hant/party_ui/party_slot_overlay_lv_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/party_ui/party_slot_overlay_lv_zh-TW.png rename to public/images/ui/text_images/zh-Hant/party_ui/party_slot_overlay_lv_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/summary/summary_dexnb_label_overlay_shiny_zh-TW.png b/public/images/ui/text_images/zh-Hant/summary/summary_dexnb_label_overlay_shiny_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_dexnb_label_overlay_shiny_zh-TW.png rename to public/images/ui/text_images/zh-Hant/summary/summary_dexnb_label_overlay_shiny_zh-Hant.png diff --git a/public/images/ui/text_images/zh-Hant/summary/summary_dexnb_label_zh-Hant.png b/public/images/ui/text_images/zh-Hant/summary/summary_dexnb_label_zh-Hant.png new file mode 100644 index 00000000000..bf568c486aa Binary files /dev/null and b/public/images/ui/text_images/zh-Hant/summary/summary_dexnb_label_zh-Hant.png differ diff --git a/public/images/ui/text_images/zh-Hant/summary/summary_moves_descriptions_title_zh-Hant.png b/public/images/ui/text_images/zh-Hant/summary/summary_moves_descriptions_title_zh-Hant.png new file mode 100644 index 00000000000..e83e8cafbfc Binary files /dev/null and b/public/images/ui/text_images/zh-Hant/summary/summary_moves_descriptions_title_zh-Hant.png differ diff --git a/public/images/ui/text_images/zh-Hant/summary/summary_moves_effect_title_zh-Hant.png b/public/images/ui/text_images/zh-Hant/summary/summary_moves_effect_title_zh-Hant.png new file mode 100644 index 00000000000..fbbaac0b260 Binary files /dev/null and b/public/images/ui/text_images/zh-Hant/summary/summary_moves_effect_title_zh-Hant.png differ diff --git a/public/images/ui/text_images/zh-Hant/summary/summary_moves_moves_title_zh-Hant.png b/public/images/ui/text_images/zh-Hant/summary/summary_moves_moves_title_zh-Hant.png new file mode 100644 index 00000000000..6bbb29c9c5f Binary files /dev/null and b/public/images/ui/text_images/zh-Hant/summary/summary_moves_moves_title_zh-Hant.png differ diff --git a/public/images/ui/text_images/zh-TW/summary/summary_moves_overlay_pp_zh-TW.png b/public/images/ui/text_images/zh-Hant/summary/summary_moves_overlay_pp_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_moves_overlay_pp_zh-TW.png rename to public/images/ui/text_images/zh-Hant/summary/summary_moves_overlay_pp_zh-Hant.png diff --git a/public/images/ui/text_images/zh-Hant/summary/summary_profile_ability_zh-Hant.png b/public/images/ui/text_images/zh-Hant/summary/summary_profile_ability_zh-Hant.png new file mode 100644 index 00000000000..a05c22b7d47 Binary files /dev/null and b/public/images/ui/text_images/zh-Hant/summary/summary_profile_ability_zh-Hant.png differ diff --git a/public/images/ui/text_images/zh-Hant/summary/summary_profile_memo_title_zh-Hant.png b/public/images/ui/text_images/zh-Hant/summary/summary_profile_memo_title_zh-Hant.png new file mode 100644 index 00000000000..3d69c20e57f Binary files /dev/null and b/public/images/ui/text_images/zh-Hant/summary/summary_profile_memo_title_zh-Hant.png differ diff --git a/public/images/ui/text_images/zh-Hant/summary/summary_profile_passive_zh-Hant.png b/public/images/ui/text_images/zh-Hant/summary/summary_profile_passive_zh-Hant.png new file mode 100644 index 00000000000..c026e87a215 Binary files /dev/null and b/public/images/ui/text_images/zh-Hant/summary/summary_profile_passive_zh-Hant.png differ diff --git a/public/images/ui/text_images/zh-Hant/summary/summary_profile_profile_title_zh-Hant.png b/public/images/ui/text_images/zh-Hant/summary/summary_profile_profile_title_zh-Hant.png new file mode 100644 index 00000000000..4170dccf682 Binary files /dev/null and b/public/images/ui/text_images/zh-Hant/summary/summary_profile_profile_title_zh-Hant.png differ diff --git a/public/images/ui/text_images/zh-TW/summary/summary_stats_exp_title_zh-TW.png b/public/images/ui/text_images/zh-Hant/summary/summary_stats_exp_title_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_stats_exp_title_zh-TW.png rename to public/images/ui/text_images/zh-Hant/summary/summary_stats_exp_title_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/summary/summary_stats_expbar_title_zh-TW.png b/public/images/ui/text_images/zh-Hant/summary/summary_stats_expbar_title_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_stats_expbar_title_zh-TW.png rename to public/images/ui/text_images/zh-Hant/summary/summary_stats_expbar_title_zh-Hant.png diff --git a/public/images/ui/text_images/zh-Hant/summary/summary_stats_item_title_zh-Hant.png b/public/images/ui/text_images/zh-Hant/summary/summary_stats_item_title_zh-Hant.png new file mode 100644 index 00000000000..42e08b3e52a Binary files /dev/null and b/public/images/ui/text_images/zh-Hant/summary/summary_stats_item_title_zh-Hant.png differ diff --git a/public/images/ui/text_images/zh-Hant/summary/summary_stats_stats_title_zh-Hant.png b/public/images/ui/text_images/zh-Hant/summary/summary_stats_stats_title_zh-Hant.png new file mode 100644 index 00000000000..f602a43c39d Binary files /dev/null and b/public/images/ui/text_images/zh-Hant/summary/summary_stats_stats_title_zh-Hant.png differ diff --git a/public/images/ui/text_images/zh-TW/summary/summary_tabs_1_zh-TW.png b/public/images/ui/text_images/zh-Hant/summary/summary_tabs_1_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_tabs_1_zh-TW.png rename to public/images/ui/text_images/zh-Hant/summary/summary_tabs_1_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/summary/summary_tabs_2_zh-TW.png b/public/images/ui/text_images/zh-Hant/summary/summary_tabs_2_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_tabs_2_zh-TW.png rename to public/images/ui/text_images/zh-Hant/summary/summary_tabs_2_zh-Hant.png diff --git a/public/images/ui/text_images/zh-TW/summary/summary_tabs_3_zh-TW.png b/public/images/ui/text_images/zh-Hant/summary/summary_tabs_3_zh-Hant.png similarity index 100% rename from public/images/ui/text_images/zh-TW/summary/summary_tabs_3_zh-TW.png rename to public/images/ui/text_images/zh-Hant/summary/summary_tabs_3_zh-Hant.png diff --git a/public/locales b/public/locales index 74de730a642..f3b0c55a0f5 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 74de730a64272c8e9ca0a4cdcf3426cbf1b0aeda +Subproject commit f3b0c55a0f5744a34bb4c526bc3790592bb3c729 diff --git a/src/@types/damage-params.ts b/src/@types/damage-params.ts new file mode 100644 index 00000000000..b656c60f0ab --- /dev/null +++ b/src/@types/damage-params.ts @@ -0,0 +1,44 @@ +import type { MoveCategory } from "#enums/move-category"; +import type { Pokemon } from "#field/pokemon"; +import type { Move } from "#types/move-types"; + +/** + * Collection of types for methods like {@linkcode Pokemon#getBaseDamage} and {@linkcode Pokemon#getAttackDamage}. + * @module + */ + +/** Base type for damage parameter methods, used for DRY */ +export interface damageParams { + /** The attacking {@linkcode Pokemon} */ + source: Pokemon; + /** The move used in the attack */ + move: Move; + /** The move's {@linkcode MoveCategory} after variable-category effects are applied */ + moveCategory: MoveCategory; + /** If `true`, ignores this Pokemon's defensive ability effects */ + ignoreAbility?: boolean; + /** If `true`, ignores the attacking Pokemon's ability effects */ + ignoreSourceAbility?: boolean; + /** If `true`, ignores the ally Pokemon's ability effects */ + ignoreAllyAbility?: boolean; + /** If `true`, ignores the ability effects of the attacking pokemon's ally */ + ignoreSourceAllyAbility?: boolean; + /** If `true`, calculates damage for a critical hit */ + isCritical?: boolean; + /** If `true`, suppresses changes to game state during the calculation */ + simulated?: boolean; + /** If defined, used in place of calculated effectiveness values */ + effectiveness?: number; +} + +/** + * Type for the parameters of {@linkcode Pokemon#getBaseDamage | getBaseDamage} + * @interface + */ +export type getBaseDamageParams = Omit; + +/** + * Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage} + * @interface + */ +export type getAttackDamageParams = Omit; diff --git a/src/@types/phase-types.ts b/src/@types/phase-types.ts index 91673053747..2324c927e3a 100644 --- a/src/@types/phase-types.ts +++ b/src/@types/phase-types.ts @@ -1,26 +1,27 @@ +import type { Pokemon } from "#app/field/pokemon"; +import type { Phase } from "#app/phase"; import type { PhaseConstructorMap } from "#app/phase-manager"; import type { ObjectValues } from "#types/type-helpers"; -// Intentionally export the types of everything in phase-manager, as this file is meant to be +// Intentionally [re-]export the types of everything in phase-manager, as this file is meant to be // the centralized place for type definitions for the phase system. export type * from "#app/phase-manager"; -// This file includes helpful types for the phase system. -// It intentionally imports the phase constructor map from the phase manager (and re-exports it) - -/** - * Map of phase names to constructors for said phase - */ +/** Map of phase names to constructors for said phase */ export type PhaseMap = { [K in keyof PhaseConstructorMap]: InstanceType; }; -/** - * Union type of all phase constructors. - */ +/** Union type of all phase constructors. */ export type PhaseClass = ObjectValues; -/** - * Union type of all phase names as strings. - */ +/** Union type of all phase names as strings. */ export type PhaseString = keyof PhaseMap; + +/** Type for predicate functions operating on a specific type of {@linkcode Phase}. */ +export type PhaseConditionFunc = (phase: PhaseMap[T]) => boolean; + +/** Interface type representing the assumption that all phases with pokemon associated are dynamic */ +export interface DynamicPhase extends Phase { + getPokemon(): Pokemon; +} diff --git a/src/@types/save-data.ts b/src/@types/save-data.ts index 4c20d63da53..ae359c20949 100644 --- a/src/@types/save-data.ts +++ b/src/@types/save-data.ts @@ -4,8 +4,10 @@ import type { BattleType } from "#enums/battle-type"; import type { GameModes } from "#enums/game-modes"; import type { MoveId } from "#enums/move-id"; import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import type { Nature } from "#enums/nature"; import type { PlayerGender } from "#enums/player-gender"; import type { PokemonType } from "#enums/pokemon-type"; +import type { SpeciesId } from "#enums/species-id"; import type { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data"; import type { Variant } from "#sprites/variant"; import type { ArenaData } from "#system/arena-data"; @@ -108,6 +110,22 @@ export interface DexAttrProps { formIndex: number; } +export interface Starter { + speciesId: SpeciesId; + shiny: boolean; + variant: Variant; + formIndex: number; + female?: boolean; + abilityIndex: number; + passive: boolean; + nature: Nature; + moveset?: StarterMoveset; + pokerus: boolean; + nickname?: string; + teraType?: PokemonType; + ivs: number[]; +} + export type RunHistoryData = Record; export interface RunEntry { diff --git a/src/battle-scene.ts b/src/battle-scene.ts index cbda368782e..289c9a8f051 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -104,7 +104,6 @@ import { import { MysteryEncounter } from "#mystery-encounters/mystery-encounter"; import { MysteryEncounterSaveData } from "#mystery-encounters/mystery-encounter-save-data"; import { allMysteryEncounters, mysteryEncountersByBiome } from "#mystery-encounters/mystery-encounters"; -import type { MovePhase } from "#phases/move-phase"; import { expSpriteKeys } from "#sprites/sprite-keys"; import { hasExpSprite } from "#sprites/sprite-utils"; import type { Variant } from "#sprites/variant"; @@ -787,12 +786,14 @@ export class BattleScene extends SceneBase { /** * Returns an array of EnemyPokemon of length 1 or 2 depending on if in a double battle or not. - * Does not actually check if the pokemon are on the field or not. + * @param active - (Default `false`) Whether to consider only {@linkcode Pokemon.isActive | active} on-field pokemon * @returns array of {@linkcode EnemyPokemon} */ - public getEnemyField(): EnemyPokemon[] { + public getEnemyField(active = false): EnemyPokemon[] { const party = this.getEnemyParty(); - return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1)); + return party + .slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1)) + .filter(p => !active || p.isActive()); } /** @@ -817,25 +818,7 @@ export class BattleScene extends SceneBase { * @param allyPokemon - The {@linkcode Pokemon} allied with the removed Pokemon; will have moves redirected to it */ redirectPokemonMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void { - // failsafe: if not a double battle just return - if (this.currentBattle.double === false) { - return; - } - if (allyPokemon?.isActive(true)) { - let targetingMovePhase: MovePhase; - do { - targetingMovePhase = this.phaseManager.findPhase( - mp => - mp.is("MovePhase") - && mp.targets.length === 1 - && mp.targets[0] === removedPokemon.getBattlerIndex() - && mp.pokemon.isPlayer() !== allyPokemon.isPlayer(), - ) as MovePhase; - if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) { - targetingMovePhase.targets[0] = allyPokemon.getBattlerIndex(); - } - } while (targetingMovePhase); - } + this.phaseManager.redirectMoves(removedPokemon, allyPokemon); } /** @@ -1433,7 +1416,7 @@ export class BattleScene extends SceneBase { } if (lastBattle?.double && !newDouble) { - this.phaseManager.tryRemovePhase((p: Phase) => p.is("SwitchPhase")); + this.phaseManager.tryRemovePhase("SwitchPhase"); for (const p of this.getPlayerField()) { p.lapseTag(BattlerTagType.COMMANDED); } diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index ebe8b816e5e..f6494548b99 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -33,6 +33,7 @@ import { CommonAnim } from "#enums/move-anims-common"; import { MoveCategory } from "#enums/move-category"; import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import { MoveResult } from "#enums/move-result"; import { MoveTarget } from "#enums/move-target"; import { MoveUseMode } from "#enums/move-use-mode"; @@ -2555,7 +2556,7 @@ export class PostIntimidateStatStageChangeAbAttr extends AbAttr { override apply({ pokemon, simulated, cancelled }: AbAttrParamsWithCancel): void { if (!simulated) { - globalScene.phaseManager.pushNew( + globalScene.phaseManager.unshiftNew( "StatStageChangePhase", pokemon.getBattlerIndex(), false, @@ -3240,6 +3241,7 @@ export class CommanderAbAttr extends AbAttr { return ( globalScene.currentBattle?.double && ally != null + && ally.isActive(true) && ally.species.speciesId === SpeciesId.DONDOZO && !(ally.isFainted() || ally.getTag(BattlerTagType.COMMANDED)) ); @@ -3254,7 +3256,7 @@ export class CommanderAbAttr extends AbAttr { // Apply boosts from this effect to the ally Dondozo pokemon.getAlly()?.addTag(BattlerTagType.COMMANDED, 0, MoveId.NONE, pokemon.id); // Cancel the source Pokemon's next move (if a move is queued) - globalScene.phaseManager.tryRemovePhase(phase => phase.is("MovePhase") && phase.pokemon === pokemon); + globalScene.phaseManager.tryRemovePhase("MovePhase", phase => phase.pokemon === pokemon); } } } @@ -5004,7 +5006,14 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { // If the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance if (move.getMove().is("AttackMove") || move.getMove().is("StatusMove")) { const target = this.getTarget(pokemon, source, targets); - globalScene.phaseManager.unshiftNew("MovePhase", pokemon, target, move, MoveUseMode.INDIRECT); + globalScene.phaseManager.unshiftNew( + "MovePhase", + pokemon, + target, + move, + MoveUseMode.INDIRECT, + MovePhaseTimingModifier.FIRST, + ); } else if (move.getMove().is("SelfStatusMove")) { // If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself globalScene.phaseManager.unshiftNew( @@ -5013,6 +5022,7 @@ export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { [pokemon.getBattlerIndex()], move, MoveUseMode.INDIRECT, + MovePhaseTimingModifier.FIRST, ); } } @@ -6028,11 +6038,6 @@ export class IllusionPostBattleAbAttr extends PostBattleAbAttr { } } -export interface BypassSpeedChanceAbAttrParams extends AbAttrBaseParams { - /** Holds whether the speed check is bypassed after ability application */ - bypass: BooleanHolder; -} - /** * If a Pokémon with this Ability selects a damaging move, it has a 30% chance of going first in its priority bracket. If the Ability activates, this is announced at the start of the turn (after move selection). * @sealed @@ -6048,26 +6053,28 @@ export class BypassSpeedChanceAbAttr extends AbAttr { this.chance = chance; } - override canApply({ bypass, simulated, pokemon }: BypassSpeedChanceAbAttrParams): boolean { + override canApply({ simulated, pokemon }: AbAttrBaseParams): boolean { // TODO: Consider whether we can move the simulated check to the `apply` method // May be difficult as we likely do not want to modify the randBattleSeed const turnCommand = globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]; - const isCommandFight = turnCommand?.command === Command.FIGHT; const move = turnCommand?.move?.move ? allMoves[turnCommand.move.move] : null; const isDamageMove = move?.category === MoveCategory.PHYSICAL || move?.category === MoveCategory.SPECIAL; return ( - !simulated && !bypass.value && pokemon.randBattleSeedInt(100) < this.chance && isCommandFight && isDamageMove + !simulated + && pokemon.randBattleSeedInt(100) < this.chance + && isDamageMove + && pokemon.canAddTag(BattlerTagType.BYPASS_SPEED) ); } /** * bypass move order in their priority bracket when pokemon choose damaging move */ - override apply({ bypass }: BypassSpeedChanceAbAttrParams): void { - bypass.value = true; + override apply({ pokemon }: AbAttrBaseParams): void { + pokemon.addTag(BattlerTagType.BYPASS_SPEED); } - override getTriggerMessage({ pokemon }: BypassSpeedChanceAbAttrParams, _abilityName: string): string { + override getTriggerMessage({ pokemon }: AbAttrBaseParams, _abilityName: string): string { return i18next.t("abilityTriggers:quickDraw", { pokemonName: getPokemonNameWithAffix(pokemon) }); } } @@ -6075,8 +6082,6 @@ export class BypassSpeedChanceAbAttr extends AbAttr { export interface PreventBypassSpeedChanceAbAttrParams extends AbAttrBaseParams { /** Holds whether the speed check is bypassed after ability application */ bypass: BooleanHolder; - /** Holds whether the Pokemon can check held items for Quick Claw's effects */ - canCheckHeldItems: BooleanHolder; } /** @@ -6103,9 +6108,8 @@ export class PreventBypassSpeedChanceAbAttr extends AbAttr { return isCommandFight && this.condition(pokemon, move!); } - override apply({ bypass, canCheckHeldItems }: PreventBypassSpeedChanceAbAttrParams): void { + override apply({ bypass }: PreventBypassSpeedChanceAbAttrParams): void { bypass.value = false; - canCheckHeldItems.value = false; } } @@ -6203,8 +6207,7 @@ class ForceSwitchOutHelper { if (switchOutTarget.hp > 0) { switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); - globalScene.phaseManager.prependNewToPhase( - "MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -6226,8 +6229,7 @@ class ForceSwitchOutHelper { const summonIndex = globalScene.currentBattle.trainer ? globalScene.currentBattle.trainer.getNextSummonIndex((switchOutTarget as EnemyPokemon).trainerSlot) : 0; - globalScene.phaseManager.prependNewToPhase( - "MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchSummonPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -7161,7 +7163,7 @@ export function initAbilities() { new Ability(AbilityId.ANALYTIC, 5) .attr(MovePowerBoostAbAttr, (user) => // Boost power if all other Pokemon have already moved (no other moves are slated to execute) - !globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.id !== user?.id), + !globalScene.phaseManager.hasPhaseOfType("MovePhase", phase => phase.pokemon.id !== user?.id), 1.3), new Ability(AbilityId.ILLUSION, 5) // The Pokemon generate an illusion if it's available diff --git a/src/data/abilities/apply-ab-attrs.ts b/src/data/abilities/apply-ab-attrs.ts index 58f63c5924a..23b16a4cac7 100644 --- a/src/data/abilities/apply-ab-attrs.ts +++ b/src/data/abilities/apply-ab-attrs.ts @@ -74,7 +74,6 @@ function applyAbAttrsInternal( for (const passive of [false, true]) { params.passive = passive; applySingleAbAttrs(attrType, params, gainedMidTurn, messages); - globalScene.phaseManager.clearPhaseQueueSplice(); } // We need to restore passive to its original state in the case that it was undefined on entry // this is necessary in case this method is called with an object that is reused. diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 7d78076e06b..fd64e271758 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -56,6 +56,7 @@ import { allMoves } from "#data/data-lists"; import { AbilityId } from "#enums/ability-id"; import { ArenaTagSide } from "#enums/arena-tag-side"; import { ArenaTagType } from "#enums/arena-tag-type"; +import type { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; import { HitResult } from "#enums/hit-result"; import { CommonAnim } from "#enums/move-anims-common"; @@ -1597,6 +1598,145 @@ export class SuppressAbilitiesTag extends SerializableArenaTag { } } +/** + * Interface containing data related to a queued healing effect from + * {@link https://bulbapedia.bulbagarden.net/wiki/Healing_Wish_(move) | Healing Wish} + * or {@link https://bulbapedia.bulbagarden.net/wiki/Lunar_Dance_(move) | Lunar Dance}. + */ +interface PendingHealEffect { + /** The {@linkcode Pokemon.id | PID} of the {@linkcode Pokemon} that created the effect. */ + readonly sourceId: number; + /** The {@linkcode MoveId} of the move that created the effect. */ + readonly moveId: MoveId; + /** If `true`, also restores the target's PP when the effect activates. */ + readonly restorePP: boolean; + /** The message to display when the effect activates */ + readonly healMessage: string; +} + +/** + * Arena tag to contain stored healing effects, namely from + * {@link https://bulbapedia.bulbagarden.net/wiki/Healing_Wish_(move) | Healing Wish} + * and {@link https://bulbapedia.bulbagarden.net/wiki/Lunar_Dance_(move) | Lunar Dance}. + * When a damaged Pokemon first enters the effect's {@linkcode BattlerIndex | field position}, + * their HP is fully restored, and they are cured of any non-volatile status condition. + * If the effect is from Lunar Dance, their PP is also restored. + */ +export class PendingHealTag extends SerializableArenaTag { + public readonly tagType = ArenaTagType.PENDING_HEAL; + /** All pending healing effects, organized by {@linkcode BattlerIndex} */ + public readonly pendingHeals: Partial> = {}; + + constructor() { + super(0); + } + + /** + * Adds a pending healing effect to the field. Effects under the same move *and* + * target index as an existing effect are ignored. + * @param targetIndex - The {@linkcode BattlerIndex} under which the effect applies + * @param healEffect - The {@linkcode PendingHealEffect | data} for the pending heal effect + */ + public queueHeal(targetIndex: BattlerIndex, healEffect: PendingHealEffect): void { + const existingHealEffects = this.pendingHeals[targetIndex]; + if (existingHealEffects) { + if (!existingHealEffects.some(he => he.moveId === healEffect.moveId)) { + existingHealEffects.push(healEffect); + } + } else { + this.pendingHeals[targetIndex] = [healEffect]; + } + } + + /** Removes default on-remove message */ + override onRemove(_arena: Arena): void {} + + /** This arena tag is removed at the end of the turn if no pending healing effects are on the field */ + override lapse(_arena: Arena): boolean { + for (const key in this.pendingHeals) { + if (this.pendingHeals[key].length > 0) { + return true; + } + } + return false; + } + + /** + * Applies a pending healing effect on the given target index. If an effect is found for + * the index, the Pokemon at that index is healed to full HP, is cured of any non-volatile status, + * and has its PP fully restored (if the effect is from Lunar Dance). + * @param arena - The {@linkcode Arena} containing this tag + * @param simulated - If `true`, suppresses changes to game state + * @param pokemon - The {@linkcode Pokemon} receiving the healing effect + * @returns `true` if the target Pokemon was healed by this effect + * @todo This should also be called when a Pokemon moves into a new position via Ally Switch + */ + override apply(arena: Arena, simulated: boolean, pokemon: Pokemon): boolean { + const targetIndex = pokemon.getBattlerIndex(); + const targetEffects = this.pendingHeals[targetIndex]; + + if (targetEffects == null || targetEffects.length === 0) { + return false; + } + + const healEffect = targetEffects.find(effect => this.canApply(effect, pokemon)); + + if (healEffect == null) { + return false; + } + + if (simulated) { + return true; + } + + const { sourceId, moveId, restorePP, healMessage } = healEffect; + const sourcePokemon = globalScene.getPokemonById(sourceId); + if (!sourcePokemon) { + console.warn(`Source of pending ${allMoves[moveId].name} effect is undefined!`); + targetEffects.splice(targetEffects.indexOf(healEffect), 1); + // Re-evaluate after the invalid heal effect is removed + return this.apply(arena, simulated, pokemon); + } + + globalScene.phaseManager.unshiftNew( + "PokemonHealPhase", + targetIndex, + pokemon.getMaxHp(), + healMessage, + true, + false, + false, + true, + false, + restorePP, + ); + + targetEffects.splice(targetEffects.indexOf(healEffect), 1); + + return healEffect != null; + } + + /** + * Determines if the given {@linkcode PendingHealEffect} can immediately heal + * the given target {@linkcode Pokemon}. + * @param healEffect - The {@linkcode PendingHealEffect} to evaluate + * @param pokemon - The {@linkcode Pokemon} to evaluate against + * @returns `true` if the Pokemon can be healed by the effect + */ + private canApply(healEffect: PendingHealEffect, pokemon: Pokemon): boolean { + return ( + !pokemon.isFullHp() + || pokemon.status != null + || (healEffect.restorePP && pokemon.getMoveset().some(mv => mv.ppUsed > 0)) + ); + } + + override loadTag(source: BaseArenaTag & Pick): void { + super.loadTag(source); + (this as Mutable).pendingHeals = source.pendingHeals; + } +} + // TODO: swap `sourceMove` and `sourceId` and make `sourceMove` an optional parameter export function getArenaTag( tagType: ArenaTagType, @@ -1660,6 +1800,8 @@ export function getArenaTag( return new FairyLockTag(turnCount, sourceId); case ArenaTagType.NEUTRALIZING_GAS: return new SuppressAbilitiesTag(sourceId); + case ArenaTagType.PENDING_HEAL: + return new PendingHealTag(); default: return null; } @@ -1708,5 +1850,6 @@ export type ArenaTagTypeMap = { [ArenaTagType.GRASS_WATER_PLEDGE]: GrassWaterPledgeTag; [ArenaTagType.FAIRY_LOCK]: FairyLockTag; [ArenaTagType.NEUTRALIZING_GAS]: SuppressAbilitiesTag; + [ArenaTagType.PENDING_HEAL]: PendingHealTag; [ArenaTagType.NONE]: NoneTag; }; diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index b6c3cf2b5a6..8abd98f4683 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -606,17 +606,7 @@ export class ShellTrapTag extends BattlerTag { // Trap should only be triggered by opponent's Physical moves if (phaseData?.move.category === MoveCategory.PHYSICAL && pokemon.isOpponent(phaseData.attacker)) { - const shellTrapPhaseIndex = globalScene.phaseManager.phaseQueue.findIndex( - phase => phase.is("MovePhase") && phase.pokemon === pokemon, - ); - const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase")); - - // Only shift MovePhase timing if it's not already next up - if (shellTrapPhaseIndex !== -1 && shellTrapPhaseIndex !== firstMovePhaseIndex) { - const shellTrapMovePhase = globalScene.phaseManager.phaseQueue.splice(shellTrapPhaseIndex, 1)[0]; - globalScene.phaseManager.prependToPhase(shellTrapMovePhase, "MovePhase"); - } - + globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === pokemon); this.activated = true; } @@ -1279,22 +1269,9 @@ export class EncoreTag extends MoveRestrictionBattlerTag { }), ); - const movePhase = globalScene.phaseManager.findPhase(m => m.is("MovePhase") && m.pokemon === pokemon); - if (movePhase) { - const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId); - if (movesetMove) { - const lastMove = pokemon.getLastXMoves(1)[0]; - globalScene.phaseManager.tryReplacePhase( - m => m.is("MovePhase") && m.pokemon === pokemon, - globalScene.phaseManager.create( - "MovePhase", - pokemon, - lastMove.targets ?? [], - movesetMove, - MoveUseMode.NORMAL, - ), - ); - } + const movesetMove = pokemon.getMoveset().find(m => m.moveId === this.moveId); + if (movesetMove) { + globalScene.phaseManager.changePhaseMove((phase: MovePhase) => phase.pokemon === pokemon, movesetMove); } } @@ -3578,6 +3555,25 @@ export class GrudgeTag extends SerializableBattlerTag { } } +/** + * Tag to allow the affected Pokemon's move to go first in its priority bracket. + * Used for {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Draw_(Ability) | Quick Draw} + * and {@link https://bulbapedia.bulbagarden.net/wiki/Quick_Claw | Quick Claw}. + */ +export class BypassSpeedTag extends BattlerTag { + public override readonly tagType = BattlerTagType.BYPASS_SPEED; + + constructor() { + super(BattlerTagType.BYPASS_SPEED, BattlerTagLapseType.TURN_END, 1); + } + + override canAdd(pokemon: Pokemon): boolean { + const bypass = new BooleanHolder(true); + applyAbAttrs("PreventBypassSpeedChanceAbAttr", { pokemon, bypass }); + return bypass.value; + } +} + /** * Tag used to heal the user of Psycho Shift of its status effect if Psycho Shift succeeds in transferring its status effect to the target Pokemon */ @@ -3863,6 +3859,8 @@ export function getBattlerTag( return new MagicCoatTag(); case BattlerTagType.SUPREME_OVERLORD: return new SupremeOverlordTag(); + case BattlerTagType.BYPASS_SPEED: + return new BypassSpeedTag(); } } @@ -3998,4 +3996,5 @@ export type BattlerTagTypeMap = { [BattlerTagType.PSYCHO_SHIFT]: PsychoShiftTag; [BattlerTagType.MAGIC_COAT]: MagicCoatTag; [BattlerTagType.SUPREME_OVERLORD]: SupremeOverlordTag; + [BattlerTagType.BYPASS_SPEED]: BypassSpeedTag; }; diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index 5f49b9adbb0..776dff1bf46 100644 --- a/src/data/daily-run.ts +++ b/src/data/daily-run.ts @@ -6,7 +6,7 @@ import { PokemonSpecies } from "#data/pokemon-species"; import { BiomeId } from "#enums/biome-id"; import { PartyMemberStrength } from "#enums/party-member-strength"; import { SpeciesId } from "#enums/species-id"; -import type { Starter } from "#ui/starter-select-ui-handler"; +import type { Starter } from "#types/save-data"; import { randSeedGauss, randSeedInt, randSeedItem } from "#utils/common"; import { getEnumValues } from "#utils/enums"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; @@ -66,8 +66,11 @@ function getDailyRunStarter(starterSpeciesForm: PokemonSpeciesForm, startingLeve const formIndex = starterSpeciesForm instanceof PokemonSpecies ? undefined : starterSpeciesForm.formIndex; const pokemon = globalScene.addPlayerPokemon(starterSpecies, startingLevel, undefined, formIndex); const starter: Starter = { - species: starterSpecies, - dexAttr: pokemon.getDexAttr(), + speciesId: starterSpecies.speciesId, + shiny: pokemon.shiny, + variant: pokemon.variant, + formIndex: pokemon.formIndex, + ivs: pokemon.ivs, abilityIndex: pokemon.abilityIndex, passive: false, nature: pokemon.getNature(), diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 0fdb0d01e43..075876d8ddd 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -6,7 +6,7 @@ import { loggedInUser } from "#app/account"; import type { GameMode } from "#app/game-mode"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import type { EntryHazardTag } from "#data/arena-tag"; +import type { EntryHazardTag, PendingHealTag } from "#data/arena-tag"; import { WeakenMoveTypeTag } from "#data/arena-tag"; import { MoveChargeAnim } from "#data/battle-anims"; import { @@ -81,10 +81,8 @@ import { applyMoveAttrs } from "#moves/apply-attrs"; import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSketchMoves, invalidSleepTalkMoves } from "#moves/invalid-moves"; import { frenzyMissFunc, getMoveTargets } from "#moves/move-utils"; import { PokemonMove } from "#moves/pokemon-move"; -import { MoveEndPhase } from "#phases/move-end-phase"; import { MovePhase } from "#phases/move-phase"; import { PokemonHealPhase } from "#phases/pokemon-heal-phase"; -import { SwitchSummonPhase } from "#phases/switch-summon-phase"; import type { AttackMoveResult } from "#types/attack-move-result"; import type { Localizable } from "#types/locales"; import type { ChargingMove, MoveAttrMap, MoveAttrString, MoveClassMap, MoveKindString, MoveMessageFunc } from "#types/move-types"; @@ -94,6 +92,7 @@ import { getEnumValues } from "#utils/enums"; import { toCamelCase, toTitleCase } from "#utils/strings"; import i18next from "i18next"; import { applyChallenges } from "#utils/challenge-utils"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import type { AbstractConstructor } from "#types/type-helpers"; /** @@ -891,6 +890,10 @@ export abstract class Move implements Localizable { applyMoveAttrs("IncrementMovePriorityAttr", user, null, this, priority); applyAbAttrs("ChangeMovePriorityAbAttr", {pokemon: user, simulated, move: this, priority}); + if (user.getTag(BattlerTagType.BYPASS_SPEED)) { + priority.value += 0.2; + } + return priority.value; } @@ -2150,24 +2153,15 @@ export class SacrificialFullRestoreAttr extends SacrificialAttr { return false; } - // We don't know which party member will be chosen, so pick the highest max HP in the party - const party = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty(); - const maxPartyMemberHp = party.map(p => p.getMaxHp()).reduce((maxHp: number, hp: number) => Math.max(hp, maxHp), 0); - - const pm = globalScene.phaseManager; - - pm.pushPhase( - pm.create("PokemonHealPhase", - user.getBattlerIndex(), - maxPartyMemberHp, - i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }), - true, - false, - false, - true, - false, - this.restorePP), - true); + // Add a tag to the field if it doesn't already exist, then queue a delayed healing effect in the user's current slot. + globalScene.arena.addTag(ArenaTagType.PENDING_HEAL, 0, move.id, user.id); // Arguments after first go completely unused + const tag = globalScene.arena.getTag(ArenaTagType.PENDING_HEAL) as PendingHealTag; + tag.queueHeal(user.getBattlerIndex(), { + sourceId: user.id, + moveId: move.id, + restorePP: this.restorePP, + healMessage: i18next.t(this.moveMessage, { pokemonName: getPokemonNameWithAffix(user) }), + }); return true; } @@ -3307,7 +3301,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr { const overridden = args[0] as BooleanHolder; - const allyMovePhase = globalScene.phaseManager.findPhase((phase) => phase.is("MovePhase") && phase.pokemon.isPlayer() === user.isPlayer()); + const allyMovePhase = globalScene.phaseManager.getMovePhase((phase) => phase.pokemon.isPlayer() === user.isPlayer()); if (allyMovePhase) { const allyMove = allyMovePhase.move.getMove(); if (allyMove !== move && allyMove.hasAttr("AwaitCombinedPledgeAttr")) { @@ -3320,11 +3314,7 @@ export class AwaitCombinedPledgeAttr extends OverrideMoveEffectAttr { })); // Move the ally's MovePhase (if needed) so that the ally moves next - const allyMovePhaseIndex = globalScene.phaseManager.phaseQueue.indexOf(allyMovePhase); - const firstMovePhaseIndex = globalScene.phaseManager.phaseQueue.findIndex((phase) => phase.is("MovePhase")); - if (allyMovePhaseIndex !== firstMovePhaseIndex) { - globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(allyMovePhaseIndex, 1)[0], "MovePhase"); - } + globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === user.getAlly()); overridden.value = true; return true; @@ -4559,28 +4549,7 @@ export class LastMoveDoublePowerAttr extends VariablePowerAttr { */ apply(user: Pokemon, _target: Pokemon, _move: Move, args: any[]): boolean { const power = args[0] as NumberHolder; - const enemy = user.getOpponent(0); - const pokemonActed: Pokemon[] = []; - - if (enemy?.turnData.acted) { - pokemonActed.push(enemy); - } - - if (globalScene.currentBattle.double) { - const userAlly = user.getAlly(); - const enemyAlly = enemy?.getAlly(); - - if (userAlly?.turnData.acted) { - pokemonActed.push(userAlly); - } - if (enemyAlly?.turnData.acted) { - pokemonActed.push(enemyAlly); - } - } - - pokemonActed.sort((a, b) => b.turnData.order - a.turnData.order); - - for (const p of pokemonActed) { + for (const p of globalScene.phaseManager.dynamicQueueManager.getLastTurnOrder().slice(0, -1).reverse()) { const [ lastMove ] = p.getLastXMoves(1); if (lastMove.result !== MoveResult.FAIL) { if ((lastMove.result === MoveResult.SUCCESS) && (lastMove.move === this.move)) { @@ -4662,20 +4631,13 @@ export class CueNextRoundAttr extends MoveEffectAttr { } override apply(user: Pokemon, target: Pokemon, move: Move, args?: any[]): boolean { - const nextRoundPhase = globalScene.phaseManager.findPhase(phase => - phase.is("MovePhase") && phase.move.moveId === MoveId.ROUND - ); + const nextRoundPhase = globalScene.phaseManager.getMovePhase(phase => phase.move.moveId === MoveId.ROUND); if (!nextRoundPhase) { return false; } - // Update the phase queue so that the next Pokemon using Round moves next - const nextRoundIndex = globalScene.phaseManager.phaseQueue.indexOf(nextRoundPhase); - const nextMoveIndex = globalScene.phaseManager.phaseQueue.findIndex(phase => phase.is("MovePhase")); - if (nextRoundIndex !== nextMoveIndex) { - globalScene.phaseManager.prependToPhase(globalScene.phaseManager.phaseQueue.splice(nextRoundIndex, 1)[0], "MovePhase"); - } + globalScene.phaseManager.forceMoveNext(phase => phase.move.moveId === MoveId.ROUND); // Mark the corresponding Pokemon as having "joined the Round" (for doubling power later) nextRoundPhase.pokemon.turnData.joinedRound = true; @@ -6300,11 +6262,11 @@ export class RevivalBlessingAttr extends MoveEffectAttr { // Handle cases where revived pokemon needs to get switched in on same turn if (allyPokemon.isFainted() || allyPokemon === pokemon) { // Enemy switch phase should be removed and replaced with the revived pkmn switching in - globalScene.phaseManager.tryRemovePhase((phase: SwitchSummonPhase) => phase.is("SwitchSummonPhase") && phase.getPokemon() === pokemon); + globalScene.phaseManager.tryRemovePhase("SwitchSummonPhase", phase => phase.getFieldIndex() === slotIndex); // If the pokemon being revived was alive earlier in the turn, cancel its move // (revived pokemon can't move in the turn they're brought back) // TODO: might make sense to move this to `FaintPhase` after checking for Rev Seed (rather than handling it in the move) - globalScene.phaseManager.findPhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel(); + globalScene.phaseManager.getMovePhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel(); if (user.fieldPosition === FieldPosition.CENTER) { user.setFieldPosition(FieldPosition.LEFT); } @@ -6385,8 +6347,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { if (this.switchType === SwitchType.FORCE_SWITCH) { switchOutTarget.leaveField(true); const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)]; - globalScene.phaseManager.prependNewToPhase( - "MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchSummonPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -6396,7 +6357,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { ); } else { switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); - globalScene.phaseManager.prependNewToPhase("MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -6425,7 +6386,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { if (this.switchType === SwitchType.FORCE_SWITCH) { switchOutTarget.leaveField(true); const slotIndex = eligibleNewIndices[user.randBattleSeedInt(eligibleNewIndices.length)]; - globalScene.phaseManager.prependNewToPhase("MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchSummonPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -6435,7 +6396,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { ); } else { switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); - globalScene.phaseManager.prependNewToPhase("MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchSummonPhase", this.switchType, switchOutTarget.getFieldIndex(), @@ -6866,7 +6827,7 @@ class CallMoveAttr extends OverrideMoveEffectAttr { : moveTargets.targets[user.randBattleSeedInt(moveTargets.targets.length)]]; globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", move.id); - globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP); + globalScene.phaseManager.unshiftNew("MovePhase", user, targets, new PokemonMove(move.id), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST); return true; } } @@ -7098,7 +7059,7 @@ export class NaturePowerAttr extends OverrideMoveEffectAttr { // Load the move's animation if we didn't already and unshift a new usage phase globalScene.phaseManager.unshiftNew("LoadMoveAnimPhase", moveId); - globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP); + globalScene.phaseManager.unshiftNew("MovePhase", user, [ target.getBattlerIndex() ], new PokemonMove(moveId), MoveUseMode.FOLLOW_UP, MovePhaseTimingModifier.FIRST); return true; } } @@ -7182,7 +7143,7 @@ export class RepeatMoveAttr extends MoveEffectAttr { targetPokemonName: getPokemonNameWithAffix(target) })); target.turnData.extraTurns++; - globalScene.phaseManager.appendNewToPhase("MoveEndPhase", "MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL); + globalScene.phaseManager.unshiftNew("MovePhase", target, moveTargets, movesetMove, MoveUseMode.NORMAL, MovePhaseTimingModifier.FIRST); return true; } @@ -7955,12 +7916,7 @@ export class AfterYouAttr extends MoveEffectAttr { */ override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:afterYou", { targetName: getPokemonNameWithAffix(target) })); - - // Will find next acting phase of the targeted pokémon, delete it and queue it right after us. - const targetNextPhase = globalScene.phaseManager.findPhase(phase => phase.pokemon === target); - if (targetNextPhase && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) { - globalScene.phaseManager.prependToPhase(targetNextPhase, "MovePhase"); - } + globalScene.phaseManager.forceMoveNext((phase: MovePhase) => phase.pokemon === target); return true; } @@ -7983,45 +7939,11 @@ export class ForceLastAttr extends MoveEffectAttr { override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { globalScene.phaseManager.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) })); - // TODO: Refactor this to be more readable and less janky - const targetMovePhase = globalScene.phaseManager.findPhase((phase) => phase.pokemon === target); - if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.phaseManager.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) { - // Finding the phase to insert the move in front of - - // Either the end of the turn or in front of another, slower move which has also been forced last - const prependPhase = globalScene.phaseManager.findPhase((phase) => - [ MovePhase, MoveEndPhase ].every(cls => !(phase instanceof cls)) - || (phase.is("MovePhase")) && phaseForcedSlower(phase, target, !!globalScene.arena.getTag(ArenaTagType.TRICK_ROOM)) - ); - if (prependPhase) { - globalScene.phaseManager.phaseQueue.splice( - globalScene.phaseManager.phaseQueue.indexOf(prependPhase), - 0, - globalScene.phaseManager.create("MovePhase", target, [ ...targetMovePhase.targets ], targetMovePhase.move, targetMovePhase.useMode, true) - ); - } - } + globalScene.phaseManager.forceMoveLast((phase: MovePhase) => phase.pokemon === target); return true; } } -/** - * Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target}. - - * TODO: - - Make this a class method - - Make this look at speed order from TurnStartPhase -*/ -const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => { - let slower: boolean; - // quashed pokemon still have speed ties - if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) { - slower = !!target.randBattleSeedInt(2); - } else { - slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD); - } - return phase.isForcedLast() && slower; -}; - const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY); const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune(); @@ -8045,7 +7967,7 @@ const userSleptOrComatoseCondition: MoveConditionFunc = (user) => user.status?.e const targetSleptOrComatoseCondition: MoveConditionFunc = (_user: Pokemon, target: Pokemon, _move: Move) => target.status?.effect === StatusEffect.SLEEP || target.hasAbility(AbilityId.COMATOSE); -const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.findPhase(phase => phase.is("MovePhase")) !== undefined; +const failIfLastCondition: MoveConditionFunc = () => globalScene.phaseManager.hasPhaseOfType("MovePhase"); const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => { const party: Pokemon[] = user.isPlayer() ? globalScene.getPlayerParty() : globalScene.getEnemyParty(); diff --git a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts index d883fdbb567..f2363ade500 100644 --- a/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts +++ b/src/data/mystery-encounters/encounters/fun-and-games-encounter.ts @@ -414,7 +414,7 @@ function summonPlayerPokemonAnimation(pokemon: PlayerPokemon): Promise { pokemon.resetTurnData(); globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); - globalScene.phaseManager.pushNew("PostSummonPhase", pokemon.getBattlerIndex()); + globalScene.phaseManager.unshiftNew("PostSummonPhase", pokemon.getBattlerIndex()); resolve(); }); }, diff --git a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts index b5084743613..67e778d8c4b 100644 --- a/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-expert-pokemon-breeder-encounter.ts @@ -669,7 +669,6 @@ function onGameOver() { // Clear any leftover battle phases globalScene.phaseManager.clearPhaseQueue(); - globalScene.phaseManager.clearPhaseQueueSplice(); // Return enemy Pokemon const pokemon = globalScene.getEnemyPokemon(); diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 86cd3fa3a32..0ba0dec896a 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -738,7 +738,7 @@ export function setEncounterRewards( if (customShopRewards) { globalScene.phaseManager.unshiftNew("SelectModifierPhase", 0, undefined, customShopRewards); } else { - globalScene.phaseManager.tryRemovePhase(p => p.is("MysteryEncounterRewardsPhase")); + globalScene.phaseManager.removeAllPhasesOfType("MysteryEncounterRewardsPhase"); } if (eggRewards) { @@ -812,8 +812,7 @@ export function leaveEncounterWithoutBattle( encounterMode: MysteryEncounterMode = MysteryEncounterMode.NO_BATTLE, ) { globalScene.currentBattle.mysteryEncounter!.encounterMode = encounterMode; - globalScene.phaseManager.clearPhaseQueue(); - globalScene.phaseManager.clearPhaseQueueSplice(); + globalScene.phaseManager.clearPhaseQueue(true); handleMysteryEncounterVictory(addHealPhase); } @@ -826,7 +825,7 @@ export function handleMysteryEncounterVictory(addHealPhase = false, doNotContinu const allowedPkm = globalScene.getPlayerParty().filter(pkm => pkm.isAllowedInBattle()); if (allowedPkm.length === 0) { - globalScene.phaseManager.clearPhaseQueue(); + globalScene.phaseManager.clearPhaseQueue(true); globalScene.phaseManager.unshiftNew("GameOverPhase"); return; } @@ -869,7 +868,7 @@ export function handleMysteryEncounterBattleFailed(addHealPhase = false, doNotCo const allowedPkm = globalScene.getPlayerParty().filter(pkm => pkm.isAllowedInBattle()); if (allowedPkm.length === 0) { - globalScene.phaseManager.clearPhaseQueue(); + globalScene.phaseManager.clearPhaseQueue(true); globalScene.phaseManager.unshiftNew("GameOverPhase"); return; } diff --git a/src/data/phase-priority-queue.ts b/src/data/phase-priority-queue.ts deleted file mode 100644 index 2c83348cc7b..00000000000 --- a/src/data/phase-priority-queue.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { globalScene } from "#app/global-scene"; -import type { Phase } from "#app/phase"; -import { TrickRoomTag } from "#data/arena-tag"; -import { DynamicPhaseType } from "#enums/dynamic-phase-type"; -import { Stat } from "#enums/stat"; -import { ActivatePriorityQueuePhase } from "#phases/activate-priority-queue-phase"; -import { PostSummonActivateAbilityPhase } from "#phases/post-summon-activate-ability-phase"; -import type { PostSummonPhase } from "#phases/post-summon-phase"; -import { BooleanHolder } from "#utils/common"; - -/** - * Stores a list of {@linkcode Phase}s - * - * Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder} - */ -export abstract class PhasePriorityQueue { - protected abstract queue: Phase[]; - - /** - * Sorts the elements in the queue - */ - public abstract reorder(): void; - - /** - * Calls {@linkcode reorder} and shifts the queue - * @returns The front element of the queue after sorting - */ - public pop(): Phase | undefined { - this.reorder(); - return this.queue.shift(); - } - - /** - * Adds a phase to the queue - * @param phase The phase to add - */ - public push(phase: Phase): void { - this.queue.push(phase); - } - - /** - * Removes all phases from the queue - */ - public clear(): void { - this.queue.splice(0, this.queue.length); - } - - /** - * Attempt to remove one or more Phases from the current queue. - * @param phaseFilter - The function to select phases for removal - * @param removeCount - The maximum number of phases to remove, or `all` to remove all matching phases; - * default `1` - * @returns The number of successfully removed phases - * @todo Remove this eventually once the patchwork bug this is used for is fixed - */ - public tryRemovePhase(phaseFilter: (phase: Phase) => boolean, removeCount: number | "all" = 1): number { - if (removeCount === "all") { - removeCount = this.queue.length; - } else if (removeCount < 1) { - return 0; - } - let numRemoved = 0; - - do { - const phaseIndex = this.queue.findIndex(phaseFilter); - if (phaseIndex === -1) { - break; - } - this.queue.splice(phaseIndex, 1); - numRemoved++; - } while (numRemoved < removeCount && this.queue.length > 0); - - return numRemoved; - } -} - -/** - * Priority Queue for {@linkcode PostSummonPhase} and {@linkcode PostSummonActivateAbilityPhase} - * - * Orders phases first by ability priority, then by the {@linkcode Pokemon}'s effective speed - */ -export class PostSummonPhasePriorityQueue extends PhasePriorityQueue { - protected override queue: PostSummonPhase[] = []; - - public override reorder(): void { - this.queue.sort((phaseA: PostSummonPhase, phaseB: PostSummonPhase) => { - if (phaseA.getPriority() === phaseB.getPriority()) { - return ( - (phaseB.getPokemon().getEffectiveStat(Stat.SPD) - phaseA.getPokemon().getEffectiveStat(Stat.SPD)) - * (isTrickRoom() ? -1 : 1) - ); - } - - return phaseB.getPriority() - phaseA.getPriority(); - }); - } - - public override push(phase: PostSummonPhase): void { - super.push(phase); - this.queueAbilityPhase(phase); - } - - /** - * Queues all necessary {@linkcode PostSummonActivateAbilityPhase}s for each pushed {@linkcode PostSummonPhase} - * @param phase The {@linkcode PostSummonPhase} that was pushed onto the queue - */ - private queueAbilityPhase(phase: PostSummonPhase): void { - const phasePokemon = phase.getPokemon(); - - phasePokemon.getAbilityPriorities().forEach((priority, idx) => { - this.queue.push(new PostSummonActivateAbilityPhase(phasePokemon.getBattlerIndex(), priority, !!idx)); - globalScene.phaseManager.appendToPhase( - new ActivatePriorityQueuePhase(DynamicPhaseType.POST_SUMMON), - "ActivatePriorityQueuePhase", - (p: ActivatePriorityQueuePhase) => p.getType() === DynamicPhaseType.POST_SUMMON, - ); - }); - } -} - -function isTrickRoom(): boolean { - const speedReversed = new BooleanHolder(false); - globalScene.arena.applyTags(TrickRoomTag, false, speedReversed); - return speedReversed.value; -} diff --git a/src/dynamic-queue-manager.ts b/src/dynamic-queue-manager.ts new file mode 100644 index 00000000000..7c65a79d743 --- /dev/null +++ b/src/dynamic-queue-manager.ts @@ -0,0 +1,187 @@ +import type { DynamicPhase, PhaseConditionFunc, PhaseString } from "#app/@types/phase-types"; +import type { PokemonMove } from "#app/data/moves/pokemon-move"; +import type { Pokemon } from "#app/field/pokemon"; +import type { Phase } from "#app/phase"; +import type { MovePhase } from "#app/phases/move-phase"; +import { MovePhasePriorityQueue } from "#app/queues/move-phase-priority-queue"; +import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; +import { PostSummonPhasePriorityQueue } from "#app/queues/post-summon-phase-priority-queue"; +import type { PriorityQueue } from "#app/queues/priority-queue"; +import type { BattlerIndex } from "#enums/battler-index"; +import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; + +// TODO: might be easier to define which phases should be dynamic instead +/** All phases which have defined a `getPokemon` method but should not be sorted dynamically */ +const nonDynamicPokemonPhases: readonly PhaseString[] = [ + "SummonPhase", + "CommandPhase", + "LearnMovePhase", + "MoveEffectPhase", + "MoveEndPhase", + "FaintPhase", + "DamageAnimPhase", + "VictoryPhase", + "PokemonHealPhase", + "WeatherEffectPhase", + "ShowAbilityPhase", + "HideAbilityPhase", + "ExpPhase", + "ShowPartyExpBarPhase", + "HidePartyExpBarPhase", +] as const; + +/** + * The dynamic queue manager holds priority queues for phases which are queued as dynamic. + * + * Dynamic phases are generally those which hold a pokemon and are unshifted, not pushed. \ + * Queues work by sorting their entries in speed order (and possibly with more complex ordering) before each time a phase is popped. + * + * As the holder, this structure is also used to access and modify queued phases. + * This is mostly used in redirection, cancellation, etc. of {@linkcode MovePhase}s. + */ +export class DynamicQueueManager { + /** Maps phase types to their corresponding queues */ + private readonly dynamicPhaseMap: Map>; + + constructor() { + this.dynamicPhaseMap = new Map(); + // PostSummon and Move phases have specialized queues + this.dynamicPhaseMap.set("PostSummonPhase", new PostSummonPhasePriorityQueue()); + this.dynamicPhaseMap.set("MovePhase", new MovePhasePriorityQueue()); + } + + /** Removes all phases from the manager */ + public clearQueues(): void { + for (const queue of this.dynamicPhaseMap.values()) { + queue.clear(); + } + } + + /** + * Adds a new phase to the manager and creates the priority queue for it if one does not exist. + * @param phase - The {@linkcode Phase} to add + * @returns `true` if the phase was added, or `false` if it is not dynamic + */ + public queueDynamicPhase(phase: T): boolean { + if (!this.isDynamicPhase(phase)) { + return false; + } + + if (!this.dynamicPhaseMap.has(phase.phaseName)) { + // TS can't figure out that T is dynamic at this point, but it does know that `typeof phase` is + this.dynamicPhaseMap.set(phase.phaseName, new PokemonPhasePriorityQueue()); + } + this.dynamicPhaseMap.get(phase.phaseName)?.push(phase); + return true; + } + + /** + * Returns the highest-priority (generally by speed) {@linkcode Phase} of the specified type + * @param type - The {@linkcode PhaseString | type} to pop + * @returns The popped {@linkcode Phase}, or `undefined` if none of the specified type exist + */ + public popNextPhase(type: PhaseString): Phase | undefined { + return this.dynamicPhaseMap.get(type)?.pop(); + } + + /** + * Determines if there is a queued dynamic {@linkcode Phase} meeting the conditions + * @param type - The {@linkcode PhaseString | type} of phase to search for + * @param condition - An optional {@linkcode PhaseConditionFunc} to add conditions to the search + * @returns Whether a matching phase exists + */ + public exists(type: T, condition?: PhaseConditionFunc): boolean { + return !!this.dynamicPhaseMap.get(type)?.has(condition); + } + + /** + * Finds and removes a single queued {@linkcode Phase} + * @param type - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase + * @returns Whether a removal occurred + */ + public removePhase(type: T, condition?: PhaseConditionFunc): boolean { + return !!this.dynamicPhaseMap.get(type)?.remove(condition); + } + + /** + * Sets the timing modifier of a move (i.e. to force it first or last) + * @param condition - A {@linkcode PhaseConditionFunc} to specify conditions for the move + * @param modifier - The {@linkcode MovePhaseTimingModifier} to switch the move to + */ + public setMoveTimingModifier(condition: PhaseConditionFunc<"MovePhase">, modifier: MovePhaseTimingModifier): void { + this.getMovePhaseQueue().setTimingModifier(condition, modifier); + } + + /** + * Finds the {@linkcode MovePhase} meeting the condition and changes its move + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + * @param move - The {@linkcode PokemonMove | move} to use in replacement + */ + public setMoveForPhase(condition: PhaseConditionFunc<"MovePhase">, move: PokemonMove): void { + this.getMovePhaseQueue().setMoveForPhase(condition, move); + } + + /** + * Redirects moves which were targeted at a {@linkcode Pokemon} that has been removed + * @param removedPokemon - The removed {@linkcode Pokemon} + * @param allyPokemon - The ally of the removed pokemon + */ + public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void { + this.getMovePhaseQueue().redirectMoves(removedPokemon, allyPokemon); + } + + /** + * Finds a {@linkcode MovePhase} meeting the condition + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + * @returns The MovePhase, or `undefined` if it does not exist + */ + public getMovePhase(condition: PhaseConditionFunc<"MovePhase">): MovePhase | undefined { + return this.getMovePhaseQueue().find(condition); + } + + /** + * Finds and cancels a {@linkcode MovePhase} meeting the condition + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + */ + public cancelMovePhase(condition: PhaseConditionFunc<"MovePhase">): void { + this.getMovePhaseQueue().cancelMove(condition); + } + + /** + * Sets the move order to a static array rather than a dynamic queue + * @param order - The order of {@linkcode BattlerIndex}s + */ + public setMoveOrder(order: BattlerIndex[]): void { + this.getMovePhaseQueue().setMoveOrder(order); + } + + /** + * @returns An in-order array of {@linkcode Pokemon}, representing the turn order as played out in the most recent turn + */ + public getLastTurnOrder(): Pokemon[] { + return this.getMovePhaseQueue().getTurnOrder(); + } + + /** Clears the stored `Move` turn order */ + public clearLastTurnOrder(): void { + this.getMovePhaseQueue().clearTurnOrder(); + } + + /** Internal helper to get the {@linkcode MovePhasePriorityQueue} */ + private getMovePhaseQueue(): MovePhasePriorityQueue { + return this.dynamicPhaseMap.get("MovePhase") as MovePhasePriorityQueue; + } + + /** + * Internal helper to determine if a phase is dynamic. + * @param phase - The {@linkcode Phase} to check + * @returns Whether `phase` is dynamic + * @privateRemarks + * Currently, this checks that `phase` has a `getPokemon` method + * and is not blacklisted in `nonDynamicPokemonPhases`. + */ + private isDynamicPhase(phase: Phase): phase is DynamicPhase { + return typeof (phase as any).getPokemon === "function" && !nonDynamicPokemonPhases.includes(phase.phaseName); + } +} diff --git a/src/enums/arena-tag-side.ts b/src/enums/arena-tag-side.ts index 5f25a74ab36..50741751fbb 100644 --- a/src/enums/arena-tag-side.ts +++ b/src/enums/arena-tag-side.ts @@ -1,3 +1,4 @@ +// TODO: rename to something else (this isn't used only for arena tags) export enum ArenaTagSide { BOTH, PLAYER, diff --git a/src/enums/arena-tag-type.ts b/src/enums/arena-tag-type.ts index 30f053b98bd..717845cf2d9 100644 --- a/src/enums/arena-tag-type.ts +++ b/src/enums/arena-tag-type.ts @@ -34,4 +34,5 @@ export enum ArenaTagType { GRASS_WATER_PLEDGE = "GRASS_WATER_PLEDGE", FAIRY_LOCK = "FAIRY_LOCK", NEUTRALIZING_GAS = "NEUTRALIZING_GAS", + PENDING_HEAL = "PENDING_HEAL", } diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 7956e506886..4f0ac491e8b 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -95,4 +95,5 @@ export enum BattlerTagType { POWDER = "POWDER", MAGIC_COAT = "MAGIC_COAT", SUPREME_OVERLORD = "SUPREME_OVERLORD", + BYPASS_SPEED = "BYPASS_SPEED", } diff --git a/src/enums/dynamic-phase-type.ts b/src/enums/dynamic-phase-type.ts deleted file mode 100644 index 3146b136dac..00000000000 --- a/src/enums/dynamic-phase-type.ts +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Enum representation of the phase types held by implementations of {@linkcode PhasePriorityQueue}. - */ -// TODO: We currently assume these are in order -export enum DynamicPhaseType { - POST_SUMMON, -} diff --git a/src/enums/move-phase-timing-modifier.ts b/src/enums/move-phase-timing-modifier.ts new file mode 100644 index 00000000000..a452d37e7ff --- /dev/null +++ b/src/enums/move-phase-timing-modifier.ts @@ -0,0 +1,16 @@ +import type { ObjectValues } from "#types/type-helpers"; + +/** + * Enum representing modifiers for the timing of MovePhases. + * + * @remarks + * This system is entirely independent of and takes precedence over move priority + */ +export const MovePhaseTimingModifier = Object.freeze({ + /** Used when moves go last regardless of speed and priority (i.e. Quash) */ + LAST: 0, + NORMAL: 1, + /** Used to trigger moves immediately (i.e. ones that were called through Instruct). */ + FIRST: 2, +}); +export type MovePhaseTimingModifier = ObjectValues; diff --git a/src/field/arena.ts b/src/field/arena.ts index 5ab50e540ee..3e214ff1ea7 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -371,9 +371,15 @@ export class Arena { /** * Function to trigger all weather based form changes + * @param source - The Pokemon causing the changes by removing itself from the field */ - triggerWeatherBasedFormChanges(): void { + triggerWeatherBasedFormChanges(source?: Pokemon): void { globalScene.getField(true).forEach(p => { + // TODO - This is a bandaid. Abilities leaving the field needs a better approach than + // calling this method for every switch out that happens + if (p === source) { + return; + } const isCastformWithForecast = p.hasAbility(AbilityId.FORECAST) && p.species.speciesId === SpeciesId.CASTFORM; const isCherrimWithFlowerGift = p.hasAbility(AbilityId.FLOWER_GIFT) && p.species.speciesId === SpeciesId.CHERRIM; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index d48f4ae8ad2..cbad6caaafa 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -141,6 +141,7 @@ import type { PokemonData } from "#system/pokemon-data"; import { RibbonData } from "#system/ribbons/ribbon-data"; import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods"; import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#types/ability-types"; +import type { getAttackDamageParams, getBaseDamageParams } from "#types/damage-params"; import type { DamageCalculationResult, DamageResult } from "#types/damage-result"; import type { IllusionData } from "#types/illusion-data"; import type { StarterDataEntry, StarterMoveset } from "#types/save-data"; @@ -169,6 +170,7 @@ import { rgbToHsv, toDmgValue, } from "#utils/common"; +import { calculateBossSegmentDamage } from "#utils/damage"; import { getEnumValues } from "#utils/enums"; import { getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; import { argbFromRgba, QuantizerCelebi, rgbaFromArgb } from "@material/material-color-utilities"; @@ -176,36 +178,6 @@ import i18next from "i18next"; import Phaser from "phaser"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; -/** Base typeclass for damage parameter methods, used for DRY */ -type damageParams = { - /** The attacking {@linkcode Pokemon} */ - source: Pokemon; - /** The move used in the attack */ - move: Move; - /** The move's {@linkcode MoveCategory} after variable-category effects are applied */ - moveCategory: MoveCategory; - /** If `true`, ignores this Pokemon's defensive ability effects */ - ignoreAbility?: boolean; - /** If `true`, ignores the attacking Pokemon's ability effects */ - ignoreSourceAbility?: boolean; - /** If `true`, ignores the ally Pokemon's ability effects */ - ignoreAllyAbility?: boolean; - /** If `true`, ignores the ability effects of the attacking pokemon's ally */ - ignoreSourceAllyAbility?: boolean; - /** If `true`, calculates damage for a critical hit */ - isCritical?: boolean; - /** If `true`, suppresses changes to game state during the calculation */ - simulated?: boolean; - /** If defined, used in place of calculated effectiveness values */ - effectiveness?: number; -}; - -/** Type for the parameters of {@linkcode Pokemon#getBaseDamage | getBaseDamage} */ -type getBaseDamageParams = Omit; - -/** Type for the parameters of {@linkcode Pokemon#getAttackDamage | getAttackDamage} */ -type getAttackDamageParams = Omit; - export abstract class Pokemon extends Phaser.GameObjects.Container { /** * This pokemon's {@link https://bulbapedia.bulbagarden.net/wiki/Personality_value | Personality value/PID}, @@ -242,20 +214,46 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @todo Make private */ public status: Status | null; + /** + * The Pokémon's current friendship value, ranging from 0 to 255. + * @see {@link https://bulbapedia.bulbagarden.net/wiki/Friendship} + */ public friendship: number; + /** + * The level at which this Pokémon was met + * @remarks + * Primarily used for displaying in the summary screen + */ public metLevel: number; + /** + * The ID of the biome this Pokémon was met in + * @remarks + * Primarily used for display in the summary screen. + */ public metBiome: BiomeId | -1; + // TODO: figure out why this is used and document it (seems only to be read for getting the Pokémon's egg moves) public metSpecies: SpeciesId; + /** The wave index at which this Pokémon was met/encountered */ public metWave: number; public luck: number; public pauseEvolutions: boolean; public pokerus: boolean; + /** + * Indicates whether this Pokémon has left or is about to leave the field + * @remarks + * When `true` on a Wild Pokemon, this indicates it is about to flee. + */ public switchOutStatus = false; public evoCounter: number; + /** The type this Pokémon turns into when Terastallized */ public teraType: PokemonType; + /** Whether this Pokémon is currently Terastallized */ public isTerastallized: boolean; + /** The set of Types that have been boosted by this Pokémon's Stellar Terastallization. */ public stellarTypesBoosted: PokemonType[]; + // TODO: Create a fusionData class / interface and move all fusion-related fields there, exposed via getters + /** If this Pokémon is a fusion, the species it is fused with; `null` if not a fusion */ public fusionSpecies: PokemonSpecies | null; public fusionFormIndex: number; public fusionAbilityIndex: number; @@ -287,11 +285,18 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** Used by Mystery Encounters to execute pokemon-specific logic (such as stat boosts) at start of battle */ public mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; + /** The position of this Pokémon on the field */ public fieldPosition: FieldPosition; public maskEnabled: boolean; public maskSprite: Phaser.GameObjects.Sprite | null; + /** + * The set of all TMs that have been used on this Pokémon + * + * @remarks + * Used to allow re-learning TM moves via, e.g., the Memory Mushroom + */ public usedTMs: MoveId[]; private shinySparkle: Phaser.GameObjects.Sprite; @@ -516,7 +521,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { abstract initBattleInfo(): void; - isOnField(): boolean { + public isOnField(): boolean { if (!globalScene) { return false; } @@ -568,7 +573,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.isAllowedInBattle() && (!onField || this.isOnField()); } - getDexAttr(): bigint { + public getDexAttr(): bigint { let ret = 0n; if (this.gender !== Gender.GENDERLESS) { ret |= this.gender !== Gender.FEMALE ? DexAttr.MALE : DexAttr.FEMALE; @@ -582,9 +587,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Sets the Pokemon's name. Only called when loading a Pokemon so this function needs to be called when * initializing hardcoded Pokemon or else it will not display the form index name properly. - * @returns n/a */ - generateName(): void { + public generateName(): void { if (!this.fusionSpecies) { this.name = this.species.getName(this.formIndex); return; @@ -838,11 +842,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Gracefully handle errors loading a variant sprite. Log if it fails and attempt to fall back on * non-experimental sprites before giving up. * - * @param cacheKey the cache key for the variant color sprite - * @param attemptedSpritePath the sprite path that failed to load - * @param useExpSprite was the attempted sprite experimental - * @param battleSpritePath the filename of the sprite - * @param optionalParams any additional params to log + * @param cacheKey - The cache key for the variant color sprite + * @param attemptedSpritePath - The sprite path that failed to load + * @param useExpSprite - Whether the attempted sprite was experimental + * @param battleSpritePath - The filename of the sprite + * @param optionalParams - Any additional params to log */ async fallbackVariantColor( cacheKey: string, @@ -908,6 +912,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.fusionSpecies.forms[this.fusionFormIndex].formKey; } + //#region Atlas and sprite ID methods // TODO: Add more documentation for all these attributes. // They may be all similar, but what each one actually _does_ is quite unclear at first glance @@ -1036,6 +1041,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { fusionVariant, ); } + //#endregion Atlas and sprite ID methods /** * Return this Pokemon's {@linkcode PokemonSpeciesForm | SpeciesForm}. @@ -1065,7 +1071,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * (such as by the effects of {@linkcode MoveId.TRANSFORM} or {@linkcode AbilityId.IMPOSTER}. * @returns Whether this Pokemon is currently transformed. */ - isTransformed(): boolean { + public isTransformed(): boolean { return this.summonData.speciesForm !== null; } @@ -1074,7 +1080,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @param target - The {@linkcode Pokemon} being transformed into * @returns Whether this Pokemon can transform into `target`. */ - canTransformInto(target: Pokemon): boolean { + public canTransformInto(target: Pokemon): boolean { return !( // Neither pokemon can be already transformed ( @@ -1095,7 +1101,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @param useIllusion - Whether to consider the species of this Pokemon's illusion; default `false` * @returns The {@linkcode PokemonSpeciesForm} of this Pokemon's fusion counterpart. */ - getFusionSpeciesForm(ignoreOverride = false, useIllusion = false): PokemonSpeciesForm { + public getFusionSpeciesForm(ignoreOverride = false, useIllusion = false): PokemonSpeciesForm { const fusionSpecies: PokemonSpecies = useIllusion && this.summonData.illusion ? this.summonData.illusion.fusionSpecies! : this.fusionSpecies!; const fusionFormIndex = @@ -1140,7 +1146,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** Resets the pokemon's field sprite properties, including position, alpha, and scale */ - resetSprite(): void { + public resetSprite(): void { // Resetting properties should not be shown on the field this.setVisible(false); @@ -1191,9 +1197,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Attempts to animate a given {@linkcode Phaser.GameObjects.Sprite} * @see {@linkcode Phaser.GameObjects.Sprite.play} - * @param sprite {@linkcode Phaser.GameObjects.Sprite} to animate - * @param tintSprite {@linkcode Phaser.GameObjects.Sprite} placed on top of the sprite to add a color tint - * @param animConfig {@linkcode String} to pass to {@linkcode Phaser.GameObjects.Sprite.play} + * @param sprite - Sprite to animate + * @param tintSprite - Sprite placed on top of the sprite to add a color tint + * @param animConfig - String to pass to the sprite's {@linkcode Phaser.GameObjects.Sprite.play | play} method * @returns true if the sprite was able to be animated */ tryPlaySprite(sprite: Phaser.GameObjects.Sprite, tintSprite: Phaser.GameObjects.Sprite, key: string): boolean { @@ -1258,7 +1264,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } - setFieldPosition(fieldPosition: FieldPosition, duration?: number): Promise { + /** + * Set the field position of this Pokémon + * @param fieldPosition - The new field position + * @param duration - How long the transition should take, in milliseconds; if `0` or `undefined`, the position is changed instantly + */ + public setFieldPosition(fieldPosition: FieldPosition, duration?: number): Promise { return new Promise(resolve => { if (fieldPosition === this.fieldPosition) { resolve(); @@ -1608,10 +1619,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return baseStats; } + // TODO: Convert this into a getter getNature(): Nature { return this.customPokemonData.nature !== -1 ? this.customPokemonData.nature : this.nature; } + // TODO: Convert this into a setter OR just add a listener for calculateStats... setNature(nature: Nature): void { this.nature = nature; this.calculateStats(); @@ -1622,7 +1635,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.calculateStats(); } - generateNature(naturePool?: Nature[]): void { + /** + * Randomly generate and set this Pokémon's nature + * @param naturePool - An optional array of Natures to choose from. If not provided, all natures will be considered. + */ + private generateNature(naturePool?: Nature[]): void { if (naturePool === undefined) { naturePool = getEnumValues(Nature); } @@ -1630,10 +1647,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.setNature(nature); } + // TODO: Convert this into a getter isFullHp(): boolean { return this.hp >= this.getMaxHp(); } + // TODO: Convert this into a getter getMaxHp(): number { return this.getStat(Stat.HP); } @@ -1643,6 +1662,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.getMaxHp() - this.hp; } + /** + * Return the ratio of this Pokémon's current HP to its maximum HP + * @param precise - Whether to return the exact HP ratio (e.g. `0.54321`), or one rounded to the nearest %; default `false` + * @returns The current HP ratio + */ getHpRatio(precise = false): number { return precise ? this.hp / this.getMaxHp() : Math.round((this.hp / this.getMaxHp()) * 100) / 100; } @@ -1680,18 +1704,30 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Check whether this Pokemon is shiny. - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * Check whether this Pokémon is shiny, including its fusion species + * + * @param useIllusion - Whether to consider an active illusion * @returns Whether this Pokemon is shiny + * @see {@linkcode isBaseShiny} */ isShiny(useIllusion = false): boolean { return this.isBaseShiny(useIllusion) || this.isFusionShiny(useIllusion); } + /** + * Get whether this Pokémon's _base_ species is shiny + * @param useIllusion - Whether to consider an active illusion; default `false` + * @returns Whether the pokemon is shiny + */ isBaseShiny(useIllusion = false) { return useIllusion ? (this.summonData.illusion?.shiny ?? this.shiny) : this.shiny; } + /** + * Get whether this Pokémon's _fusion_ species is shiny + * @param useIllusion - Whether to consider an active illusion; default `true` + * @returns Whether this Pokémon's fusion species is shiny, or `false` if there is no fusion + */ isFusionShiny(useIllusion = false) { if (!this.isFusion(useIllusion)) { return false; @@ -1701,7 +1737,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Check whether this Pokemon is doubly shiny (both normal and fusion are shiny). - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns Whether this pokemon's base and fusion counterparts are both shiny. */ isDoubleShiny(useIllusion = false): boolean { @@ -1709,10 +1745,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Return this Pokemon's {@linkcode Variant | shiny variant}. + * Return this Pokemon's shiny variant. * If a fusion, returns the maximum of the two variants. * Only meaningful if this pokemon is actually shiny. - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns The shiny variant of this Pokemon. */ getVariant(useIllusion = false): Variant { @@ -1727,6 +1763,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Return the base pokemon's variant. Equivalent to {@linkcode getVariant} if this pokemon is not a fusion. + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns The shiny variant of this Pokemon's base species. */ getBaseVariant(useIllusion = false): Variant { @@ -1735,10 +1772,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Return the fused pokemon's variant. + * Get the shiny variant of this Pokémon's _fusion_ species * * @remarks * Always returns `0` if the pokemon is not a fusion. + * @param useIllusion - Whether to consider an active illusion * @returns The shiny variant of this pokemon's fusion species. */ getFusionVariant(useIllusion = false): Variant { @@ -1759,7 +1797,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Return whether this {@linkcode Pokemon} is currently fused with anything. - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns Whether this Pokemon is currently fused with another species. */ isFusion(useIllusion = false): boolean { @@ -1768,7 +1806,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Return this {@linkcode Pokemon}'s name. - * @param useIllusion - Whether to consider this pokemon's illusion if present; default `false` + * @param useIllusion - Whether to consider an active illusion; default `false` * @returns This Pokemon's name. * @see {@linkcode getNameToRender} - gets this Pokemon's display name. */ @@ -1874,8 +1912,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @param includeTeraType - Whether to use this Pokemon's tera type if Terastallized; default `false` * @param forDefend - Whether this Pokemon is currently receiving an attack; default `false` * @param ignoreOverride - Whether to ignore any overrides caused by {@linkcode MoveId.TRANSFORM | Transform}; default `false` - * @param useIllusion - Whether to consider this Pokemon's illusion if present; default `false` - * @returns An array of {@linkcode PokemonType}s corresponding to this Pokemon's typing (real or percieved). + * @param useIllusion - Whether to consider an active illusion; default `false` + * @returns An array of {@linkcode PokemonType}s corresponding to this Pokemon's typing (real or perceived). */ public getTypes( includeTeraType = false, @@ -2083,10 +2121,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Sets the {@linkcode Pokemon}'s ability and activates it if it normally activates on summon + * Set this Pokémon's temporary ability, activating it if it normally activates on summon * * Also clears primal weather if it is from the ability being changed - * @param ability New Ability + * @param ability - The temporary ability to set + * @param passive - Whether to set the passive ability instead of the non-passive one; default `false` */ public setTempAbility(ability: Ability, passive = false): void { applyOnLoseAbAttrs({ pokemon: this, passive }); @@ -2146,11 +2185,13 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Checks whether an ability of a pokemon can be currently applied. This should rarely be + * Check whether this Pokémon can apply its current ability + * + * @remarks + * This should rarely be * directly called, as {@linkcode hasAbility} and {@linkcode hasAbilityWithAttr} already call this. - * @see {@linkcode hasAbility} {@linkcode hasAbilityWithAttr} Intended ways to check abilities in most cases - * @param passive If true, check if passive can be applied instead of non-passive - * @returns `true` if the ability can be applied + * @param passive - Whether to check the passive (`true`) or non-passive (`false`) ability; default `false` + * @returns Whether the ability can be applied */ public canApplyAbility(passive = false): boolean { if (passive && !this.hasPassive()) { @@ -2365,14 +2406,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Calculates the effectiveness of a move against the Pokémon. - * This includes modifiers from move and ability attributes. - * @param source {@linkcode Pokemon} The attacking Pokémon. - * @param move {@linkcode Move} The move being used by the attacking Pokémon. - * @param ignoreAbility Whether to ignore abilities that might affect type effectiveness or immunity (defaults to `false`). - * @param simulated Whether to apply abilities via simulated calls (defaults to `true`) - * @param cancelled {@linkcode BooleanHolder} Stores whether the move was cancelled by a non-type-based immunity. - * @param useIllusion - Whether we want the attack move effectiveness on the illusion or not + * Calculate the effectiveness of the move against this Pokémon, including + * modifiers from move and ability attributes + * @param source - The attacking Pokémon. + * @param move - The move being used by the attacking Pokémon. + * @param ignoreAbility - Whether to ignore abilities that might affect type effectiveness or immunity (defaults to `false`). + * @param simulated - Whether to apply abilities via simulated calls (defaults to `true`) + * @param cancelled - Stores whether the move was cancelled by a non-type-based immunity. + * @param useIllusion - Whether to consider an active illusion * @returns The type damage multiplier, indicating the effectiveness of the move */ getMoveEffectiveness( @@ -2426,14 +2467,15 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { if (!cancelledHolder.value) { const defendingSidePlayField = this.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); - defendingSidePlayField.forEach(p => + defendingSidePlayField.forEach((p: (typeof defendingSidePlayField)[0]) => { applyAbAttrs("FieldPriorityMoveImmunityAbAttr", { pokemon: p, opponent: source, move, cancelled: cancelledHolder, - }), - ); + simulated, + }); + }); } } @@ -2454,7 +2496,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { typeMultiplier.value = 0; } - return (!cancelledHolder.value ? typeMultiplier.value : 0) as TypeDamageMultiplier; + return (cancelledHolder.value ? 0 : typeMultiplier.value) as TypeDamageMultiplier; } /** @@ -2540,10 +2582,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Computes the given Pokemon's matchup score against this Pokemon. + * Compute the given Pokémon's matchup score against this Pokémon + * @remarks * In most cases, this score ranges from near-zero to 16, but the maximum possible matchup score is 64. - * @param opponent {@linkcode Pokemon} The Pokemon to compare this Pokemon against - * @returns A score value based on how favorable this Pokemon is when fighting the given Pokemon + * @param opponent - The Pokemon to compare this Pokémon against + * @returns A score value based on how favorable this Pokémon is when fighting the given Pokémon */ getMatchupScore(opponent: Pokemon): number { const enemyTypes = opponent.getTypes(true, false, false, true); @@ -2620,6 +2663,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return (atkScore + defScore) * Math.min(hpDiffRatio, 1); } + /** + * Get the first evolution this Pokémon meets the conditions to evolve into + * @remarks + * Fusion evolutions are also considered. + * @returns The evolution this pokemon can currently evolve into, or `null` if it cannot evolve + */ getEvolution(): SpeciesFormEvolution | null { if (pokemonEvolutions.hasOwnProperty(this.species.speciesId)) { const evolutions = pokemonEvolutions[this.species.speciesId]; @@ -2645,11 +2694,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets all level up moves in a given range for a particular pokemon. - * @param startingLevel Don't include moves below this level - * @param includeEvolutionMoves Whether to include evolution moves - * @param simulateEvolutionChain Whether to include moves from prior evolutions - * @param includeRelearnerMoves Whether to include moves that would require a relearner. Note the move relearner inherently allows evolution moves + * Get all level up moves in a given range for a particular pokemon. + * @param startingLevel - Don't include moves below this level + * @param includeEvolutionMoves - Whether to include evolution moves + * @param simulateEvolutionChain - Whether to include moves from prior evolutions + * @param includeRelearnerMoves - Whether to include moves that would require a relearner. Note the move relearner inherently allows evolution moves * @returns A list of moves and the levels they can be learned at */ getLevelMoves( @@ -2781,12 +2830,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Helper function for getLevelMoves. + * Helper function for getLevelMoves + * + * @remarks * Finds all non-duplicate items from the input, and pushes them into the output. * Two items count as duplicate if they have the same Move, regardless of level. * - * @param levelMoves the input array to search for non-duplicates from - * @param ret the output array to be pushed into. + * @param levelMoves - The input array to search for non-duplicates from + * @param ret - The output array to be pushed into. */ private static getUniqueMoves(levelMoves: LevelMoves, ret: LevelMoves): void { const uniqueMoves: MoveId[] = []; @@ -2800,13 +2851,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Get a list of all egg moves - * * @returns list of egg moves */ getEggMoves(): MoveId[] | undefined { return speciesEggMoves[this.getSpeciesForm().getRootSpeciesId()]; } + /** + * Create a new {@linkcode PokemonMove} and set it to the specified move index in this Pokémon's moveset. + * @param moveIndex - The index of the move to set + * @param moveId - The ID of the move to set + */ setMove(moveIndex: number, moveId: MoveId): void { if (moveId === MoveId.NONE) { return; @@ -2819,14 +2874,16 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Function that tries to set a Pokemon shiny based on the trainer's trainer ID and secret ID. + * Attempt to set the Pokémon's shininess based on the trainer's trainer ID and secret ID. * Endless Pokemon in the end biome are unable to be set to shiny * + * @remarks + * * The exact mechanic is that it calculates E as the XOR of the player's trainer ID and secret ID. * F is calculated as the XOR of the first 16 bits of the Pokemon's ID with the last 16 bits. * The XOR of E and F are then compared to the {@linkcode shinyThreshold} (or {@linkcode thresholdOverride} if set) to see whether or not to generate a shiny. * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / 65536 - * @param thresholdOverride number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) + * @param thresholdOverride - number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) * @returns true if the Pokemon has been set as a shiny, false otherwise */ trySetShiny(thresholdOverride?: number): boolean { @@ -2867,14 +2924,16 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Function that tries to set a Pokemon shiny based on seed. + * Tries to set a Pokémon's shininess based on seed + * + * @remarks * For manual use only, usually to roll a Pokemon's shiny chance a second time. * If it rolls shiny, or if it's already shiny, also sets a random variant and give the Pokemon the associated luck. * * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536` - * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) - * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} - * @returns `true` if the Pokemon has been set as a shiny, `false` otherwise + * @param thresholdOverride - number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) + * @param applyModifiersToOverride - If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} + * @returns Whether this Pokémon was set to shiny */ public trySetShinySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { if (!this.shiny) { @@ -2900,11 +2959,18 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Generates a shiny variant - * @returns `0-2`, with the following probabilities: - * - Has a 10% chance of returning `2` (epic variant) - * - Has a 30% chance of returning `1` (rare variant) - * - Has a 60% chance of returning `0` (basic shiny) + * Randomly generate a shiny variant + * + * @remarks + * Variants are returned with the following probabilities: + * + * | Variant | Description | Probability | + * |---------|----------------|-------------| + * | 0 | Basic shiny | 60% | + * | 1 | Rare variant | 30% | + * | 2 | Epic variant | 10% | + * + * @returns The randomly chosen shiny variant */ protected generateShinyVariant(): Variant { const formIndex: number = this.formIndex; @@ -2940,12 +3006,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Function that tries to set a Pokemon to have its hidden ability based on seed, if it exists. + * Function that tries to set this Pokemon to have its hidden ability based on seed, if it exists. + * + * @remarks * For manual use only, usually to roll a Pokemon's hidden ability chance a second time. * * The base hidden ability odds are {@linkcode BASE_HIDDEN_ABILITY_CHANCE} / `65536` - * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the HA chance, overrides {@linkcode haThreshold} if set (bypassing HA rate modifiers such as Ability Charm) - * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Ability Charm to {@linkcode thresholdOverride} + * @param thresholdOverride - number that is divided by `2^16` (`65536`) to get the HA chance, overrides {@linkcode haThreshold} if set (bypassing HA rate modifiers such as Ability Charm) + * @param applyModifiersToOverride - If {@linkcode thresholdOverride} is set and this is true, will apply Ability Charm to {@linkcode thresholdOverride} * @returns `true` if the Pokemon has been set to have its hidden ability, `false` otherwise */ public tryRerollHiddenAbilitySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { @@ -2964,6 +3032,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.abilityIndex === 2; } + /** + * Generate a fusion species and add it to this Pokémon + * @param forStarter - Whether this fusion is being generated for a starter Pokémon; default `false` + */ public generateFusionSpecies(forStarter?: boolean): void { const hiddenAbilityChance = new NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); if (!this.hasTrainer()) { @@ -3030,6 +3102,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.generateName(); } + /** Remove the fusion species from this Pokémon */ public clearFusionSpecies(): void { this.fusionSpecies = null; this.fusionFormIndex = 0; @@ -3044,7 +3117,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.calculateStats(); } - /** Generates a semi-random moveset for a Pokemon */ + /** Generate a semi-random moveset for this Pokémon */ public generateAndPopulateMoveset(): void { generateMoveset(this); @@ -3063,6 +3136,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return move?.isUsable(this, ignorePp) ?? false; } + /** Show this Pokémon's info panel */ showInfo(): void { if (!this.battleInfo.visible) { const otherBattleInfo = globalScene.fieldUI @@ -3091,7 +3165,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } - hideInfo(): Promise { + /** Hide this Pokémon's info panel */ + async hideInfo(): Promise { return new Promise(resolve => { if (this.battleInfo?.visible) { globalScene.tweens.add({ @@ -3115,14 +3190,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); } - /** - * sets if the pokemon is switching out (if it's a enemy wild implies it's going to flee) - * @param status - boolean - */ - setSwitchOutStatus(status: boolean): void { - this.switchOutStatus = status; - } - updateInfo(instant?: boolean): Promise { return this.battleInfo.updateInfo(this, instant); } @@ -3133,8 +3200,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Adds experience to this PlayerPokemon, subject to wave based level caps. - * @param exp The amount of experience to add - * @param ignoreLevelCap Whether to ignore level caps when adding experience (defaults to false) + * @param exp - The amount of experience to add + * @param ignoreLevelCap - Whether to ignore level caps when adding experience; default `false` */ addExp(exp: number, ignoreLevelCap = false) { const maxExpLevel = globalScene.getMaxExpLevel(ignoreLevelCap); @@ -3151,8 +3218,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Compares if `this` and {@linkcode target} are on the same team. - * @param target the {@linkcode Pokemon} to compare against. + * Check whether the specified Pokémon is an opponent + * @param target - The {@linkcode Pokemon} to compare against * @returns `true` if the two pokemon are allies, `false` otherwise */ public isOpponent(target: Pokemon): boolean { @@ -3197,17 +3264,18 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Calculates the stat stage multiplier of the user against an opponent. + * Calculate the stat stage multiplier of the user against an opponent * - * Note that this does not apply to evasion or accuracy + * @remarks + * This does not apply to evasion or accuracy * @see {@linkcode getAccuracyMultiplier} * @param stat - The {@linkcode EffectiveStat} to calculate * @param opponent - The {@linkcode Pokemon} being targeted * @param move - The {@linkcode Move} being used - * @param ignoreOppAbility determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored (`false` by default) - * @param isCritical determines whether a critical hit has occurred or not (`false` by default) - * @param simulated determines whether effects are applied without altering game state (`true` by default) - * @param ignoreHeldItems determines whether this Pokemon's held items should be ignored during the stat calculation, default `false` + * @param ignoreOppAbility - determines whether the effects of the opponent's abilities (i.e. Unaware) should be ignored; default `false` + * @param isCritical - determines whether a critical hit has occurred or not; default `false` + * @param simulated - determines whether effects are applied without altering game state; default `true` + * @param ignoreHeldItems - determines whether this Pokemon's held items should be ignored during the stat calculation; default `false` * @returns the stat stage multiplier to be used for effective stat calculation */ getStatStageMultiplier( @@ -3344,15 +3412,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Calculates the base damage of the given move against this Pokemon when attacked by the given source. * Used during damage calculation and for Shell Side Arm's forecasting effect. - * @param source - The attacking {@linkcode Pokemon}. - * @param move - The {@linkcode Move} used in the attack. - * @param moveCategory - The move's {@linkcode MoveCategory} after variable-category effects are applied. - * @param ignoreAbility - If `true`, ignores this Pokemon's defensive ability effects (defaults to `false`). - * @param ignoreSourceAbility - If `true`, ignore's the attacking Pokemon's ability effects (defaults to `false`). - * @param ignoreAllyAbility - If `true`, ignores the ally Pokemon's ability effects (defaults to `false`). - * @param ignoreSourceAllyAbility - If `true`, ignores the attacking Pokemon's ally's ability effects (defaults to `false`). - * @param isCritical - if `true`, calculates effective stats as if the hit were critical (defaults to `false`). - * @param simulated - if `true`, suppresses changes to game state during calculation (defaults to `true`). + * @param __namedParameters.source - Needed for proper typedoc rendering * @returns The move's base damage against this Pokemon when used by the source Pokemon. */ getBaseDamage({ @@ -3470,15 +3530,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Calculates the damage of an attack made by another Pokemon against this Pokemon - * @param source {@linkcode Pokemon} the attacking Pokemon - * @param move The {@linkcode Move} used in the attack - * @param ignoreAbility If `true`, ignores this Pokemon's defensive ability effects - * @param ignoreSourceAbility If `true`, ignores the attacking Pokemon's ability effects - * @param ignoreAllyAbility If `true`, ignores the ally Pokemon's ability effects - * @param ignoreSourceAllyAbility If `true`, ignores the ability effects of the attacking pokemon's ally - * @param isCritical If `true`, calculates damage for a critical hit. - * @param simulated If `true`, suppresses changes to game state during the calculation. - * @param effectiveness If defined, used in place of calculated effectiveness values + * @param __namedParameters.source - Needed for proper typedoc rendering * @returns The {@linkcode DamageCalculationResult} */ getAttackDamage({ @@ -3807,12 +3859,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Called by damageAndUpdate() - * @param damage integer - * @param ignoreSegments boolean, not currently used - * @param preventEndure used to update damage if endure or sturdy - * @param ignoreFaintPhase flag on whether to add FaintPhase if pokemon after applying damage faints - * @returns integer representing damage dealt + * Submethod called by {@linkcode damageAndUpdate} to apply damage to this Pokemon and adjust its HP. + * @param damage - The damage to deal + * @param _ignoreSegments - Whether to ignore boss segments; default `false` + * @param preventEndure - Whether to allow the damage to bypass an Endure/Sturdy effect + * @param ignoreFaintPhase - Whether to ignore adding a FaintPhase if this damage causes a faint + * @returns The actual damage dealt */ damage(damage: number, _ignoreSegments = false, preventEndure = false, ignoreFaintPhase = false): number { if (this.isFainted()) { @@ -3840,15 +3892,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { damage = Math.min(damage, this.hp); this.hp = this.hp - damage; if (this.isFainted() && !ignoreFaintPhase) { - /** - * When adding the FaintPhase, want to toggle future unshiftPhase() and queueMessage() calls - * to appear before the FaintPhase (as FaintPhase will potentially end the encounter and add Phases such as - * GameOverPhase, VictoryPhase, etc.. that will interfere with anything else that happens during this MoveEffectPhase) - * - * Once the MoveEffectPhase is over (and calls it's .end() function, shiftPhase() will reset the PhaseQueueSplice via clearPhaseQueueSplice() ) - */ - globalScene.phaseManager.setPhaseQueueSplice(); - globalScene.phaseManager.unshiftNew("FaintPhase", this.getBattlerIndex(), preventEndure); + globalScene.phaseManager.queueFaintPhase(this.getBattlerIndex(), preventEndure); this.destroySubstitute(); this.lapseTag(BattlerTagType.COMMANDED); } @@ -3858,14 +3902,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Given the damage, adds a new DamagePhase and update HP values, etc. * - * Checks for 'Indirect' HitResults to account for Endure/Reviver Seed applying correctly - * @param damage integer - passed to damage() - * @param result an enum if it's super effective, not very, etc. - * @param isCritical boolean if move is a critical hit - * @param ignoreSegments boolean, passed to damage() and not used currently - * @param preventEndure boolean, ignore endure properties of pokemon, passed to damage() - * @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage() - * @returns integer of damage done + * @remarks + * Checks for {@linkcode HitResult.INDIRECT | Indirect} hits to account for Endure/Reviver Seed applying correctly + * @param damage - The damage to inflict on this Pokémon + * @param __namedParameters.source - Needed for proper typedoc rendering + * @returns Amount of damage actually done */ damageAndUpdate( damage: number, @@ -3876,10 +3917,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ignoreFaintPhase = false, source, }: { + /** + * An enum if it's super effective, not very effective, etc; default {@linkcode HitResult.EFFECTIVE} + */ result?: DamageResult; + /** Whether the attack was a critical hit */ isCritical?: boolean; + /** Whether to ignore boss segments */ ignoreSegments?: boolean; + /** Whether to ignore adding a FaintPhase if this damage causes a faint; default `false` */ ignoreFaintPhase?: boolean; + /** The Pokémon inflicting the damage, or undefined if not caused by a Pokémon */ source?: Pokemon; } = {}, ): number { @@ -3896,11 +3944,6 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { damage = 0; } damage = this.damage(damage, ignoreSegments, isIndirectDamage, ignoreFaintPhase); - // Ensure the battle-info bar's HP is updated, though only if the battle info is visible - // TODO: When battle-info UI is refactored, make this only update the HP bar - if (this.battleInfo.visible) { - this.updateInfo(); - } // Damage amount may have changed, but needed to be queued before calling damage function damagePhase.updateAmount(damage); /** @@ -3913,17 +3956,25 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return damage; } - heal(amount: number): number { + /** + * Restore a specific amount of HP to this Pokémon + * @param amount - The amount of HP to restore + * @returns The true amount of HP restored; may be less than `amount` if `amount` would overheal + */ + public heal(amount: number): number { const healAmount = Math.min(amount, this.getMaxHp() - this.hp); this.hp += healAmount; return healAmount; } - isBossImmune(): boolean { + public isBossImmune(): boolean { return this.isBoss(); } - isMax(): boolean { + /** + * @returns Whether this Pokémon is in a Dynamax or Gigantamax form + */ + public isMax(): boolean { const maxForms = [ SpeciesFormKey.GIGANTAMAX, SpeciesFormKey.GIGANTAMAX_RAPID, @@ -3935,7 +3986,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ); } - isMega(): boolean { + /** + * @returns Whether this Pokémon is in a Mega or Primal form + */ + public isMega(): boolean { const megaForms = [ SpeciesFormKey.MEGA, SpeciesFormKey.MEGA_X, @@ -3948,7 +4002,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { ); } - canAddTag(tagType: BattlerTagType): boolean { + /** + * Check whether a battler tag can be added to this Pokémon + * + * @param tagType - The tag to check + * @returns - Whether the tag can be added + * @see {@linkcode addTag} + */ + public canAddTag(tagType: BattlerTagType): boolean { if (this.getTag(tagType)) { return false; } @@ -3972,7 +4033,20 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return !cancelled.value; } - addTag(tagType: BattlerTagType, turnCount = 0, sourceMove?: MoveId, sourceId?: number): boolean { + /** + * Add a new {@linkcode BattlerTag} of the specified `tagType` + * + * @remarks + * Also ensures the tag is able to be applied, similar to {@linkcode canAddTag} + * + * @param tagType - The type of tag to add + * @param turnCount - The number of turns the tag should last; default `0` + * @param sourceMove - The id of the move that causing the tag to be added, if caused by a move + * @param sourceId - The {@linkcode Pokemon#id | id} of the pokemon causing the tag to be added, if caused by a Pokémon + * @returns Whether the tag was successfully added + * @see {@linkcode canAddTag} + */ + public addTag(tagType: BattlerTagType, turnCount = 0, sourceMove?: MoveId, sourceId?: number): boolean { const existingTag = this.getTag(tagType); if (existingTag) { existingTag.onOverlap(this); @@ -3981,6 +4055,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { const newTag = getBattlerTag(tagType, turnCount, sourceMove!, sourceId!); // TODO: are the bangs correct? + // TODO: Just call canAddTag() here? Can possibly overload it to accept an actual tag instead of just a type const cancelled = new BooleanHolder(false); applyAbAttrs("BattlerTagImmunityAbAttr", { pokemon: this, tag: newTag, cancelled }); if (cancelled.value) { @@ -4003,31 +4078,46 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return false; } - getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | undefined; - getTag(tagType: BattlerTagType.SUBSTITUTE): SubstituteTag | undefined; - getTag(tagType: BattlerTagType): BattlerTag | undefined; - getTag(tagType: Constructor): T | undefined; - getTag(tagType: BattlerTagType | Constructor): BattlerTag | undefined { + // TODO: Utilize a type map for these so we can avoid overloads + public getTag(tagType: BattlerTagType.GRUDGE): GrudgeTag | undefined; + public getTag(tagType: BattlerTagType.SUBSTITUTE): SubstituteTag | undefined; + public getTag(tagType: BattlerTagType): BattlerTag | undefined; + public getTag(tagType: Constructor): T | undefined; + public getTag(tagType: BattlerTagType | Constructor): BattlerTag | undefined { return typeof tagType === "function" ? this.summonData.tags.find(t => t instanceof tagType) : this.summonData.tags.find(t => t.tagType === tagType); } - findTag(tagFilter: (tag: BattlerTag) => boolean) { - return this.summonData.tags.find(t => tagFilter(t)); + /** + * Find the first `BattlerTag` matching the specified predicate + * @remarks + * Equivalent to `this.summonData.tags.find(tagFilter)`. + * @param tagFilter - The predicate to match against + * @returns The first matching tag, or `undefined` if none match + */ + public findTag(tagFilter: (tag: BattlerTag) => boolean) { + return this.summonData.tags.find(tagFilter); } - findTags(tagFilter: (tag: BattlerTag) => boolean): BattlerTag[] { - return this.summonData.tags.filter(t => tagFilter(t)); + /** + * Return the list of `BattlerTag`s that satisfy the given predicate + * @remarks + * Equivalent to `this.summonData.tags.filter(tagFilter)`. + * @param tagFilter - The predicate to match against + * @returns The filtered list of tags + */ + public findTags(tagFilter: (tag: BattlerTag) => boolean): BattlerTag[] { + return this.summonData.tags.filter(tagFilter); } /** * Tick down the first {@linkcode BattlerTag} found matching the given {@linkcode BattlerTagType}, * removing it if its duration goes below 0. - * @param tagType the {@linkcode BattlerTagType} to check against - * @returns `true` if the tag was present + * @param tagType - The `BattlerTagType` to lapse + * @returns Whether the tag was present */ - lapseTag(tagType: BattlerTagType): boolean { + public lapseTag(tagType: BattlerTagType): boolean { const tags = this.summonData.tags; const tag = tags.find(t => t.tagType === tagType); if (!tag) { @@ -4042,11 +4132,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Tick down all {@linkcode BattlerTags} matching the given {@linkcode BattlerTagLapseType}, - * removing any whose durations fall below 0. - * @param tagType the {@linkcode BattlerTagLapseType} to tick down + * Tick down all {@linkcode BattlerTags} that lapse on the provided + * `lapseType`, removing any whose durations fall below 0. + * @param lapseType - The type of lapse to process */ - lapseTags(lapseType: BattlerTagLapseType): void { + public lapseTags(lapseType: BattlerTagLapseType): void { const tags = this.summonData.tags; tags .filter( @@ -4061,10 +4151,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Remove the first tag matching the given {@linkcode BattlerTagType}. - * @param tagType the {@linkcode BattlerTagType} to search for and remove + * Remove the first tag matching `tagType` and invoke its + * {@linkcode BattlerTag#onRemove | onRemove} method. + * @remarks + * Only removes the first matching tag, if multiple are present; to remove all + * matching tags, use {@linkcode findAndRemoveTags} instead. + * @param tagType - The tag type to search for and remove */ - removeTag(tagType: BattlerTagType): void { + public removeTag(tagType: BattlerTagType): void { const tags = this.summonData.tags; const tag = tags.find(t => t.tagType === tagType); if (tag) { @@ -4074,10 +4168,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Find and remove all {@linkcode BattlerTag}s matching the given function. - * @param tagFilter a function dictating which tags to remove + * Find and remove all {@linkcode BattlerTag}s matching the given function and + * invoke their {@linkcode BattlerTag#onRemove | onRemove} methods. + * @remarks + * Removes all matching tags; to remove only the first matching tag, use + * {@linkcode removeTag} instead. + * @param tagFilter - A function dictating which tags to remove */ - findAndRemoveTags(tagFilter: (tag: BattlerTag) => boolean): void { + public findAndRemoveTags(tagFilter: (tag: BattlerTag) => boolean): void { const tags = this.summonData.tags; const tagsToRemove = tags.filter(t => tagFilter(t)); for (const tag of tagsToRemove) { @@ -4087,11 +4185,22 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } - removeTagsBySourceId(sourceId: number): void { + /** + * Remove all tags that were applied by a Pokémon with the given `sourceId`, + * invoking their {@linkcode BattlerTag#onRemove | onRemove} methods. + * @param sourceId - Tags with this {@linkcode Pokemon#id | id} as their {@linkcode BattlerTag#sourceId | sourceId} will be removed + * @see {@linkcode findAndRemoveTags} + */ + public removeTagsBySourceId(sourceId: number): void { this.findAndRemoveTags(t => t.isSourceLinked() && t.sourceId === sourceId); } - transferTagsBySourceId(sourceId: number, newSourceId: number): void { + /** + * Change the `sourceId` of all tags on this Pokémon with the given `sourceId` to `newSourceId`. + * @param sourceId - The {@linkcode Pokemon#id | id} of the pokemon whose tags are to be transferred + * @param newSourceId - The {@linkcode Pokemon#id | id} of the pokemon to which the tags are being transferred + */ + public transferTagsBySourceId(sourceId: number, newSourceId: number): void { this.summonData.tags.forEach(t => { if (t.sourceId === sourceId) { t.sourceId = newSourceId; @@ -4100,11 +4209,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Transferring stat changes and Tags - * @param source {@linkcode Pokemon} the pokemon whose stats/Tags are to be passed on from, ie: the Pokemon using Baton Pass + * Transfer stat changes and Tags from another Pokémon + * + * @remarks + * Used to implement Baton Pass and switching via the Baton item. + * + * @param source - The pokemon whose stats/Tags are to be passed on from, ie: the Pokemon using Baton Pass */ - transferSummon(source: Pokemon): void { - // Copy all stat stages + public transferSummon(source: Pokemon): void { for (const s of BATTLE_STATS) { const sourceStage = source.getStatStage(s); if (this.isPlayer() && sourceStage === 6) { @@ -4134,9 +4246,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets whether the given move is currently disabled for this Pokemon. + * Get whether the given move is currently disabled for this Pokémon * - * @param moveId - The {@linkcode MoveId} ID of the move to check + * @param moveId - The ID of the move to check * @returns `true` if the move is disabled for this Pokemon, otherwise `false` * * @see {@linkcode MoveRestrictionBattlerTag} @@ -4146,9 +4258,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets whether the given move is currently disabled for the user based on the player's target selection + * Get whether the given move is currently disabled for the user based on the player's target selection * - * @param moveId - The {@linkcode MoveId} ID of the move to check + * @param moveId - The ID of the move to check * @param user - The move user * @param target - The target of the move * @@ -4166,11 +4278,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists. + * Get the {@link MoveRestrictionBattlerTag} that is restricting a move, if it exists. * - * @param moveId - {@linkcode MoveId} ID of the move to check - * @param user - {@linkcode Pokemon} the move user, optional and used when the target is a factor in the move's restricted status - * @param target - {@linkcode Pokemon} the target of the move, optional and used when the target is a factor in the move's restricted status + * @param moveId - The ID of the move to check + * @param user - The move user, optional and used when the target is a factor in the move's restricted status + * @param target - The target of the move; optional, and used when the target is a factor in the move's restricted status * @returns The first tag on this Pokemon that restricts the move, or `null` if the move is not restricted. */ getRestrictingTag(moveId: MoveId, user?: Pokemon, target?: Pokemon): MoveRestrictionBattlerTag | null { @@ -4195,6 +4307,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return this.summonData.moveHistory; } + /** + * Add a new entry to this Pokemon's move history + * @remarks + * Does nothing if this Pokemon is not currently on the field. + * @param turnMove - The move to add to the history + */ public pushMoveHistory(turnMove: TurnMove): void { if (!this.isOnField()) { return; @@ -4212,7 +4330,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @returns An array of {@linkcode TurnMove}, as specified above. */ // TODO: Update documentation in dancer PR to mention "getLastNonVirtualMove" - getLastXMoves(moveCount = 1): TurnMove[] { + public getLastXMoves(moveCount = 1): TurnMove[] { const moveHistory = this.getMoveHistory(); if (moveCount > 0) { return moveHistory.slice(Math.max(moveHistory.length - moveCount, 0)).reverse(); @@ -4230,7 +4348,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * @returns The last move this Pokemon has used satisfying the aforementioned conditions, * or `undefined` if no applicable moves have been used since switching in. */ - getLastNonVirtualMove(ignoreStruggle = false, ignoreFollowUp = true): TurnMove | undefined { + public getLastNonVirtualMove(ignoreStruggle = false, ignoreFollowUp = true): TurnMove | undefined { return this.getLastXMoves(-1).find( m => m.move !== MoveId.NONE @@ -4243,7 +4361,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Return this Pokemon's move queue, consisting of all the moves it is slated to perform. * @returns An array of {@linkcode TurnMove}, as described above */ - getMoveQueue(): TurnMove[] { + public getMoveQueue(): TurnMove[] { return this.summonData.moveQueue; } @@ -4251,11 +4369,17 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Add a new entry to the end of this Pokemon's move queue. * @param queuedMove - A {@linkcode TurnMove} to push to this Pokemon's queue. */ - pushMoveQueue(queuedMove: TurnMove): void { + public pushMoveQueue(queuedMove: TurnMove): void { this.summonData.moveQueue.push(queuedMove); } - changeForm(formChange: SpeciesFormChange): Promise { + /** + * Change this Pokémon's form to the specified form, loading the required + * assets and updating its stats and info display. + * @param formChange - The form to change to + * @returns A Promise that resolves once the form change has completed. + */ + public async changeForm(formChange: SpeciesFormChange): Promise { return new Promise(resolve => { this.formIndex = Math.max( this.species.forms.findIndex(f => f.formKey === formChange.formKey), @@ -4277,7 +4401,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); } - cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound | null { + /** + * Play this Pokémon's cry sound + * @param soundConfig - Optional sound configuration to apply to the cry + * @param sceneOverride - Optional scene to use instead of the global scene + */ + public cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound | null { const scene = sceneOverride ?? globalScene; // TODO: is `sceneOverride` needed? const cry = this.getSpeciesForm(undefined, true).cry(soundConfig); if (!cry) { @@ -4316,8 +4445,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return cry; } - // biome-ignore lint: there are a ton of issues.. - faintCry(callback: Function): void { + /** + * Play this Pokémon's faint cry, pausing its animation until the cry is finished. + * @param callback - A function to be called once the cry has finished playing + */ + public faintCry(callback: () => any): void { if (this.fusionSpecies && this.getSpeciesForm() !== this.getFusionSpeciesForm()) { this.fusionFaintCry(callback); return; @@ -4389,8 +4521,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); } - // biome-ignore lint/complexity/noBannedTypes: Consider refactoring to change type of Function - private fusionFaintCry(callback: Function): void { + /** + * Play this Pokémon's fusion faint cry, which is a mixture of the faint cries + * for both of its species + * @param callback - A function to be called once the cry has finished playing + */ + private fusionFaintCry(callback: () => any): void { const key = this.species.getCryKey(this.formIndex); let i = 0; let rate = 0.85; @@ -4500,7 +4636,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { }); } - isOppositeGender(pokemon: Pokemon): boolean { + /** + * Check the specified pokemon is considered to be the opposite gender as this pokemon + * @param pokemon - The Pokémon to compare against + * @returns Whether the pokemon are considered to be opposite genders + */ + public isOppositeGender(pokemon: Pokemon): boolean { return ( this.gender !== Gender.GENDERLESS && pokemon.gender === (this.gender === Gender.MALE ? Gender.FEMALE : Gender.MALE) @@ -4510,12 +4651,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Display an immunity message for a failed status application. * @param quiet - Whether to suppress message and return early - * @param reason - The reason for the status application failure - + * @param reason - The reason for the status application failure; * can be "overlap" (already has same status), "other" (generic fail message) * or a {@linkcode TerrainType} for terrain-based blockages. * Default `"other"` */ - queueStatusImmuneMessage( + public queueStatusImmuneMessage( quiet: boolean, reason: "overlap" | "other" | Exclude = "other", ): void { @@ -4742,7 +4883,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. */ - doSetStatus(effect: Exclude): void; + public doSetStatus(effect: Exclude): void; /** * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. * @param effect - {@linkcode StatusEffect.SLEEP} @@ -4752,7 +4893,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. */ - doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void; + public doSetStatus(effect: StatusEffect.SLEEP, sleepTurnsRemaining?: number): void; /** * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. * @param effect - The {@linkcode StatusEffect} to set @@ -4763,7 +4904,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. */ - doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void; + public doSetStatus(effect: StatusEffect, sleepTurnsRemaining?: number): void; /** * Set this Pokemon's {@linkcode status | non-volatile status condition} to the specified effect. * @param effect - The {@linkcode StatusEffect} to set @@ -4775,7 +4916,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * ⚠️ This method does **not** check for feasibility; that is the responsibility of the caller. * @todo Make this and all related fields private and change tests to use a field-based helper or similar */ - doSetStatus( + public doSetStatus( effect: StatusEffect, sleepTurnsRemaining = effect !== StatusEffect.SLEEP ? 0 : this.randBattleSeedIntRange(2, 4), ): void { @@ -4824,11 +4965,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Resets the status of a pokemon. - * @param revive Whether revive should be cured; defaults to true. - * @param confusion Whether resetStatus should include confusion or not; defaults to false. - * @param reloadAssets Whether to reload the assets or not; defaults to false. - * @param asPhase Whether to reset the status in a phase or immediately + * Reset this Pokémon's status + * @param revive - Whether revive should be cured; default `true` + * @param confusion - Whether to also cure confusion; default `false` + * @param reloadAssets - Whether to reload the assets or not; default `false` + * @param asPhase - Whether to reset the status in a phase or immediately; default `true` */ resetStatus(revive = true, confusion = false, reloadAssets = false, asPhase = true): void { const lastStatus = this.status?.effect; @@ -4844,9 +4985,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Performs the action of clearing a Pokemon's status - * + * Perform the action of clearing a Pokemon's status + * @remarks * This is a helper to {@linkcode resetStatus}, which should be called directly instead of this method + * @param confusion - Whether to also clear this Pokémon's confusion + * @param reloadAssets - Whether to reload this pokemon's assets */ public clearStatus(confusion: boolean, reloadAssets: boolean) { const lastStatus = this.status?.effect; @@ -4865,11 +5008,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Checks if this Pokemon is protected by Safeguard - * @param attacker the {@linkcode Pokemon} inflicting status on this Pokemon - * @returns `true` if this Pokemon is protected by Safeguard; `false` otherwise. + * Check if this Pokémon is protected by Safeguard + * @param attacker - The Pokémon responsible for the interaction that needs to check against Safeguard + * @returns Whether this Pokémon is protected by Safeguard */ - isSafeguarded(attacker: Pokemon): boolean { + public isSafeguarded(attacker: Pokemon): boolean { const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; if (globalScene.arena.getTagOnSide(ArenaTagType.SAFEGUARD, defendingSide)) { const bypassed = new BooleanHolder(false); @@ -4882,11 +5025,11 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Performs miscellaneous setup for when the Pokemon is summoned, like generating the substitute sprite + * Perform miscellaneous setup for when the Pokemon is summoned, like generating the substitute sprite * @param resetSummonData - Whether to additionally reset the Pokemon's summon data (default: `false`) */ public fieldSetup(resetSummonData?: boolean): void { - this.setSwitchOutStatus(false); + this.switchOutStatus = false; if (globalScene) { globalScene.triggerPokemonFormChange(this, SpeciesFormChangePostMoveTrigger, true); } @@ -4914,7 +5057,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * Reset this Pokemon's {@linkcode PokemonSummonData | SummonData} and {@linkcode PokemonTempSummonData | TempSummonData} * in preparation for switching pokemon, as well as removing any relevant on-switch tags. */ - resetSummonData(): void { + public resetSummonData(): void { const illusion: IllusionData | null = this.summonData.illusion; if (this.summonData.speciesForm) { this.summonData.speciesForm = null; @@ -4927,17 +5070,21 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Reset a {@linkcode Pokemon}'s per-battle {@linkcode PokemonBattleData | battleData}, + * Reset this Pokémon's per-battle {@linkcode PokemonBattleData | battleData} * as well as any transient {@linkcode PokemonWaveData | waveData} for the current wave. + * + * @remarks * Should be called once per arena transition (new biome/trainer battle/Mystery Encounter). */ - resetBattleAndWaveData(): void { + public resetBattleAndWaveData(): void { this.battleData = new PokemonBattleData(); this.resetWaveData(); } /** - * Reset a {@linkcode Pokemon}'s {@linkcode PokemonWaveData | waveData}. + * Reset this Pokémon's {@linkcode PokemonWaveData | waveData} + * + * @remarks * Should be called upon starting a new wave in addition to whenever an arena transition occurs. * @see {@linkcode resetBattleAndWaveData} */ @@ -4946,6 +5093,16 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.tempSummonData.waveTurnCount = 1; } + /** + * Reset this Pokémon's Terastallization state + * + * @remarks + * Responsible for all of the cleanup required when a pokemon goes from being + * terastallized to no longer terastallized: + * - Resetting stellar type boosts + * - Updating the Pokémon's terastallization-dependent form + * - Adjusting the sprite pipeline to remove the Tera effect + */ resetTera(): void { const wasTerastallized = this.isTerastallized; this.isTerastallized = false; @@ -4956,6 +5113,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } + /** + * Clear this Pokémon's transient turn data + */ resetTurnData(): void { this.turnData = new PokemonTurnData(); } @@ -4965,6 +5125,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { return (this.getSpeciesForm().getBaseExp() * this.level) / 5 + 1; } + //#region Sprite and Animation Methods setFrameRate(frameRate: number) { globalScene.anims.get(this.getBattleSpriteKey()).frameRate = frameRate; try { @@ -5041,6 +5202,7 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } } + /** Play the shiny sparkle animation and effects, if applicable */ sparkle(): void { if (this.shinySparkle) { doShinySparkleAnim(this.shinySparkle, this.variant); @@ -5372,15 +5534,19 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { fusionCanvas.remove(); } + //#endregion Sprite and Animation Methods + /** - * Generates a random number using the current battle's seed, or the global seed if `globalScene.currentBattle` is falsy + * Generate a random number using the current battle's seed, or the global seed if `globalScene.currentBattle` is falsy + * + * @remarks * This calls either {@linkcode BattleScene.randBattleSeedInt}({@linkcode range}, {@linkcode min}) in `src/battle-scene.ts` * which calls {@linkcode Battle.randSeedInt}({@linkcode range}, {@linkcode min}) in `src/battle.ts` * which calls {@linkcode randSeedInt randSeedInt}({@linkcode range}, {@linkcode min}) in `src/utils.ts`, * or it directly calls {@linkcode randSeedInt randSeedInt}({@linkcode range}, {@linkcode min}) in `src/utils.ts` if there is no current battle * - * @param range How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min} - * @param min The minimum integer to pick, default `0` + * @param range - How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min} + * @param min - The minimum integer to pick; default `0` * @returns A random integer between {@linkcode min} and ({@linkcode min} + {@linkcode range} - 1) */ randBattleSeedInt(range: number, min = 0): number { @@ -5388,10 +5554,10 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Generates a random number using the current battle's seed, or the global seed if `globalScene.currentBattle` is falsy - * @param min The minimum integer to generate - * @param max The maximum integer to generate - * @returns a random integer between {@linkcode min} and {@linkcode max} inclusive + * Generate a random number using the current battle's seed, or the global seed if `globalScene.currentBattle` is falsy + * @param min - The minimum integer to generate + * @param max - The maximum integer to generate + * @returns A random integer between {@linkcode min} and {@linkcode max} (inclusive) */ randBattleSeedIntRange(min: number, max: number): number { return globalScene.currentBattle ? globalScene.randBattleSeedInt(max - min + 1, min) : randSeedIntRange(min, max); @@ -5399,9 +5565,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Causes a Pokemon to leave the field (such as in preparation for a switch out/escape). - * @param clearEffects Indicates if effects should be cleared (true) or passed - * to the next pokemon, such as during a baton pass (false) - * @param hideInfo Indicates if this should also play the animation to hide the Pokemon's + * @param clearEffects - Indicates if effects should be cleared (true) or passed + * to the next pokemon, such as during a baton pass (false) + * @param hideInfo - Indicates if this should also play the animation to hide the Pokemon's * info container. */ leaveField(clearEffects = true, hideInfo = true, destroy = false) { @@ -5421,25 +5587,33 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } // Trigger abilities that activate upon leaving the field applyAbAttrs("PreLeaveFieldAbAttr", { pokemon: this }); - this.setSwitchOutStatus(true); + this.switchOutStatus = true; globalScene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true); globalScene.field.remove(this, destroy); } + /** + * @inheritdoc {@linkcode Phaser.GameObjects.Container#destroy} + * + * ### Custom Behavior + * In addition to the base `destroy` behavior, this also destroys the Pokemon's + * {@linkcode battleInfo} and substitute sprite (as applicable). + */ destroy(): void { this.battleInfo?.destroy(); this.destroySubstitute(); super.destroy(); } + // TODO: Turn this into a getter getBattleInfo(): BattleInfo { return this.battleInfo; } /** - * Checks whether or not the Pokemon's root form has the same ability - * @param abilityIndex the given ability index we are checking - * @returns true if the abilities are the same + * Check whether or not this Pokémon's root form has the same ability + * @param abilityIndex - The ability index to check + * @returns Whether the Pokemon's root form has the same ability */ hasSameAbilityInRootForm(abilityIndex: number): boolean { const currentAbilityIndex = this.abilityIndex; @@ -5448,9 +5622,9 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Helper function to check if the player already owns the starter data of the Pokemon's + * Helper function to check if the player already owns the starter data of this Pokémon's * current ability - * @param ownedAbilityAttrs the owned abilityAttr of this Pokemon's root form + * @param ownedAbilityAttrs - The owned abilityAttr of this Pokemon's root form * @returns true if the player already has it, false otherwise */ checkIfPlayerHasAbilityOfStarter(ownedAbilityAttrs: number): boolean { @@ -5491,8 +5665,8 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { /** * Record a berry being eaten for ability and move triggers. * Only tracks things that proc _every_ time a berry is eaten. - * @param berryType The type of berry being eaten. - * @param updateHarvest Whether to track the berry for harvest; default `true`. + * @param berryType - The type of berry being eaten. + * @param updateHarvest - Whether to track the berry for harvest; default `true`. */ public recordEatenBerry(berryType: BerryType, updateHarvest = true) { this.battleData.hasEatenBerry = true; @@ -5503,6 +5677,14 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { this.turnData.berriesEaten.push(berryType); } + /** + * Get the number of persistent treasure items this Pokemon has + * @remarks + * Persistent treasure items are defined as held items that give money + * after battle, such as the Lucky Egg or the Amulet Coin. + * Used exclusively for Gimmighoul's evolution condition + * @returns The number of persistent treasure items this Pokémon has + */ getPersistentTreasureCount(): number { return ( this.getHeldItems().filter(m => m.is("DamageMoneyRewardModifier")).length @@ -5633,10 +5815,10 @@ export class PlayerPokemon extends Pokemon { } /** - * Causes this mon to leave the field (via {@linkcode leaveField}) and then - * opens the party switcher UI to switch a new mon in - * @param switchType the {@linkcode SwitchType} for this switch-out. If this is - * `BATON_PASS` or `SHED_TAIL`, this Pokemon's effects are not cleared upon leaving + * Cause this Pokémon to leave the field (via {@linkcode leaveField}) and then + * open the party switcher UI to switch in a new Pokémon + * @param switchType - The type of this switch-out. If this is + * `BATON_PASS` or `SHED_TAIL`, this Pokémon's effects are not cleared upon leaving * the field. */ switchOut(switchType: SwitchType = SwitchType.SWITCH): Promise { @@ -5649,8 +5831,7 @@ export class PlayerPokemon extends Pokemon { this.getFieldIndex(), (slotIndex: number, _option: PartyOption) => { if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) { - globalScene.phaseManager.prependNewToPhase( - "MoveEndPhase", + globalScene.phaseManager.queueDeferred( "SwitchSummonPhase", switchType, this.getFieldIndex(), @@ -5970,8 +6151,8 @@ export class PlayerPokemon extends Pokemon { } /** - * Returns a Promise to fuse two PlayerPokemon together - * @param pokemon The PlayerPokemon to fuse to this one + * Fuse another PlayerPokemon into this one + * @param pokemon - The PlayerPokemon to fuse to this one */ fuse(pokemon: PlayerPokemon): void { this.fusionSpecies = pokemon.species; @@ -6462,7 +6643,7 @@ export class EnemyPokemon extends Pokemon { /** * Determines the Pokemon the given move would target if used by this Pokemon - * @param moveId {@linkcode MoveId} The move to be used + * @param moveId - The move to be used * @returns The indexes of the Pokemon the given move would target */ getNextTargets(moveId: MoveId): BattlerIndex[] { @@ -6575,37 +6756,26 @@ export class EnemyPokemon extends Pokemon { return 0; } - damage(damage: number, ignoreSegments = false, preventEndure = false, ignoreFaintPhase = false): number { + /** + * @inheritdoc + * @param ignoreSegments - Whether to ignore boss segments when applying damage + */ + public damage(damage: number, ignoreSegments = false, preventEndure = false, ignoreFaintPhase = false): number { if (this.isFainted()) { return 0; } + const segmentSize = this.getMaxHp() / this.bossSegments; + let clearedBossSegmentIndex = this.isBoss() ? this.bossSegmentIndex + 1 : 0; if (this.isBoss() && !ignoreSegments) { - const segmentSize = this.getMaxHp() / this.bossSegments; - for (let s = this.bossSegmentIndex; s > 0; s--) { - const hpThreshold = segmentSize * s; - const roundedHpThreshold = Math.round(hpThreshold); - if (this.hp >= roundedHpThreshold) { - if (this.hp - damage <= roundedHpThreshold) { - const hpRemainder = this.hp - roundedHpThreshold; - let segmentsBypassed = 0; - while ( - segmentsBypassed < this.bossSegmentIndex - && this.canBypassBossSegments(segmentsBypassed + 1) - && damage - hpRemainder >= Math.round(segmentSize * Math.pow(2, segmentsBypassed + 1)) - ) { - segmentsBypassed++; - //console.log('damage', damage, 'segment', segmentsBypassed + 1, 'segment size', segmentSize, 'damage needed', Math.round(segmentSize * Math.pow(2, segmentsBypassed + 1))); - } - - damage = toDmgValue(this.hp - hpThreshold + segmentSize * segmentsBypassed); - clearedBossSegmentIndex = s - segmentsBypassed; - } - break; - } - } + [damage, clearedBossSegmentIndex] = calculateBossSegmentDamage( + damage, + this.hp, + segmentSize, + this.getMinimumSegmentIndex(), + ); } switch (globalScene.currentBattle.battleSpec) { @@ -6619,7 +6789,6 @@ export class EnemyPokemon extends Pokemon { if (this.isBoss()) { if (ignoreSegments) { - const segmentSize = this.getMaxHp() / this.bossSegments; clearedBossSegmentIndex = Math.ceil(this.hp / segmentSize); } if (clearedBossSegmentIndex <= this.bossSegmentIndex) { @@ -6631,24 +6800,22 @@ export class EnemyPokemon extends Pokemon { return ret; } - canBypassBossSegments(segmentCount = 1): boolean { - if ( - globalScene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS - && !this.formIndex - && this.bossSegmentIndex - segmentCount < 1 - ) { - return false; + private getMinimumSegmentIndex(): number { + if (globalScene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS && !this.formIndex) { + return 1; } - return true; + return 0; } /** * Go through a boss' health segments and give stats boosts for each newly cleared segment + * + * @remarks * The base boost is 1 to a random stat that's not already maxed out per broken shield * For Pokemon with 3 health segments or more, breaking the last shield gives +2 instead * For Pokemon with 5 health segments or more, breaking the last two shields give +2 each - * @param segmentIndex index of the segment to get down to (0 = no shield left, 1 = 1 shield left, etc.) + * @param segmentIndex - index of the segment to get down to (0 = no shield left, 1 = 1 shield left, etc.) */ handleBossSegmentCleared(segmentIndex: number): void { let doStatBoost = !this.hasTrainer(); @@ -6710,22 +6877,22 @@ export class EnemyPokemon extends Pokemon { } } - getFieldIndex(): number { + public getFieldIndex(): number { return globalScene.getEnemyField().indexOf(this); } - getBattlerIndex(): BattlerIndex { + public getBattlerIndex(): BattlerIndex { return BattlerIndex.ENEMY + this.getFieldIndex(); } /** * Add a new pokemon to the player's party (at `slotIndex` if set). * The new pokemon's visibility will be set to `false`. - * @param pokeballType the type of pokeball the pokemon was caught with - * @param slotIndex an optional index to place the pokemon in the party - * @returns the pokemon that was added or null if the pokemon could not be added + * @param pokeballType - The type of pokeball the pokemon was caught with + * @param slotIndex - An optional index to place the pokemon in the party + * @returns The pokemon that was added or null if the pokemon could not be added */ - addToParty(pokeballType: PokeballType, slotIndex = -1) { + public addToParty(pokeballType: PokeballType, slotIndex = -1) { const party = globalScene.getPlayerParty(); let ret: PlayerPokemon | null = null; @@ -6768,11 +6935,11 @@ export class EnemyPokemon extends Pokemon { * Show or hide the type effectiveness multiplier window * Passing undefined will hide the window */ - updateEffectiveness(effectiveness?: string) { + public updateEffectiveness(effectiveness?: string) { this.battleInfo.updateEffectiveness(effectiveness); } - toggleFlyout(visible: boolean): void { + public toggleFlyout(visible: boolean): void { this.battleInfo.toggleFlyout(visible); } } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index b94c479e96e..19ddc77d436 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -13,7 +13,6 @@ import { getStatusEffectHealText } from "#data/status-effect"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; import { Color, ShadowColor } from "#enums/color"; -import { Command } from "#enums/command"; import type { FormChangeItem } from "#enums/form-change-item"; import { LearnMoveType } from "#enums/learn-move-type"; import type { MoveId } from "#enums/move-id"; @@ -1542,30 +1541,16 @@ export class BypassSpeedChanceModifier extends PokemonHeldItemModifier { return new BypassSpeedChanceModifier(this.type, this.pokemonId, this.stackCount); } - /** - * Checks if {@linkcode BypassSpeedChanceModifier} should be applied - * @param pokemon the {@linkcode Pokemon} that holds the item - * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed - * @returns `true` if {@linkcode BypassSpeedChanceModifier} should be applied - */ - override shouldApply(pokemon?: Pokemon, doBypassSpeed?: BooleanHolder): boolean { - return super.shouldApply(pokemon, doBypassSpeed) && !!doBypassSpeed; - } - /** * Applies {@linkcode BypassSpeedChanceModifier} * @param pokemon the {@linkcode Pokemon} that holds the item - * @param doBypassSpeed {@linkcode BooleanHolder} that is `true` if speed should be bypassed * @returns `true` if {@linkcode BypassSpeedChanceModifier} has been applied */ - override apply(pokemon: Pokemon, doBypassSpeed: BooleanHolder): boolean { - if (!doBypassSpeed.value && pokemon.randBattleSeedInt(10) < this.getStackCount()) { - doBypassSpeed.value = true; - const isCommandFight = - globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]?.command === Command.FIGHT; + override apply(pokemon: Pokemon): boolean { + if (pokemon.randBattleSeedInt(10) < this.getStackCount() && pokemon.addTag(BattlerTagType.BYPASS_SPEED)) { const hasQuickClaw = this.type.is("PokemonHeldItemModifierType") && this.type.id === "QUICK_CLAW"; - if (isCommandFight && hasQuickClaw) { + if (hasQuickClaw) { globalScene.phaseManager.queueMessage( i18next.t("modifier:bypassSpeedChanceApply", { pokemonName: getPokemonNameWithAffix(pokemon), diff --git a/src/phase-manager.ts b/src/phase-manager.ts index 3fbf68de60d..350e77e52eb 100644 --- a/src/phase-manager.ts +++ b/src/phase-manager.ts @@ -8,12 +8,14 @@ */ import { PHASE_START_COLOR } from "#app/constants/colors"; +import { DynamicQueueManager } from "#app/dynamic-queue-manager"; import { globalScene } from "#app/global-scene"; import type { Phase } from "#app/phase"; -import { type PhasePriorityQueue, PostSummonPhasePriorityQueue } from "#data/phase-priority-queue"; -import type { DynamicPhaseType } from "#enums/dynamic-phase-type"; +import { PhaseTree } from "#app/phase-tree"; +import { BattleType } from "#enums/battle-type"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import type { Pokemon } from "#field/pokemon"; -import { ActivatePriorityQueuePhase } from "#phases/activate-priority-queue-phase"; +import type { PokemonMove } from "#moves/pokemon-move"; import { AddEnemyBuffModifierPhase } from "#phases/add-enemy-buff-modifier-phase"; import { AttemptCapturePhase } from "#phases/attempt-capture-phase"; import { AttemptRunPhase } from "#phases/attempt-run-phase"; @@ -25,6 +27,7 @@ import { CheckSwitchPhase } from "#phases/check-switch-phase"; import { CommandPhase } from "#phases/command-phase"; import { CommonAnimPhase } from "#phases/common-anim-phase"; import { DamageAnimPhase } from "#phases/damage-anim-phase"; +import { DynamicPhaseMarker } from "#phases/dynamic-phase-marker"; import { EggHatchPhase } from "#phases/egg-hatch-phase"; import { EggLapsePhase } from "#phases/egg-lapse-phase"; import { EggSummaryPhase } from "#phases/egg-summary-phase"; @@ -109,8 +112,7 @@ import { UnavailablePhase } from "#phases/unavailable-phase"; import { UnlockPhase } from "#phases/unlock-phase"; import { VictoryPhase } from "#phases/victory-phase"; import { WeatherEffectPhase } from "#phases/weather-effect-phase"; -import type { PhaseMap, PhaseString } from "#types/phase-types"; -import { type Constructor, coerceArray } from "#utils/common"; +import type { PhaseConditionFunc, PhaseMap, PhaseString } from "#types/phase-types"; /** * Object that holds all of the phase constructors. @@ -121,7 +123,6 @@ import { type Constructor, coerceArray } from "#utils/common"; * This allows for easy creation of new phases without needing to import each phase individually. */ const PHASES = Object.freeze({ - ActivatePriorityQueuePhase, AddEnemyBuffModifierPhase, AttemptCapturePhase, AttemptRunPhase, @@ -133,6 +134,7 @@ const PHASES = Object.freeze({ CommandPhase, CommonAnimPhase, DamageAnimPhase, + DynamicPhaseMarker, EggHatchPhase, EggLapsePhase, EggSummaryPhase, @@ -221,33 +223,30 @@ const PHASES = Object.freeze({ /** Maps Phase strings to their constructors */ export type PhaseConstructorMap = typeof PHASES; +/** Phases pushed at the end of each {@linkcode TurnStartPhase} */ +const turnEndPhases: readonly PhaseString[] = [ + "WeatherEffectPhase", + "PositionalTagPhase", + "BerryPhase", + "CheckStatusEffectPhase", + "TurnEndPhase", +] as const; + /** * PhaseManager is responsible for managing the phases in the battle scene */ export class PhaseManager { /** PhaseQueue: dequeue/remove the first element to get the next phase */ - public phaseQueue: Phase[] = []; - public conditionalQueue: Array<[() => boolean, Phase]> = []; - /** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */ - private phaseQueuePrepend: Phase[] = []; + private readonly phaseQueue: PhaseTree = new PhaseTree(); - /** overrides default of inserting phases to end of phaseQueuePrepend array. Useful for inserting Phases "out of order" */ - private phaseQueuePrependSpliceIndex = -1; - private nextCommandPhaseQueue: Phase[] = []; - - /** Storage for {@linkcode PhasePriorityQueue}s which hold phases whose order dynamically changes */ - private dynamicPhaseQueues: PhasePriorityQueue[]; - /** Parallel array to {@linkcode dynamicPhaseQueues} - matches phase types to their queues */ - private dynamicPhaseTypes: Constructor[]; + /** Holds priority queues for dynamically ordered phases */ + public dynamicQueueManager = new DynamicQueueManager(); + /** The currently-running phase */ private currentPhase: Phase; + /** The phase put on standby if {@linkcode overridePhase} is called */ private standbyPhase: Phase | null = null; - constructor() { - this.dynamicPhaseQueues = [new PostSummonPhasePriorityQueue()]; - this.dynamicPhaseTypes = [PostSummonPhase]; - } - /** * Clear all previously set phases, then add a new {@linkcode TitlePhase} to transition to the title screen. * @param addLogin - Whether to add a new {@linkcode LoginPhase} before the {@linkcode TitlePhase} @@ -275,123 +274,76 @@ export class PhaseManager { } /** - * Adds a phase to the conditional queue and ensures it is executed only when the specified condition is met. - * - * This method allows deferring the execution of a phase until certain conditions are met, which is useful for handling - * situations like abilities and entry hazards that depend on specific game states. - * - * @param phase - The phase to be added to the conditional queue. - * @param condition - A function that returns a boolean indicating whether the phase should be executed. - * + * Adds a phase to the end of the queue + * @param phase - The {@linkcode Phase} to add */ - pushConditionalPhase(phase: Phase, condition: () => boolean): void { - this.conditionalQueue.push([condition, phase]); + public pushPhase(phase: Phase): void { + this.phaseQueue.pushPhase(this.checkDynamic(phase)); } /** - * Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false - * @param phase {@linkcode Phase} the phase to add - * @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue + * Queue a phase to be run immediately after the current phase finishes. \ + * Unshifted phases are run in FIFO order if multiple are queued during a single phase's execution. + * @param phase - The {@linkcode Phase} to add */ - pushPhase(phase: Phase, defer = false): void { - if (this.getDynamicPhaseType(phase) !== undefined) { - this.pushDynamicPhase(phase); - } else { - (!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase); - } + public unshiftPhase(phase: Phase): void { + const toAdd = this.checkDynamic(phase); + phase.is("MovePhase") ? this.phaseQueue.addAfter(toAdd, "MoveEndPhase") : this.phaseQueue.addPhase(toAdd); } /** - * Adds Phase(s) to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex - * @param phases {@linkcode Phase} the phase(s) to add + * Helper method to queue a phase as dynamic if necessary + * @param phase - The phase to check + * @returns The {@linkcode Phase} or a {@linkcode DynamicPhaseMarker} to be used in its place */ - unshiftPhase(...phases: Phase[]): void { - if (this.phaseQueuePrependSpliceIndex === -1) { - this.phaseQueuePrepend.push(...phases); - } else { - this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, ...phases); + private checkDynamic(phase: Phase): Phase { + if (this.dynamicQueueManager.queueDynamicPhase(phase)) { + return new DynamicPhaseMarker(phase.phaseName); } + return phase; } /** * Clears the phaseQueue + * @param leaveUnshifted - If `true`, leaves the top level of the tree intact; default `false` */ - clearPhaseQueue(): void { - this.phaseQueue.splice(0, this.phaseQueue.length); + public clearPhaseQueue(leaveUnshifted = false): void { + this.phaseQueue.clear(leaveUnshifted); } - /** - * Clears all phase-related stuff, including all phase queues, the current and standby phases, and a splice index - */ - clearAllPhases(): void { - for (const queue of [this.phaseQueue, this.phaseQueuePrepend, this.conditionalQueue, this.nextCommandPhaseQueue]) { - queue.splice(0, queue.length); - } - this.dynamicPhaseQueues.forEach(queue => queue.clear()); + /** Clears all phase queues and the standby phase */ + public clearAllPhases(): void { + this.clearPhaseQueue(); + this.dynamicQueueManager.clearQueues(); this.standbyPhase = null; - this.clearPhaseQueueSplice(); } /** - * Used by function unshiftPhase(), sets index to start inserting at current length instead of the end of the array, useful if phaseQueuePrepend gets longer with Phases + * Determines the next phase to run and starts it. + * @privateRemarks + * This is called by {@linkcode Phase.end} by default, and should not be called by other methods. */ - setPhaseQueueSplice(): void { - this.phaseQueuePrependSpliceIndex = this.phaseQueuePrepend.length; - } - - /** - * Resets phaseQueuePrependSpliceIndex to -1, implies that calls to unshiftPhase will insert at end of phaseQueuePrepend - */ - clearPhaseQueueSplice(): void { - this.phaseQueuePrependSpliceIndex = -1; - } - - /** - * Is called by each Phase implementations "end()" by default - * We dump everything from phaseQueuePrepend to the start of of phaseQueue - * then removes first Phase and starts it - */ - shiftPhase(): void { + public shiftPhase(): void { if (this.standbyPhase) { this.currentPhase = this.standbyPhase; this.standbyPhase = null; return; } - if (this.phaseQueuePrependSpliceIndex > -1) { - this.clearPhaseQueueSplice(); - } - this.phaseQueue.unshift(...this.phaseQueuePrepend); - this.phaseQueuePrepend.splice(0); + let nextPhase = this.phaseQueue.getNextPhase(); - const unactivatedConditionalPhases: [() => boolean, Phase][] = []; - // Check if there are any conditional phases queued - for (const [condition, phase] of this.conditionalQueue) { - // Evaluate the condition associated with the phase - if (condition()) { - // If the condition is met, add the phase to the phase queue - this.pushPhase(phase); - } else { - // If the condition is not met, re-add the phase back to the end of the conditional queue - unactivatedConditionalPhases.push([condition, phase]); - } + if (nextPhase?.is("DynamicPhaseMarker")) { + nextPhase = this.dynamicQueueManager.popNextPhase(nextPhase.phaseType); } - this.conditionalQueue = unactivatedConditionalPhases; - - // If no phases are left, unshift phases to start a new turn. - if (this.phaseQueue.length === 0) { - this.populatePhaseQueue(); - // Clear the conditionalQueue if there are no phases left in the phaseQueue - this.conditionalQueue = []; + if (nextPhase == null) { + this.turnStart(); + } else { + this.currentPhase = nextPhase; } - // Bang is justified as `populatePhaseQueue` ensures we always have _something_ in the queue at all times - this.currentPhase = this.phaseQueue.shift()!; - this.startCurrentPhase(); } - /** * Helper method to start and log the current phase. */ @@ -400,7 +352,14 @@ export class PhaseManager { this.currentPhase.start(); } - overridePhase(phase: Phase): boolean { + /** + * Overrides the currently running phase with another + * @param phase - The {@linkcode Phase} to override the current one with + * @returns If the override succeeded + * + * @todo This is antithetical to the phase structure and used a single time. Remove it. + */ + public overridePhase(phase: Phase): boolean { if (this.standbyPhase) { return false; } @@ -413,173 +372,47 @@ export class PhaseManager { } /** - * Find a specific {@linkcode Phase} in the phase queue. + * Determine if there is a queued {@linkcode Phase} meeting the specified conditions. + * @param type - The {@linkcode PhaseString | type} of phase to search for + * @param condition - An optional {@linkcode PhaseConditionFunc} to add conditions to the search + * @returns Whether a matching phase exists + */ + public hasPhaseOfType(type: T, condition?: PhaseConditionFunc): boolean { + return this.dynamicQueueManager.exists(type, condition) || this.phaseQueue.exists(type, condition); + } + + /** + * Attempt to find and remove the first queued {@linkcode Phase} matching the given conditions. + * @param type - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - An optional {@linkcode PhaseConditionFunc} to add conditions to the search + * @returns Whether a phase was successfully removed + */ + public tryRemovePhase(type: T, phaseFilter?: PhaseConditionFunc): boolean { + if (this.dynamicQueueManager.removePhase(type, phaseFilter)) { + return true; + } + return this.phaseQueue.remove(type, phaseFilter); + } + + /** + * Removes all {@linkcode Phase}s of the given type from the queue + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for * - * @param phaseFilter filter function to use to find the wanted phase - * @returns the found phase or undefined if none found + * @remarks + * This is not intended to be used with dynamically ordered phases, and does not operate on the dynamic queue. \ + * However, it does remove {@linkcode DynamicPhaseMarker}s and so would prevent such phases from activating. */ - findPhase

(phaseFilter: (phase: P) => boolean): P | undefined { - return this.phaseQueue.find(phaseFilter) as P | undefined; - } - - tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean { - const phaseIndex = this.phaseQueue.findIndex(phaseFilter); - if (phaseIndex > -1) { - this.phaseQueue[phaseIndex] = phase; - return true; - } - return false; - } - - tryRemovePhase(phaseFilter: (phase: Phase) => boolean): boolean { - const phaseIndex = this.phaseQueue.findIndex(phaseFilter); - if (phaseIndex > -1) { - this.phaseQueue.splice(phaseIndex, 1); - return true; - } - return false; + public removeAllPhasesOfType(type: PhaseString): void { + this.phaseQueue.removeAll(type); } /** - * Will search for a specific phase in {@linkcode phaseQueuePrepend} via filter, and remove the first result if a match is found. - * @param phaseFilter filter function - */ - tryRemoveUnshiftedPhase(phaseFilter: (phase: Phase) => boolean): boolean { - const phaseIndex = this.phaseQueuePrepend.findIndex(phaseFilter); - if (phaseIndex > -1) { - this.phaseQueuePrepend.splice(phaseIndex, 1); - return true; - } - return false; - } - - /** - * Tries to add the input phase to index before target phase in the phaseQueue, else simply calls unshiftPhase() - * @param phase - The phase to be added - * @param targetPhase - The phase to search for in phaseQueue - * @returns boolean if a targetPhase was found and added - */ - prependToPhase(phase: Phase | Phase[], targetPhase: PhaseString): boolean { - phase = coerceArray(phase); - const target = PHASES[targetPhase]; - const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target); - - if (targetIndex !== -1) { - this.phaseQueue.splice(targetIndex, 0, ...phase); - return true; - } - this.unshiftPhase(...phase); - return false; - } - - /** - * Tries to add the input phase(s) to index after target phase in the {@linkcode phaseQueue}, else simply calls {@linkcode unshiftPhase()} - * @param phase {@linkcode Phase} the phase(s) to be added - * @param targetPhase {@linkcode Phase} the type of phase to search for in {@linkcode phaseQueue} - * @param condition Condition the target phase must meet to be appended to - * @returns `true` if a `targetPhase` was found to append to - */ - appendToPhase(phase: Phase | Phase[], targetPhase: PhaseString, condition?: (p: Phase) => boolean): boolean { - phase = coerceArray(phase); - const target = PHASES[targetPhase]; - const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof target && (!condition || condition(ph))); - - if (targetIndex !== -1 && this.phaseQueue.length > targetIndex) { - this.phaseQueue.splice(targetIndex + 1, 0, ...phase); - return true; - } - this.unshiftPhase(...phase); - return false; - } - - /** - * Checks a phase and returns the matching {@linkcode DynamicPhaseType}, or undefined if it does not match one - * @param phase The phase to check - * @returns The corresponding {@linkcode DynamicPhaseType} or `undefined` - */ - public getDynamicPhaseType(phase: Phase | null): DynamicPhaseType | undefined { - let phaseType: DynamicPhaseType | undefined; - this.dynamicPhaseTypes.forEach((cls, index) => { - if (phase instanceof cls) { - phaseType = index; - } - }); - - return phaseType; - } - - /** - * Pushes a phase onto its corresponding dynamic queue and marks the activation point in {@linkcode phaseQueue} - * - * The {@linkcode ActivatePriorityQueuePhase} will run the top phase in the dynamic queue (not necessarily {@linkcode phase}) - * @param phase The phase to push - */ - public pushDynamicPhase(phase: Phase): void { - const type = this.getDynamicPhaseType(phase); - if (type === undefined) { - return; - } - - this.pushPhase(new ActivatePriorityQueuePhase(type)); - this.dynamicPhaseQueues[type].push(phase); - } - - /** - * Attempt to remove one or more Phases from the given DynamicPhaseQueue, removing the equivalent amount of {@linkcode ActivatePriorityQueuePhase}s from the queue. - * @param type - The {@linkcode DynamicPhaseType} to check - * @param phaseFilter - The function to select phases for removal - * @param removeCount - The maximum number of phases to remove, or `all` to remove all matching phases; - * default `1` - * @todo Remove this eventually once the patchwork bug this is used for is fixed - */ - public tryRemoveDynamicPhase( - type: DynamicPhaseType, - phaseFilter: (phase: Phase) => boolean, - removeCount: number | "all" = 1, - ): void { - const numRemoved = this.dynamicPhaseQueues[type].tryRemovePhase(phaseFilter, removeCount); - for (let x = 0; x < numRemoved; x++) { - this.tryRemovePhase(p => p.is("ActivatePriorityQueuePhase")); - } - } - - /** - * Unshifts the top phase from the corresponding dynamic queue onto {@linkcode phaseQueue} - * @param type {@linkcode DynamicPhaseType} The type of dynamic phase to start - */ - public startDynamicPhaseType(type: DynamicPhaseType): void { - const phase = this.dynamicPhaseQueues[type].pop(); - if (phase) { - this.unshiftPhase(phase); - } - } - - /** - * Unshifts an {@linkcode ActivatePriorityQueuePhase} for {@linkcode phase}, then pushes {@linkcode phase} to its dynamic queue - * - * This is the same as {@linkcode pushDynamicPhase}, except the activation phase is unshifted - * - * {@linkcode phase} is not guaranteed to be the next phase from the queue to run (if the queue is not empty) - * @param phase The phase to add - * @returns - */ - public startDynamicPhase(phase: Phase): void { - const type = this.getDynamicPhaseType(phase); - if (type === undefined) { - return; - } - - this.unshiftPhase(new ActivatePriorityQueuePhase(type)); - this.dynamicPhaseQueues[type].push(phase); - } - - /** - * Adds a MessagePhase, either to PhaseQueuePrepend or nextCommandPhaseQueue + * Adds a `MessagePhase` to the queue * @param message - string for MessagePhase * @param callbackDelay - optional param for MessagePhase constructor * @param prompt - optional param for MessagePhase constructor * @param promptDelay - optional param for MessagePhase constructor - * @param defer - Whether to allow the phase to be deferred + * @param defer - If `true`, push the phase instead of unshifting; default `false` * * @see {@linkcode MessagePhase} for more details on the parameters */ @@ -591,20 +424,18 @@ export class PhaseManager { defer?: boolean | null, ) { const phase = new MessagePhase(message, callbackDelay, prompt, promptDelay); - if (!defer) { - // adds to the end of PhaseQueuePrepend - this.unshiftPhase(phase); - } else { - //remember that pushPhase adds it to nextCommandPhaseQueue + if (defer) { this.pushPhase(phase); + } else { + this.unshiftPhase(phase); } } /** - * Queue a phase to show or hide the ability flyout bar. + * Queues an ability bar flyout phase via {@linkcode unshiftPhase} * @param pokemon - The {@linkcode Pokemon} whose ability is being activated * @param passive - Whether the ability is a passive - * @param show - Whether to show or hide the bar + * @param show - If `true`, show the bar. Otherwise, hide it */ public queueAbilityDisplay(pokemon: Pokemon, passive: boolean, show: boolean): void { this.unshiftPhase(show ? new ShowAbilityPhase(pokemon.getBattlerIndex(), passive) : new HideAbilityPhase()); @@ -620,14 +451,12 @@ export class PhaseManager { } /** - * Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order) + * Clear all dynamic queues and begin a new {@linkcode TurnInitPhase} for the new turn. + * Called whenever the current phase queue is empty. */ - private populatePhaseQueue(): void { - if (this.nextCommandPhaseQueue.length > 0) { - this.phaseQueue.push(...this.nextCommandPhaseQueue); - this.nextCommandPhaseQueue.splice(0, this.nextCommandPhaseQueue.length); - } - this.phaseQueue.push(new TurnInitPhase()); + private turnStart(): void { + this.dynamicQueueManager.clearQueues(); + this.currentPhase = new TurnInitPhase(); } /** @@ -669,50 +498,119 @@ export class PhaseManager { } /** - * Create a new phase and immediately prepend it to an existing phase in the phase queue. - * Equivalent to calling {@linkcode create} followed by {@linkcode prependToPhase}. - * @param targetPhase - The phase to search for in phaseQueue - * @param phase - The name of the phase to create + * Add a {@linkcode FaintPhase} to the queue * @param args - The arguments to pass to the phase constructor - * @returns `true` if a `targetPhase` was found to prepend to + * + * @remarks + * + * Faint phases are ordered in a special way to allow battle effects to settle before the pokemon faints. + * @see {@linkcode PhaseTree.addPhase} */ - public prependNewToPhase( - targetPhase: PhaseString, - phase: T, - ...args: ConstructorParameters - ): boolean { - return this.prependToPhase(this.create(phase, ...args), targetPhase); + public queueFaintPhase(...args: ConstructorParameters): void { + this.phaseQueue.addPhase(this.create("FaintPhase", ...args), true); } /** - * Create a new phase and immediately append it to an existing phase the phase queue. - * Equivalent to calling {@linkcode create} followed by {@linkcode appendToPhase}. - * @param targetPhase - The phase to search for in phaseQueue - * @param phase - The name of the phase to create - * @param args - The arguments to pass to the phase constructor - * @returns `true` if a `targetPhase` was found to append to + * Attempts to add {@linkcode PostSummonPhase}s for the enemy pokemon + * + * This is used to ensure that wild pokemon (which have no {@linkcode SummonPhase}) do not queue a {@linkcode PostSummonPhase} + * until all pokemon are on the field. */ - public appendNewToPhase( - targetPhase: PhaseString, - phase: T, - ...args: ConstructorParameters - ): boolean { - return this.appendToPhase(this.create(phase, ...args), targetPhase); + public tryAddEnemyPostSummonPhases(): void { + if ( + ![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType) + && !this.phaseQueue.exists("SummonPhase") + ) { + globalScene.getEnemyField().forEach(p => { + this.pushPhase(new PostSummonPhase(p.getBattlerIndex(), "SummonPhase")); + }); + } } - public startNewDynamicPhase( + /** + * Create a new phase and queue it to run after all others queued by the currently running phase. + * @param phase - The name of the phase to create + * @param args - The arguments to pass to the phase constructor + * + * @deprecated Only used for switches and should be phased out eventually. + */ + public queueDeferred( phase: T, ...args: ConstructorParameters ): void { - this.startDynamicPhase(this.create(phase, ...args)); + this.phaseQueue.unshiftToCurrent(this.create(phase, ...args)); + } + + /** + * Finds the first {@linkcode MovePhase} meeting the condition + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + * @returns The MovePhase, or `undefined` if it does not exist + */ + public getMovePhase(phaseCondition: PhaseConditionFunc<"MovePhase">): MovePhase | undefined { + return this.dynamicQueueManager.getMovePhase(phaseCondition); + } + + /** + * Finds and cancels the first {@linkcode MovePhase} meeting the condition + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + */ + public cancelMove(phaseCondition: PhaseConditionFunc<"MovePhase">): void { + this.dynamicQueueManager.cancelMovePhase(phaseCondition); + } + + /** + * Finds the first {@linkcode MovePhase} meeting the condition and forces it next + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + */ + public forceMoveNext(phaseCondition: PhaseConditionFunc<"MovePhase">): void { + this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.FIRST); + } + + /** + * Finds the first {@linkcode MovePhase} meeting the condition and forces it last + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + */ + public forceMoveLast(phaseCondition: PhaseConditionFunc<"MovePhase">): void { + this.dynamicQueueManager.setMoveTimingModifier(phaseCondition, MovePhaseTimingModifier.LAST); + } + + /** + * Finds the first {@linkcode MovePhase} meeting the condition and changes its move + * @param phaseCondition - The {@linkcode PhaseConditionFunc | condition} function + * @param move - The {@linkcode PokemonMove | move} to use in replacement + */ + public changePhaseMove(phaseCondition: PhaseConditionFunc<"MovePhase">, move: PokemonMove): void { + this.dynamicQueueManager.setMoveForPhase(phaseCondition, move); + } + + /** + * Redirects moves which were targeted at a {@linkcode Pokemon} that has been removed + * @param removedPokemon - The removed {@linkcode Pokemon} + * @param allyPokemon - The ally of the removed pokemon + */ + public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void { + this.dynamicQueueManager.redirectMoves(removedPokemon, allyPokemon); + } + + /** Queues phases which run at the end of each turn */ + public queueTurnEndPhases(): void { + turnEndPhases.forEach(p => { + this.pushNew(p); + }); } /** Prevents end of turn effects from triggering when transitioning to a new biome on a X0 wave */ public onInterlude(): void { - const phasesToRemove = ["WeatherEffectPhase", "BerryPhase", "CheckStatusEffectPhase"]; - this.phaseQueue = this.phaseQueue.filter(p => !phasesToRemove.includes(p.phaseName)); + const phasesToRemove: readonly PhaseString[] = [ + "WeatherEffectPhase", + "BerryPhase", + "CheckStatusEffectPhase", + ] as const; + for (const phaseType of phasesToRemove) { + this.phaseQueue.removeAll(phaseType); + } - const turnEndPhase = this.findPhase(p => p.phaseName === "TurnEndPhase"); + const turnEndPhase = this.phaseQueue.find("TurnEndPhase"); if (turnEndPhase) { turnEndPhase.upcomingInterlude = true; } diff --git a/src/phase-tree.ts b/src/phase-tree.ts new file mode 100644 index 00000000000..69bb72ca4f0 --- /dev/null +++ b/src/phase-tree.ts @@ -0,0 +1,205 @@ +// biome-ignore-start lint/correctness/noUnusedImports: TSDoc imports +import type { PhaseManager } from "#app/@types/phase-types"; +import type { DynamicPhaseMarker } from "#phases/dynamic-phase-marker"; +// biome-ignore-end lint/correctness/noUnusedImports: TSDoc imports + +import type { PhaseMap, PhaseString } from "#app/@types/phase-types"; +import type { Phase } from "#app/phase"; +import type { PhaseConditionFunc } from "#types/phase-types"; + +/** + * The PhaseTree is the central storage location for {@linkcode Phase}s by the {@linkcode PhaseManager}. + * + * It has a tiered structure, where unshifted phases are added one level above the currently running Phase. Phases are generally popped from the Tree in FIFO order. + * + * Dynamically ordered phases are queued into the Tree only as {@linkcode DynamicPhaseMarker | Marker}s and as such are not guaranteed to run FIFO (otherwise, they would not be dynamic) + */ +export class PhaseTree { + /** Storage for all levels in the tree. This is a simple array because only one Phase may have "children" at a time. */ + private levels: Phase[][] = [[]]; + /** The level of the currently running {@linkcode Phase} in the Tree (note that such phase is not actually in the Tree while it is running) */ + private currentLevel = 0; + /** + * True if a "deferred" level exists + * @see {@linkcode addPhase} + */ + private deferredActive = false; + + /** + * Adds a {@linkcode Phase} to the specified level + * @param phase - The phase to add + * @param level - The numeric level to add the phase + * @throws Error if `level` is out of legal bounds + */ + private add(phase: Phase, level: number): void { + const addLevel = this.levels[level]; + if (addLevel == null) { + throw new Error("Attempted to add a phase to a nonexistent level of the PhaseTree!\nLevel: " + level.toString()); + } + this.levels[level].push(phase); + } + + /** + * Used by the {@linkcode PhaseManager} to add phases to the Tree + * @param phase - The {@linkcode Phase} to be added + * @param defer - Whether to defer the execution of this phase by allowing subsequently-added phases to run before it + * + * @privateRemarks + * Deferral is implemented by moving the queue at {@linkcode currentLevel} up one level and inserting the new phase below it. + * {@linkcode deferredActive} is set until the moved queue (and anything added to it) is exhausted. + * + * If {@linkcode deferredActive} is `true` when a deferred phase is added, the phase will be pushed to the second-highest level queue. + * That is, it will execute after the originally deferred phase, but there is no possibility for nesting with deferral. + * + * @todo `setPhaseQueueSplice` had strange behavior. This is simpler, but there are probably some remnant edge cases with the current implementation + */ + public addPhase(phase: Phase, defer = false): void { + if (defer && !this.deferredActive) { + this.deferredActive = true; + this.levels.splice(-1, 0, []); + } + this.add(phase, this.levels.length - 1 - +defer); + } + + /** + * Adds a {@linkcode Phase} after the first occurence of the given type, or to the top of the Tree if no such phase exists + * @param phase - The {@linkcode Phase} to be added + * @param type - A {@linkcode PhaseString} representing the type to search for + */ + public addAfter(phase: Phase, type: PhaseString): void { + for (let i = this.levels.length - 1; i >= 0; i--) { + const insertIdx = this.levels[i].findIndex(p => p.is(type)) + 1; + if (insertIdx !== 0) { + this.levels[i].splice(insertIdx, 0, phase); + return; + } + } + + this.addPhase(phase); + } + + /** + * Unshifts a {@linkcode Phase} to the current level. + * This is effectively the same as if the phase were added immediately after the currently-running phase, before it started. + * @param phase - The {@linkcode Phase} to be added + */ + public unshiftToCurrent(phase: Phase): void { + this.levels[this.currentLevel].unshift(phase); + } + + /** + * Pushes a {@linkcode Phase} to the last level of the queue. It will run only after all previously queued phases have been executed. + * @param phase - The {@linkcode Phase} to be added + */ + public pushPhase(phase: Phase): void { + this.add(phase, 0); + } + + /** + * Removes and returns the first {@linkcode Phase} from the topmost level of the tree + * @returns - The next {@linkcode Phase}, or `undefined` if the Tree is empty + */ + public getNextPhase(): Phase | undefined { + this.currentLevel = this.levels.length - 1; + while (this.currentLevel > 0 && this.levels[this.currentLevel].length === 0) { + this.deferredActive = false; + this.levels.pop(); + this.currentLevel--; + } + + // TODO: right now, this is preventing properly marking when one set of unshifted phases ends + this.levels.push([]); + return this.levels[this.currentLevel].shift(); + } + + /** + * Finds a particular {@linkcode Phase} in the Tree by searching in pop order + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase + * @returns The matching {@linkcode Phase}, or `undefined` if none exists + */ + public find

(phaseType: P, phaseFilter?: PhaseConditionFunc

): PhaseMap[P] | undefined { + for (let i = this.levels.length - 1; i >= 0; i--) { + const level = this.levels[i]; + const phase = level.find((p): p is PhaseMap[P] => p.is(phaseType) && (!phaseFilter || phaseFilter(p))); + if (phase) { + return phase; + } + } + } + + /** + * Finds a particular {@linkcode Phase} in the Tree by searching in pop order + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase + * @returns The matching {@linkcode Phase}, or `undefined` if none exists + */ + public findAll

(phaseType: P, phaseFilter?: PhaseConditionFunc

): PhaseMap[P][] { + const phases: PhaseMap[P][] = []; + for (let i = this.levels.length - 1; i >= 0; i--) { + const level = this.levels[i]; + const levelPhases = level.filter((p): p is PhaseMap[P] => p.is(phaseType) && (!phaseFilter || phaseFilter(p))); + phases.push(...levelPhases); + } + return phases; + } + + /** + * Clears the Tree + * @param leaveFirstLevel - If `true`, leaves the top level of the tree intact + * + * @privateremarks + * The parameter on this method exists because {@linkcode PhaseManager.clearPhaseQueue} previously (probably by mistake) ignored `phaseQueuePrepend`. + * + * This is (probably by mistake) relied upon by certain ME functions. + */ + public clear(leaveFirstLevel = false) { + this.levels = [leaveFirstLevel ? (this.levels.at(-1) ?? []) : []]; + } + + /** + * Finds and removes a single {@linkcode Phase} from the Tree + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase + * @returns Whether a removal occurred + */ + public remove

(phaseType: P, phaseFilter?: PhaseConditionFunc

): boolean { + for (let i = this.levels.length - 1; i >= 0; i--) { + const level = this.levels[i]; + const phaseIndex = level.findIndex(p => p.is(phaseType) && (!phaseFilter || phaseFilter(p))); + if (phaseIndex !== -1) { + level.splice(phaseIndex, 1); + return true; + } + } + return false; + } + + /** + * Removes all occurrences of {@linkcode Phase}s of the given type + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for + */ + public removeAll(phaseType: PhaseString): void { + for (let i = 0; i < this.levels.length; i++) { + const level = this.levels[i].filter(phase => !phase.is(phaseType)); + this.levels[i] = level; + } + } + + /** + * Determines if a particular phase exists in the Tree + * @param phaseType - The {@linkcode PhaseString | type} of phase to search for + * @param phaseFilter - A {@linkcode PhaseConditionFunc} to specify conditions for the phase + * @returns Whether a matching phase exists + */ + public exists

(phaseType: P, phaseFilter?: PhaseConditionFunc

): boolean { + for (const level of this.levels) { + for (const phase of level) { + if (phase.is(phaseType) && (!phaseFilter || phaseFilter(phase))) { + return true; + } + } + } + return false; + } +} diff --git a/src/phases/activate-priority-queue-phase.ts b/src/phases/activate-priority-queue-phase.ts deleted file mode 100644 index a31d3291a60..00000000000 --- a/src/phases/activate-priority-queue-phase.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { globalScene } from "#app/global-scene"; -import { Phase } from "#app/phase"; -import type { DynamicPhaseType } from "#enums/dynamic-phase-type"; - -export class ActivatePriorityQueuePhase extends Phase { - public readonly phaseName = "ActivatePriorityQueuePhase"; - private type: DynamicPhaseType; - - constructor(type: DynamicPhaseType) { - super(); - this.type = type; - } - - override start() { - super.start(); - globalScene.phaseManager.startDynamicPhaseType(this.type); - this.end(); - } - - public getType(): DynamicPhaseType { - return this.type; - } -} diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index 8a798d67554..45b0db76ced 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -18,23 +18,11 @@ export class BattleEndPhase extends BattlePhase { super.start(); // cull any extra `BattleEnd` phases from the queue. - globalScene.phaseManager.phaseQueue = globalScene.phaseManager.phaseQueue.filter(phase => { - if (phase.is("BattleEndPhase")) { - this.isVictory ||= phase.isVictory; - return false; - } - return true; - }); - // `phaseQueuePrepend` is private, so we have to use this inefficient loop. - while ( - globalScene.phaseManager.tryRemoveUnshiftedPhase(phase => { - if (phase.is("BattleEndPhase")) { - this.isVictory ||= phase.isVictory; - return true; - } - return false; - }) - ) {} + this.isVictory ||= globalScene.phaseManager.hasPhaseOfType( + "BattleEndPhase", + (phase: BattleEndPhase) => phase.isVictory, + ); + globalScene.phaseManager.removeAllPhasesOfType("BattleEndPhase"); globalScene.gameData.gameStats.battles++; if ( diff --git a/src/phases/check-status-effect-phase.ts b/src/phases/check-status-effect-phase.ts index bdaa536986a..5955cd42c55 100644 --- a/src/phases/check-status-effect-phase.ts +++ b/src/phases/check-status-effect-phase.ts @@ -1,20 +1,14 @@ import { globalScene } from "#app/global-scene"; import { Phase } from "#app/phase"; -import type { BattlerIndex } from "#enums/battler-index"; export class CheckStatusEffectPhase extends Phase { public readonly phaseName = "CheckStatusEffectPhase"; - private order: BattlerIndex[]; - constructor(order: BattlerIndex[]) { - super(); - this.order = order; - } start() { const field = globalScene.getField(); - for (const o of this.order) { - if (field[o].status?.isPostTurn()) { - globalScene.phaseManager.unshiftNew("PostTurnStatusEffectPhase", o); + for (const p of field) { + if (p?.status?.isPostTurn()) { + globalScene.phaseManager.unshiftNew("PostTurnStatusEffectPhase", p.getBattlerIndex()); } } this.end(); diff --git a/src/phases/check-switch-phase.ts b/src/phases/check-switch-phase.ts index 504bb6eb4bd..a55db4203bc 100644 --- a/src/phases/check-switch-phase.ts +++ b/src/phases/check-switch-phase.ts @@ -28,7 +28,8 @@ export class CheckSwitchPhase extends BattlePhase { // ...if the user is playing in Set Mode if (globalScene.battleStyle === BattleStyle.SET) { - return super.end(); + this.end(true); + return; } // ...if the checked Pokemon is somehow not on the field @@ -44,7 +45,8 @@ export class CheckSwitchPhase extends BattlePhase { .slice(1) .filter(p => p.isActive()).length === 0 ) { - return super.end(); + this.end(true); + return; } // ...or if any player Pokemon has an effect that prevents the checked Pokemon from switching @@ -53,7 +55,8 @@ export class CheckSwitchPhase extends BattlePhase { || pokemon.isTrapped() || globalScene.getPlayerField().some(p => p.getTag(BattlerTagType.COMMANDED)) ) { - return super.end(); + this.end(true); + return; } globalScene.ui.showText( @@ -71,10 +74,17 @@ export class CheckSwitchPhase extends BattlePhase { }, () => { globalScene.ui.setMode(UiMode.MESSAGE); - this.end(); + this.end(true); }, ); }, ); } + + public override end(queuePostSummon = false): void { + if (queuePostSummon) { + globalScene.phaseManager.unshiftNew("PostSummonPhase", this.fieldIndex); + } + super.end(); + } } diff --git a/src/phases/dynamic-phase-marker.ts b/src/phases/dynamic-phase-marker.ts new file mode 100644 index 00000000000..e2b241f29de --- /dev/null +++ b/src/phases/dynamic-phase-marker.ts @@ -0,0 +1,17 @@ +import type { PhaseString } from "#app/@types/phase-types"; +import { Phase } from "#app/phase"; + +/** + * This phase exists for the sole purpose of marking the location and type of a dynamic phase for the phase manager + */ +export class DynamicPhaseMarker extends Phase { + public override readonly phaseName = "DynamicPhaseMarker"; + + /** The type of phase which this phase is a marker for */ + public phaseType: PhaseString; + + constructor(type: PhaseString) { + super(); + this.phaseType = type; + } +} diff --git a/src/phases/egg-hatch-phase.ts b/src/phases/egg-hatch-phase.ts index 946288c4fd8..3f9b999e0c1 100644 --- a/src/phases/egg-hatch-phase.ts +++ b/src/phases/egg-hatch-phase.ts @@ -225,7 +225,7 @@ export class EggHatchPhase extends Phase { } end() { - if (globalScene.phaseManager.findPhase(p => p.is("EggHatchPhase"))) { + if (globalScene.phaseManager.hasPhaseOfType("EggHatchPhase")) { this.eggHatchHandler.clear(); } else { globalScene.time.delayedCall(250, () => globalScene.setModifiersVisible(true)); diff --git a/src/phases/encounter-phase.ts b/src/phases/encounter-phase.ts index 0918ced65e5..9345170e718 100644 --- a/src/phases/encounter-phase.ts +++ b/src/phases/encounter-phase.ts @@ -565,29 +565,6 @@ export class EncounterPhase extends BattlePhase { }); if (![BattleType.TRAINER, BattleType.MYSTERY_ENCOUNTER].includes(globalScene.currentBattle.battleType)) { - enemyField.map(p => - globalScene.phaseManager.pushConditionalPhase( - globalScene.phaseManager.create("PostSummonPhase", p.getBattlerIndex()), - () => { - // if there is not a player party, we can't continue - if (globalScene.getPlayerParty().length === 0) { - return false; - } - // how many player pokemon are on the field ? - const pokemonsOnFieldCount = globalScene.getPlayerParty().filter(p => p.isOnField()).length; - // if it's a 2vs1, there will never be a 2nd pokemon on our field even - const requiredPokemonsOnField = Math.min( - globalScene.getPlayerParty().filter(p => !p.isFainted()).length, - 2, - ); - // if it's a double, there should be 2, otherwise 1 - if (globalScene.currentBattle.double) { - return pokemonsOnFieldCount === requiredPokemonsOnField; - } - return pokemonsOnFieldCount === 1; - }, - ), - ); const ivScannerModifier = globalScene.findModifier(m => m instanceof IvScannerModifier); if (ivScannerModifier) { enemyField.map(p => globalScene.phaseManager.pushNew("ScanIvsPhase", p.getBattlerIndex())); @@ -596,36 +573,30 @@ export class EncounterPhase extends BattlePhase { if (!this.loaded) { const availablePartyMembers = globalScene.getPokemonAllowedInBattle(); + const minPartySize = globalScene.currentBattle.double ? 2 : 1; + const currentBattle = globalScene.currentBattle; + const checkSwitch = + currentBattle.battleType !== BattleType.TRAINER + && (currentBattle.waveIndex > 1 || !globalScene.gameMode.isDaily) + && availablePartyMembers.length > minPartySize; + const phaseManager = globalScene.phaseManager; if (!availablePartyMembers[0].isOnField()) { - globalScene.phaseManager.pushNew("SummonPhase", 0); + phaseManager.pushNew("SummonPhase", 0, true, false, checkSwitch); } - if (globalScene.currentBattle.double) { + if (currentBattle.double) { if (availablePartyMembers.length > 1) { - globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", true); + phaseManager.pushNew("ToggleDoublePositionPhase", true); if (!availablePartyMembers[1].isOnField()) { - globalScene.phaseManager.pushNew("SummonPhase", 1); + phaseManager.pushNew("SummonPhase", 1, true, false, checkSwitch); } } } else { if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) { globalScene.phaseManager.pushNew("ReturnPhase", 1); } - globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", false); - } - - if ( - globalScene.currentBattle.battleType !== BattleType.TRAINER - && (globalScene.currentBattle.waveIndex > 1 || !globalScene.gameMode.isDaily) - ) { - const minPartySize = globalScene.currentBattle.double ? 2 : 1; - if (availablePartyMembers.length > minPartySize) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double); - if (globalScene.currentBattle.double) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double); - } - } + phaseManager.pushNew("ToggleDoublePositionPhase", false); } } handleTutorial(Tutorial.Access_Menu).then(() => super.end()); diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index dd29b97d590..f229f872958 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -84,19 +84,12 @@ export class GameOverPhase extends BattlePhase { globalScene.phaseManager.pushNew("EncounterPhase", true); const availablePartyMembers = globalScene.getPokemonAllowedInBattle().length; - - globalScene.phaseManager.pushNew("SummonPhase", 0); - if (globalScene.currentBattle.double && availablePartyMembers > 1) { - globalScene.phaseManager.pushNew("SummonPhase", 1); - } - if ( + const checkSwitch = globalScene.currentBattle.waveIndex > 1 - && globalScene.currentBattle.battleType !== BattleType.TRAINER - ) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double); - if (globalScene.currentBattle.double && availablePartyMembers > 1) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double); - } + && globalScene.currentBattle.battleType !== BattleType.TRAINER; + globalScene.phaseManager.pushNew("SummonPhase", 0, true, false, checkSwitch); + if (globalScene.currentBattle.double && availablePartyMembers > 1) { + globalScene.phaseManager.pushNew("SummonPhase", 1, true, false, checkSwitch); } globalScene.ui.fadeIn(1250); @@ -267,7 +260,6 @@ export class GameOverPhase extends BattlePhase { .then(success => doGameOver(!globalScene.gameMode.isDaily || !!success)) .catch(_err => { globalScene.phaseManager.clearPhaseQueue(); - globalScene.phaseManager.clearPhaseQueueSplice(); globalScene.phaseManager.unshiftNew("MessagePhase", i18next.t("menu:serverCommunicationFailed"), 2500); // force the game to reload after 2 seconds. setTimeout(() => { diff --git a/src/phases/learn-move-phase.ts b/src/phases/learn-move-phase.ts index 4fc38b08d16..bbd1d0f5a2e 100644 --- a/src/phases/learn-move-phase.ts +++ b/src/phases/learn-move-phase.ts @@ -187,7 +187,7 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { pokemon.usedTMs = []; } pokemon.usedTMs.push(this.moveId); - globalScene.phaseManager.tryRemovePhase(phase => phase.is("SelectModifierPhase")); + globalScene.phaseManager.tryRemovePhase("SelectModifierPhase"); } else if (this.learnMoveType === LearnMoveType.MEMORY) { if (this.cost !== -1) { if (!Overrides.WAIVE_ROLL_FEE_OVERRIDE) { @@ -197,7 +197,7 @@ export class LearnMovePhase extends PlayerPartyMemberPokemonPhase { } globalScene.playSound("se/buy"); } else { - globalScene.phaseManager.tryRemovePhase(phase => phase.is("SelectModifierPhase")); + globalScene.phaseManager.tryRemovePhase("SelectModifierPhase"); } } pokemon.setMove(index, this.moveId); diff --git a/src/phases/move-charge-phase.ts b/src/phases/move-charge-phase.ts index 0c83db10511..5dd75f4bab8 100644 --- a/src/phases/move-charge-phase.ts +++ b/src/phases/move-charge-phase.ts @@ -75,7 +75,7 @@ export class MoveChargePhase extends PokemonPhase { // Otherwise, add the attack portion to the user's move queue to execute next turn. // TODO: This checks status twice for a single-turn usage... if (instantCharge.value) { - globalScene.phaseManager.tryRemovePhase(phase => phase.is("MoveEndPhase") && phase.getPokemon() === user); + globalScene.phaseManager.tryRemovePhase("MoveEndPhase", phase => phase.getPokemon() === user); globalScene.phaseManager.unshiftNew("MovePhase", user, [this.targetIndex], this.move, this.useMode); } else { user.pushMoveQueue({ move: move.id, targets: [this.targetIndex], useMode: this.useMode }); diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 18e25b328f8..be6d0164698 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -1,7 +1,6 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; -import type { Phase } from "#app/phase"; import { ConditionalProtectTag } from "#data/arena-tag"; import { MoveAnim } from "#data/battle-anims"; import { DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag, TypeBoostTag } from "#data/battler-tags"; @@ -17,6 +16,7 @@ import { MoveCategory } from "#enums/move-category"; import { MoveEffectTrigger } from "#enums/move-effect-trigger"; import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import { MoveResult } from "#enums/move-result"; import { MoveTarget } from "#enums/move-target"; import { isReflected, MoveUseMode } from "#enums/move-use-mode"; @@ -67,12 +67,6 @@ export class MoveEffectPhase extends PokemonPhase { /** Is this the last strike of a move? */ private lastHit: boolean; - /** - * Phases queued during moves; used to add a new MovePhase for reflected moves after triggering. - * TODO: Remove this and move the reflection logic to ability-side - */ - private queuedPhases: Phase[] = []; - /** * @param useMode - The {@linkcode MoveUseMode} corresponding to how this move was used. */ @@ -148,7 +142,7 @@ export class MoveEffectPhase extends PokemonPhase { } /** - * Queue the phaes that should occur when the target reflects the move back to the user + * Queue the phases that should occur when the target reflects the move back to the user * @param user - The {@linkcode Pokemon} using this phase's invoked move * @param target - The {@linkcode Pokemon} that is reflecting the move * TODO: Rework this to use `onApply` of Magic Coat @@ -159,24 +153,21 @@ export class MoveEffectPhase extends PokemonPhase { : [user.getBattlerIndex()]; // TODO: ability displays should be handled by the ability if (!target.getTag(BattlerTagType.MAGIC_COAT)) { - this.queuedPhases.push( - globalScene.phaseManager.create( - "ShowAbilityPhase", - target.getBattlerIndex(), - target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"), - ), + globalScene.phaseManager.unshiftNew( + "ShowAbilityPhase", + target.getBattlerIndex(), + target.getPassiveAbility().hasAttr("ReflectStatusMoveAbAttr"), ); - this.queuedPhases.push(globalScene.phaseManager.create("HideAbilityPhase")); + globalScene.phaseManager.unshiftNew("HideAbilityPhase"); } - this.queuedPhases.push( - globalScene.phaseManager.create( - "MovePhase", - target, - newTargets, - new PokemonMove(this.move.id), - MoveUseMode.REFLECTED, - ), + globalScene.phaseManager.unshiftNew( + "MovePhase", + target, + newTargets, + new PokemonMove(this.move.id), + MoveUseMode.REFLECTED, + MovePhaseTimingModifier.FIRST, ); } @@ -344,9 +335,6 @@ export class MoveEffectPhase extends PokemonPhase { return; } - if (this.queuedPhases.length > 0) { - globalScene.phaseManager.appendToPhase(this.queuedPhases, "MoveEndPhase"); - } const moveType = user.getMoveType(this.move, true); if (this.move.category !== MoveCategory.STATUS && !user.stellarTypesBoosted.includes(moveType)) { user.stellarTypesBoosted.push(moveType); @@ -905,10 +893,7 @@ export class MoveEffectPhase extends PokemonPhase { * @param target - The {@linkcode Pokemon} that fainted */ protected onFaintTarget(user: Pokemon, target: Pokemon): void { - // set splice index here, so future scene queues happen before FaintedPhase - globalScene.phaseManager.setPhaseQueueSplice(); - - globalScene.phaseManager.unshiftNew("FaintPhase", target.getBattlerIndex(), false, user); + globalScene.phaseManager.queueFaintPhase(target.getBattlerIndex(), false, user); target.destroySubstitute(); target.lapseTag(BattlerTagType.COMMANDED); diff --git a/src/phases/move-header-phase.ts b/src/phases/move-header-phase.ts index 5c69dcd1217..5b8a6f998a1 100644 --- a/src/phases/move-header-phase.ts +++ b/src/phases/move-header-phase.ts @@ -5,8 +5,8 @@ import { BattlePhase } from "#phases/battle-phase"; export class MoveHeaderPhase extends BattlePhase { public readonly phaseName = "MoveHeaderPhase"; - public pokemon: Pokemon; public move: PokemonMove; + public pokemon: Pokemon; constructor(pokemon: Pokemon, move: PokemonMove) { super(); @@ -15,6 +15,10 @@ export class MoveHeaderPhase extends BattlePhase { this.move = move; } + public getPokemon(): Pokemon { + return this.pokemon; + } + canMove(): boolean { return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon); } diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 96943065ff0..5e85401db77 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -3,6 +3,7 @@ import { MOVE_COLOR } from "#app/constants/colors"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; +import { PokemonPhase } from "#app/phases/pokemon-phase"; import { CenterOfAttentionTag } from "#data/battler-tags"; import { SpeciesFormChangePreMoveTrigger } from "#data/form-change-triggers"; import { getStatusEffectActivationText, getStatusEffectHealText } from "#data/status-effect"; @@ -15,6 +16,7 @@ import { BattlerTagType } from "#enums/battler-tag-type"; import { CommonAnim } from "#enums/move-anims-common"; import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; +import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import { MoveResult } from "#enums/move-result"; import { isIgnorePP, isIgnoreStatus, isReflected, isVirtual, MoveUseMode } from "#enums/move-use-mode"; import { PokemonType } from "#enums/pokemon-type"; @@ -24,20 +26,19 @@ import type { Pokemon } from "#field/pokemon"; import { applyMoveAttrs } from "#moves/apply-attrs"; import { frenzyMissFunc } from "#moves/move-utils"; import type { PokemonMove } from "#moves/pokemon-move"; -import { BattlePhase } from "#phases/battle-phase"; import type { TurnMove } from "#types/turn-move"; import { NumberHolder } from "#utils/common"; import { enumValueToKey } from "#utils/enums"; import i18next from "i18next"; -export class MovePhase extends BattlePhase { +export class MovePhase extends PokemonPhase { public readonly phaseName = "MovePhase"; protected _pokemon: Pokemon; - protected _move: PokemonMove; + public move: PokemonMove; protected _targets: BattlerIndex[]; public readonly useMode: MoveUseMode; // Made public for quash - /** Whether the current move is forced last (used for Quash). */ - protected forcedLast: boolean; + /** The timing modifier of the move (used by Quash and to force called moves to the front of their queue) */ + public timingModifier: MovePhaseTimingModifier; /** Whether the current move should fail but still use PP. */ protected failed = false; /** Whether the current move should fail and retain PP. */ @@ -59,14 +60,6 @@ export class MovePhase extends BattlePhase { this._pokemon = pokemon; } - public get move(): PokemonMove { - return this._move; - } - - protected set move(move: PokemonMove) { - this._move = move; - } - public get targets(): BattlerIndex[] { return this._targets; } @@ -81,16 +74,22 @@ export class MovePhase extends BattlePhase { * @param move - The {@linkcode PokemonMove} to use * @param useMode - The {@linkcode MoveUseMode} corresponding to this move's means of execution (usually `MoveUseMode.NORMAL`). * Not marked optional to ensure callers correctly pass on `useModes`. - * @param forcedLast - Whether to force this phase to occur last in order (for {@linkcode MoveId.QUASH}); default `false` + * @param timingModifier - The {@linkcode MovePhaseTimingModifier} for the move; Default {@linkcode MovePhaseTimingModifier.NORMAL} */ - constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, useMode: MoveUseMode, forcedLast = false) { - super(); + constructor( + pokemon: Pokemon, + targets: BattlerIndex[], + move: PokemonMove, + useMode: MoveUseMode, + timingModifier: MovePhaseTimingModifier = MovePhaseTimingModifier.NORMAL, + ) { + super(pokemon.getBattlerIndex()); this.pokemon = pokemon; this.targets = targets; this.move = move; this.useMode = useMode; - this.forcedLast = forcedLast; + this.timingModifier = timingModifier; this.moveHistoryEntry = { move: MoveId.NONE, targets, @@ -121,14 +120,6 @@ export class MovePhase extends BattlePhase { this.cancelled = true; } - /** - * Shows whether the current move has been forced to the end of the turn - * Needed for speed order, see {@linkcode MoveId.QUASH} - */ - public isForcedLast(): boolean { - return this.forcedLast; - } - public start(): void { super.start(); diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 4f50b40c965..bb3f4a92033 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -48,7 +48,6 @@ export class MysteryEncounterPhase extends Phase { // Clears out queued phases that are part of standard battle globalScene.phaseManager.clearPhaseQueue(); - globalScene.phaseManager.clearPhaseQueueSplice(); const encounter = globalScene.currentBattle.mysteryEncounter!; encounter.updateSeedOffset(); @@ -233,9 +232,7 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase { }); // Remove any status tick phases - while (globalScene.phaseManager.findPhase(p => p.is("PostTurnStatusEffectPhase"))) { - globalScene.phaseManager.tryRemovePhase(p => p.is("PostTurnStatusEffectPhase")); - } + globalScene.phaseManager.removeAllPhasesOfType("PostTurnStatusEffectPhase"); // The total number of Pokemon in the player's party that can legally fight const legalPlayerPokemon = globalScene.getPokemonAllowedInBattle(); @@ -412,16 +409,21 @@ export class MysteryEncounterBattlePhase extends Phase { } const availablePartyMembers = globalScene.getPlayerParty().filter(p => p.isAllowedInBattle()); + const minPartySize = globalScene.currentBattle.double ? 2 : 1; + const checkSwitch = + encounterMode !== MysteryEncounterMode.TRAINER_BATTLE + && !this.disableSwitch + && availablePartyMembers.length > minPartySize; if (!availablePartyMembers[0].isOnField()) { - globalScene.phaseManager.pushNew("SummonPhase", 0); + globalScene.phaseManager.pushNew("SummonPhase", 0, true, false, checkSwitch); } if (globalScene.currentBattle.double) { if (availablePartyMembers.length > 1) { globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", true); if (!availablePartyMembers[1].isOnField()) { - globalScene.phaseManager.pushNew("SummonPhase", 1); + globalScene.phaseManager.pushNew("SummonPhase", 1, true, false, checkSwitch); } } } else { @@ -432,16 +434,6 @@ export class MysteryEncounterBattlePhase extends Phase { globalScene.phaseManager.pushNew("ToggleDoublePositionPhase", false); } - if (encounterMode !== MysteryEncounterMode.TRAINER_BATTLE && !this.disableSwitch) { - const minPartySize = globalScene.currentBattle.double ? 2 : 1; - if (availablePartyMembers.length > minPartySize) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double); - if (globalScene.currentBattle.double) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double); - } - } - } - this.end(); } @@ -540,7 +532,7 @@ export class MysteryEncounterRewardsPhase extends Phase { if (encounter.doEncounterRewards) { encounter.doEncounterRewards(); } else if (this.addHealPhase) { - globalScene.phaseManager.tryRemovePhase(p => p.is("SelectModifierPhase")); + globalScene.phaseManager.removeAllPhasesOfType("SelectModifierPhase"); globalScene.phaseManager.unshiftNew("SelectModifierPhase", 0, undefined, { fillRemaining: false, rerollMultiplier: -1, diff --git a/src/phases/new-battle-phase.ts b/src/phases/new-battle-phase.ts index b9a57161bd0..7b5d132ccd2 100644 --- a/src/phases/new-battle-phase.ts +++ b/src/phases/new-battle-phase.ts @@ -6,12 +6,7 @@ export class NewBattlePhase extends BattlePhase { start() { super.start(); - // cull any extra `NewBattle` phases from the queue. - globalScene.phaseManager.phaseQueue = globalScene.phaseManager.phaseQueue.filter( - phase => !phase.is("NewBattlePhase"), - ); - // `phaseQueuePrepend` is private, so we have to use this inefficient loop. - while (globalScene.phaseManager.tryRemoveUnshiftedPhase(phase => phase.is("NewBattlePhase"))) {} + globalScene.phaseManager.removeAllPhasesOfType("NewBattlePhase"); globalScene.newBattle(); diff --git a/src/phases/party-member-pokemon-phase.ts b/src/phases/party-member-pokemon-phase.ts index 9536dafda60..545799cf36a 100644 --- a/src/phases/party-member-pokemon-phase.ts +++ b/src/phases/party-member-pokemon-phase.ts @@ -22,4 +22,8 @@ export abstract class PartyMemberPokemonPhase extends FieldPhase { getPokemon(): Pokemon { return this.getParty()[this.partyMemberIndex]; } + + isPlayer(): boolean { + return this.player; + } } diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index 02bb3a0b968..258ddb0b624 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -64,7 +64,8 @@ export class PokemonHealPhase extends CommonAnimPhase { } const hasMessage = !!this.message; - const healOrDamage = !pokemon.isFullHp() || this.hpHealed < 0; + const canRestorePP = this.fullRestorePP && pokemon.getMoveset().some(mv => mv.ppUsed > 0); + const healOrDamage = !pokemon.isFullHp() || this.hpHealed < 0 || canRestorePP; const healBlock = pokemon.getTag(BattlerTagType.HEAL_BLOCK) as HealBlockTag; let lastStatusEffect = StatusEffect.NONE; diff --git a/src/phases/post-summon-activate-ability-phase.ts b/src/phases/post-summon-activate-ability-phase.ts index 5f790c01ad1..a2b6c059bee 100644 --- a/src/phases/post-summon-activate-ability-phase.ts +++ b/src/phases/post-summon-activate-ability-phase.ts @@ -6,8 +6,8 @@ import { PostSummonPhase } from "#phases/post-summon-phase"; * Helper to {@linkcode PostSummonPhase} which applies abilities */ export class PostSummonActivateAbilityPhase extends PostSummonPhase { - private priority: number; - private passive: boolean; + private readonly priority: number; + private readonly passive: boolean; constructor(battlerIndex: BattlerIndex, priority: number, passive: boolean) { super(battlerIndex); diff --git a/src/phases/post-summon-phase.ts b/src/phases/post-summon-phase.ts index 5de068f2ae5..136f2fbd601 100644 --- a/src/phases/post-summon-phase.ts +++ b/src/phases/post-summon-phase.ts @@ -1,21 +1,35 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; +import type { PhaseString } from "#app/@types/phase-types"; import { globalScene } from "#app/global-scene"; import { EntryHazardTag } from "#data/arena-tag"; import { MysteryEncounterPostSummonTag } from "#data/battler-tags"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import type { BattlerIndex } from "#enums/battler-index"; import { BattlerTagType } from "#enums/battler-tag-type"; import { StatusEffect } from "#enums/status-effect"; import { PokemonPhase } from "#phases/pokemon-phase"; export class PostSummonPhase extends PokemonPhase { public readonly phaseName = "PostSummonPhase"; + /** Used to determine whether to push or unshift {@linkcode PostSummonActivateAbilityPhase}s */ + public readonly source: PhaseString; + + constructor(battlerIndex?: BattlerIndex | number, source: PhaseString = "SwitchSummonPhase") { + super(battlerIndex); + this.source = source; + } + start() { super.start(); const pokemon = this.getPokemon(); - + console.log("Ran PSP for:", pokemon.name); if (pokemon.status?.effect === StatusEffect.TOXIC) { pokemon.status.toxicTurnCount = 0; } + + globalScene.arena.applyTags(ArenaTagType.PENDING_HEAL, false, pokemon); + globalScene.arena.applyTags(EntryHazardTag, false, pokemon); // If this is mystery encounter and has post summon phase tag, apply post summon effects @@ -25,8 +39,7 @@ export class PostSummonPhase extends PokemonPhase { ) { pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON); } - - const field = pokemon.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); + const field = pokemon.isPlayer() ? globalScene.getPlayerField(true) : globalScene.getEnemyField(true); for (const p of field) { applyAbAttrs("CommanderAbAttr", { pokemon: p }); } diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index ef53b16cc56..920ff2252b8 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -9,7 +9,6 @@ import { BattleSpec } from "#enums/battle-spec"; import { BattlerTagType } from "#enums/battler-tag-type"; import type { Pokemon } from "#field/pokemon"; import { BattlePhase } from "#phases/battle-phase"; -import type { MovePhase } from "#phases/move-phase"; export class QuietFormChangePhase extends BattlePhase { public readonly phaseName = "QuietFormChangePhase"; @@ -170,12 +169,7 @@ export class QuietFormChangePhase extends BattlePhase { this.pokemon.initBattleInfo(); this.pokemon.cry(); - const movePhase = globalScene.phaseManager.findPhase( - p => p.is("MovePhase") && p.pokemon === this.pokemon, - ) as MovePhase; - if (movePhase) { - movePhase.cancel(); - } + globalScene.phaseManager.cancelMove(p => p.pokemon === this.pokemon); } if (this.formChange.trigger instanceof SpeciesFormChangeTeraTrigger) { const params = { pokemon: this.pokemon }; diff --git a/src/phases/select-starter-phase.ts b/src/phases/select-starter-phase.ts index 27e2150fd06..e923efaa678 100644 --- a/src/phases/select-starter-phase.ts +++ b/src/phases/select-starter-phase.ts @@ -4,11 +4,10 @@ import { Phase } from "#app/phase"; import { SpeciesFormChangeMoveLearnedTrigger } from "#data/form-change-triggers"; import { Gender } from "#data/gender"; import { ChallengeType } from "#enums/challenge-type"; -import type { SpeciesId } from "#enums/species-id"; import { UiMode } from "#enums/ui-mode"; import { overrideHeldItems, overrideModifiers } from "#modifiers/modifier"; -import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler"; -import type { Starter } from "#ui/starter-select-ui-handler"; +import type { Starter } from "#types/save-data"; +import { SaveSlotUiMode } from "#ui/handlers/save-slot-select-ui-handler"; import { applyChallenges } from "#utils/challenge-utils"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; @@ -44,33 +43,32 @@ export class SelectStarterPhase extends Phase { const loadPokemonAssets: Promise[] = []; starters.forEach((starter: Starter, i: number) => { if (!i && Overrides.STARTER_SPECIES_OVERRIDE) { - starter.species = getPokemonSpecies(Overrides.STARTER_SPECIES_OVERRIDE as SpeciesId); + starter.speciesId = Overrides.STARTER_SPECIES_OVERRIDE; } - const starterProps = globalScene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); - let starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); + const species = getPokemonSpecies(starter.speciesId); + let starterFormIndex = starter.formIndex; if ( - starter.species.speciesId in Overrides.STARTER_FORM_OVERRIDES - && Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId] != null - && starter.species.forms[Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]!] + starter.speciesId in Overrides.STARTER_FORM_OVERRIDES + && Overrides.STARTER_FORM_OVERRIDES[starter.speciesId] != null + && species.forms[Overrides.STARTER_FORM_OVERRIDES[starter.speciesId]!] ) { - starterFormIndex = Overrides.STARTER_FORM_OVERRIDES[starter.species.speciesId]!; + starterFormIndex = Overrides.STARTER_FORM_OVERRIDES[starter.speciesId]!; } let starterGender = - starter.species.malePercent !== null ? (!starterProps.female ? Gender.MALE : Gender.FEMALE) : Gender.GENDERLESS; + species.malePercent !== null ? (!starter.female ? Gender.MALE : Gender.FEMALE) : Gender.GENDERLESS; if (Overrides.GENDER_OVERRIDE !== null) { starterGender = Overrides.GENDER_OVERRIDE; } - const starterIvs = globalScene.gameData.dexData[starter.species.speciesId].ivs.slice(0); const starterPokemon = globalScene.addPlayerPokemon( - starter.species, + species, globalScene.gameMode.getStartingLevel(), starter.abilityIndex, starterFormIndex, starterGender, - starterProps.shiny, - starterProps.variant, - starterIvs, + starter.shiny, + starter.variant, + starter.ivs, starter.nature, ); starter.moveset && starterPokemon.tryPopulateMoveset(starter.moveset); @@ -78,7 +76,7 @@ export class SelectStarterPhase extends Phase { starterPokemon.passive = true; } starterPokemon.luck = globalScene.gameData.getDexAttrLuck( - globalScene.gameData.dexData[starter.species.speciesId].caughtAttr, + globalScene.gameData.dexData[species.speciesId].caughtAttr, ); if (starter.pokerus) { starterPokemon.pokerus = true; diff --git a/src/phases/stat-stage-change-phase.ts b/src/phases/stat-stage-change-phase.ts index 2731c037d5f..3c2d1cb5fad 100644 --- a/src/phases/stat-stage-change-phase.ts +++ b/src/phases/stat-stage-change-phase.ts @@ -223,10 +223,7 @@ export class StatStageChangePhase extends PokemonPhase { }); // Look for any other stat change phases; if this is the last one, do White Herb check - const existingPhase = globalScene.phaseManager.findPhase( - p => p.is("StatStageChangePhase") && p.battlerIndex === this.battlerIndex, - ); - if (!existingPhase?.is("StatStageChangePhase")) { + if (!globalScene.phaseManager.hasPhaseOfType("StatStageChangePhase", p => p.battlerIndex === this.battlerIndex)) { // Apply White Herb if needed const whiteHerb = globalScene.applyModifier( ResetNegativeStatStageModifier, @@ -297,49 +294,6 @@ export class StatStageChangePhase extends PokemonPhase { } } - aggregateStatStageChanges(): void { - const accEva: BattleStat[] = [Stat.ACC, Stat.EVA]; - const isAccEva = accEva.some(s => this.stats.includes(s)); - let existingPhase: StatStageChangePhase; - if (this.stats.length === 1) { - while ( - (existingPhase = globalScene.phaseManager.findPhase( - p => - p.is("StatStageChangePhase") - && p.battlerIndex === this.battlerIndex - && p.stats.length === 1 - && p.stats[0] === this.stats[0] - && p.selfTarget === this.selfTarget - && p.showMessage === this.showMessage - && p.ignoreAbilities === this.ignoreAbilities, - ) as StatStageChangePhase) - ) { - this.stages += existingPhase.stages; - - if (!globalScene.phaseManager.tryRemovePhase(p => p === existingPhase)) { - break; - } - } - } - while ( - (existingPhase = globalScene.phaseManager.findPhase( - p => - p.is("StatStageChangePhase") - && p.battlerIndex === this.battlerIndex - && p.selfTarget === this.selfTarget - && accEva.some(s => p.stats.includes(s)) === isAccEva - && p.stages === this.stages - && p.showMessage === this.showMessage - && p.ignoreAbilities === this.ignoreAbilities, - ) as StatStageChangePhase) - ) { - this.stats.push(...existingPhase.stats); - if (!globalScene.phaseManager.tryRemovePhase(p => p === existingPhase)) { - break; - } - } - } - getStatStageChangeMessages(stats: BattleStat[], stages: number, relStages: number[]): string[] { const messages: string[] = []; diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index dda70f46ec9..26a8ba40ffc 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -16,12 +16,14 @@ import i18next from "i18next"; export class SummonPhase extends PartyMemberPokemonPhase { // The union type is needed to keep typescript happy as these phases extend from SummonPhase public readonly phaseName: "SummonPhase" | "SummonMissingPhase" | "SwitchSummonPhase" | "ReturnPhase" = "SummonPhase"; - private loaded: boolean; + private readonly loaded: boolean; + private readonly checkSwitch: boolean; - constructor(fieldIndex: number, player = true, loaded = false) { + constructor(fieldIndex: number, player = true, loaded = false, checkSwitch = false) { super(fieldIndex, player); this.loaded = loaded; + this.checkSwitch = checkSwitch; } start() { @@ -288,7 +290,17 @@ export class SummonPhase extends PartyMemberPokemonPhase { } queuePostSummon(): void { - globalScene.phaseManager.pushNew("PostSummonPhase", this.getPokemon().getBattlerIndex()); + if (this.checkSwitch) { + globalScene.phaseManager.pushNew( + "CheckSwitchPhase", + this.getPokemon().getFieldIndex(), + globalScene.currentBattle.double, + ); + } else { + globalScene.phaseManager.pushNew("PostSummonPhase", this.getPokemon().getBattlerIndex(), this.phaseName); + } + + globalScene.phaseManager.tryAddEnemyPostSummonPhases(); } end() { @@ -296,4 +308,8 @@ export class SummonPhase extends PartyMemberPokemonPhase { super.end(); } + + public getFieldIndex(): number { + return this.fieldIndex; + } } diff --git a/src/phases/switch-phase.ts b/src/phases/switch-phase.ts index 83a699b6b08..9ab06ec827c 100644 --- a/src/phases/switch-phase.ts +++ b/src/phases/switch-phase.ts @@ -1,5 +1,4 @@ import { globalScene } from "#app/global-scene"; -import { DynamicPhaseType } from "#enums/dynamic-phase-type"; import { SwitchType } from "#enums/switch-type"; import { UiMode } from "#enums/ui-mode"; import { BattlePhase } from "#phases/battle-phase"; @@ -77,14 +76,6 @@ export class SwitchPhase extends BattlePhase { fieldIndex, (slotIndex: number, option: PartyOption) => { if (slotIndex >= globalScene.currentBattle.getBattlerCount() && slotIndex < 6) { - // Remove any pre-existing PostSummonPhase under the same field index. - // Pre-existing PostSummonPhases may occur when this phase is invoked during a prompt to switch at the start of a wave. - // TODO: Separate the animations from `SwitchSummonPhase` and co. into another phase and use that on initial switch - this is a band-aid fix - globalScene.phaseManager.tryRemoveDynamicPhase( - DynamicPhaseType.POST_SUMMON, - p => p.is("PostSummonPhase") && p.player && p.fieldIndex === this.fieldIndex, - "all", - ); const switchType = option === PartyOption.PASS_BATON ? SwitchType.BATON_PASS : this.switchType; globalScene.phaseManager.unshiftNew("SwitchSummonPhase", switchType, fieldIndex, slotIndex, this.doReturn); } diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index ac47068c619..8cc7843b55f 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -241,11 +241,11 @@ export class SwitchSummonPhase extends SummonPhase { globalScene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); // Reverts to weather-based forms when weather suppressors (Cloud Nine/Air Lock) are switched out - globalScene.arena.triggerWeatherBasedFormChanges(); + globalScene.arena.triggerWeatherBasedFormChanges(pokemon); } queuePostSummon(): void { - globalScene.phaseManager.startNewDynamicPhase("PostSummonPhase", this.getPokemon().getBattlerIndex()); + globalScene.phaseManager.unshiftNew("PostSummonPhase", this.getPokemon().getBattlerIndex()); } /** diff --git a/src/phases/title-phase.ts b/src/phases/title-phase.ts index 414be4c820c..9535ea1c8e9 100644 --- a/src/phases/title-phase.ts +++ b/src/phases/title-phase.ts @@ -19,6 +19,7 @@ import type { SessionSaveData } from "#types/save-data"; import type { OptionSelectConfig, OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import { SaveSlotUiMode } from "#ui/save-slot-select-ui-handler"; import { isLocal, isLocalServerConnected } from "#utils/common"; +import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; export class TitlePhase extends Phase { @@ -218,23 +219,19 @@ export class TitlePhase extends Phase { const party = globalScene.getPlayerParty(); const loadPokemonAssets: Promise[] = []; for (const starter of starters) { - const starterProps = globalScene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); - const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); + const species = getPokemonSpecies(starter.speciesId); + const starterFormIndex = starter.formIndex; const starterGender = - starter.species.malePercent !== null - ? !starterProps.female - ? Gender.MALE - : Gender.FEMALE - : Gender.GENDERLESS; + species.malePercent !== null ? (starter.female ? Gender.FEMALE : Gender.MALE) : Gender.GENDERLESS; const starterPokemon = globalScene.addPlayerPokemon( - starter.species, + species, startingLevel, starter.abilityIndex, starterFormIndex, starterGender, - starterProps.shiny, - starterProps.variant, - undefined, + starter.shiny, + starter.variant, + starter.ivs, starter.nature, ); starterPokemon.setVisible(false); @@ -315,23 +312,15 @@ export class TitlePhase extends Phase { if (this.loaded) { const availablePartyMembers = globalScene.getPokemonAllowedInBattle().length; - - globalScene.phaseManager.pushNew("SummonPhase", 0, true, true); - if (globalScene.currentBattle.double && availablePartyMembers > 1) { - globalScene.phaseManager.pushNew("SummonPhase", 1, true, true); - } - - if ( + const minPartySize = globalScene.currentBattle.double ? 2 : 1; + const checkSwitch = globalScene.currentBattle.battleType !== BattleType.TRAINER && (globalScene.currentBattle.waveIndex > 1 || !globalScene.gameMode.isDaily) - ) { - const minPartySize = globalScene.currentBattle.double ? 2 : 1; - if (availablePartyMembers > minPartySize) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 0, globalScene.currentBattle.double); - if (globalScene.currentBattle.double) { - globalScene.phaseManager.pushNew("CheckSwitchPhase", 1, globalScene.currentBattle.double); - } - } + && availablePartyMembers > minPartySize; + + globalScene.phaseManager.pushNew("SummonPhase", 0, true, true, checkSwitch); + if (globalScene.currentBattle.double && availablePartyMembers > 1) { + globalScene.phaseManager.pushNew("SummonPhase", 1, true, true, checkSwitch); } } diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index 463f26e73a2..22ebbd2607b 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -25,6 +25,7 @@ export class TurnEndPhase extends FieldPhase { globalScene.currentBattle.incrementTurn(); globalScene.eventTarget.dispatchEvent(new TurnEndEvent(globalScene.currentBattle.turn)); + globalScene.phaseManager.dynamicQueueManager.clearLastTurnOrder(); globalScene.phaseManager.hideAbilityBar(); diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 1733901d527..cd45a73c813 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -1,89 +1,31 @@ import { applyAbAttrs } from "#abilities/apply-ab-attrs"; import type { TurnCommand } from "#app/battle"; import { globalScene } from "#app/global-scene"; -import { TrickRoomTag } from "#data/arena-tag"; -import { allMoves } from "#data/data-lists"; -import { BattlerIndex } from "#enums/battler-index"; +import { ArenaTagSide } from "#enums/arena-tag-side"; +import type { BattlerIndex } from "#enums/battler-index"; import { Command } from "#enums/command"; -import { Stat } from "#enums/stat"; import { SwitchType } from "#enums/switch-type"; import type { Pokemon } from "#field/pokemon"; import { BypassSpeedChanceModifier } from "#modifiers/modifier"; import { PokemonMove } from "#moves/pokemon-move"; import { FieldPhase } from "#phases/field-phase"; -import { BooleanHolder, randSeedShuffle } from "#utils/common"; +import { inSpeedOrder } from "#utils/speed-order-generator"; export class TurnStartPhase extends FieldPhase { public readonly phaseName = "TurnStartPhase"; /** - * Helper method to retrieve the current speed order of the combattants. - * It also checks for Trick Room and reverses the array if it is present. - * @returns The {@linkcode BattlerIndex}es of all on-field Pokemon, sorted in speed order. - * @todo Make this private - */ - getSpeedOrder(): BattlerIndex[] { - const playerField = globalScene.getPlayerField().filter(p => p.isActive()); - const enemyField = globalScene.getEnemyField().filter(p => p.isActive()); - - // Shuffle the list before sorting so speed ties produce random results - // This is seeded with the current turn to prevent turn order varying - // based on how long since you last reloaded. - let orderedTargets = (playerField as Pokemon[]).concat(enemyField); - globalScene.executeWithSeedOffset( - () => { - orderedTargets = randSeedShuffle(orderedTargets); - }, - globalScene.currentBattle.turn, - globalScene.waveSeed, - ); - - // Check for Trick Room and reverse sort order if active. - // Notably, Pokerogue does NOT have the "outspeed trick room" glitch at >1809 spd. - const speedReversed = new BooleanHolder(false); - globalScene.arena.applyTags(TrickRoomTag, false, speedReversed); - - orderedTargets.sort((a: Pokemon, b: Pokemon) => { - const aSpeed = a.getEffectiveStat(Stat.SPD); - const bSpeed = b.getEffectiveStat(Stat.SPD); - - return speedReversed.value ? aSpeed - bSpeed : bSpeed - aSpeed; - }); - - return orderedTargets.map(t => t.getFieldIndex() + (t.isEnemy() ? BattlerIndex.ENEMY : BattlerIndex.PLAYER)); - } - - /** - * This takes the result of {@linkcode getSpeedOrder} and applies priority / bypass speed attributes to it. - * This also considers the priority levels of various commands and changes the result of `getSpeedOrder` based on such. - * @returns The `BattlerIndex`es of all on-field Pokemon sorted in action order. + * Returns an ordering of the current field based on command priority + * @returns The sequence of commands for this turn */ getCommandOrder(): BattlerIndex[] { - let moveOrder = this.getSpeedOrder(); - // The creation of the battlerBypassSpeed object contains checks for the ability Quick Draw and the held item Quick Claw - // The ability Mycelium Might disables Quick Claw's activation when using a status move - // This occurs before the main loop because of battles with more than two Pokemon - const battlerBypassSpeed = {}; - - globalScene.getField(true).forEach(p => { - const bypassSpeed = new BooleanHolder(false); - const canCheckHeldItems = new BooleanHolder(true); - applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon: p, bypass: bypassSpeed }); - applyAbAttrs("PreventBypassSpeedChanceAbAttr", { - pokemon: p, - bypass: bypassSpeed, - canCheckHeldItems, - }); - if (canCheckHeldItems.value) { - globalScene.applyModifiers(BypassSpeedChanceModifier, p.isPlayer(), p, bypassSpeed); - } - battlerBypassSpeed[p.getBattlerIndex()] = bypassSpeed; - }); + const playerField = globalScene.getPlayerField(true).map(p => p.getBattlerIndex()); + const enemyField = globalScene.getEnemyField(true).map(p => p.getBattlerIndex()); + const orderedTargets: BattlerIndex[] = playerField.concat(enemyField); // The function begins sorting orderedTargets based on command priority, move priority, and possible speed bypasses. // Non-FIGHT commands (SWITCH, BALL, RUN) have a higher command priority and will always occur before any FIGHT commands. - moveOrder = moveOrder.slice(0); - moveOrder.sort((a, b) => { + orderedTargets.sort((a, b) => { const aCommand = globalScene.currentBattle.turnCommands[a]; const bCommand = globalScene.currentBattle.turnCommands[b]; @@ -94,41 +36,14 @@ export class TurnStartPhase extends FieldPhase { if (bCommand?.command === Command.FIGHT) { return -1; } - } else if (aCommand?.command === Command.FIGHT) { - const aMove = allMoves[aCommand.move!.move]; - const bMove = allMoves[bCommand!.move!.move]; - - const aUser = globalScene.getField(true).find(p => p.getBattlerIndex() === a)!; - const bUser = globalScene.getField(true).find(p => p.getBattlerIndex() === b)!; - - const aPriority = aMove.getPriority(aUser, false); - const bPriority = bMove.getPriority(bUser, false); - - // The game now checks for differences in priority levels. - // If the moves share the same original priority bracket, it can check for differences in battlerBypassSpeed and return the result. - // This conditional is used to ensure that Quick Claw can still activate with abilities like Stall and Mycelium Might (attack moves only) - // Otherwise, the game returns the user of the move with the highest priority. - const isSameBracket = Math.ceil(aPriority) - Math.ceil(bPriority) === 0; - if (aPriority !== bPriority) { - if (isSameBracket && battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { - return battlerBypassSpeed[a].value ? -1 : 1; - } - return aPriority < bPriority ? 1 : -1; - } } - // If there is no difference between the move's calculated priorities, - // check for differences in battlerBypassSpeed and returns the result. - if (battlerBypassSpeed[a].value !== battlerBypassSpeed[b].value) { - return battlerBypassSpeed[a].value ? -1 : 1; - } - - const aIndex = moveOrder.indexOf(a); - const bIndex = moveOrder.indexOf(b); + const aIndex = orderedTargets.indexOf(a); + const bIndex = orderedTargets.indexOf(b); return aIndex < bIndex ? -1 : aIndex > bIndex ? 1 : 0; }); - return moveOrder; + return orderedTargets; } // TODO: Refactor this alongside `CommandPhase.handleCommand` to use SEPARATE METHODS @@ -139,9 +54,8 @@ export class TurnStartPhase extends FieldPhase { const field = globalScene.getField(); const moveOrder = this.getCommandOrder(); - for (const o of this.getSpeedOrder()) { - const pokemon = field[o]; - const preTurnCommand = globalScene.currentBattle.preTurnCommands[o]; + for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) { + const preTurnCommand = globalScene.currentBattle.preTurnCommands[pokemon.getBattlerIndex()]; if (preTurnCommand?.skip) { continue; @@ -154,6 +68,10 @@ export class TurnStartPhase extends FieldPhase { } const phaseManager = globalScene.phaseManager; + for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) { + applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon }); + globalScene.applyModifiers(BypassSpeedChanceModifier, pokemon.isPlayer(), pokemon); + } moveOrder.forEach((o, index) => { const pokemon = field[o]; @@ -178,13 +96,8 @@ export class TurnStartPhase extends FieldPhase { // TODO: Re-order these phases to be consistent with mainline turn order: // https://www.smogon.com/forums/threads/sword-shield-battle-mechanics-research.3655528/page-64#post-9244179 - phaseManager.pushNew("WeatherEffectPhase"); - phaseManager.pushNew("PositionalTagPhase"); - phaseManager.pushNew("BerryPhase"); - - phaseManager.pushNew("CheckStatusEffectPhase", moveOrder); - - phaseManager.pushNew("TurnEndPhase"); + // TODO: In an ideal world, this is handled by the phase manager. The change is nontrivial due to the ordering of post-turn phases like those queued by VictoryPhase + globalScene.phaseManager.queueTurnEndPhases(); /* * `this.end()` will call `PhaseManager#shiftPhase()`, which dumps everything from `phaseQueuePrepend` diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index b6a5bacc7ba..724a14e33c5 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -171,18 +171,18 @@ export async function initI18n(): Promise { i18next.use(new KoreanPostpositionProcessor()); await i18next.init({ fallbackLng: { - "es-MX": ["es-ES", "en"], + "es-419": ["es-ES", "en"], default: ["en"], }, supportedLngs: [ "en", "es-ES", - "es-MX", + "es-419", // LATAM Spanish "fr", "it", "de", - "zh-CN", - "zh-TW", + "zh-Hans", + "zh-Hant", "pt-BR", "ko", "ja", diff --git a/src/queues/move-phase-priority-queue.ts b/src/queues/move-phase-priority-queue.ts new file mode 100644 index 00000000000..5f0b20c3c2e --- /dev/null +++ b/src/queues/move-phase-priority-queue.ts @@ -0,0 +1,103 @@ +import type { PokemonMove } from "#app/data/moves/pokemon-move"; +import type { Pokemon } from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import type { MovePhase } from "#app/phases/move-phase"; +import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; +import type { BattlerIndex } from "#enums/battler-index"; +import type { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; +import type { PhaseConditionFunc } from "#types/phase-types"; + +/** A priority queue responsible for the ordering of {@linkcode MovePhase}s */ +export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue { + private lastTurnOrder: Pokemon[] = []; + + protected override reorder(): void { + super.reorder(); + this.sortPostSpeed(); + } + + public cancelMove(condition: PhaseConditionFunc<"MovePhase">): void { + this.queue.find(p => condition(p))?.cancel(); + } + + public setTimingModifier(condition: PhaseConditionFunc<"MovePhase">, modifier: MovePhaseTimingModifier): void { + const phase = this.queue.find(p => condition(p)); + if (phase != null) { + phase.timingModifier = modifier; + } + } + + public setMoveForPhase(condition: PhaseConditionFunc<"MovePhase">, move: PokemonMove) { + const phase = this.queue.find(p => condition(p)); + if (phase != null) { + phase.move = move; + } + } + + public redirectMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void { + // failsafe: if not a double battle just return + if (!globalScene.currentBattle.double) { + return; + } + + // TODO: simplify later + if (allyPokemon?.isActive(true)) { + this.queue + .filter( + mp => + mp.targets.length === 1 + && mp.targets[0] === removedPokemon.getBattlerIndex() + && mp.pokemon.isPlayer() !== allyPokemon.isPlayer(), + ) + .forEach(targetingMovePhase => { + if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) { + targetingMovePhase.targets[0] = allyPokemon.getBattlerIndex(); + } + }); + } + } + + public setMoveOrder(order: BattlerIndex[]) { + this.setOrder = order; + } + + public override pop(): MovePhase | undefined { + this.reorder(); + const phase = this.queue.shift(); + if (phase) { + this.lastTurnOrder.push(phase.pokemon); + } + return phase; + } + + public getTurnOrder(): Pokemon[] { + return this.lastTurnOrder; + } + + public clearTurnOrder(): void { + this.lastTurnOrder = []; + } + + public override clear(): void { + this.setOrder = undefined; + this.lastTurnOrder = []; + super.clear(); + } + + private sortPostSpeed(): void { + this.queue.sort((a: MovePhase, b: MovePhase) => { + const priority = [a, b].map(movePhase => { + const move = movePhase.move.getMove(); + return move.getPriority(movePhase.pokemon, true); + }); + + const timingModifiers = [a, b].map(movePhase => movePhase.timingModifier); + + if (timingModifiers[0] !== timingModifiers[1]) { + return timingModifiers[1] - timingModifiers[0]; + } + + return priority[1] - priority[0]; + }); + } +} diff --git a/src/queues/pokemon-phase-priority-queue.ts b/src/queues/pokemon-phase-priority-queue.ts new file mode 100644 index 00000000000..3098c5be435 --- /dev/null +++ b/src/queues/pokemon-phase-priority-queue.ts @@ -0,0 +1,20 @@ +import type { DynamicPhase } from "#app/@types/phase-types"; +import { PriorityQueue } from "#app/queues/priority-queue"; +import { sortInSpeedOrder } from "#app/utils/speed-order"; +import type { BattlerIndex } from "#enums/battler-index"; + +/** A generic speed-based priority queue of {@linkcode DynamicPhase}s */ +export class PokemonPhasePriorityQueue extends PriorityQueue { + protected setOrder: BattlerIndex[] | undefined; + protected override reorder(): void { + const setOrder = this.setOrder; + if (setOrder) { + this.queue.sort( + (a, b) => + setOrder.indexOf(a.getPokemon().getBattlerIndex()) - setOrder.indexOf(b.getPokemon().getBattlerIndex()), + ); + } else { + this.queue = sortInSpeedOrder(this.queue); + } + } +} diff --git a/src/queues/pokemon-priority-queue.ts b/src/queues/pokemon-priority-queue.ts new file mode 100644 index 00000000000..597bfb32c0d --- /dev/null +++ b/src/queues/pokemon-priority-queue.ts @@ -0,0 +1,10 @@ +import type { Pokemon } from "#app/field/pokemon"; +import { PriorityQueue } from "#app/queues/priority-queue"; +import { sortInSpeedOrder } from "#app/utils/speed-order"; + +/** A priority queue of {@linkcode Pokemon}s */ +export class PokemonPriorityQueue extends PriorityQueue { + protected override reorder(): void { + this.queue = sortInSpeedOrder(this.queue); + } +} diff --git a/src/queues/post-summon-phase-priority-queue.ts b/src/queues/post-summon-phase-priority-queue.ts new file mode 100644 index 00000000000..37da90a1427 --- /dev/null +++ b/src/queues/post-summon-phase-priority-queue.ts @@ -0,0 +1,45 @@ +import { globalScene } from "#app/global-scene"; +import { PostSummonActivateAbilityPhase } from "#app/phases/post-summon-activate-ability-phase"; +import type { PostSummonPhase } from "#app/phases/post-summon-phase"; +import { PokemonPhasePriorityQueue } from "#app/queues/pokemon-phase-priority-queue"; +import { sortInSpeedOrder } from "#app/utils/speed-order"; + +/** + * Priority Queue for {@linkcode PostSummonPhase} and {@linkcode PostSummonActivateAbilityPhase} + * + * Orders phases first by ability priority, then by the {@linkcode Pokemon}'s effective speed + */ +export class PostSummonPhasePriorityQueue extends PokemonPhasePriorityQueue { + protected override reorder(): void { + this.queue = sortInSpeedOrder(this.queue, false); + this.queue.sort((phaseA, phaseB) => phaseB.getPriority() - phaseA.getPriority()); + } + + public override push(phase: PostSummonPhase): void { + super.push(phase); + this.queueAbilityPhase(phase); + } + + /** + * Queues all necessary {@linkcode PostSummonActivateAbilityPhase}s for each pushed {@linkcode PostSummonPhase} + * @param phase - The {@linkcode PostSummonPhase} that was pushed onto the queue + */ + private queueAbilityPhase(phase: PostSummonPhase): void { + if (phase instanceof PostSummonActivateAbilityPhase) { + return; + } + + const phasePokemon = phase.getPokemon(); + + phasePokemon.getAbilityPriorities().forEach((priority, idx) => { + const activateAbilityPhase = new PostSummonActivateAbilityPhase( + phasePokemon.getBattlerIndex(), + priority, + idx !== 0, + ); + phase.source === "SummonPhase" + ? globalScene.phaseManager.pushPhase(activateAbilityPhase) + : globalScene.phaseManager.unshiftPhase(activateAbilityPhase); + }); + } +} diff --git a/src/queues/priority-queue.ts b/src/queues/priority-queue.ts new file mode 100644 index 00000000000..b53cfec3f4d --- /dev/null +++ b/src/queues/priority-queue.ts @@ -0,0 +1,78 @@ +/** + * Stores a list of elements. + * + * Dynamically updates ordering to always pop the highest "priority", based on implementation of {@linkcode reorder}. + */ +export abstract class PriorityQueue { + protected queue: T[] = []; + + /** + * Sorts the elements in the queue + */ + protected abstract reorder(): void; + + /** + * Calls {@linkcode reorder} and shifts the queue + * @returns The front element of the queue after sorting, or `undefined` if the queue is empty + * @sealed + */ + public pop(): T | undefined { + if (this.isEmpty()) { + return; + } + + this.reorder(); + return this.queue.shift(); + } + + /** + * Adds an element to the queue + * @param element The element to add + */ + public push(element: T): void { + this.queue.push(element); + } + + /** + * Removes all elements from the queue + * @sealed + */ + public clear(): void { + this.queue.splice(0, this.queue.length); + } + + /** + * @returns Whether the queue is empty + * @sealed + */ + public isEmpty(): boolean { + return this.queue.length === 0; + } + + /** + * Removes the first element matching the condition + * @param condition - An optional condition function (defaults to a function that always returns `true`) + * @returns Whether a removal occurred + */ + public remove(condition: (t: T) => boolean = () => true): boolean { + // Reorder to remove the first element + this.reorder(); + const index = this.queue.findIndex(condition); + if (index === -1) { + return false; + } + + this.queue.splice(index, 1); + return true; + } + + /** @returns An element matching the condition function */ + public find(condition?: (t: T) => boolean): T | undefined { + return this.queue.find(e => !condition || condition(e)); + } + + /** @returns Whether an element matching the condition function exists */ + public has(condition?: (t: T) => boolean): boolean { + return this.queue.some(e => !condition || condition(e)); + } +} diff --git a/src/system/settings/settings-language.ts b/src/system/settings/settings-language.ts index 3fd90477e65..1ab71117604 100644 --- a/src/system/settings/settings-language.ts +++ b/src/system/settings/settings-language.ts @@ -36,7 +36,7 @@ export const languageOptions = [ }, { label: "Español (LATAM)", - handler: () => changeLocaleHandler("es-MX"), + handler: () => changeLocaleHandler("es-419"), }, { label: "Français", @@ -64,11 +64,11 @@ export const languageOptions = [ }, { label: "简体中文", - handler: () => changeLocaleHandler("zh-CN"), + handler: () => changeLocaleHandler("zh-Hans"), }, { label: "繁體中文", - handler: () => changeLocaleHandler("zh-TW"), + handler: () => changeLocaleHandler("zh-Hant"), }, { label: "Català (Needs Help)", diff --git a/src/timed-event-manager.ts b/src/timed-event-manager.ts index 7db89b2a0ef..75f4afa772e 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -80,7 +80,7 @@ const timedEvents: TimedEvent[] = [ endDate: new Date(Date.UTC(2025, 0, 4, 0)), bannerKey: "winter_holidays2024-event", scale: 0.21, - availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"], + availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-Hans"], eventEncounters: [ { species: SpeciesId.GIMMIGHOUL, blockEvolution: true }, { species: SpeciesId.DELIBIRD }, @@ -136,7 +136,7 @@ const timedEvents: TimedEvent[] = [ endDate: new Date(Date.UTC(2025, 1, 3, 0)), bannerKey: "yearofthesnakeevent", scale: 0.21, - availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"], + availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-Hans"], eventEncounters: [ { species: SpeciesId.EKANS }, { species: SpeciesId.ONIX }, @@ -208,7 +208,7 @@ const timedEvents: TimedEvent[] = [ shinyMultiplier: 2, bannerKey: "valentines2025event", scale: 0.21, - availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"], + availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-Hans"], eventEncounters: [ { species: SpeciesId.NIDORAN_F }, { species: SpeciesId.NIDORAN_M }, @@ -247,7 +247,7 @@ const timedEvents: TimedEvent[] = [ classicFriendshipMultiplier: 4, bannerKey: "pkmnday2025event", scale: 0.21, - availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"], + availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-Hans"], eventEncounters: [ { species: SpeciesId.PIKACHU, formIndex: 1, blockEvolution: true }, // Partner Form { species: SpeciesId.EEVEE, formIndex: 1, blockEvolution: true }, // Partner Form @@ -297,7 +297,7 @@ const timedEvents: TimedEvent[] = [ endDate: new Date(Date.UTC(2025, 3, 3)), bannerKey: "aprf25", scale: 0.21, - availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "es-MX", "pt-BR", "zh-CN"], + availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "es-419", "pt-BR", "zh-Hans"], trainerShinyChance: 13107, // 13107/65536 = 1/5 music: [ ["title", "title_afd"], @@ -317,7 +317,7 @@ const timedEvents: TimedEvent[] = [ endDate: new Date(Date.UTC(2025, 4, 13)), bannerKey: "spr25event", scale: 0.21, - availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "es-MX", "pt-BR", "zh-CN"], + availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "es-419", "pt-BR", "zh-Hans"], shinyMultiplier: 2, upgradeUnlockedVouchers: true, eventEncounters: [ @@ -358,7 +358,7 @@ const timedEvents: TimedEvent[] = [ endDate: new Date(Date.UTC(2025, 5, 30)), bannerKey: "pride2025", scale: 0.105, - availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "es-MX", "pt-BR", "zh-CN", "zh-TW"], + availableLangs: ["en", "de", "it", "fr", "ja", "ko", "es-ES", "es-419", "pt-BR", "zh-Hans", "zh-Hant"], shinyMultiplier: 2, eventEncounters: [ { species: SpeciesId.CHARMANDER }, diff --git a/src/ui/containers/arena-flyout.ts b/src/ui/containers/arena-flyout.ts index ab95d1a3e7a..355f3edb293 100644 --- a/src/ui/containers/arena-flyout.ts +++ b/src/ui/containers/arena-flyout.ts @@ -285,6 +285,12 @@ export class ArenaFlyout extends Phaser.GameObjects.Container { switch (arenaEffectChangedEvent.constructor) { case TagAddedEvent: { const tagAddedEvent = arenaEffectChangedEvent as TagAddedEvent; + + const excludedTags = [ArenaTagType.PENDING_HEAL]; + if (excludedTags.includes(tagAddedEvent.arenaTagType)) { + return; + } + const isArenaTrapTag = globalScene.arena.getTag(tagAddedEvent.arenaTagType) instanceof EntryHazardTag; let arenaEffectType: ArenaEffectType; diff --git a/src/ui/handlers/egg-gacha-ui-handler.ts b/src/ui/handlers/egg-gacha-ui-handler.ts index f24c8c04fdb..c90d4a12139 100644 --- a/src/ui/handlers/egg-gacha-ui-handler.ts +++ b/src/ui/handlers/egg-gacha-ui-handler.ts @@ -81,7 +81,7 @@ export class EggGachaUiHandler extends MessageUiHandler { let pokemonIconX = -20; let pokemonIconY = 6; - if (["de", "es-ES", "es-MX", "fr", "ko", "pt-BR", "ja", "ru"].includes(currentLanguage)) { + if (["de", "es-ES", "es-419", "fr", "ko", "pt-BR", "ja", "ru"].includes(currentLanguage)) { gachaTextStyle = TextStyle.SMALLER_WINDOW_ALT; gachaX = 2; gachaY = 2; @@ -89,7 +89,7 @@ export class EggGachaUiHandler extends MessageUiHandler { let legendaryLabelX = gachaX; let legendaryLabelY = gachaY; - if (["de", "es-ES", "es-MX"].includes(currentLanguage)) { + if (["de", "es-ES", "es-419"].includes(currentLanguage)) { pokemonIconX = -25; pokemonIconY = 10; legendaryLabelX = -6; diff --git a/src/ui/handlers/game-stats-ui-handler.ts b/src/ui/handlers/game-stats-ui-handler.ts index 24ff842a902..58b00e3d50f 100644 --- a/src/ui/handlers/game-stats-ui-handler.ts +++ b/src/ui/handlers/game-stats-ui-handler.ts @@ -244,7 +244,7 @@ export class GameStatsUiHandler extends UiHandler { const resolvedLang = i18next.resolvedLanguage ?? "en"; // NOTE TO TRANSLATION TEAM: Add more languages that want to display // in a single-column inside of the `[]` (e.g. `["ru", "fr"]`) - return ["fr", "es-ES", "es-MX", "it", "ja", "pt-BR", "ru"].includes(resolvedLang); + return ["fr", "es-ES", "es-419", "it", "ja", "pt-BR", "ru"].includes(resolvedLang); } /** The number of columns used by this menu in the resolved language */ private get columnCount(): 1 | 2 { diff --git a/src/ui/handlers/pokedex-page-ui-handler.ts b/src/ui/handlers/pokedex-page-ui-handler.ts index 31e2998b850..684ead7d45a 100644 --- a/src/ui/handlers/pokedex-page-ui-handler.ts +++ b/src/ui/handlers/pokedex-page-ui-handler.ts @@ -85,7 +85,7 @@ const languageSettings: { [key: string]: LanguageSetting } = { starterInfoYOffset: 0.5, starterInfoXPos: 38, }, - "es-MX": { + "es-419": { starterInfoTextSize: "50px", instructionTextSize: "38px", starterInfoYOffset: 0.5, diff --git a/src/ui/handlers/starter-select-ui-handler.ts b/src/ui/handlers/starter-select-ui-handler.ts index 53e566864b1..d0bef69aa81 100644 --- a/src/ui/handlers/starter-select-ui-handler.ts +++ b/src/ui/handlers/starter-select-ui-handler.ts @@ -49,7 +49,7 @@ import { achvs } from "#system/achv"; import { RibbonData } from "#system/ribbons/ribbon-data"; import { SettingKeyboard } from "#system/settings-keyboard"; import type { DexEntry } from "#types/dex-data"; -import type { DexAttrProps, StarterAttributes, StarterDataEntry, StarterMoveset } from "#types/save-data"; +import type { Starter, StarterAttributes, StarterDataEntry, StarterMoveset } from "#types/save-data"; import type { OptionSelectItem } from "#ui/abstract-option-select-ui-handler"; import { DropDown, DropDownLabel, DropDownOption, DropDownState, DropDownType, SortCriteria } from "#ui/dropdown"; import { FilterBar } from "#ui/filter-bar"; @@ -82,18 +82,6 @@ import type BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext"; export type StarterSelectCallback = (starters: Starter[]) => void; -export interface Starter { - species: PokemonSpecies; - dexAttr: bigint; - abilityIndex: number; - passive: boolean; - nature: Nature; - moveset?: StarterMoveset; - pokerus: boolean; - nickname?: string; - teraType?: PokemonType; -} - interface LanguageSetting { starterInfoTextSize: string; instructionTextSize: string; @@ -117,7 +105,7 @@ const languageSettings: { [key: string]: LanguageSetting } = { starterInfoYOffset: 0.5, starterInfoXPos: 38, }, - "es-MX": { + "es-419": { starterInfoTextSize: "50px", instructionTextSize: "38px", starterInfoYOffset: 0.5, @@ -364,15 +352,13 @@ export class StarterSelectUiHandler extends MessageUiHandler { private allSpecies: PokemonSpecies[] = []; private lastSpecies: PokemonSpecies; private speciesLoaded: Map = new Map(); + + private starters: Starter[] = []; public starterSpecies: PokemonSpecies[] = []; private pokerusSpecies: PokemonSpecies[] = []; - private starterAttr: bigint[] = []; - private starterAbilityIndexes: number[] = []; - private starterNatures: Nature[] = []; - private starterTeras: PokemonType[] = []; - private starterMovesets: StarterMoveset[] = []; private speciesStarterDexEntry: DexEntry | null; private speciesStarterMoves: MoveId[]; + private canCycleShiny: boolean; private canCycleForm: boolean; private canCycleGender: boolean; @@ -2758,12 +2744,26 @@ export class StarterSelectUiHandler extends MessageUiHandler { props.variant, ); + const { dexEntry, starterDataEntry } = this.getSpeciesData(species.speciesId); + + const starter = { + speciesId: species.speciesId, + shiny: props.shiny, + variant: props.variant, + formIndex: props.formIndex, + female: props.female, + abilityIndex, + passive: !(starterDataEntry.passiveAttr ^ (PassiveAttr.ENABLED | PassiveAttr.UNLOCKED)), + nature, + moveset, + pokerus: this.pokerusSpecies.includes(species), + nickname: this.starterPreferences[species.speciesId]?.nickname, + teraType, + ivs: dexEntry.ivs, + }; + + this.starters.push(starter); this.starterSpecies.push(species); - this.starterAttr.push(dexAttr); - this.starterAbilityIndexes.push(abilityIndex); - this.starterNatures.push(nature); - this.starterTeras.push(teraType); - this.starterMovesets.push(moveset); if (this.speciesLoaded.get(species.speciesId) || randomSelection) { getPokemonSpeciesForm(species.speciesId, props.formIndex).cry(); } @@ -2833,7 +2833,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { for (const [index, species] of this.starterSpecies.entries()) { if (species.speciesId === id) { - this.starterMovesets[index] = this.starterMoveset; + this.starters[index].moveset = this.starterMoveset; } } } @@ -3640,20 +3640,20 @@ export class StarterSelectUiHandler extends MessageUiHandler { const starterIndex = this.starterSpecies.indexOf(species); - let props: DexAttrProps; + const props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); if (starterIndex > -1) { - props = globalScene.gameData.getSpeciesDexAttrProps(species, this.starterAttr[starterIndex]); + const starter = this.starters[starterIndex]; this.setSpeciesDetails( species, { - shiny: props.shiny, - formIndex: props.formIndex, - female: props.female, - variant: props.variant, - abilityIndex: this.starterAbilityIndexes[starterIndex], - natureIndex: this.starterNatures[starterIndex], - teraType: this.starterTeras[starterIndex], + shiny: starter.shiny, + formIndex: starter.formIndex, + female: starter.female, + variant: starter.variant, + abilityIndex: starter.abilityIndex, + natureIndex: starter.nature, + teraType: starter.teraType, }, false, ); @@ -3664,7 +3664,6 @@ export class StarterSelectUiHandler extends MessageUiHandler { const { dexEntry } = this.getSpeciesData(species.speciesId); const defaultNature = starterAttributes?.nature || globalScene.gameData.getSpeciesDefaultNature(species, dexEntry); - props = globalScene.gameData.getSpeciesDexAttrProps(species, defaultDexAttr); if (starterAttributes?.variant && !Number.isNaN(starterAttributes.variant) && props.shiny) { props.variant = starterAttributes.variant as Variant; } @@ -3910,10 +3909,15 @@ export class StarterSelectUiHandler extends MessageUiHandler { const starterIndex = this.starterSpecies.indexOf(species); if (starterIndex > -1) { - this.starterAttr[starterIndex] = this.dexAttrCursor; - this.starterAbilityIndexes[starterIndex] = this.abilityCursor; - this.starterNatures[starterIndex] = this.natureCursor; - this.starterTeras[starterIndex] = this.teraCursor; + const starter = this.starters[starterIndex]; + const props = globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor); + starter.shiny = props.shiny; + starter.variant = props.variant; + starter.female = props.female; + starter.formIndex = props.formIndex; + starter.abilityIndex = this.abilityCursor; + starter.nature = this.natureCursor; + starter.teraType = this.teraCursor; } const assetLoadCancelled = new BooleanHolder(false); @@ -4215,11 +4219,7 @@ export class StarterSelectUiHandler extends MessageUiHandler { popStarter(index: number): void { this.starterSpecies.splice(index, 1); - this.starterAttr.splice(index, 1); - this.starterAbilityIndexes.splice(index, 1); - this.starterNatures.splice(index, 1); - this.starterTeras.splice(index, 1); - this.starterMovesets.splice(index, 1); + this.starters.splice(index, 1); for (let s = 0; s < this.starterSpecies.length; s++) { const species = this.starterSpecies[s]; @@ -4443,27 +4443,11 @@ export class StarterSelectUiHandler extends MessageUiHandler { () => { const startRun = () => { globalScene.money = globalScene.gameMode.getStartingMoney(); + const starters = this.starters.slice(0); ui.setMode(UiMode.STARTER_SELECT); - const thisObj = this; const originalStarterSelectCallback = this.starterSelectCallback; this.starterSelectCallback = null; - originalStarterSelectCallback?.( - new Array(this.starterSpecies.length).fill(0).map((_, i) => { - const starterSpecies = thisObj.starterSpecies[i]; - const { starterDataEntry } = this.getSpeciesData(starterSpecies.speciesId); - return { - species: starterSpecies, - dexAttr: thisObj.starterAttr[i], - abilityIndex: thisObj.starterAbilityIndexes[i], - passive: !(starterDataEntry.passiveAttr ^ (PassiveAttr.ENABLED | PassiveAttr.UNLOCKED)), - nature: thisObj.starterNatures[i] as Nature, - teraType: thisObj.starterTeras[i] as PokemonType, - moveset: thisObj.starterMovesets[i], - pokerus: thisObj.pokerusSpecies.includes(starterSpecies), - nickname: thisObj.starterPreferences[starterSpecies.speciesId]?.nickname, - }; - }), - ); + originalStarterSelectCallback?.(starters); }; startRun(); }, @@ -4492,10 +4476,17 @@ export class StarterSelectUiHandler extends MessageUiHandler { */ isPartyValid(): boolean { let canStart = false; - for (const species of this.starterSpecies) { + for (let s = 0; s < this.starterSpecies.length; s++) { + const species = this.starterSpecies[s]; + const starter = this.starters[s]; const isValidForChallenge = checkStarterValidForChallenge( species, - globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), + { + formIndex: starter.formIndex, + shiny: starter.shiny, + variant: starter.variant, + female: starter.female ?? false, + }, false, ); canStart ||= isValidForChallenge; diff --git a/src/ui/settings/settings-display-ui-handler.ts b/src/ui/settings/settings-display-ui-handler.ts index 1a0481b8e8d..d513ed8fd11 100644 --- a/src/ui/settings/settings-display-ui-handler.ts +++ b/src/ui/settings/settings-display-ui-handler.ts @@ -32,7 +32,7 @@ export class SettingsDisplayUiHandler extends AbstractSettingsUiHandler { label: "Español (ES)", }; break; - case "es-MX": + case "es-419": this.settings[languageIndex].options[0] = { value: "Español (LATAM)", label: "Español (LATAM)", @@ -75,13 +75,13 @@ export class SettingsDisplayUiHandler extends AbstractSettingsUiHandler { label: "日本語", }; break; - case "zh-CN": + case "zh-Hans": this.settings[languageIndex].options[0] = { value: "简体中文", label: "简体中文", }; break; - case "zh-TW": + case "zh-Hant": this.settings[languageIndex].options[0] = { value: "繁體中文", label: "繁體中文", diff --git a/src/ui/text.ts b/src/ui/text.ts index 4338a73179d..87444fd2c7c 100644 --- a/src/ui/text.ts +++ b/src/ui/text.ts @@ -340,8 +340,8 @@ export function getTextStyleOptions( styleOptions.fontSize = defaultFontSize - 38; styleOptions.padding = { top: 4, left: 6 }; break; - case "zh-CN": - case "zh-TW": + case "zh-Hans": + case "zh-Hant": styleOptions.fontSize = defaultFontSize - 42; styleOptions.padding = { top: 5, left: 14 }; break; diff --git a/src/utils/common.ts b/src/utils/common.ts index f0166b1e74c..569333209bf 100644 --- a/src/utils/common.ts +++ b/src/utils/common.ts @@ -408,13 +408,13 @@ export function hasAllLocalizedSprites(lang?: string): boolean { switch (lang) { case "es-ES": - case "es-MX": + case "es-419": case "fr": case "da": case "de": case "it": - case "zh-CN": - case "zh-TW": + case "zh-Hans": + case "zh-Hant": case "pt-BR": case "ro": case "tr": diff --git a/src/utils/damage.ts b/src/utils/damage.ts new file mode 100644 index 00000000000..5ec683d8cd6 --- /dev/null +++ b/src/utils/damage.ts @@ -0,0 +1,65 @@ +/* + * SPDX-Copyright-Text: 2025 Pagefault Games + * + * SPDX-License-Identifier: AGPL-3.0-only + */ +/** + * Utility functions relating to damage calculations. + * @module + */ + +import { toDmgValue } from "#utils/common"; + +/** + * Calculate the adjusted damage and number of boss segments bypassed for a damage interaction + * @param damage - The raw damage dealt. + * @param currentHp - The target's current HP + * @param segmentHp - The HP in each segment (total HP / number of segments) + * @param minSegmentIndex - The minimum segment index that can be cleared; default `0` (all segments). Used for the final boss + * @returns A tuple consisting of the adjusted damage and index of the boss segment the target is in after damage is applied. + */ +export function calculateBossSegmentDamage( + damage: number, + currentHp: number, + segmentHp: number, + minSegmentIndex = 0, +): [adjustedDamage: number, clearedBossSegmentIndex: number] { + const segmentIndex = Math.ceil(currentHp / segmentHp) - 1; + if (segmentIndex <= 0) { + return [damage, 1]; + } + + /** + * The HP that the current segment ends at. + * + * @example + * If a Pokemon has 3 segments and 300 max HP, each segment is 100 HP. + * In the first iteration, this would be 200 HP, as the first segment ends at 200 HP. + */ + const segmentThreshold = segmentHp * segmentIndex; + const roundedSegmentThreshold = Math.round(segmentThreshold); + + const remainingSegmentHp = currentHp - roundedSegmentThreshold; + + const leftoverDamage = damage - remainingSegmentHp; + + // Insufficient damage to get down to current segment HP, return original damage + // Segment index + 1 because this segment is not considered cleared + if (leftoverDamage < 0) { + return [damage, segmentIndex + 1]; + } + if (leftoverDamage === 0) { + return [damage, segmentIndex]; + } + + // Breaking the nth segment requires dealing at least segmentHp * 2^(n-1) damage + // and must ensure at least `segmentIndex - minSegmentIndex` segments remain + const segmentsBypassed = Math.min( + Math.max(Math.floor(Math.log2(leftoverDamage / segmentHp)), 0), + segmentIndex - minSegmentIndex, + ); + const adjustedDamage = toDmgValue(currentHp - segmentThreshold + segmentHp * segmentsBypassed); + const clearedBossSegmentIndex = segmentIndex - segmentsBypassed; + + return [adjustedDamage, clearedBossSegmentIndex]; +} diff --git a/src/utils/speed-order-generator.ts b/src/utils/speed-order-generator.ts new file mode 100644 index 00000000000..24f95de665f --- /dev/null +++ b/src/utils/speed-order-generator.ts @@ -0,0 +1,39 @@ +import { globalScene } from "#app/global-scene"; +import { PokemonPriorityQueue } from "#app/queues/pokemon-priority-queue"; +import { ArenaTagSide } from "#enums/arena-tag-side"; +import type { Pokemon } from "#field/pokemon"; + +/** + * A generator function which uses a priority queue to yield each pokemon from a given side of the field in speed order. + * @param side - The {@linkcode ArenaTagSide | side} of the field to use + * @returns A {@linkcode Generator} of {@linkcode Pokemon} + * + * @remarks + * This should almost always be used by iteration in a `for...of` loop + */ +export function* inSpeedOrder(side: ArenaTagSide = ArenaTagSide.BOTH): Generator { + let pokemonList: Pokemon[]; + switch (side) { + case ArenaTagSide.PLAYER: + pokemonList = globalScene.getPlayerField(true); + break; + case ArenaTagSide.ENEMY: + pokemonList = globalScene.getEnemyField(true); + break; + default: + pokemonList = globalScene.getField(true); + } + + const queue = new PokemonPriorityQueue(); + let i = 0; + pokemonList.forEach(p => { + queue.push(p); + }); + while (!queue.isEmpty()) { + // If the queue is not empty, this can never be undefined + i++; + yield queue.pop()!; + } + + return i; +} diff --git a/src/utils/speed-order.ts b/src/utils/speed-order.ts new file mode 100644 index 00000000000..1d894369bb3 --- /dev/null +++ b/src/utils/speed-order.ts @@ -0,0 +1,57 @@ +import { Pokemon } from "#app/field/pokemon"; +import { globalScene } from "#app/global-scene"; +import { BooleanHolder, randSeedShuffle } from "#app/utils/common"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { Stat } from "#enums/stat"; + +/** Interface representing an object associated with a specific Pokemon */ +interface hasPokemon { + getPokemon(): Pokemon; +} + +/** + * Sorts an array of {@linkcode Pokemon} by speed, taking Trick Room into account. + * @param pokemonList - The list of Pokemon or objects containing Pokemon + * @param shuffleFirst - Whether to shuffle the list before sorting (to handle speed ties). Default `true`. + * @returns The sorted array of {@linkcode Pokemon} + */ +export function sortInSpeedOrder(pokemonList: T[], shuffleFirst = true): T[] { + pokemonList = shuffleFirst ? shufflePokemonList(pokemonList) : pokemonList; + sortBySpeed(pokemonList); + return pokemonList; +} + +/** + * @param pokemonList - The array of Pokemon or objects containing Pokemon + * @returns The shuffled array + */ +function shufflePokemonList(pokemonList: T[]): T[] { + // This is seeded with the current turn to prevent an inconsistency where it + // was varying based on how long since you last reloaded + globalScene.executeWithSeedOffset( + () => { + pokemonList = randSeedShuffle(pokemonList); + }, + globalScene.currentBattle.turn * 1000 + pokemonList.length, + globalScene.waveSeed, + ); + return pokemonList; +} + +/** Sorts an array of {@linkcode Pokemon} by speed (without shuffling) */ +function sortBySpeed(pokemonList: T[]): void { + pokemonList.sort((a, b) => { + const aSpeed = (a instanceof Pokemon ? a : a.getPokemon()).getEffectiveStat(Stat.SPD); + const bSpeed = (b instanceof Pokemon ? b : b.getPokemon()).getEffectiveStat(Stat.SPD); + + return bSpeed - aSpeed; + }); + + /** 'true' if Trick Room is on the field. */ + const speedReversed = new BooleanHolder(false); + globalScene.arena.applyTags(ArenaTagType.TRICK_ROOM, false, speedReversed); + + if (speedReversed.value) { + pokemonList.reverse(); + } +} diff --git a/test/@types/vitest.d.ts b/test/@types/vitest.d.ts index 9a6f07b4afb..43e9df190aa 100644 --- a/test/@types/vitest.d.ts +++ b/test/@types/vitest.d.ts @@ -1,7 +1,7 @@ import "vitest"; -import type { Phase } from "#app/phase"; import type Overrides from "#app/overrides"; +import type { Phase } from "#app/phase"; import type { ArenaTag } from "#data/arena-tag"; import type { TerrainType } from "#data/terrain"; import type { AbilityId } from "#enums/ability-id"; @@ -10,10 +10,14 @@ import type { ArenaTagType } from "#enums/arena-tag-type"; import type { BattlerTagType } from "#enums/battler-tag-type"; import type { MoveId } from "#enums/move-id"; import type { PokemonType } from "#enums/pokemon-type"; +import type { PositionalTag } from "#data/positional-tags/positional-tag"; import type { PositionalTagType } from "#enums/positional-tag-type"; import type { BattleStat, EffectiveStat } from "#enums/stat"; import type { WeatherType } from "#enums/weather-type"; +import type { Pokemon } from "#field/pokemon"; +import type { GameManager } from "#test/test-utils/game-manager"; import type { toHaveArenaTagOptions } from "#test/test-utils/matchers/to-have-arena-tag"; +import type { toHaveBattlerTagOptions } from "#test/test-utils/matchers/to-have-battler-tag"; import type { toHaveEffectiveStatOptions } from "#test/test-utils/matchers/to-have-effective-stat"; import type { toHavePositionalTagOptions } from "#test/test-utils/matchers/to-have-positional-tag"; import type { expectedStatusType } from "#test/test-utils/matchers/to-have-status-effect"; @@ -23,175 +27,212 @@ import type { TurnMove } from "#types/turn-move"; import type { AtLeastOne } from "#types/type-helpers"; import type { toDmgValue } from "#utils/common"; import type { expect } from "vitest"; -import type { toHaveBattlerTagOptions } from "#test/test-utils/matchers/to-have-battler-tag"; +// #region Boilerplate/Helpers declare module "vitest" { - interface Assertion { - // #region Generic Matchers - - /** - * Check whether an array contains EXACTLY the given items (in any order). - * - * Different from {@linkcode expect.arrayContaining} as the latter only checks for subset equality - * (as opposed to full equality). - * - * @param expected - The expected contents of the array, in any order - * @see {@linkcode expect.arrayContaining} - */ - toEqualArrayUnsorted(expected: T[]): void; - - // #endregion Generic Matchers - - // #region GameManager Matchers - - /** - * Check if the {@linkcode GameManager} has shown the given message at least once in the current battle. - * @param expectedMessage - The expected message - */ - toHaveShownMessage(expectedMessage: string): void; - /** - * @param expectedPhase - The expected {@linkcode PhaseString} - */ - toBeAtPhase(expectedPhase: PhaseString): void; - // #endregion GameManager Matchers - - // #region Arena Matchers - - /** - * Check whether the current {@linkcode WeatherType} is as expected. - * @param expectedWeatherType - The expected {@linkcode WeatherType} - */ - toHaveWeather(expectedWeatherType: WeatherType): void; - - /** - * Check whether the current {@linkcode TerrainType} is as expected. - * @param expectedTerrainType - The expected {@linkcode TerrainType} - */ - toHaveTerrain(expectedTerrainType: TerrainType): void; - - /** - * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. - * @param expectedTag - A partially-filled {@linkcode ArenaTag} containing the desired properties - */ - toHaveArenaTag(expectedTag: toHaveArenaTagOptions): void; - /** - * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. - * @param expectedType - The {@linkcode ArenaTagType} of the desired tag - * @param side - The {@linkcode ArenaTagSide | side(s) of the field} the tag should affect; default {@linkcode ArenaTagSide.BOTH} - */ - toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void; - - /** - * Check whether the current {@linkcode Arena} contains the given {@linkcode PositionalTag}. - * @param expectedTag - A partially-filled `PositionalTag` containing the desired properties - */ - toHavePositionalTag

(expectedTag: toHavePositionalTagOptions

): void; - /** - * Check whether the current {@linkcode Arena} contains the given number of {@linkcode PositionalTag}s. - * @param expectedType - The {@linkcode PositionalTagType} of the desired tag - * @param count - The number of instances of {@linkcode expectedType} that should be active; - * defaults to `1` and must be within the range `[0, 4]` - */ - toHavePositionalTag(expectedType: PositionalTagType, count?: number): void; - - // #endregion Arena Matchers - - // #region Pokemon Matchers - - /** - * Check whether a {@linkcode Pokemon}'s current typing includes the given types. - * @param expectedTypes - The expected {@linkcode PokemonType}s to check against; must have length `>0` - * @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher - */ - toHaveTypes(expectedTypes: PokemonType[], options?: toHaveTypesOptions): void; - - /** - * Check whether a {@linkcode Pokemon} has used a move matching the given criteria. - * @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used, - * or a partially filled {@linkcode TurnMove} containing the desired properties to check - * @param index - The index of the move history entry to check, in order from most recent to least recent; default `0` - * @see {@linkcode Pokemon.getLastXMoves} - */ - toHaveUsedMove(expectedMove: MoveId | AtLeastOne, index?: number): void; - - /** - * Check whether a {@linkcode Pokemon}'s effective stat is as expected - * (checked after all stat value modifications). - * @param stat - The {@linkcode EffectiveStat} to check - * @param expectedValue - The expected value of {@linkcode stat} - * @param options - The {@linkcode toHaveEffectiveStatOptions | options} passed to the matcher - * @remarks - * If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead. - */ - toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: toHaveEffectiveStatOptions): void; - - /** - * Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}. - * @param expectedStatusEffect - The {@linkcode StatusEffect} the Pokemon is expected to have, - * or a partially filled {@linkcode Status} containing the desired properties - */ - toHaveStatusEffect(expectedStatusEffect: expectedStatusType): void; - - /** - * Check whether a {@linkcode Pokemon} has a specific {@linkcode Stat} stage. - * @param stat - The {@linkcode BattleStat} to check - * @param expectedStage - The expected stat stage value of {@linkcode stat} - */ - toHaveStatStage(stat: BattleStat, expectedStage: number): void; - - /** - * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}. - * @param expectedTag - A partially-filled {@linkcode BattlerTag} containing the desired properties - */ - toHaveBattlerTag(expectedTag: toHaveBattlerTagOptions): void; - /** - * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}. - * @param expectedType - The expected {@linkcode BattlerTagType} - */ - toHaveBattlerTag(expectedType: BattlerTagType): void; - - /** - * Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}. - * @param expectedAbilityId - The `AbilityId` to check for - */ - toHaveAbilityApplied(expectedAbilityId: AbilityId): void; - - /** - * Check whether a {@linkcode Pokemon} has a specific amount of {@linkcode Stat.HP | HP}. - * @param expectedHp - The expected amount of {@linkcode Stat.HP | HP} to have - */ - toHaveHp(expectedHp: number): void; - - /** - * Check whether a {@linkcode Pokemon} has taken a specific amount of damage. - * @param expectedDamageTaken - The expected amount of damage taken - * @param roundDown - Whether to round down `expectedDamageTaken` with {@linkcode toDmgValue}; default `true` - */ - toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void; - - /** - * Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}). - * @remarks - * When checking whether an enemy wild Pokemon is fainted, one must store a reference to it in a variable _before_ the fainting effect occurs. - * Otherwise, the Pokemon will be removed from the field and garbage collected. - */ - toHaveFainted(): void; - - /** - * Check whether a {@linkcode Pokemon} is at full HP. - */ - toHaveFullHp(): void; - /** - * Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves. - * @param moveId - The {@linkcode MoveId} corresponding to the {@linkcode PokemonMove} that should have consumed PP - * @param ppUsed - The numerical amount of PP that should have been consumed, - * or `all` to indicate the move should be _out_ of PP - * @remarks - * If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.ENEMY_MOVESET_OVERRIDE} - * or does not contain exactly one copy of `moveId`, this will fail the test. - */ - toHaveUsedPP(moveId: MoveId, ppUsed: number | "all"): void; - - // #endregion Pokemon Matchers - } + interface Assertion + extends GenericMatchers, + RestrictMatcher, + RestrictMatcher, + RestrictMatcher {} } + +/** + * Utility type to restrict matchers' properties based on the type of `T`. + * If it does not extend `R`, all methods inside `M` will have their types resolved to `never`. + * @typeParam M - The type of the matchers object to restrict + * @typeParam T - The type parameter of the assertion + * @typeParam R - The type to restrict T based off of + * @privateRemarks + * We cannot remove incompatible methods outright as Typescript requires that + * interfaces extend solely off of types with statically known members. + */ +type RestrictMatcher = { + [k in keyof M]: T extends R ? M[k] : never; +}; +// #endregion Boilerplate/Helpers + +// #region Generic Matchers +interface GenericMatchers { + /** + * Check whether an array contains EXACTLY the given items (in any order). + * + * Different from {@linkcode expect.arrayContaining} as the latter only checks for subset equality + * (as opposed to full equality). + * + * @param expected - The expected contents of the array, in any order + * @see {@linkcode expect.arrayContaining} + */ + toEqualUnsorted: T extends (infer U)[] ? (expected: U[]) => void : never; + + /** + * Check whether a {@linkcode Map} contains the given key, disregarding its value. + * @param expectedKey - The key whose inclusion is being checked + * @privateRemarks + * While this functionality _could_ be simulated by writing + * `expect(x.get(y)).toBeDefined()` or + * `expect(x).toContain([y, expect.anything()])`, + * this is still preferred due to being more ergonomic and provides better error messsages. + */ + toHaveKey: T extends Map ? (expectedKey: K) => void : never; +} +// #endregion Generic Matchers + +// #region GameManager Matchers +interface GameManagerMatchers { + /** + * Check if the {@linkcode GameManager} has shown the given message at least once in the current test case. + * @param expectedMessage - The expected message to be displayed + * @remarks + * Strings consumed by this function should _always_ be produced by a call to `i18next.t` + * to avoid hardcoding text into test files. + */ + toHaveShownMessage(expectedMessage: string): void; + + /** + * Check if the currently-running {@linkcode Phase} is of the given type. + * @param expectedPhase - The expected {@linkcode PhaseString | name of the phase} + */ + toBeAtPhase(expectedPhase: PhaseString): void; +} // #endregion GameManager Matchers + +// #region Arena Matchers +interface ArenaMatchers { + /** + * Check whether the current {@linkcode WeatherType} is as expected. + * @param expectedWeatherType - The expected `WeatherType` + */ + toHaveWeather(expectedWeatherType: WeatherType): void; + + /** + * Check whether the current {@linkcode TerrainType} is as expected. + * @param expectedTerrainType - The expected `TerrainType` + */ + toHaveTerrain(expectedTerrainType: TerrainType): void; + + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. + * @param expectedTag - A partially-filled `ArenaTag` containing the desired properties + */ + toHaveArenaTag(expectedTag: toHaveArenaTagOptions): void; + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode ArenaTag}. + * @param expectedType - The {@linkcode ArenaTagType} of the desired tag + * @param side - The {@linkcode ArenaTagSide | side(s) of the field} the tag should affect; default {@linkcode ArenaTagSide.BOTH} + */ + toHaveArenaTag(expectedType: ArenaTagType, side?: ArenaTagSide): void; + + /** + * Check whether the current {@linkcode Arena} contains the given {@linkcode PositionalTag}. + * @param expectedTag - A partially-filled `PositionalTag` containing the desired properties + */ + toHavePositionalTag

(expectedTag: toHavePositionalTagOptions

): void; + /** + * Check whether the current {@linkcode Arena} contains the given number of {@linkcode PositionalTag}s. + * @param expectedType - The {@linkcode PositionalTagType} of the desired tag + * @param count - The number of instances of `expectedType` that should be active; + * defaults to `1` and must be within the range `[0, 4]` + */ + toHavePositionalTag(expectedType: PositionalTagType, count?: number): void; +} + +// #endregion Arena Matchers + +// #region Pokemon Matchers +interface PokemonMatchers { + /** + * Check whether a {@linkcode Pokemon}'s current typing includes the given types. + * @param expectedTypes - The expected {@linkcode PokemonType}s to check against; must have length `>0` + * @param options - The {@linkcode toHaveTypesOptions | options} passed to the matcher + */ + toHaveTypes(expectedTypes: PokemonType[], options?: toHaveTypesOptions): void; + + /** + * Check whether a {@linkcode Pokemon} has used a move matching the given criteria. + * @param expectedMove - The {@linkcode MoveId} the Pokemon is expected to have used, + * or a partially filled {@linkcode TurnMove} containing the desired properties to check + * @param index - The index of the move history entry to check, in order from most recent to least recent; default `0` + * @see {@linkcode Pokemon.getLastXMoves} + */ + toHaveUsedMove(expectedMove: MoveId | AtLeastOne, index?: number): void; + + /** + * Check whether a {@linkcode Pokemon}'s effective stat is as expected + * (checked after all stat value modifications). + * @param stat - The {@linkcode EffectiveStat} to check + * @param expectedValue - The expected value of {@linkcode stat} + * @param options - The {@linkcode toHaveEffectiveStatOptions | options} passed to the matcher + * @remarks + * If you want to check the stat **before** modifiers are applied, use {@linkcode Pokemon.getStat} instead. + */ + toHaveEffectiveStat(stat: EffectiveStat, expectedValue: number, options?: toHaveEffectiveStatOptions): void; + + /** + * Check whether a {@linkcode Pokemon} has a specific {@linkcode StatusEffect | non-volatile status effect}. + * @param expectedStatusEffect - The {@linkcode StatusEffect} the Pokemon is expected to have, + * or a partially filled {@linkcode Status} containing the desired properties + */ + toHaveStatusEffect(expectedStatusEffect: expectedStatusType): void; + + /** + * Check whether a {@linkcode Pokemon} has a specific {@linkcode Stat} stage. + * @param stat - The {@linkcode BattleStat} to check + * @param expectedStage - The expected stat stage value of {@linkcode stat} + */ + toHaveStatStage(stat: BattleStat, expectedStage: number): void; + + /** + * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}. + * @param expectedTag - A partially-filled {@linkcode BattlerTag} containing the desired properties + */ + toHaveBattlerTag(expectedTag: toHaveBattlerTagOptions): void; + /** + * Check whether a {@linkcode Pokemon} has the given {@linkcode BattlerTag}. + * @param expectedType - The expected {@linkcode BattlerTagType} + */ + toHaveBattlerTag(expectedType: BattlerTagType): void; + + /** + * Check whether a {@linkcode Pokemon} has applied a specific {@linkcode AbilityId}. + * @param expectedAbilityId - The `AbilityId` to check for + */ + toHaveAbilityApplied(expectedAbilityId: AbilityId): void; + + /** + * Check whether a {@linkcode Pokemon} has a specific amount of {@linkcode Stat.HP | HP}. + * @param expectedHp - The expected amount of {@linkcode Stat.HP | HP} to have + */ + toHaveHp(expectedHp: number): void; + + /** + * Check whether a {@linkcode Pokemon} has taken a specific amount of damage. + * @param expectedDamageTaken - The expected amount of damage taken + * @param roundDown - Whether to round down `expectedDamageTaken` with {@linkcode toDmgValue}; default `true` + */ + toHaveTakenDamage(expectedDamageTaken: number, roundDown?: boolean): void; + + /** + * Check whether a {@linkcode Pokemon} is currently fainted (as determined by {@linkcode Pokemon.isFainted}). + * @remarks + * When checking whether an enemy wild Pokemon is fainted, one must store a reference to it in a variable _before_ the fainting effect occurs. + * Otherwise, the Pokemon will be removed from the field and garbage collected. + */ + toHaveFainted(): void; + + /** + * Check whether a {@linkcode Pokemon} is at full HP. + */ + toHaveFullHp(): void; + + /** + * Check whether a {@linkcode Pokemon} has consumed the given amount of PP for one of its moves. + * @param moveId - The {@linkcode MoveId} corresponding to the {@linkcode PokemonMove} that should have consumed PP + * @param ppUsed - The numerical amount of PP that should have been consumed, + * or `all` to indicate the move should be _out_ of PP + * @remarks + * If the Pokemon's moveset has been set via {@linkcode Overrides.MOVESET_OVERRIDE}/{@linkcode Overrides.ENEMY_MOVESET_OVERRIDE} + * or does not contain exactly one copy of `moveId`, this will fail the test. + */ + toHaveUsedPP(moveId: MoveId, ppUsed: number | "all"): void; +} +// #endregion Pokemon Matchers diff --git a/test/abilities/dancer.test.ts b/test/abilities/dancer.test.ts index e640e326d58..e206152715e 100644 --- a/test/abilities/dancer.test.ts +++ b/test/abilities/dancer.test.ts @@ -34,7 +34,7 @@ describe("Abilities - Dancer", () => { game.override.enemyAbility(AbilityId.DANCER).enemySpecies(SpeciesId.MAGIKARP).enemyMoveset(MoveId.VICTORY_DANCE); await game.classicMode.startBattle([SpeciesId.ORICORIO, SpeciesId.FEEBAS]); - const [oricorio, feebas] = game.scene.getPlayerField(); + const [oricorio, feebas, magikarp1] = game.scene.getField(); game.move.changeMoveset(oricorio, [MoveId.SWORDS_DANCE, MoveId.VICTORY_DANCE, MoveId.SPLASH]); game.move.changeMoveset(feebas, [MoveId.SWORDS_DANCE, MoveId.SPLASH]); @@ -44,8 +44,9 @@ describe("Abilities - Dancer", () => { await game.phaseInterceptor.to("MovePhase"); // feebas uses swords dance await game.phaseInterceptor.to("MovePhase", false); // oricorio copies swords dance + // Dancer order will be Magikarp, Oricorio, Magikarp based on set turn order let currentPhase = game.scene.phaseManager.getCurrentPhase() as MovePhase; - expect(currentPhase.pokemon).toBe(oricorio); + expect(currentPhase.pokemon).toBe(magikarp1); expect(currentPhase.move.moveId).toBe(MoveId.SWORDS_DANCE); await game.phaseInterceptor.to("MoveEndPhase"); // end oricorio's move diff --git a/test/abilities/mycelium-might.test.ts b/test/abilities/mycelium-might.test.ts index c3b7b4753b6..21b856d341e 100644 --- a/test/abilities/mycelium-might.test.ts +++ b/test/abilities/mycelium-might.test.ts @@ -2,8 +2,6 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { TurnEndPhase } from "#phases/turn-end-phase"; -import { TurnStartPhase } from "#phases/turn-start-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -45,65 +43,50 @@ describe("Abilities - Mycelium Might", () => { it("should move last in its priority bracket and ignore protective abilities", async () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const enemyPokemon = game.field.getEnemyPokemon(); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = enemyPokemon.getBattlerIndex(); + const enemy = game.field.getEnemyPokemon(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.BABY_DOLL_EYES); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The opponent Pokemon (without Mycelium Might) goes first despite having lower speed than the player Pokemon. // The player Pokemon (with Mycelium Might) goes last despite having higher speed than the opponent. - expect(speedOrder).toEqual([playerIndex, enemyIndex]); - expect(commandOrder).toEqual([enemyIndex, playerIndex]); - await game.phaseInterceptor.to(TurnEndPhase); + expect(player.hp).not.toEqual(player.getMaxHp()); + await game.phaseInterceptor.to("TurnEndPhase"); // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced. - expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(enemy.getStatStage(Stat.ATK)).toBe(-1); }); it("should still go first if a status move that is in a higher priority bracket than the opponent's move is used", async () => { game.override.enemyMoveset(MoveId.TACKLE); await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const enemyPokemon = game.field.getEnemyPokemon(); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = enemyPokemon.getBattlerIndex(); + const enemy = game.field.getEnemyPokemon(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.BABY_DOLL_EYES); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The player Pokemon (with M.M.) goes first because its move is still within a higher priority bracket than its opponent. // The enemy Pokemon goes second because its move is in a lower priority bracket. - expect(speedOrder).toEqual([playerIndex, enemyIndex]); - expect(commandOrder).toEqual([playerIndex, enemyIndex]); - await game.phaseInterceptor.to(TurnEndPhase); + expect(player.hp).toEqual(player.getMaxHp()); + await game.phaseInterceptor.to("TurnEndPhase"); // Despite the opponent's ability (Clear Body), its ATK stat stage is still reduced. - expect(enemyPokemon.getStatStage(Stat.ATK)).toBe(-1); + expect(enemy.getStatStage(Stat.ATK)).toBe(-1); }); it("should not affect non-status moves", async () => { await game.classicMode.startBattle([SpeciesId.REGIELEKI]); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.QUICK_ATTACK); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The player Pokemon (with M.M.) goes first because it has a higher speed and did not use a status move. // The enemy Pokemon (without M.M.) goes second because its speed is lower. // This means that the commandOrder should be identical to the speedOrder - expect(speedOrder).toEqual([playerIndex, enemyIndex]); - expect(commandOrder).toEqual([playerIndex, enemyIndex]); + expect(player.hp).toEqual(player.getMaxHp()); }); }); diff --git a/test/abilities/neutralizing-gas.test.ts b/test/abilities/neutralizing-gas.test.ts index 555e5f8a19c..fd9138e4174 100644 --- a/test/abilities/neutralizing-gas.test.ts +++ b/test/abilities/neutralizing-gas.test.ts @@ -59,7 +59,7 @@ describe("Abilities - Neutralizing Gas", () => { expect(game.field.getPlayerPokemon().getStatStage(Stat.ATK)).toBe(1); }); - it.todo("should activate before other abilities", async () => { + it("should activate before other abilities", async () => { game.override.enemySpecies(SpeciesId.ACCELGOR).enemyLevel(100).enemyAbility(AbilityId.INTIMIDATE); await game.classicMode.startBattle([SpeciesId.FEEBAS]); diff --git a/test/abilities/quick-draw.test.ts b/test/abilities/quick-draw.test.ts index ce5873af3a8..257892145e5 100644 --- a/test/abilities/quick-draw.test.ts +++ b/test/abilities/quick-draw.test.ts @@ -5,7 +5,7 @@ import { SpeciesId } from "#enums/species-id"; import { FaintPhase } from "#phases/faint-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, test, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Abilities - Quick Draw", () => { let phaserGame: Phaser.Game; @@ -25,7 +25,6 @@ describe("Abilities - Quick Draw", () => { game = new GameManager(phaserGame); game.override .battleStyle("single") - .starterSpecies(SpeciesId.MAGIKARP) .ability(AbilityId.QUICK_DRAW) .moveset([MoveId.TACKLE, MoveId.TAIL_WHIP]) .enemyLevel(100) @@ -40,8 +39,8 @@ describe("Abilities - Quick Draw", () => { ).mockReturnValue(100); }); - test("makes pokemon going first in its priority bracket", async () => { - await game.classicMode.startBattle(); + it("makes pokemon go first in its priority bracket", async () => { + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const pokemon = game.field.getPlayerPokemon(); const enemy = game.field.getEnemyPokemon(); @@ -57,33 +56,27 @@ describe("Abilities - Quick Draw", () => { expect(pokemon.waveData.abilitiesApplied).toContain(AbilityId.QUICK_DRAW); }); - test( - "does not triggered by non damage moves", - { - retry: 5, - }, - async () => { - await game.classicMode.startBattle(); + it("is not triggered by non damaging moves", async () => { + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); - const pokemon = game.field.getPlayerPokemon(); - const enemy = game.field.getEnemyPokemon(); + const pokemon = game.field.getPlayerPokemon(); + const enemy = game.field.getEnemyPokemon(); - pokemon.hp = 1; - enemy.hp = 1; + pokemon.hp = 1; + enemy.hp = 1; - game.move.select(MoveId.TAIL_WHIP); - await game.phaseInterceptor.to(FaintPhase, false); + game.move.select(MoveId.TAIL_WHIP); + await game.phaseInterceptor.to(FaintPhase, false); - expect(pokemon.isFainted()).toBe(true); - expect(enemy.isFainted()).toBe(false); - expect(pokemon.waveData.abilitiesApplied).not.contain(AbilityId.QUICK_DRAW); - }, - ); + expect(pokemon.isFainted()).toBe(true); + expect(enemy.isFainted()).toBe(false); + expect(pokemon.waveData.abilitiesApplied).not.contain(AbilityId.QUICK_DRAW); + }); - test("does not increase priority", async () => { + it("does not increase priority", async () => { game.override.enemyMoveset([MoveId.EXTREME_SPEED]); - await game.classicMode.startBattle(); + await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const pokemon = game.field.getPlayerPokemon(); const enemy = game.field.getEnemyPokemon(); diff --git a/test/abilities/stall.test.ts b/test/abilities/stall.test.ts index 5b4e38f7099..b6a88964e09 100644 --- a/test/abilities/stall.test.ts +++ b/test/abilities/stall.test.ts @@ -1,7 +1,6 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { TurnStartPhase } from "#phases/turn-start-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; @@ -40,56 +39,41 @@ describe("Abilities - Stall", () => { it("Pokemon with Stall should move last in its priority bracket regardless of speed", async () => { await game.classicMode.startBattle([SpeciesId.SHUCKLE]); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.QUICK_ATTACK); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The player Pokemon (without Stall) goes first despite having lower speed than the opponent. // The opponent Pokemon (with Stall) goes last despite having higher speed than the player Pokemon. - expect(speedOrder).toEqual([enemyIndex, playerIndex]); - expect(commandOrder).toEqual([playerIndex, enemyIndex]); + expect(player).toHaveFullHp(); }); it("Pokemon with Stall will go first if a move that is in a higher priority bracket than the opponent's move is used", async () => { await game.classicMode.startBattle([SpeciesId.SHUCKLE]); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The opponent Pokemon (with Stall) goes first because its move is still within a higher priority bracket than its opponent. // The player Pokemon goes second because its move is in a lower priority bracket. - expect(speedOrder).toEqual([enemyIndex, playerIndex]); - expect(commandOrder).toEqual([enemyIndex, playerIndex]); + expect(player).not.toHaveFullHp(); }); it("If both Pokemon have stall and use the same move, speed is used to determine who goes first.", async () => { game.override.ability(AbilityId.STALL); await game.classicMode.startBattle([SpeciesId.SHUCKLE]); - const playerIndex = game.field.getPlayerPokemon().getBattlerIndex(); - const enemyIndex = game.field.getEnemyPokemon().getBattlerIndex(); + const player = game.field.getPlayerPokemon(); game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to(TurnStartPhase, false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const speedOrder = phase.getSpeedOrder(); - const commandOrder = phase.getCommandOrder(); + await game.phaseInterceptor.to("MoveEndPhase", false); // The opponent Pokemon (with Stall) goes first because it has a higher speed. // The player Pokemon (with Stall) goes second because its speed is lower. - expect(speedOrder).toEqual([enemyIndex, playerIndex]); - expect(commandOrder).toEqual([enemyIndex, playerIndex]); + expect(player).not.toHaveFullHp(); }); }); diff --git a/test/battle/battle-order.test.ts b/test/battle/battle-order.test.ts index 0b24fcbfa7d..de13b22df79 100644 --- a/test/battle/battle-order.test.ts +++ b/test/battle/battle-order.test.ts @@ -1,7 +1,8 @@ import { AbilityId } from "#enums/ability-id"; +import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import type { TurnStartPhase } from "#phases/turn-start-phase"; +import type { MovePhase } from "#phases/move-phase"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -34,38 +35,34 @@ describe("Battle order", () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR]); const playerPokemon = game.field.getPlayerPokemon(); + const playerStartHp = playerPokemon.hp; const enemyPokemon = game.field.getEnemyPokemon(); + const enemyStartHp = enemyPokemon.hp; + vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set playerPokemon's speed to 50 vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150 - game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to("TurnStartPhase", false); - const playerPokemonIndex = playerPokemon.getBattlerIndex(); - const enemyPokemonIndex = enemyPokemon.getBattlerIndex(); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - expect(order[0]).toBe(enemyPokemonIndex); - expect(order[1]).toBe(playerPokemonIndex); + await game.phaseInterceptor.to("MoveEndPhase", false); + expect(playerPokemon.hp).not.toEqual(playerStartHp); + expect(enemyPokemon.hp).toEqual(enemyStartHp); }); it("Player faster than opponent 150 vs 50", async () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR]); const playerPokemon = game.field.getPlayerPokemon(); + const playerStartHp = playerPokemon.hp; const enemyPokemon = game.field.getEnemyPokemon(); + const enemyStartHp = enemyPokemon.hp; vi.spyOn(playerPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set playerPokemon's speed to 150 vi.spyOn(enemyPokemon, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50]); // set enemyPokemon's speed to 50 game.move.select(MoveId.TACKLE); - await game.phaseInterceptor.to("TurnStartPhase", false); - const playerPokemonIndex = playerPokemon.getBattlerIndex(); - const enemyPokemonIndex = enemyPokemon.getBattlerIndex(); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - expect(order[0]).toBe(playerPokemonIndex); - expect(order[1]).toBe(enemyPokemonIndex); + await game.phaseInterceptor.to("MoveEndPhase", false); + expect(playerPokemon.hp).toEqual(playerStartHp); + expect(enemyPokemon.hp).not.toEqual(enemyStartHp); }); it("double - both opponents faster than player 50/50 vs 150/150", async () => { @@ -73,23 +70,24 @@ describe("Battle order", () => { await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.BLASTOISE]); const playerPokemon = game.scene.getPlayerField(); + const playerHps = playerPokemon.map(p => p.hp); const enemyPokemon = game.scene.getEnemyField(); + const enemyHps = enemyPokemon.map(p => p.hp); playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 50])); // set both playerPokemons' speed to 50 enemyPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150])); // set both enemyPokemons' speed to 150 - const playerIndices = playerPokemon.map(p => p?.getBattlerIndex()); - const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex()); game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); - await game.phaseInterceptor.to("TurnStartPhase", false); + await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER); + await game.move.selectEnemyMove(MoveId.TACKLE, BattlerIndex.PLAYER_2); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - expect(order.slice(0, 2).includes(enemyIndices[0])).toBe(true); - expect(order.slice(0, 2).includes(enemyIndices[1])).toBe(true); - expect(order.slice(2, 4).includes(playerIndices[0])).toBe(true); - expect(order.slice(2, 4).includes(playerIndices[1])).toBe(true); + await game.phaseInterceptor.to("MoveEndPhase", true); + await game.phaseInterceptor.to("MoveEndPhase", false); + for (let i = 0; i < 2; i++) { + expect(playerPokemon[i].hp).not.toEqual(playerHps[i]); + expect(enemyPokemon[i].hp).toEqual(enemyHps[i]); + } }); it("double - speed tie except 1 - 100/100 vs 100/150", async () => { @@ -101,18 +99,13 @@ describe("Battle order", () => { playerPokemon.forEach(p => vi.spyOn(p, "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100])); //set both playerPokemons' speed to 100 vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set enemyPokemon's speed to 100 vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set enemyPokemon's speed to 150 - const playerIndices = playerPokemon.map(p => p?.getBattlerIndex()); - const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex()); game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); - await game.phaseInterceptor.to("TurnStartPhase", false); + await game.phaseInterceptor.to("MovePhase", false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - // enemy 2 should be first, followed by some other assortment of the other 3 pokemon - expect(order[0]).toBe(enemyIndices[1]); - expect(order.slice(1, 4)).toEqual(expect.arrayContaining([enemyIndices[0], ...playerIndices])); + const phase = game.scene.phaseManager.getCurrentPhase() as MovePhase; + expect(phase.pokemon).toEqual(enemyPokemon[1]); }); it("double - speed tie 100/150 vs 100/150", async () => { @@ -125,17 +118,13 @@ describe("Battle order", () => { vi.spyOn(playerPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other playerPokemon's speed to 150 vi.spyOn(enemyPokemon[0], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 100]); // set one enemyPokemon's speed to 100 vi.spyOn(enemyPokemon[1], "stats", "get").mockReturnValue([20, 20, 20, 20, 20, 150]); // set other enemyPokemon's speed to 150 - const playerIndices = playerPokemon.map(p => p?.getBattlerIndex()); - const enemyIndices = enemyPokemon.map(p => p?.getBattlerIndex()); game.move.select(MoveId.TACKLE); game.move.select(MoveId.TACKLE, 1); - await game.phaseInterceptor.to("TurnStartPhase", false); - const phase = game.scene.phaseManager.getCurrentPhase() as TurnStartPhase; - const order = phase.getCommandOrder(); - // P2/E2 should be randomly first/second, then P1/E1 randomly 3rd/4th - expect(order.slice(0, 2)).toStrictEqual(expect.arrayContaining([playerIndices[1], enemyIndices[1]])); - expect(order.slice(2, 4)).toStrictEqual(expect.arrayContaining([playerIndices[0], enemyIndices[0]])); + await game.phaseInterceptor.to("MovePhase", false); + + const phase = game.scene.phaseManager.getCurrentPhase() as MovePhase; + expect(enemyPokemon[1] === phase.pokemon || playerPokemon[1] === phase.pokemon); }); }); diff --git a/test/boss-pokemon.test.ts b/test/boss-pokemon.test.ts index 6ad405d58e6..7803d5c97b0 100644 --- a/test/boss-pokemon.test.ts +++ b/test/boss-pokemon.test.ts @@ -1,7 +1,7 @@ import { AbilityId } from "#enums/ability-id"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; -import { EFFECTIVE_STATS } from "#enums/stat"; +import { EFFECTIVE_STATS, Stat } from "#enums/stat"; import type { EnemyPokemon } from "#field/pokemon"; import { GameManager } from "#test/test-utils/game-manager"; import { toDmgValue } from "#utils/common"; @@ -73,8 +73,9 @@ describe("Boss Pokemon / Shields", () => { expect(boss2.bossSegments).toBe(2); }); - it("shields should stop overflow damage and give stat stage boosts when broken", async () => { - game.override.startingWave(150); // Floor 150 > 2 shields / 3 health segments + // TODO: This test is flaky. It passes when run individually, but not in tandem with others + it.todo("shields should stop overflow damage and give stat stage boosts when broken", async () => { + game.override.startingWave(150).startingLevel(5000); // Floor 150 > 2 shields / 3 health segments await game.classicMode.startBattle([SpeciesId.MEWTWO]); @@ -137,14 +138,17 @@ describe("Boss Pokemon / Shields", () => { it("the number of stat stage boosts is consistent when several shields are broken at once", async () => { const shieldsToBreak = 4; + const segmentHp = 100; game.override.battleStyle("double").enemyHealthSegments(shieldsToBreak + 1); await game.classicMode.startBattle([SpeciesId.MEWTWO]); const boss1 = game.field.getEnemyPokemon(); - const boss1SegmentHp = boss1.getMaxHp() / boss1.bossSegments; - const singleShieldDamage = Math.ceil(boss1SegmentHp); + boss1.setStat(Stat.HP, (shieldsToBreak + 1) * segmentHp); // Set HP to a known value for easier calculations + boss1.hp = boss1.getMaxHp(); + const boss1SegmentHp = segmentHp; + const singleShieldDamage = boss1SegmentHp; // Damage to break a single shield expect(boss1.isBoss()).toBe(true); expect(boss1.bossSegments).toBe(shieldsToBreak + 1); expect(boss1.bossSegmentIndex).toBe(shieldsToBreak); diff --git a/test/moves/baton-pass.test.ts b/test/moves/baton-pass.test.ts index f9bd92a63cd..caabcfa7158 100644 --- a/test/moves/baton-pass.test.ts +++ b/test/moves/baton-pass.test.ts @@ -76,12 +76,7 @@ describe("Moves - Baton Pass", () => { expect(game.field.getEnemyPokemon().getStatStage(Stat.SPATK)).toEqual(2); // confirm that a switch actually happened. can't use species because I // can't find a way to override trainer parties with more than 1 pokemon species - expect(game.phaseInterceptor.log.slice(-4)).toEqual([ - "MoveEffectPhase", - "SwitchSummonPhase", - "SummonPhase", - "PostSummonPhase", - ]); + expect(game.field.getEnemyPokemon().summonData.moveHistory).toHaveLength(0); }); it("doesn't transfer effects that aren't transferrable", async () => { diff --git a/test/moves/delayed-attack.test.ts b/test/moves/delayed-attack.test.ts index 6817c7fd17a..e31c7f28e48 100644 --- a/test/moves/delayed-attack.test.ts +++ b/test/moves/delayed-attack.test.ts @@ -193,7 +193,7 @@ describe("Moves - Delayed Attacks", () => { // All attacks have concluded at this point, unshifting new `MoveEffectPhase`s to the queue. expectFutureSightActive(0); - const MEPs = game.scene.phaseManager.phaseQueue.filter(p => p.is("MoveEffectPhase")); + const MEPs = game.scene.phaseManager["phaseQueue"].findAll("MoveEffectPhase"); expect(MEPs).toHaveLength(4); expect(MEPs.map(mep => mep.getPokemon())).toEqual(oldOrder); }); diff --git a/test/moves/focus-punch.test.ts b/test/moves/focus-punch.test.ts index d7b40569aaa..06594e85e27 100644 --- a/test/moves/focus-punch.test.ts +++ b/test/moves/focus-punch.test.ts @@ -3,7 +3,6 @@ import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { BerryPhase } from "#phases/berry-phase"; import { MessagePhase } from "#phases/message-phase"; -import { MoveHeaderPhase } from "#phases/move-header-phase"; import { SwitchSummonPhase } from "#phases/switch-summon-phase"; import { TurnStartPhase } from "#phases/turn-start-phase"; import { GameManager } from "#test/test-utils/game-manager"; @@ -116,7 +115,7 @@ describe("Moves - Focus Punch", () => { await game.phaseInterceptor.to(TurnStartPhase); expect(game.scene.phaseManager.getCurrentPhase() instanceof SwitchSummonPhase).toBeTruthy(); - expect(game.scene.phaseManager.phaseQueue.find(phase => phase instanceof MoveHeaderPhase)).toBeDefined(); + expect(game.scene.phaseManager.hasPhaseOfType("MoveHeaderPhase")).toBe(true); }); it("should replace the 'but it failed' text when the user gets hit", async () => { game.override.enemyMoveset([MoveId.TACKLE]); diff --git a/test/moves/healing-wish-lunar-dance.test.ts b/test/moves/healing-wish-lunar-dance.test.ts new file mode 100644 index 00000000000..0dcf993aeac --- /dev/null +++ b/test/moves/healing-wish-lunar-dance.test.ts @@ -0,0 +1,245 @@ +import { AbilityId } from "#enums/ability-id"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { Challenges } from "#enums/challenges"; +import { MoveId } from "#enums/move-id"; +import { MoveResult } from "#enums/move-result"; +import { PokemonType } from "#enums/pokemon-type"; +import { SpeciesId } from "#enums/species-id"; +import { StatusEffect } from "#enums/status-effect"; +import { GameManager } from "#test/test-utils/game-manager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Lunar Dance and Healing Wish", () => { + 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("double").enemyAbility(AbilityId.BALL_FETCH).enemyMoveset(MoveId.SPLASH); + }); + + describe.each([ + { moveName: "Healing Wish", moveId: MoveId.HEALING_WISH }, + { moveName: "Lunar Dance", moveId: MoveId.LUNAR_DANCE }, + ])("$moveName", ({ moveId }) => { + it("should sacrifice the user to restore the switched in Pokemon's HP", async () => { + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + + const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty(); + squirtle.hp = 1; + + game.move.use(MoveId.SPLASH, 0); + game.move.use(moveId, 1); + game.doSelectPartyPokemon(2); + + await game.toNextTurn(); + + expect(bulbasaur.isFullHp()).toBe(true); + expect(charmander.isFainted()).toBe(true); + expect(squirtle.isFullHp()).toBe(true); + }); + + it("should sacrifice the user to cure the switched in Pokemon's status", async () => { + game.override.statusEffect(StatusEffect.BURN); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty(); + + game.move.use(MoveId.SPLASH, 0); + game.move.use(moveId, 1); + game.doSelectPartyPokemon(2); + + await game.toNextTurn(); + + expect(bulbasaur.status?.effect).toBe(StatusEffect.BURN); + expect(charmander.isFainted()).toBe(true); + expect(squirtle.status?.effect).toBeUndefined(); + }); + + it("should fail if the user has no non-fainted allies in their party", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); + const [bulbasaur, charmander] = game.scene.getPlayerParty(); + + game.move.use(MoveId.MEMENTO); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + + expect(bulbasaur.isFainted()).toBe(true); + expect(charmander.isActive(true)).toBe(true); + + game.move.use(moveId); + + await game.toEndOfTurn(); + + expect(charmander.isFullHp()); + expect(charmander.getLastXMoves()[0].result).toBe(MoveResult.FAIL); + }); + + it("should fail if the user has no challenge-eligible allies", async () => { + game.override.battleStyle("single"); + // Mono normal challenge + game.challengeMode.addChallenge(Challenges.SINGLE_TYPE, PokemonType.NORMAL + 1, 0); + await game.challengeMode.startBattle([SpeciesId.RATICATE, SpeciesId.ODDISH]); + + const raticate = game.field.getPlayerPokemon(); + + game.move.use(moveId); + await game.toNextTurn(); + + expect(raticate.isFullHp()).toBe(true); + expect(raticate.getLastXMoves()[0].result).toEqual(MoveResult.FAIL); + }); + + it("should store its effect if the switched-in Pokemon would be unaffected", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER, SpeciesId.SQUIRTLE]); + + const [bulbasaur, charmander, squirtle] = game.scene.getPlayerParty(); + squirtle.hp = 1; + + game.move.use(moveId); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + + // Bulbasaur fainted and stored a healing effect + expect(bulbasaur.isFainted()).toBe(true); + expect(charmander.isFullHp()).toBe(true); + expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase"); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined(); + + // Switch to damaged Squirtle. HW/LD's effect should activate + game.doSwitchPokemon(2); + + await game.toEndOfTurn(); + expect(squirtle.isFullHp()).toBe(true); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeUndefined(); + + // Set Charmander's HP to 1, then switch back to Charmander. + // HW/LD shouldn't activate again + charmander.hp = 1; + game.doSwitchPokemon(2); + + await game.toEndOfTurn(); + expect(charmander.hp).toBe(1); + }); + + it("should only store one charge of the effect at a time", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([ + SpeciesId.BULBASAUR, + SpeciesId.CHARMANDER, + SpeciesId.SQUIRTLE, + SpeciesId.PIKACHU, + ]); + + const [bulbasaur, charmander, squirtle, pikachu] = game.scene.getPlayerParty(); + [squirtle, pikachu].forEach(p => (p.hp = 1)); + + // Use HW/LD and send in Charmander. HW/LD's effect should be stored + game.move.use(moveId); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + expect(bulbasaur.isFainted()).toBe(true); + expect(charmander.isFullHp()).toBe(true); + expect(charmander.isFullHp()); + expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase"); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined(); + + // Use HW/LD again, sending in Squirtle. HW/LD should activate and heal Squirtle + game.move.use(moveId); + game.doSelectPartyPokemon(2); + + await game.toNextTurn(); + expect(charmander.isFainted()).toBe(true); + expect(squirtle.isFullHp()).toBe(true); + expect(squirtle.isFullHp()); + + // Switch again to Pikachu. HW/LD's effect shouldn't be present + game.doSwitchPokemon(3); + + expect(pikachu.isFullHp()).toBe(false); + }); + }); + + it("Lunar Dance should sacrifice the user to restore the switched in Pokemon's PP", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.CHARMANDER]); + + const [bulbasaur, charmander] = game.scene.getPlayerParty(); + + game.move.use(MoveId.SPLASH); + await game.toNextTurn(); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + + game.move.use(MoveId.LUNAR_DANCE); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + expect(charmander.isFainted()).toBeTruthy(); + bulbasaur.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(0)); + }); + + it("should stack with each other", async () => { + game.override.battleStyle("single"); + + await game.classicMode.startBattle([ + SpeciesId.BULBASAUR, + SpeciesId.CHARMANDER, + SpeciesId.SQUIRTLE, + SpeciesId.PIKACHU, + ]); + + const [bulbasaur, charmander, squirtle, pikachu] = game.scene.getPlayerParty(); + [squirtle, pikachu].forEach(p => { + p.hp = 1; + p.getMoveset().forEach(mv => (mv.ppUsed = 1)); + }); + + game.move.use(MoveId.LUNAR_DANCE); + game.doSelectPartyPokemon(1); + + await game.toNextTurn(); + expect(bulbasaur.isFainted()).toBe(true); + expect(charmander.isFullHp()).toBe(true); + expect(game.phaseInterceptor.log).not.toContain("PokemonHealPhase"); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined(); + + game.move.use(MoveId.HEALING_WISH); + game.doSelectPartyPokemon(2); + + // Lunar Dance should apply first since it was used first, restoring Squirtle's HP and PP + await game.toNextTurn(); + expect(squirtle.isFullHp()).toBe(true); + squirtle.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(0)); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeDefined(); + + game.doSwitchPokemon(3); + + // Healing Wish should apply on the next switch, restoring Pikachu's HP + await game.toEndOfTurn(); + expect(pikachu.isFullHp()).toBe(true); + pikachu.getMoveset().forEach(mv => expect(mv.ppUsed).toBe(1)); + expect(game.scene.arena.getTag(ArenaTagType.PENDING_HEAL)).toBeUndefined(); + }); +}); diff --git a/test/moves/lunar-dance.test.ts b/test/moves/lunar-dance.test.ts deleted file mode 100644 index 7386d15079b..00000000000 --- a/test/moves/lunar-dance.test.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { AbilityId } from "#enums/ability-id"; -import { MoveId } from "#enums/move-id"; -import { SpeciesId } from "#enums/species-id"; -import { StatusEffect } from "#enums/status-effect"; -import { GameManager } from "#test/test-utils/game-manager"; -import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; - -describe("Moves - Lunar Dance", () => { - 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 - .statusEffect(StatusEffect.BURN) - .battleStyle("double") - .enemyAbility(AbilityId.BALL_FETCH) - .enemyMoveset(MoveId.SPLASH); - }); - - it("should full restore HP, PP and status of switched in pokemon, then fail second use because no remaining backup pokemon in party", async () => { - await game.classicMode.startBattle([SpeciesId.BULBASAUR, SpeciesId.ODDISH, SpeciesId.RATTATA]); - - const [bulbasaur, oddish, rattata] = game.scene.getPlayerParty(); - game.move.changeMoveset(bulbasaur, [MoveId.LUNAR_DANCE, MoveId.SPLASH]); - game.move.changeMoveset(oddish, [MoveId.LUNAR_DANCE, MoveId.SPLASH]); - game.move.changeMoveset(rattata, [MoveId.LUNAR_DANCE, MoveId.SPLASH]); - - game.move.select(MoveId.SPLASH, 0); - game.move.select(MoveId.SPLASH, 1); - await game.toNextTurn(); - - // Bulbasaur should still be burned and have used a PP for splash and not at max hp - expect(bulbasaur.status?.effect).toBe(StatusEffect.BURN); - expect(bulbasaur.moveset[1]?.ppUsed).toBe(1); - expect(bulbasaur.hp).toBeLessThan(bulbasaur.getMaxHp()); - - // Switch out Bulbasaur for Rattata so we can swtich bulbasaur back in with lunar dance - game.doSwitchPokemon(2); - game.move.select(MoveId.SPLASH, 1); - await game.toNextTurn(); - - game.move.select(MoveId.SPLASH, 0); - game.move.select(MoveId.LUNAR_DANCE); - game.doSelectPartyPokemon(2); - await game.phaseInterceptor.to("SwitchPhase", false); - await game.toNextTurn(); - - // Bulbasaur should NOT have any status and have full PP for splash and be at max hp - expect(bulbasaur.status?.effect).toBeUndefined(); - expect(bulbasaur.moveset[1]?.ppUsed).toBe(0); - expect(bulbasaur.isFullHp()).toBe(true); - - game.move.select(MoveId.SPLASH, 0); - game.move.select(MoveId.LUNAR_DANCE); - await game.toNextTurn(); - - // Using Lunar dance again should fail because nothing in party and rattata should be alive - expect(rattata.status?.effect).toBe(StatusEffect.BURN); - expect(rattata.hp).toBeLessThan(rattata.getMaxHp()); - }); -}); diff --git a/test/moves/rage-fist.test.ts b/test/moves/rage-fist.test.ts index 61164b5710c..c58d1296ac5 100644 --- a/test/moves/rage-fist.test.ts +++ b/test/moves/rage-fist.test.ts @@ -166,7 +166,6 @@ describe("Moves - Rage Fist", () => { // Charizard hit game.move.select(MoveId.SPLASH); - await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); await game.toNextTurn(); expect(getPartyHitCount()).toEqual([1, 0]); diff --git a/test/moves/revival-blessing.test.ts b/test/moves/revival-blessing.test.ts index 4dc7cb97f2d..8c751458ff7 100644 --- a/test/moves/revival-blessing.test.ts +++ b/test/moves/revival-blessing.test.ts @@ -119,17 +119,16 @@ describe("Moves - Revival Blessing", () => { game.override .battleStyle("double") .enemyMoveset([MoveId.REVIVAL_BLESSING]) - .moveset([MoveId.SPLASH]) + .moveset([MoveId.SPLASH, MoveId.JUDGMENT]) + .startingLevel(100) .startingWave(25); // 2nd rival battle - must have 3+ pokemon await game.classicMode.startBattle([SpeciesId.ARCEUS, SpeciesId.GIRATINA]); const enemyFainting = game.scene.getEnemyField()[0]; - game.move.select(MoveId.SPLASH, 0); + game.move.use(MoveId.JUDGMENT, 0, BattlerIndex.ENEMY); game.move.select(MoveId.SPLASH, 1); - await game.killPokemon(enemyFainting); - await game.phaseInterceptor.to("BerryPhase"); await game.toNextTurn(); // If there are incorrectly two switch phases into this slot, the fainted pokemon will end up in slot 3 // Make sure it's still in slot 1 diff --git a/test/moves/shell-trap.test.ts b/test/moves/shell-trap.test.ts index 5ecad3116af..2a83f2c3266 100644 --- a/test/moves/shell-trap.test.ts +++ b/test/moves/shell-trap.test.ts @@ -48,7 +48,7 @@ describe("Moves - Shell Trap", () => { await game.setTurnOrder([BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER, BattlerIndex.PLAYER_2]); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); const movePhase = game.scene.phaseManager.getCurrentPhase(); expect(movePhase instanceof MovePhase).toBeTruthy(); diff --git a/test/moves/trick-room.test.ts b/test/moves/trick-room.test.ts index a1d81efb17e..d970dc9762d 100644 --- a/test/moves/trick-room.test.ts +++ b/test/moves/trick-room.test.ts @@ -5,10 +5,10 @@ import { BattlerIndex } from "#enums/battler-index"; import { MoveId } from "#enums/move-id"; import { SpeciesId } from "#enums/species-id"; import { Stat } from "#enums/stat"; -import { TurnStartPhase } from "#phases/turn-start-phase"; +import { WeatherType } from "#enums/weather-type"; import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Move - Trick Room", () => { let phaserGame: Phaser.Game; @@ -56,13 +56,11 @@ describe("Move - Trick Room", () => { turnCount: 4, // The 5 turn limit _includes_ the current turn! }); - // Now, check that speed was indeed reduced - const turnOrderSpy = vi.spyOn(TurnStartPhase.prototype, "getSpeedOrder"); - - game.move.use(MoveId.SPLASH); + game.move.use(MoveId.SUNNY_DAY); + await game.move.forceEnemyMove(MoveId.RAIN_DANCE); await game.toEndOfTurn(); - expect(turnOrderSpy).toHaveLastReturnedWith([BattlerIndex.ENEMY, BattlerIndex.PLAYER]); + expect(game.scene.arena.getWeatherType()).toBe(WeatherType.SUNNY); }); it("should be removed when overlapped", async () => { diff --git a/test/moves/wish.test.ts b/test/moves/wish.test.ts index 1c1f3f3b8ba..b64a15ac654 100644 --- a/test/moves/wish.test.ts +++ b/test/moves/wish.test.ts @@ -135,7 +135,7 @@ describe("Move - Wish", () => { // all wishes have activated and added healing phases expect(game).toHavePositionalTag(PositionalTagType.WISH, 0); - const healPhases = game.scene.phaseManager.phaseQueue.filter(p => p.is("PokemonHealPhase")); + const healPhases = game.scene.phaseManager["phaseQueue"].findAll("PokemonHealPhase"); expect(healPhases).toHaveLength(4); expect.soft(healPhases.map(php => php.getPokemon())).toEqual(oldOrder); diff --git a/test/mystery-encounter/encounter-test-utils.ts b/test/mystery-encounter/encounter-test-utils.ts index 4aad0e000d9..165678a88da 100644 --- a/test/mystery-encounter/encounter-test-utils.ts +++ b/test/mystery-encounter/encounter-test-utils.ts @@ -70,7 +70,6 @@ export async function runMysteryEncounterToEnd( // If a battle is started, fast forward to end of the battle game.onNextPrompt("CommandPhase", UiMode.COMMAND, () => { game.scene.phaseManager.clearPhaseQueue(); - game.scene.phaseManager.clearPhaseQueueSplice(); game.scene.phaseManager.unshiftPhase(new VictoryPhase(0)); game.endPhase(); }); @@ -196,7 +195,6 @@ async function handleSecondaryOptionSelect(game: GameManager, pokemonNo: number, */ export async function skipBattleRunMysteryEncounterRewardsPhase(game: GameManager, runRewardsPhase = true) { game.scene.phaseManager.clearPhaseQueue(); - game.scene.phaseManager.clearPhaseQueueSplice(); game.scene.getEnemyParty().forEach(p => { p.hp = 0; p.status = new Status(StatusEffect.FAINT); diff --git a/test/mystery-encounter/encounters/part-timer-encounter.test.ts b/test/mystery-encounter/encounters/part-timer-encounter.test.ts index 1826c75381a..15d2664364c 100644 --- a/test/mystery-encounter/encounters/part-timer-encounter.test.ts +++ b/test/mystery-encounter/encounters/part-timer-encounter.test.ts @@ -168,6 +168,7 @@ describe("Part-Timer - Mystery Encounter", () => { // Override party levels to 50 so stats can be fully reflective scene.getPlayerParty().forEach(p => { p.level = 50; + p.ivs = [0, 0, 0, 0, 0, 0]; p.calculateStats(); }); await runMysteryEncounterToEnd(game, 2, { pokemonNo: 3 }); diff --git a/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts b/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts index 814e2ee07fb..3bbb858a15d 100644 --- a/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts +++ b/test/mystery-encounter/encounters/the-winstrate-challenge-encounter.test.ts @@ -355,7 +355,6 @@ describe("The Winstrate Challenge - Mystery Encounter", () => { */ async function skipBattleToNextBattle(game: GameManager, isFinalBattle = false) { game.scene.phaseManager.clearPhaseQueue(); - game.scene.phaseManager.clearPhaseQueueSplice(); const commandUiHandler = game.scene.ui.handlers[UiMode.COMMAND]; commandUiHandler.clear(); game.scene.getEnemyParty().forEach(p => { diff --git a/test/setup/matchers.setup.ts b/test/setup/matchers.setup.ts index 88ca0a5c6bc..8ad14c8679a 100644 --- a/test/setup/matchers.setup.ts +++ b/test/setup/matchers.setup.ts @@ -1,5 +1,11 @@ +/** + * Setup file for custom matchers. + * Make sure to define the call signatures in `#test/@types/vitest.d.ts` too! + * @module + */ + import { toBeAtPhase } from "#test/test-utils/matchers/to-be-at-phase"; -import { toEqualArrayUnsorted } from "#test/test-utils/matchers/to-equal-array-unsorted"; +import { toEqualUnsorted } from "#test/test-utils/matchers/to-equal-unsorted"; import { toHaveAbilityApplied } from "#test/test-utils/matchers/to-have-ability-applied"; import { toHaveArenaTag } from "#test/test-utils/matchers/to-have-arena-tag"; import { toHaveBattlerTag } from "#test/test-utils/matchers/to-have-battler-tag"; @@ -7,6 +13,7 @@ import { toHaveEffectiveStat } from "#test/test-utils/matchers/to-have-effective import { toHaveFainted } from "#test/test-utils/matchers/to-have-fainted"; import { toHaveFullHp } from "#test/test-utils/matchers/to-have-full-hp"; import { toHaveHp } from "#test/test-utils/matchers/to-have-hp"; +import { toHaveKey } from "#test/test-utils/matchers/to-have-key"; import { toHavePositionalTag } from "#test/test-utils/matchers/to-have-positional-tag"; import { toHaveShownMessage } from "#test/test-utils/matchers/to-have-shown-message"; import { toHaveStatStage } from "#test/test-utils/matchers/to-have-stat-stage"; @@ -19,13 +26,9 @@ import { toHaveUsedPP } from "#test/test-utils/matchers/to-have-used-pp"; import { toHaveWeather } from "#test/test-utils/matchers/to-have-weather"; import { expect } from "vitest"; -/* - * Setup file for custom matchers. - * Make sure to define the call signatures in `#test/@types/vitest.d.ts` too! - */ - expect.extend({ - toEqualArrayUnsorted, + toEqualUnsorted, + toHaveKey, toHaveShownMessage, toBeAtPhase, toHaveWeather, diff --git a/test/test-utils/game-manager-utils.ts b/test/test-utils/game-manager-utils.ts index 26b7ccf1020..4be05bf0ddb 100644 --- a/test/test-utils/game-manager-utils.ts +++ b/test/test-utils/game-manager-utils.ts @@ -8,8 +8,7 @@ import { GameModes } from "#enums/game-modes"; import type { MoveId } from "#enums/move-id"; import type { SpeciesId } from "#enums/species-id"; import { PlayerPokemon } from "#field/pokemon"; -import type { StarterMoveset } from "#types/save-data"; -import type { Starter } from "#ui/starter-select-ui-handler"; +import type { Starter, StarterMoveset } from "#types/save-data"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; /** Function to convert Blob to string */ @@ -33,24 +32,24 @@ export function holdOn(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)); } -export function generateStarter(scene: BattleScene, species?: SpeciesId[]): Starter[] { +export function generateStarters(scene: BattleScene, speciesIds?: SpeciesId[]): Starter[] { const seed = "test"; - const starters = getTestRunStarters(seed, species); + const starters = getTestRunStarters(seed, speciesIds); const startingLevel = scene.gameMode.getStartingLevel(); for (const starter of starters) { - const starterProps = scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); - const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); + const species = getPokemonSpecies(starter.speciesId); + const starterFormIndex = starter.formIndex; const starterGender = - starter.species.malePercent !== null ? (!starterProps.female ? Gender.MALE : Gender.FEMALE) : Gender.GENDERLESS; + species.malePercent !== null ? (starter.female ? Gender.FEMALE : Gender.MALE) : Gender.GENDERLESS; const starterPokemon = scene.addPlayerPokemon( - starter.species, + species, startingLevel, starter.abilityIndex, starterFormIndex, starterGender, - starterProps.shiny, - starterProps.variant, - undefined, + starter.shiny, + starter.variant, + starter.ivs, starter.nature, ); const moveset: MoveId[] = []; @@ -62,20 +61,23 @@ export function generateStarter(scene: BattleScene, species?: SpeciesId[]): Star return starters; } -function getTestRunStarters(seed: string, species?: SpeciesId[]): Starter[] { - if (!species) { +function getTestRunStarters(seed: string, speciesIds?: SpeciesId[]): Starter[] { + if (!speciesIds || speciesIds.length === 0) { return getDailyRunStarters(seed); } const starters: Starter[] = []; const startingLevel = getGameMode(GameModes.CLASSIC).getStartingLevel(); - for (const specie of species) { - const starterSpeciesForm = getPokemonSpeciesForm(specie, 0); + for (const speciesId of speciesIds) { + const starterSpeciesForm = getPokemonSpeciesForm(speciesId, 0); const starterSpecies = getPokemonSpecies(starterSpeciesForm.speciesId); const pokemon = new PlayerPokemon(starterSpecies, startingLevel, undefined, 0); const starter: Starter = { - species: starterSpecies, - dexAttr: pokemon.getDexAttr(), + speciesId, + shiny: pokemon.shiny, + variant: pokemon.variant, + formIndex: pokemon.formIndex, + ivs: pokemon.ivs, abilityIndex: pokemon.abilityIndex, passive: false, nature: pokemon.getNature(), @@ -89,22 +91,20 @@ function getTestRunStarters(seed: string, species?: SpeciesId[]): Starter[] { /** * Useful for populating party, wave index, etc. without having to spin up and run through an entire EncounterPhase */ -export function initSceneWithoutEncounterPhase(scene: BattleScene, species?: SpeciesId[]): void { - const starters = generateStarter(scene, species); +export function initSceneWithoutEncounterPhase(scene: BattleScene, speciesIds?: SpeciesId[]): void { + const starters = generateStarters(scene, speciesIds); starters.forEach(starter => { - const starterProps = scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr); - const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0)); + const starterFormIndex = starter.formIndex; const starterGender = Gender.MALE; - const starterIvs = scene.gameData.dexData[starter.species.speciesId].ivs.slice(0); const starterPokemon = scene.addPlayerPokemon( - starter.species, + getPokemonSpecies(starter.speciesId), scene.gameMode.getStartingLevel(), starter.abilityIndex, starterFormIndex, starterGender, - starterProps.shiny, - starterProps.variant, - starterIvs, + starter.shiny, + starter.variant, + starter.ivs, starter.nature, ); starter.moveset && starterPokemon.tryPopulateMoveset(starter.moveset); diff --git a/test/test-utils/game-manager.ts b/test/test-utils/game-manager.ts index f9db964ad26..abe0b8cfcf6 100644 --- a/test/test-utils/game-manager.ts +++ b/test/test-utils/game-manager.ts @@ -29,7 +29,7 @@ import { TurnEndPhase } from "#phases/turn-end-phase"; import { TurnInitPhase } from "#phases/turn-init-phase"; import { TurnStartPhase } from "#phases/turn-start-phase"; import { ErrorInterceptor } from "#test/test-utils/error-interceptor"; -import { generateStarter } from "#test/test-utils/game-manager-utils"; +import { generateStarters } from "#test/test-utils/game-manager-utils"; import { GameWrapper } from "#test/test-utils/game-wrapper"; import { ChallengeModeHelper } from "#test/test-utils/helpers/challenge-mode-helper"; import { ClassicModeHelper } from "#test/test-utils/helpers/classic-mode-helper"; @@ -215,7 +215,7 @@ export class GameManager { this.onNextPrompt("TitlePhase", UiMode.TITLE, () => { this.scene.gameMode = getGameMode(mode); - const starters = generateStarter(this.scene, species); + const starters = generateStarters(this.scene, species); const selectStarterPhase = new SelectStarterPhase(); this.scene.phaseManager.pushPhase(new EncounterPhase(false)); selectStarterPhase.initBattle(starters); @@ -251,7 +251,7 @@ export class GameManager { UiMode.TITLE, () => { this.scene.gameMode = getGameMode(GameModes.CLASSIC); - const starters = generateStarter(this.scene, species); + const starters = generateStarters(this.scene, species); const selectStarterPhase = new SelectStarterPhase(); this.scene.phaseManager.pushPhase(new EncounterPhase(false)); selectStarterPhase.initBattle(starters); @@ -464,6 +464,9 @@ export class GameManager { * Faint a player or enemy pokemon instantly by setting their HP to 0. * @param pokemon - The player/enemy pokemon being fainted * @returns A Promise that resolves once the fainted pokemon's FaintPhase finishes running. + * @remarks + * This method *pushes* a FaintPhase and runs until it's finished. This may cause a turn to play out unexpectedly + * @todo Consider whether running the faint phase immediately can be done */ async killPokemon(pokemon: PlayerPokemon | EnemyPokemon) { pokemon.hp = 0; @@ -533,7 +536,7 @@ export class GameManager { } /** - * Intercepts `TurnStartPhase` and mocks {@linkcode TurnStartPhase.getSpeedOrder}'s return value. + * Modifies the queue manager to return move phases in a particular order * Used to manually modify Pokemon turn order. * Note: This *DOES NOT* account for priority. * @param order - The turn order to set as an array of {@linkcode BattlerIndex}es. @@ -545,7 +548,7 @@ export class GameManager { async setTurnOrder(order: BattlerIndex[]): Promise { await this.phaseInterceptor.to("TurnStartPhase", false); - vi.spyOn(this.scene.phaseManager.getCurrentPhase() as TurnStartPhase, "getSpeedOrder").mockReturnValue(order); + this.scene.phaseManager.dynamicQueueManager.setMoveOrder(order); } /** diff --git a/test/test-utils/helpers/challenge-mode-helper.ts b/test/test-utils/helpers/challenge-mode-helper.ts index 7bc40aec035..a1fe114013d 100644 --- a/test/test-utils/helpers/challenge-mode-helper.ts +++ b/test/test-utils/helpers/challenge-mode-helper.ts @@ -9,7 +9,7 @@ import { CommandPhase } from "#phases/command-phase"; import { EncounterPhase } from "#phases/encounter-phase"; import { SelectStarterPhase } from "#phases/select-starter-phase"; import { TurnInitPhase } from "#phases/turn-init-phase"; -import { generateStarter } from "#test/test-utils/game-manager-utils"; +import { generateStarters } from "#test/test-utils/game-manager-utils"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; /** @@ -34,7 +34,7 @@ export class ChallengeModeHelper extends GameManagerHelper { * @param gameMode - Optional game mode to set. * @returns A promise that resolves when the summon phase is reached. */ - async runToSummon(species?: SpeciesId[]) { + async runToSummon(speciesIds?: SpeciesId[]) { await this.game.runToTitle(); if (this.game.override.disableShinies) { @@ -43,7 +43,7 @@ export class ChallengeModeHelper extends GameManagerHelper { this.game.onNextPrompt("TitlePhase", UiMode.TITLE, () => { this.game.scene.gameMode.challenges = this.challenges; - const starters = generateStarter(this.game.scene, species); + const starters = generateStarters(this.game.scene, speciesIds); const selectStarterPhase = new SelectStarterPhase(); this.game.scene.phaseManager.pushPhase(new EncounterPhase(false)); selectStarterPhase.initBattle(starters); diff --git a/test/test-utils/helpers/classic-mode-helper.ts b/test/test-utils/helpers/classic-mode-helper.ts index f813a8f797e..896de7a8b6f 100644 --- a/test/test-utils/helpers/classic-mode-helper.ts +++ b/test/test-utils/helpers/classic-mode-helper.ts @@ -10,7 +10,7 @@ import { CommandPhase } from "#phases/command-phase"; import { EncounterPhase } from "#phases/encounter-phase"; import { SelectStarterPhase } from "#phases/select-starter-phase"; import { TurnInitPhase } from "#phases/turn-init-phase"; -import { generateStarter } from "#test/test-utils/game-manager-utils"; +import { generateStarters } from "#test/test-utils/game-manager-utils"; import { GameManagerHelper } from "#test/test-utils/helpers/game-manager-helper"; /** @@ -35,7 +35,7 @@ export class ClassicModeHelper extends GameManagerHelper { // biome-ignore lint/style/useUnifiedTypeSignatures: Marks the overload for deprecation async runToSummon(): Promise; async runToSummon(species: SpeciesId[] | undefined): Promise; - async runToSummon(species?: SpeciesId[]): Promise { + async runToSummon(speciesIds?: SpeciesId[]): Promise { await this.game.runToTitle(); if (this.game.override.disableShinies) { @@ -50,7 +50,7 @@ export class ClassicModeHelper extends GameManagerHelper { this.game.onNextPrompt("TitlePhase", UiMode.TITLE, () => { this.game.scene.gameMode = getGameMode(GameModes.CLASSIC); - const starters = generateStarter(this.game.scene, species); + const starters = generateStarters(this.game.scene, speciesIds); const selectStarterPhase = new SelectStarterPhase(); this.game.scene.phaseManager.pushPhase(new EncounterPhase(false)); selectStarterPhase.initBattle(starters); diff --git a/test/test-utils/helpers/modifiers-helper.ts b/test/test-utils/helpers/modifiers-helper.ts index bfda35427fa..7d3e29c420f 100644 --- a/test/test-utils/helpers/modifiers-helper.ts +++ b/test/test-utils/helpers/modifiers-helper.ts @@ -40,10 +40,7 @@ export class ModifierHelper extends GameManagerHelper { * @returns `this` */ testCheck(modifier: ModifierTypeKeys, expectToBePreset: boolean): this { - if (expectToBePreset) { - expect(itemPoolChecks.get(modifier)).toBeTruthy(); - } - expect(itemPoolChecks.get(modifier)).toBeFalsy(); + (expectToBePreset ? expect(itemPoolChecks) : expect(itemPoolChecks).not).toHaveKey(modifier); return this; } diff --git a/test/test-utils/matchers/to-equal-array-unsorted.ts b/test/test-utils/matchers/to-equal-unsorted.ts similarity index 92% rename from test/test-utils/matchers/to-equal-array-unsorted.ts rename to test/test-utils/matchers/to-equal-unsorted.ts index 97398689032..c3d85288815 100644 --- a/test/test-utils/matchers/to-equal-array-unsorted.ts +++ b/test/test-utils/matchers/to-equal-unsorted.ts @@ -8,11 +8,7 @@ import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; * @param expected - The array to check equality with * @returns Whether the matcher passed */ -export function toEqualArrayUnsorted( - this: MatcherState, - received: unknown, - expected: unknown[], -): SyncExpectationResult { +export function toEqualUnsorted(this: MatcherState, received: unknown, expected: unknown[]): SyncExpectationResult { if (!Array.isArray(received)) { return { pass: this.isNot, diff --git a/test/test-utils/matchers/to-have-key.ts b/test/test-utils/matchers/to-have-key.ts new file mode 100644 index 00000000000..73d442fc979 --- /dev/null +++ b/test/test-utils/matchers/to-have-key.ts @@ -0,0 +1,47 @@ +import { getOnelineDiffStr } from "#test/test-utils/string-utils"; +import { receivedStr } from "#test/test-utils/test-utils"; +import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; + +/** + * Matcher that checks if a {@linkcode Map} contains the given key, regardless of its value. + * @param received - The received value. Should be a Map + * @param expectedKey - The key whose inclusion in the map is being checked + * @returns Whether the matcher passed + */ +export function toHaveKey(this: MatcherState, received: unknown, expectedKey: unknown): SyncExpectationResult { + if (!(received instanceof Map)) { + return { + pass: this.isNot, + message: () => `Expected to receive a Map, but got ${receivedStr(received)}!`, + }; + } + + if (received.size === 0) { + return { + pass: this.isNot, + message: () => "Expected to receive a non-empty Map, but received map was empty!", + expected: expectedKey, + actual: received, + }; + } + + const keys = [...received.keys()]; + const pass = this.equals(keys, expectedKey, [ + ...this.customTesters, + this.utils.iterableEquality, + this.utils.subsetEquality, + ]); + + const actualStr = getOnelineDiffStr.call(this, received); + const expectedStr = getOnelineDiffStr.call(this, expectedKey); + + return { + pass, + message: () => + pass + ? `Expected ${actualStr} to NOT have the key ${expectedStr}, but it did!` + : `Expected ${actualStr} to have the key ${expectedStr}, but it didn't!`, + expected: expectedKey, + actual: keys, + }; +} diff --git a/test/test-utils/matchers/to-have-terrain.ts b/test/test-utils/matchers/to-have-terrain.ts index f951abed0b3..9b6939168f0 100644 --- a/test/test-utils/matchers/to-have-terrain.ts +++ b/test/test-utils/matchers/to-have-terrain.ts @@ -8,8 +8,8 @@ import { isGameManagerInstance, receivedStr } from "#test/test-utils/test-utils" import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher that checks if the {@linkcode TerrainType} is as expected - * @param received - The object to check. Should be an instance of {@linkcode GameManager}. + * Matcher that checks if the current {@linkcode TerrainType} is as expected. + * @param received - The object to check. Should be the current {@linkcode GameManager}. * @param expectedTerrainType - The expected {@linkcode TerrainType}, or {@linkcode TerrainType.NONE} if no terrain should be active * @returns Whether the matcher passed */ diff --git a/test/test-utils/matchers/to-have-weather.ts b/test/test-utils/matchers/to-have-weather.ts index ffb1e0aad97..7604cd5f890 100644 --- a/test/test-utils/matchers/to-have-weather.ts +++ b/test/test-utils/matchers/to-have-weather.ts @@ -8,8 +8,8 @@ import { toTitleCase } from "#utils/strings"; import type { MatcherState, SyncExpectationResult } from "@vitest/expect"; /** - * Matcher that checks if the {@linkcode WeatherType} is as expected - * @param received - The object to check. Expects an instance of {@linkcode GameManager}. + * Matcher that checks if the current {@linkcode WeatherType} is as expected. + * @param received - The object to check. Should be the current {@linkcode GameManager} * @param expectedWeatherType - The expected {@linkcode WeatherType} * @returns Whether the matcher passed */ diff --git a/test/utils/damage.test.ts b/test/utils/damage.test.ts new file mode 100644 index 00000000000..07149fe51f6 --- /dev/null +++ b/test/utils/damage.test.ts @@ -0,0 +1,57 @@ +import { calculateBossSegmentDamage } from "#utils/damage"; +import { describe, expect, it } from "vitest"; + +describe("Unit Test - calculateBossSegmentDamage", () => { + it("Allows multiple segments to be cleared", () => { + // Deal 850 damage to a target with 800 max HP that has 8 segments. + const [adjusted, clearedIndex] = calculateBossSegmentDamage(850, 800, 100); + expect(adjusted).toEqual(300); + expect(clearedIndex).toEqual(5); + }); + + it("returns original damage and next segment index if damage is insufficient to clear current segment", () => { + // segmentHp = 100, currentHp = 250 (segmentIndex = 2), damage = 30 + // remainingSegmentHp = 250 - 200 = 50, leftoverDamage = 30 - 50 = -20 + const [adjusted, clearedIndex] = calculateBossSegmentDamage(30, 250, 100); + expect(adjusted).toBe(30); + expect(clearedIndex).toBe(3); // segmentIndex + 1 + }); + + it("returns adjusted damage and decremented segment index when damage clears one segment", () => { + // segmentHp = 100, currentHp = 250 (segmentIndex = 2), damage = 60 + // remainingSegmentHp = 50, leftoverDamage = 10 + const [adjusted, clearedIndex] = calculateBossSegmentDamage(60, 250, 100); + expect(adjusted).toBeGreaterThanOrEqual(50); // at least remainingSegmentHp + expect(clearedIndex).toBe(2); // segmentIndex - segmentsBypassed (0) + }); + + it("handles exact segment boundary", () => { + // segmentHp = 100, currentHp = 200 (segmentIndex = 1), damage = 150 + // remainingSegmentHp = 200 = 100, leftoverDamage = 0 + const [adjusted, clearedIndex] = calculateBossSegmentDamage(150, 200, 100); + expect(adjusted).toBe(100); + expect(clearedIndex).toBe(1); + }); + + it("handles exact segment boundary for small damage", () => { + // segmentHp = 100, currentHp = 200 (segmentIndex = 1), damage = 150 + // remainingSegmentHp = 200 = 100, leftoverDamage = 0 + const [adjusted, clearedIndex] = calculateBossSegmentDamage(50, 200, 100); + expect(adjusted).toBe(50); + expect(clearedIndex).toBe(2); + }); + + it("handles single segment case", () => { + // segmentHp = 100, currentHp = 100, damage = 50 + const [adjusted, clearedIndex] = calculateBossSegmentDamage(50, 100, 100); + expect(adjusted).toBe(50); + expect(clearedIndex).toBe(1); + }); + + it("handles zero damage", () => { + // segmentHp = 100, currentHp = 250, damage = 0 + const [adjusted, clearedIndex] = calculateBossSegmentDamage(0, 250, 100); + expect(adjusted).toBe(0); + expect(clearedIndex).toBe(3); + }); +});